diff --git a/README.md b/README.md index 1eabc77..ebcae50 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Install it like this: Now you can do things like this: - $ sqlite-utils tables dogs.db - dogs + $ sqlite-utils tables dogs.db --counts + [{"table": "dogs", "count": 2}] $ sqlite-utils dogs.db "select * from dogs" [{"id": 1, "age": 4, "name": "Cleo"}, diff --git a/docs/cli.rst b/docs/cli.rst index 5281d02..6202c79 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -92,14 +92,37 @@ Listing tables You can list the names of tables in a database using the ``tables`` subcommand:: $ sqlite-utils tables mydb.db + [{"table": "dogs"}, + {"table": "cats"}, + {"table": "chickens"}] + +You can output this list in CSV using the ``-csv`` option:: + + $ sqlite-utils tables mydb.db --csv --no-headers dogs cats chickens If you just want to see the FTS4 tables, you can use ``--fts4`` (or ``--fts5`` for FTS5 tables):: - $ sqlite-utils tables --fts4 docs.db - docs_fts + $ sqlite-utils tables docs.db --fts4 + [{"table": "docs_fts"}] + +Use ``--counts`` to include a count of the number of rows in each table:: + + $ sqlite-utils tables mydb.db --counts + [{"table": "dogs", "count": 12}, + {"table": "cats", "count": 332}, + {"table": "chickens", "count": 9}] + +Use ``--columns`` to include a list of columns in each table:: + + $ sqlite-utils tables dogs.db --counts --columns + [{"table": "Gosh", "count": 0, "columns": ["c1", "c2", "c3"]}, + {"table": "Gosh2", "count": 0, "columns": ["c1", "c2", "c3"]}, + {"table": "dogs", "count": 2, "columns": ["id", "age", "name"]}] + +The ``--nl``, ``--csv`` and ``--no-headers`` options are all available. .. _cli_inserting_data: diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 568f3eb..2d5d5df 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -8,6 +8,29 @@ import csv as csv_std import sqlite3 +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 + + @click.group(cls=DefaultGroup, default="query", default_if_no_args=True) @click.version_option() def cli(): @@ -27,11 +50,47 @@ def cli(): @click.option( "--fts5", help="Just show FTS5 enabled tables", default=False, is_flag=True ) -def tables(path, fts4, fts5): +@click.option( + "--counts", help="Include row counts per table", default=False, is_flag=True +) +@output_options +@click.option( + "--columns", + help="Include list of columns for each table", + is_flag=True, + default=False, +) +def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, columns): """List the tables in the database""" db = sqlite_utils.Database(path) - for name in db.table_names(fts4=fts4, fts5=fts5): - print(name) + headers = ["table"] + if counts: + headers.append("count") + if columns: + headers.append("columns") + + def _iter(): + for name in db.table_names(fts4=fts4, fts5=fts5): + row = [name] + if counts: + row.append(db[name].count) + if columns: + cols = [c.name for c in db[name].columns] + if csv: + row.append("\n".join(cols)) + else: + row.append(cols) + yield row + + if csv: + writer = csv_std.writer(sys.stdout) + if not no_headers: + writer.writerow(headers) + for row in _iter(): + writer.writerow(row) + else: + for line in output_rows(_iter(), headers, nl, arrays): + click.echo(line) @cli.command() @@ -104,29 +163,6 @@ def populate_fts(path, table, 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): for decorator in reversed( ( diff --git a/tests/test_cli.py b/tests/test_cli.py index 8ca35fb..23c6891 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -22,19 +22,51 @@ def db_path(tmpdir): def test_tables(db_path): result = CliRunner().invoke(cli.cli, ["tables", db_path]) - assert "Gosh\nGosh2" == result.output.strip() + assert '[{"table": "Gosh"},\n {"table": "Gosh2"}]' == result.output.strip() def test_tables_fts4(db_path): Database(db_path)["Gosh"].enable_fts(["c2"], fts_version="FTS4") result = CliRunner().invoke(cli.cli, ["tables", "--fts4", db_path]) - assert "Gosh_fts" == result.output.strip() + assert '[{"table": "Gosh_fts"}]' == result.output.strip() def test_tables_fts5(db_path): Database(db_path)["Gosh"].enable_fts(["c2"], fts_version="FTS5") result = CliRunner().invoke(cli.cli, ["tables", "--fts5", db_path]) - assert "Gosh_fts" == result.output.strip() + assert '[{"table": "Gosh_fts"}]' == result.output.strip() + + +def test_tables_counts_and_columns(db_path): + db = Database(db_path) + with db.conn: + db["lots"].insert_all([{"id": i, "age": i + 1} for i in range(30)]) + result = CliRunner().invoke(cli.cli, ["tables", "--counts", "--columns", db_path]) + assert ( + '[{"table": "Gosh", "count": 0, "columns": ["c1", "c2", "c3"]},\n' + ' {"table": "Gosh2", "count": 0, "columns": ["c1", "c2", "c3"]},\n' + ' {"table": "lots", "count": 30, "columns": ["id", "age"]}]' + ) == result.output.strip() + + +def test_tables_counts_and_columns_csv(db_path): + db = Database(db_path) + with db.conn: + db["lots"].insert_all([{"id": i, "age": i + 1} for i in range(30)]) + result = CliRunner().invoke( + cli.cli, ["tables", "--counts", "--columns", "--csv", db_path] + ) + assert ( + "table,count,columns\n" + 'Gosh,0,"c1\n' + "c2\n" + 'c3"\n' + 'Gosh2,0,"c1\n' + "c2\n" + 'c3"\n' + 'lots,30,"id\n' + 'age"' + ) == result.output.strip() def test_enable_fts(db_path):