Skip to content

Commit 5466c97

Browse files
committed
sqlite-utils json dogs.db "select * from dogs"
1 parent 7689ca7 commit 5466c97

File tree

4 files changed

+158
-6
lines changed

4 files changed

+158
-6
lines changed

docs/cli.rst

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,59 @@ Running queries and returning CSV
1111

1212
You can execute a SQL query against a database and get the results back as CSV like this::
1313

14-
$ sqlite-utils csv docs.db "select id, title, author from documents"
14+
$ sqlite-utils csv dogs.db "select * from dogs"
15+
id,age,name
16+
1,4,Cleo
17+
2,2,Pancakes
1518

1619
This will default to including the column names as a header row. To exclude the headers, use ``--no-headers``::
1720

18-
$ sqlite-utils csv docs.db "select id, title, author from documents" --no-headers
21+
$ sqlite-utils csv dogs.db "select * from dogs" --no-headers
22+
1,4,Cleo
23+
2,2,Pancakes
24+
25+
Running queries and returning JSON
26+
==================================
27+
28+
You can execute a SQL query against a database and get the results back as JSON like this::
29+
30+
$ sqlite-utils json dogs.db "select * from dogs"
31+
[{"id": 1, "age": 4, "name": "Cleo"},
32+
{"id": 2, "age": 2, "name": "Pancakes"}]
33+
34+
Use ``--nl`` to get back newline-delimited JSON objects::
35+
36+
$ sqlite-utils json --nl dogs.db "select * from dogs"
37+
{"id": 1, "age": 4, "name": "Cleo"}
38+
{"id": 2, "age": 2, "name": "Pancakes"}
39+
40+
You can use ``--arrays`` to request ararys instead of objects::
41+
42+
$ sqlite-utils json --arrays dogs.db "select * from dogs"
43+
[[1, 4, "Cleo"],
44+
[2, 2, "Pancakes"]]
45+
46+
You can also combine ``--arrays`` and ``--nl``::
47+
48+
$ sqlite-utils json --arrays --nl dogs.db "select * from dogs"
49+
[1, 4, "Cleo"]
50+
[2, 2, "Pancakes"]
51+
52+
If you want to pretty-print the output further, you can pipe it through ``python -mjson.tool``::
53+
54+
$ sqlite-utils json dogs.db "select * from dogs" | python -mjson.tool
55+
[
56+
{
57+
"id": 1,
58+
"age": 4,
59+
"name": "Cleo"
60+
},
61+
{
62+
"id": 2,
63+
"age": 2,
64+
"name": "Pancakes"
65+
}
66+
]
1967

2068
Listing tables
2169
==============
@@ -71,7 +119,6 @@ You can import all three records into an automatically created ``dogs`` table an
71119

72120
$ sqlite-utils insert dogs.db dogs dogs.json --pk=id
73121

74-
75122
Upserting data
76123
==============
77124

sqlite_utils/cli.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import click
22
import sqlite_utils
3-
import json
3+
from sqlite_utils.utils import iter_pairs
4+
import json as json_std
45
import sys
56
import csv as csv_std
7+
import sqlite3
68

79

810
@click.group()
@@ -72,7 +74,7 @@ def optimize(path, no_vacuum):
7274
def insert(path, table, json_file, pk):
7375
"Insert records from JSON file into the table, create table if it is missing"
7476
db = sqlite_utils.Database(path)
75-
docs = json.load(json_file)
77+
docs = json_std.load(json_file)
7678
if isinstance(docs, dict):
7779
docs = [docs]
7880
db[table].insert_all(docs, pk=pk)
@@ -90,7 +92,7 @@ def insert(path, table, json_file, pk):
9092
def upsert(path, table, json_file, pk):
9193
"Upsert records based on their primary key"
9294
db = sqlite_utils.Database(path)
93-
docs = json.load(json_file)
95+
docs = json_std.load(json_file)
9496
if isinstance(docs, dict):
9597
docs = [docs]
9698
db[table].upsert_all(docs, pk=pk)
@@ -115,3 +117,42 @@ def csv(path, sql, no_headers):
115117
writer.writerow([c[0] for c in cursor.description])
116118
for row in cursor:
117119
writer.writerow(row)
120+
121+
122+
@cli.command()
123+
@click.argument(
124+
"path",
125+
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
126+
required=True,
127+
)
128+
@click.argument("sql")
129+
@click.option("--nl", help="Output newline-delimited JSON", is_flag=True, default=False)
130+
@click.option(
131+
"--arrays",
132+
help="Output rows as arrays instead of objects",
133+
is_flag=True,
134+
default=False,
135+
)
136+
def json(path, sql, nl, arrays):
137+
"Execute SQL query and return the results as JSON"
138+
db = sqlite_utils.Database(path)
139+
cursor = iter(db.conn.execute(sql))
140+
# We have to iterate two-at-a-time so we can know if we
141+
# should output a trailing comma or if we have reached
142+
# the last row.
143+
row = None
144+
first = True
145+
headers = [c[0] for c in cursor.description]
146+
for row, next_row, is_last in iter_pairs(cursor):
147+
# We now reliably have row and next_row
148+
data = row
149+
if not arrays:
150+
data = dict(zip(headers, row))
151+
line = "{firstchar}{serialized}{maybecomma}{lastchar}".format(
152+
firstchar=("[" if first else " ") if not nl else "",
153+
serialized=json_std.dumps(data),
154+
maybecomma="," if (not nl and not is_last) else "",
155+
lastchar="]" if (is_last and not nl) else "",
156+
)
157+
click.echo(line)
158+
first = False

sqlite_utils/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
def iter_pairs(iterator):
2+
# Yields (item, next_item, is_last) pairs
3+
# Last row is (item, None, True)
4+
first = True
5+
next_item = None
6+
while True:
7+
next_done = False
8+
if first:
9+
item, done = next_with_done(iterator)
10+
if done:
11+
break
12+
next_item, next_done = next_with_done(iterator)
13+
first = False
14+
else:
15+
item = next_item
16+
next_item, next_done = next_with_done(iterator)
17+
18+
yield item, next_item, next_done
19+
20+
if next_done:
21+
break
22+
23+
24+
def next_with_done(iterator):
25+
try:
26+
return next(iterator), False
27+
except StopIteration:
28+
return None, True

tests/test_cli.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,39 @@ def test_csv(db_path):
151151
cli.cli, ["csv", db_path, "select id, name, age from dogs", "--no-headers"]
152152
)
153153
assert "1,Cleo,4\n2,Pancakes,2\n" == result.output
154+
155+
156+
@pytest.mark.parametrize(
157+
"args,expected",
158+
[
159+
(
160+
[],
161+
'[{"id": 1, "name": "Cleo", "age": 4},\n {"id": 2, "name": "Pancakes", "age": 2}]',
162+
),
163+
(
164+
["--nl"],
165+
'{"id": 1, "name": "Cleo", "age": 4}\n{"id": 2, "name": "Pancakes", "age": 2}',
166+
),
167+
(["--arrays"], '[[1, "Cleo", 4],\n [2, "Pancakes", 2]]'),
168+
(["--arrays", "--nl"], '[1, "Cleo", 4]\n[2, "Pancakes", 2]'),
169+
],
170+
)
171+
def test_json(db_path, args, expected):
172+
db = Database(db_path)
173+
with db.conn:
174+
db["dogs"].insert_all(
175+
[
176+
{"id": 1, "age": 4, "name": "Cleo"},
177+
{"id": 2, "age": 2, "name": "Pancakes"},
178+
]
179+
)
180+
result = CliRunner().invoke(
181+
cli.cli, ["csv", db_path, "select id, name, age from dogs"]
182+
)
183+
assert 0 == result.exit_code
184+
assert "id,name,age\n1,Cleo,4\n2,Pancakes,2\n" == result.output
185+
# Test the no-headers option:
186+
result = CliRunner().invoke(
187+
cli.cli, ["json", db_path, "select id, name, age from dogs"] + args
188+
)
189+
assert expected == result.output.strip()

0 commit comments

Comments
 (0)