mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-10-07 18:07:03 +02:00
Default command now executes queries, --csv or --json
I replaced the following commands:
sqlite-utils json db.db "select * from table"
sqlite-utils csv db.db "select * from table"
With a unified 'query' command, which is now set as the default:
sqlite-utils db.db "select * from table"
sqlite-utils db.db "select * from table" --csv
This commit is contained in:
parent
3095f2e671
commit
f2ca48c0da
5 changed files with 76 additions and 60 deletions
|
|
@ -48,7 +48,7 @@ The ``Database()`` constructor can now accept a ``pathlib.Path`` object in addit
|
||||||
|
|
||||||
Two new commands: ``sqlite-utils csv`` and ``sqlite-utils json``
|
Two new commands: ``sqlite-utils csv`` and ``sqlite-utils json``
|
||||||
|
|
||||||
These commands execute a SQL query and return the results as CSV or JSON. See :ref:`cli_csv` and :ref:`cli_json` for more details.
|
These commands execute a SQL query and return the results as CSV or JSON. See :ref:`cli_query_csv` and :ref:`cli_query_json` for more details.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
|
||||||
48
docs/cli.rst
48
docs/cli.rst
|
|
@ -6,56 +6,38 @@
|
||||||
|
|
||||||
The ``sqlite-utils`` command-line tool can be used to manipulate SQLite databases in a number of different ways.
|
The ``sqlite-utils`` command-line tool can be used to manipulate SQLite databases in a number of different ways.
|
||||||
|
|
||||||
.. _cli_csv:
|
.. _cli_query_json:
|
||||||
|
|
||||||
Running queries and returning CSV
|
|
||||||
=================================
|
|
||||||
|
|
||||||
You can execute a SQL query against a database and get the results back as CSV like this::
|
|
||||||
|
|
||||||
$ sqlite-utils csv dogs.db "select * from dogs"
|
|
||||||
id,age,name
|
|
||||||
1,4,Cleo
|
|
||||||
2,2,Pancakes
|
|
||||||
|
|
||||||
This will default to including the column names as a header row. To exclude the headers, use ``--no-headers``::
|
|
||||||
|
|
||||||
$ sqlite-utils csv dogs.db "select * from dogs" --no-headers
|
|
||||||
1,4,Cleo
|
|
||||||
2,2,Pancakes
|
|
||||||
|
|
||||||
.. _cli_json:
|
|
||||||
|
|
||||||
Running queries and returning JSON
|
Running queries and returning JSON
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
You can execute a SQL query against a database and get the results back as JSON like this::
|
You can execute a SQL query against a database and get the results back as JSON like this::
|
||||||
|
|
||||||
$ sqlite-utils json dogs.db "select * from dogs"
|
$ sqlite-utils dogs.db "select * from dogs"
|
||||||
[{"id": 1, "age": 4, "name": "Cleo"},
|
[{"id": 1, "age": 4, "name": "Cleo"},
|
||||||
{"id": 2, "age": 2, "name": "Pancakes"}]
|
{"id": 2, "age": 2, "name": "Pancakes"}]
|
||||||
|
|
||||||
Use ``--nl`` to get back newline-delimited JSON objects::
|
Use ``--nl`` to get back newline-delimited JSON objects::
|
||||||
|
|
||||||
$ sqlite-utils json --nl dogs.db "select * from dogs"
|
$ sqlite-utils dogs.db "select * from dogs" --nl
|
||||||
{"id": 1, "age": 4, "name": "Cleo"}
|
{"id": 1, "age": 4, "name": "Cleo"}
|
||||||
{"id": 2, "age": 2, "name": "Pancakes"}
|
{"id": 2, "age": 2, "name": "Pancakes"}
|
||||||
|
|
||||||
You can use ``--arrays`` to request ararys instead of objects::
|
You can use ``--arrays`` to request ararys instead of objects::
|
||||||
|
|
||||||
$ sqlite-utils json --arrays dogs.db "select * from dogs"
|
$ sqlite-utils dogs.db "select * from dogs" --arrays
|
||||||
[[1, 4, "Cleo"],
|
[[1, 4, "Cleo"],
|
||||||
[2, 2, "Pancakes"]]
|
[2, 2, "Pancakes"]]
|
||||||
|
|
||||||
You can also combine ``--arrays`` and ``--nl``::
|
You can also combine ``--arrays`` and ``--nl``::
|
||||||
|
|
||||||
$ sqlite-utils json --arrays --nl dogs.db "select * from dogs"
|
$ sqlite-utils dogs.db "select * from dogs" --arrays --nl
|
||||||
[1, 4, "Cleo"]
|
[1, 4, "Cleo"]
|
||||||
[2, 2, "Pancakes"]
|
[2, 2, "Pancakes"]
|
||||||
|
|
||||||
If you want to pretty-print the output further, you can pipe it through ``python -mjson.tool``::
|
If you want to pretty-print the output further, you can pipe it through ``python -mjson.tool``::
|
||||||
|
|
||||||
$ sqlite-utils json dogs.db "select * from dogs" | python -mjson.tool
|
$ sqlite-utils dogs.db "select * from dogs" | python -mjson.tool
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -69,6 +51,24 @@ If you want to pretty-print the output further, you can pipe it through ``python
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. _cli_query_csv:
|
||||||
|
|
||||||
|
Running queries and returning CSV
|
||||||
|
=================================
|
||||||
|
|
||||||
|
You can use the ``--csv`` option to return results as CSV::
|
||||||
|
|
||||||
|
$ sqlite-utils dogs.db "select * from dogs" --csv
|
||||||
|
id,age,name
|
||||||
|
1,4,Cleo
|
||||||
|
2,2,Pancakes
|
||||||
|
|
||||||
|
This will default to including the column names as a header row. To exclude the headers, use ``--no-headers``::
|
||||||
|
|
||||||
|
$ sqlite-utils dogs.db "select * from dogs" --csv --no-headers
|
||||||
|
1,4,Cleo
|
||||||
|
2,2,Pancakes
|
||||||
|
|
||||||
Listing tables
|
Listing tables
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -22,7 +22,7 @@ setup(
|
||||||
version=VERSION,
|
version=VERSION,
|
||||||
license="Apache License, Version 2.0",
|
license="Apache License, Version 2.0",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=["click"],
|
install_requires=["click", "click-default-group"],
|
||||||
setup_requires=["pytest-runner"],
|
setup_requires=["pytest-runner"],
|
||||||
extras_require={"test": ["pytest", "black"]},
|
extras_require={"test": ["pytest", "black"]},
|
||||||
entry_points="""
|
entry_points="""
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import click
|
import click
|
||||||
|
from click_default_group import DefaultGroup
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import csv
|
import csv as csv_std
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group(cls=DefaultGroup, default="query", default_if_no_args=True)
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
def cli():
|
def cli():
|
||||||
"Commands for interacting with a SQLite database"
|
"Commands for interacting with a SQLite database"
|
||||||
|
|
@ -103,6 +104,29 @@ def populate_fts(path, table, column):
|
||||||
db[table].populate_fts(column)
|
db[table].populate_fts(column)
|
||||||
|
|
||||||
|
|
||||||
|
def output_options(fn):
|
||||||
|
for decorator in reversed(
|
||||||
|
(
|
||||||
|
click.option(
|
||||||
|
"--nl",
|
||||||
|
help="Output newline-delimited JSON",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
click.option(
|
||||||
|
"--arrays",
|
||||||
|
help="Output rows as arrays instead of objects",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
click.option("--csv", is_flag=True, help="Output CSV"),
|
||||||
|
click.option("--no-headers", is_flag=True, help="Omit CSV headers"),
|
||||||
|
)
|
||||||
|
):
|
||||||
|
fn = decorator(fn)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
def insert_upsert_options(fn):
|
def insert_upsert_options(fn):
|
||||||
for decorator in reversed(
|
for decorator in reversed(
|
||||||
(
|
(
|
||||||
|
|
@ -133,7 +157,7 @@ def insert_upsert_implementation(
|
||||||
click.echo("Use just one of --nl and --csv", err=True)
|
click.echo("Use just one of --nl and --csv", err=True)
|
||||||
return
|
return
|
||||||
if csv:
|
if csv:
|
||||||
reader = csv.reader(json_file)
|
reader = csv_std.reader(json_file)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
docs = (dict(zip(headers, row)) for row in reader)
|
docs = (dict(zip(headers, row)) for row in reader)
|
||||||
elif nl:
|
elif nl:
|
||||||
|
|
@ -176,34 +200,14 @@ def upsert(path, table, json_file, pk, nl, csv, batch_size):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="csv")
|
@cli.command()
|
||||||
@click.argument(
|
|
||||||
"path",
|
|
||||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
@click.argument("sql")
|
|
||||||
@click.option(
|
|
||||||
"--no-headers", help="Exclude headers from CSV output", is_flag=True, default=False
|
|
||||||
)
|
|
||||||
def csv_cmd(path, sql, no_headers):
|
|
||||||
"Execute SQL query and return the results as CSV"
|
|
||||||
db = sqlite_utils.Database(path)
|
|
||||||
cursor = db.conn.execute(sql)
|
|
||||||
writer = csv.writer(sys.stdout)
|
|
||||||
if not no_headers:
|
|
||||||
writer.writerow([c[0] for c in cursor.description])
|
|
||||||
for row in cursor:
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="json")
|
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"path",
|
"path",
|
||||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
@click.argument("sql")
|
@click.argument("sql")
|
||||||
|
@output_options
|
||||||
@click.option("--nl", help="Output newline-delimited JSON", is_flag=True, default=False)
|
@click.option("--nl", help="Output newline-delimited JSON", is_flag=True, default=False)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--arrays",
|
"--arrays",
|
||||||
|
|
@ -211,17 +215,29 @@ def csv_cmd(path, sql, no_headers):
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
def json_cmd(path, sql, nl, arrays):
|
def query(path, sql, nl, arrays, csv, no_headers):
|
||||||
"Execute SQL query and return the results as JSON"
|
"Execute SQL query and return the results as JSON"
|
||||||
db = sqlite_utils.Database(path)
|
db = sqlite_utils.Database(path)
|
||||||
cursor = iter(db.conn.execute(sql))
|
cursor = iter(db.conn.execute(sql))
|
||||||
|
headers = [c[0] for c in cursor.description]
|
||||||
|
if csv:
|
||||||
|
writer = csv_std.writer(sys.stdout)
|
||||||
|
if not no_headers:
|
||||||
|
writer.writerow([c[0] for c in cursor.description])
|
||||||
|
for row in cursor:
|
||||||
|
writer.writerow(row)
|
||||||
|
else:
|
||||||
|
for line in output_rows(cursor, headers, nl, arrays):
|
||||||
|
click.echo(line)
|
||||||
|
|
||||||
|
|
||||||
|
def output_rows(iterator, headers, nl, arrays):
|
||||||
# We have to iterate two-at-a-time so we can know if we
|
# We have to iterate two-at-a-time so we can know if we
|
||||||
# should output a trailing comma or if we have reached
|
# should output a trailing comma or if we have reached
|
||||||
# the last row.
|
# the last row.
|
||||||
current_iter, next_iter = itertools.tee(cursor, 2)
|
current_iter, next_iter = itertools.tee(iterator, 2)
|
||||||
next(next_iter, None)
|
next(next_iter, None)
|
||||||
first = True
|
first = True
|
||||||
headers = [c[0] for c in cursor.description]
|
|
||||||
for row, next_row in itertools.zip_longest(current_iter, next_iter):
|
for row, next_row in itertools.zip_longest(current_iter, next_iter):
|
||||||
is_last = next_row is None
|
is_last = next_row is None
|
||||||
data = row
|
data = row
|
||||||
|
|
@ -233,5 +249,5 @@ def json_cmd(path, sql, nl, arrays):
|
||||||
maybecomma="," if (not nl and not is_last) else "",
|
maybecomma="," if (not nl and not is_last) else "",
|
||||||
lastchar="]" if (is_last and not nl) else "",
|
lastchar="]" if (is_last and not nl) else "",
|
||||||
)
|
)
|
||||||
click.echo(line)
|
yield line
|
||||||
first = False
|
first = False
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ def test_upsert(db_path, tmpdir):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_csv(db_path):
|
def test_query_csv(db_path):
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
with db.conn:
|
with db.conn:
|
||||||
db["dogs"].insert_all(
|
db["dogs"].insert_all(
|
||||||
|
|
@ -189,13 +189,13 @@ def test_csv(db_path):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli, ["csv", db_path, "select id, name, age from dogs"]
|
cli.cli, [db_path, "select id, name, age from dogs", "--csv"]
|
||||||
)
|
)
|
||||||
assert 0 == result.exit_code
|
assert 0 == result.exit_code
|
||||||
assert "id,name,age\n1,Cleo,4\n2,Pancakes,2\n" == result.output
|
assert "id,name,age\n1,Cleo,4\n2,Pancakes,2\n" == result.output
|
||||||
# Test the no-headers option:
|
# Test the no-headers option:
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli, ["csv", db_path, "select id, name, age from dogs", "--no-headers"]
|
cli.cli, [db_path, "select id, name, age from dogs", "--no-headers", "--csv"]
|
||||||
)
|
)
|
||||||
assert "1,Cleo,4\n2,Pancakes,2\n" == result.output
|
assert "1,Cleo,4\n2,Pancakes,2\n" == result.output
|
||||||
|
|
||||||
|
|
@ -225,7 +225,7 @@ _one_query = "select id, name, age from dogs where id = 1"
|
||||||
(_one_query, ["--arrays", "--nl"], '[1, "Cleo", 4]'),
|
(_one_query, ["--arrays", "--nl"], '[1, "Cleo", 4]'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_json(db_path, sql, args, expected):
|
def test_query_json(db_path, sql, args, expected):
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
with db.conn:
|
with db.conn:
|
||||||
db["dogs"].insert_all(
|
db["dogs"].insert_all(
|
||||||
|
|
@ -234,5 +234,5 @@ def test_json(db_path, sql, args, expected):
|
||||||
{"id": 2, "age": 2, "name": "Pancakes"},
|
{"id": 2, "age": 2, "name": "Pancakes"},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
result = CliRunner().invoke(cli.cli, ["json", db_path, sql] + args)
|
result = CliRunner().invoke(cli.cli, [db_path, sql] + args)
|
||||||
assert expected == result.output.strip()
|
assert expected == result.output.strip()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue