diff --git a/docs/cli.rst b/docs/cli.rst index 445cef2..a90f99c 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -261,7 +261,7 @@ The ``--nl``, ``--csv``, ``--tsv`` and ``--table`` options are all available. Listing views ============= -The `views` command shows any views defined in the database:: +The ``views`` command shows any views defined in the database:: $ sqlite-utils views sf-trees.db --table --counts --columns --schema view count columns schema @@ -279,6 +279,34 @@ It takes the same options as the ``tables`` command: * ``--tsv`` * ``--table`` +.. _cli_triggers: + +Listing triggers +================ + +The ``triggers`` command shows any triggers configured for the database:: + + % sqlite-utils triggers global-power-plants.db --table + name table sql + --------------- --------- ----------------------------------------------------------------- + plants_insert plants CREATE TRIGGER [plants_insert] AFTER INSERT ON [plants] + BEGIN + INSERT OR REPLACE INTO [_counts] + VALUES ( + 'plants', + COALESCE( + (SELECT count FROM [_counts] WHERE [table] = 'plants'), + 0 + ) + 1 + ); + END + +It defaults to showing triggers for all tables. To see triggers for one or more specific tables pass their names as arguments:: + + % sqlite-utils triggers global-power-plants.db plants + +The command takes the same format options as the ``tables`` and ``views`` commands. + .. _cli_analyze_tables: Analyzing tables diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 6312327..b56e557 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1100,6 +1100,53 @@ def rows( ) +@cli.command() +@click.argument( + "path", + type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("tables", nargs=-1) +@output_options +@load_extension_option +@click.pass_context +def triggers( + ctx, + path, + tables, + nl, + arrays, + csv, + tsv, + no_headers, + table, + fmt, + json_cols, + load_extension, +): + "Show triggers configured in this database" + sql = "select name, tbl_name as [table], sql from sqlite_master where type = 'trigger'" + if tables: + quote = sqlite_utils.Database(memory=True).escape + sql += " and [table] in ({})".format( + ", ".join(quote(table) for table in tables) + ) + ctx.invoke( + query, + path=path, + sql=sql, + nl=nl, + arrays=arrays, + csv=csv, + tsv=tsv, + no_headers=no_headers, + table=table, + fmt=fmt, + json_cols=json_cols, + load_extension=load_extension, + ) + + @cli.command() @click.argument( "path", diff --git a/tests/test_cli.py b/tests/test_cli.py index c335c46..59770c3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,6 +5,7 @@ import json import os import pytest from sqlite_utils.utils import sqlite3, find_spatialite +import textwrap from .utils import collapse_whitespace @@ -1736,3 +1737,40 @@ def test_search(tmpdir, fts, extra_arg, expected): ) assert result.exit_code == 0 assert result.output == expected + + +_TRIGGERS_EXPECTED = '[{"name": "blah", "table": "articles", "sql": "CREATE TRIGGER blah AFTER INSERT ON articles\\nBEGIN\\n UPDATE counter SET count = count + 1;\\nEND"}]\n' + + +@pytest.mark.parametrize( + "extra_args,expected", + [([], _TRIGGERS_EXPECTED), (["articles"], _TRIGGERS_EXPECTED), (["counter"], "")], +) +def test_triggers(tmpdir, extra_args, expected): + db_path = str(tmpdir / "test.db") + db = Database(db_path) + db["articles"].insert( + {"id": 1, "title": "Title the first"}, + pk="id", + ) + db["counter"].insert({"count": 1}) + db.conn.execute( + textwrap.dedent( + """ + CREATE TRIGGER blah AFTER INSERT ON articles + BEGIN + UPDATE counter SET count = count + 1; + END + """ + ) + ) + args = ["triggers", db_path] + if extra_args: + args.extend(extra_args) + result = CliRunner().invoke( + cli.cli, + args, + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert result.output == expected