mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-09-21 16:04:10 +02:00
Add --transpose/-x option to rows, query, memory and search for wide records
Displays each row as a psql-style key/value block (\x extended display) instead of a wide table, while leaving JSON/CSV/table output untouched (#535).
This commit is contained in:
parent
f6d83ec8be
commit
050f11f31f
4 changed files with 126 additions and 2 deletions
|
|
@ -136,6 +136,8 @@ See :ref:`cli_query`.
|
||||||
escaped strings
|
escaped strings
|
||||||
--ascii Escape non-ASCII characters in JSON output as
|
--ascii Escape non-ASCII characters in JSON output as
|
||||||
\uXXXX
|
\uXXXX
|
||||||
|
-x, --transpose Transpose wide rows, one key/value pair per line
|
||||||
|
(like psql's \x)
|
||||||
-r, --raw Raw output, first column of first row
|
-r, --raw Raw output, first column of first row
|
||||||
--raw-lines Raw output, first column of each row
|
--raw-lines Raw output, first column of each row
|
||||||
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
||||||
|
|
@ -207,6 +209,8 @@ See :ref:`cli_memory`.
|
||||||
escaped strings
|
escaped strings
|
||||||
--ascii Escape non-ASCII characters in JSON output as
|
--ascii Escape non-ASCII characters in JSON output as
|
||||||
\uXXXX
|
\uXXXX
|
||||||
|
-x, --transpose Transpose wide rows, one key/value pair per line
|
||||||
|
(like psql's \x)
|
||||||
-r, --raw Raw output, first column of first row
|
-r, --raw Raw output, first column of first row
|
||||||
--raw-lines Raw output, first column of each row
|
--raw-lines Raw output, first column of each row
|
||||||
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
||||||
|
|
@ -478,6 +482,8 @@ See :ref:`cli_search`.
|
||||||
--json-cols Detect JSON cols and output them as JSON, not escaped
|
--json-cols Detect JSON cols and output them as JSON, not escaped
|
||||||
strings
|
strings
|
||||||
--ascii Escape non-ASCII characters in JSON output as \uXXXX
|
--ascii Escape non-ASCII characters in JSON output as \uXXXX
|
||||||
|
-x, --transpose Transpose wide rows, one key/value pair per line (like
|
||||||
|
psql's \x)
|
||||||
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
|
@ -842,6 +848,8 @@ See :ref:`cli_rows`.
|
||||||
escaped strings
|
escaped strings
|
||||||
--ascii Escape non-ASCII characters in JSON output as
|
--ascii Escape non-ASCII characters in JSON output as
|
||||||
\uXXXX
|
\uXXXX
|
||||||
|
-x, --transpose Transpose wide rows, one key/value pair per line
|
||||||
|
(like psql's \x)
|
||||||
--load-extension TEXT Path to SQLite extension, with optional
|
--load-extension TEXT Path to SQLite extension, with optional
|
||||||
:entrypoint
|
:entrypoint
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
|
||||||
29
docs/cli.rst
29
docs/cli.rst
|
|
@ -337,6 +337,31 @@ Available ``--fmt`` options are:
|
||||||
|
|
||||||
This list can also be found by running ``sqlite-utils query --help``.
|
This list can also be found by running ``sqlite-utils query --help``.
|
||||||
|
|
||||||
|
.. _cli_query_transpose:
|
||||||
|
|
||||||
|
Transposed output for wide rows
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
If your rows have a lot of columns they can be hard to read as a table.
|
||||||
|
Use the ``--transpose`` option (or the ``-x`` shortcut, matching ``psql``'s ``\x``) to display each row as a block of key/value pairs instead:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils dogs.db "select * from dogs" --transpose
|
||||||
|
|
||||||
|
.. code-block:: output
|
||||||
|
|
||||||
|
-[ RECORD 1 ]--
|
||||||
|
id | 1
|
||||||
|
age | 4
|
||||||
|
name | Cleo
|
||||||
|
-[ RECORD 2 ]--
|
||||||
|
id | 2
|
||||||
|
age | 2
|
||||||
|
name | Pancakes
|
||||||
|
|
||||||
|
Combine this with ``--no-headers`` to omit the ``-[ RECORD n ]-`` separator lines.
|
||||||
|
|
||||||
.. _cli_query_raw:
|
.. _cli_query_raw:
|
||||||
|
|
||||||
Returning raw data, such as binary content
|
Returning raw data, such as binary content
|
||||||
|
|
@ -480,7 +505,7 @@ Without any extra arguments, this command executes SQL against the in-memory dat
|
||||||
|
|
||||||
[{"sqlite_version()": "3.35.5"}]
|
[{"sqlite_version()": "3.35.5"}]
|
||||||
|
|
||||||
It takes all of the same output formatting options as :ref:`sqlite-utils query <cli_query>`: ``--csv`` and ``--csv`` and ``--table`` and ``--nl``:
|
It takes all of the same output formatting options as :ref:`sqlite-utils query <cli_query>`: ``--csv`` and ``--table`` and ``--nl`` and ``--transpose``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
|
@ -690,7 +715,7 @@ You can return every row in a specified table using the ``rows`` command:
|
||||||
[{"id": 1, "age": 4, "name": "Cleo"},
|
[{"id": 1, "age": 4, "name": "Cleo"},
|
||||||
{"id": 2, "age": 2, "name": "Pancakes"}]
|
{"id": 2, "age": 2, "name": "Pancakes"}]
|
||||||
|
|
||||||
This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv``, ``--tsv``, ``--no-headers``, ``--table`` and ``--fmt``.
|
This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv``, ``--tsv``, ``--no-headers``, ``--table``, ``--fmt`` and ``--transpose`` (see :ref:`cli_query_transpose`).
|
||||||
|
|
||||||
You can use the ``-c`` option to specify a subset of columns to return:
|
You can use the ``-c`` option to specify a subset of columns to return:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,15 @@ def load_extension_option(fn):
|
||||||
)(fn)
|
)(fn)
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_option(fn):
|
||||||
|
return click.option(
|
||||||
|
"-x",
|
||||||
|
"--transpose",
|
||||||
|
is_flag=True,
|
||||||
|
help="Transpose wide rows, one key/value pair per line (like psql's \\x)",
|
||||||
|
)(fn)
|
||||||
|
|
||||||
|
|
||||||
def functions_option(fn):
|
def functions_option(fn):
|
||||||
return click.option(
|
return click.option(
|
||||||
"--functions",
|
"--functions",
|
||||||
|
|
@ -1992,6 +2001,7 @@ def drop_view(path, view, ignore, load_extension):
|
||||||
help="Additional databases to attach - specify alias and filepath",
|
help="Additional databases to attach - specify alias and filepath",
|
||||||
)
|
)
|
||||||
@output_options
|
@output_options
|
||||||
|
@transpose_option
|
||||||
@click.option("-r", "--raw", is_flag=True, help="Raw output, first column of first row")
|
@click.option("-r", "--raw", is_flag=True, help="Raw output, first column of first row")
|
||||||
@click.option("--raw-lines", is_flag=True, help="Raw output, first column of each row")
|
@click.option("--raw-lines", is_flag=True, help="Raw output, first column of each row")
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|
@ -2016,6 +2026,7 @@ def query(
|
||||||
fmt,
|
fmt,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose,
|
||||||
raw,
|
raw,
|
||||||
raw_lines,
|
raw_lines,
|
||||||
param,
|
param,
|
||||||
|
|
@ -2063,6 +2074,7 @@ def query(
|
||||||
arrays,
|
arrays,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2087,6 +2099,7 @@ def query(
|
||||||
help='Flatten nested JSON objects, so {"foo": {"bar": 1}} becomes {"foo_bar": 1}',
|
help='Flatten nested JSON objects, so {"foo": {"bar": 1}} becomes {"foo_bar": 1}',
|
||||||
)
|
)
|
||||||
@output_options
|
@output_options
|
||||||
|
@transpose_option
|
||||||
@click.option("-r", "--raw", is_flag=True, help="Raw output, first column of first row")
|
@click.option("-r", "--raw", is_flag=True, help="Raw output, first column of first row")
|
||||||
@click.option("--raw-lines", is_flag=True, help="Raw output, first column of each row")
|
@click.option("--raw-lines", is_flag=True, help="Raw output, first column of each row")
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|
@ -2134,6 +2147,7 @@ def memory(
|
||||||
fmt,
|
fmt,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose,
|
||||||
raw,
|
raw,
|
||||||
raw_lines,
|
raw_lines,
|
||||||
param,
|
param,
|
||||||
|
|
@ -2274,6 +2288,7 @@ def memory(
|
||||||
arrays,
|
arrays,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2292,6 +2307,7 @@ def _execute_query(
|
||||||
arrays,
|
arrays,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose=False,
|
||||||
):
|
):
|
||||||
with db.conn:
|
with db.conn:
|
||||||
try:
|
try:
|
||||||
|
|
@ -2319,6 +2335,9 @@ def _execute_query(
|
||||||
sys.stdout.buffer.write(data + b"\n")
|
sys.stdout.buffer.write(data + b"\n")
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(str(data) + "\n")
|
sys.stdout.write(str(data) + "\n")
|
||||||
|
elif transpose:
|
||||||
|
for line in output_transpose(cursor, headers, no_headers):
|
||||||
|
click.echo(line)
|
||||||
elif fmt or table:
|
elif fmt or table:
|
||||||
print(
|
print(
|
||||||
tabulate.tabulate(
|
tabulate.tabulate(
|
||||||
|
|
@ -2358,6 +2377,7 @@ def _execute_query(
|
||||||
)
|
)
|
||||||
@click.option("--quote", is_flag=True, help="Apply FTS quoting rules to search term")
|
@click.option("--quote", is_flag=True, help="Apply FTS quoting rules to search term")
|
||||||
@output_options
|
@output_options
|
||||||
|
@transpose_option
|
||||||
@load_extension_option
|
@load_extension_option
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def search(
|
def search(
|
||||||
|
|
@ -2379,6 +2399,7 @@ def search(
|
||||||
fmt,
|
fmt,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose,
|
||||||
load_extension,
|
load_extension,
|
||||||
):
|
):
|
||||||
"""Execute a full-text search against this table
|
"""Execute a full-text search against this table
|
||||||
|
|
@ -2424,6 +2445,7 @@ def search(
|
||||||
fmt=fmt,
|
fmt=fmt,
|
||||||
json_cols=json_cols,
|
json_cols=json_cols,
|
||||||
ascii_=ascii_,
|
ascii_=ascii_,
|
||||||
|
transpose=transpose,
|
||||||
param=[("query", q)],
|
param=[("query", q)],
|
||||||
load_extension=load_extension,
|
load_extension=load_extension,
|
||||||
)
|
)
|
||||||
|
|
@ -2464,6 +2486,7 @@ def search(
|
||||||
help="SQL offset to use",
|
help="SQL offset to use",
|
||||||
)
|
)
|
||||||
@output_options
|
@output_options
|
||||||
|
@transpose_option
|
||||||
@load_extension_option
|
@load_extension_option
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def rows(
|
def rows(
|
||||||
|
|
@ -2485,6 +2508,7 @@ def rows(
|
||||||
fmt,
|
fmt,
|
||||||
json_cols,
|
json_cols,
|
||||||
ascii_,
|
ascii_,
|
||||||
|
transpose,
|
||||||
load_extension,
|
load_extension,
|
||||||
):
|
):
|
||||||
"""Output all rows in the specified table
|
"""Output all rows in the specified table
|
||||||
|
|
@ -2520,6 +2544,7 @@ def rows(
|
||||||
param=param,
|
param=param,
|
||||||
json_cols=json_cols,
|
json_cols=json_cols,
|
||||||
ascii_=ascii_,
|
ascii_=ascii_,
|
||||||
|
transpose=transpose,
|
||||||
load_extension=load_extension,
|
load_extension=load_extension,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -3798,6 +3823,20 @@ def output_rows(iterator, headers, nl, arrays, json_cols, ascii_=False):
|
||||||
yield "[]"
|
yield "[]"
|
||||||
|
|
||||||
|
|
||||||
|
def output_transpose(rows, headers, no_headers):
|
||||||
|
# psql-style extended display: one "key = value" block per row
|
||||||
|
headers = [str(h) for h in headers]
|
||||||
|
str_rows = [["" if v is None else str(v) for v in row] for row in rows]
|
||||||
|
key_width = max([len(h) for h in headers], default=0)
|
||||||
|
val_width = max([len(v) for row in str_rows for v in row], default=0)
|
||||||
|
total_width = key_width + 3 + val_width
|
||||||
|
for i, row in enumerate(str_rows, start=1):
|
||||||
|
if not no_headers:
|
||||||
|
yield "-[ RECORD {} ]-".format(i).ljust(total_width, "-")
|
||||||
|
for header, value in zip(headers, row):
|
||||||
|
yield "{} | {}".format(header.ljust(key_width), value)
|
||||||
|
|
||||||
|
|
||||||
def maybe_json(value):
|
def maybe_json(value):
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
|
|
@ -760,6 +760,58 @@ def test_query_csv(db_path, format, expected):
|
||||||
assert result.output.strip().replace("\r", "") == expected_rest
|
assert result.output.strip().replace("\r", "") == expected_rest
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_transpose(db_path):
|
||||||
|
db = Database(db_path)
|
||||||
|
with db.conn:
|
||||||
|
db["dogs"].insert_all(
|
||||||
|
[
|
||||||
|
{"id": 1, "age": 4, "name": "Cleo"},
|
||||||
|
{"id": 2, "age": 2, "name": "Pancakes"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[db_path, "select id, name, age from dogs", "--transpose"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == (
|
||||||
|
"-[ RECORD 1 ]--\n"
|
||||||
|
"id | 1\n"
|
||||||
|
"name | Cleo\n"
|
||||||
|
"age | 4\n"
|
||||||
|
"-[ RECORD 2 ]--\n"
|
||||||
|
"id | 2\n"
|
||||||
|
"name | Pancakes\n"
|
||||||
|
"age | 2\n"
|
||||||
|
)
|
||||||
|
# -x is a shorthand for --transpose
|
||||||
|
result2 = CliRunner().invoke(
|
||||||
|
cli.cli, [db_path, "select id, name, age from dogs", "-x"]
|
||||||
|
)
|
||||||
|
assert result2.output == result.output
|
||||||
|
# --no-headers drops the "-[ RECORD n ]-" separators
|
||||||
|
result3 = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[db_path, "select id, name, age from dogs", "--transpose", "--no-headers"],
|
||||||
|
)
|
||||||
|
assert result3.exit_code == 0
|
||||||
|
assert (
|
||||||
|
result3.output
|
||||||
|
== "id | 1\nname | Cleo\nage | 4\nid | 2\nname | Pancakes\nage | 2\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rows_transpose(db_path):
|
||||||
|
db = Database(db_path)
|
||||||
|
with db.conn:
|
||||||
|
db["dogs"].insert_all(
|
||||||
|
[{"id": 1, "name": "Cleo", "age": 4}], column_order=("id", "name", "age")
|
||||||
|
)
|
||||||
|
result = CliRunner().invoke(cli.cli, ["rows", db_path, "dogs", "-x"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == "-[ RECORD 1 ]-\nid | 1\nname | Cleo\nage | 4\n"
|
||||||
|
|
||||||
|
|
||||||
_all_query = "select id, name, age from dogs"
|
_all_query = "select id, name, age from dogs"
|
||||||
_one_query = "select id, name, age from dogs where id = 1"
|
_one_query = "select id, name, age from dogs where id = 1"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue