From 7a52214624ae0e2c3fdf07215c1bcfc1393dbd93 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 7 Jul 2026 21:00:43 -0700 Subject: [PATCH] drop-index command and table.drop_index(index_name) Closes #626 --- docs/changelog.rst | 1 + docs/cli-reference.rst | 26 +++++++++++++++++++++++++- docs/cli.rst | 13 +++++++++++++ docs/python-api.rst | 8 ++++++++ sqlite_utils/cli.py | 28 ++++++++++++++++++++++++++++ sqlite_utils/db.py | 16 ++++++++++++++++ tests/test_cli.py | 18 ++++++++++++++++++ tests/test_create.py | 28 ++++++++++++++++++++++++++++ 8 files changed, 137 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e03f39..d2c07da 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,7 @@ Unreleased - ``sqlite-utils query`` can now read the SQL query from standard input by passing ``-`` in place of the query, for example ``echo "select * from dogs" | sqlite-utils query dogs.db -``. (:issue:`765`) - ``sqlite-utils insert`` and ``sqlite-utils upsert`` now accept a ``--code`` option for :ref:`providing a block of Python code ` (or a path to a ``.py`` file) that defines a ``rows()`` function or ``rows`` iterable of rows to insert, as an alternative to importing from a file. (:issue:`684`) - ``sqlite-utils insert`` and ``sqlite-utils upsert`` now accept ``--type column-name type`` to :ref:`override the type automatically chosen when the table is created `. This is useful for CSV or TSV columns such as ZIP codes that look like integers but should be stored as ``TEXT`` to preserve leading zeros. (:issue:`131`) +- New ``table.drop_index(name)`` method and ``sqlite-utils drop-index`` command for dropping an index by name. Both accept ``ignore=True``/``--ignore`` to ignore a missing index. (:issue:`626`) .. _v4_0: diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 39226ac..71e8377 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -19,7 +19,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command. go_first = [ "query", "memory", "insert", "upsert", "bulk", "search", "transform", "extract", "schema", "insert-files", "analyze-tables", "convert", "tables", "views", "rows", - "triggers", "indexes", "create-database", "create-table", "create-index", + "triggers", "indexes", "create-database", "create-table", "create-index", "drop-index", "migrate", "enable-fts", "populate-fts", "rebuild-fts", "disable-fts" ] refs = { @@ -46,6 +46,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command. "add-foreign-keys": "cli_add_foreign_keys", "index-foreign-keys": "cli_index_foreign_keys", "create-index": "cli_create_index", + "drop-index": "cli_drop_index", "enable-wal": "cli_wal", "enable-counts": "cli_enable_counts", "bulk": "cli_bulk", @@ -1006,6 +1007,29 @@ See :ref:`cli_create_index`. -h, --help Show this message and exit. +.. _cli_ref_drop_index: + +drop-index +========== + +See :ref:`cli_drop_index`. + +:: + + Usage: sqlite-utils drop-index [OPTIONS] PATH TABLE INDEX + + Drop an index by index name from the specified table + + Example: + + sqlite-utils drop-index chickens.db chickens idx_chickens_name + + Options: + --ignore Ignore if index does not exist + --load-extension TEXT Path to SQLite extension, with optional :entrypoint + -h, --help Show this message and exit. + + .. _cli_ref_migrate: migrate diff --git a/docs/cli.rst b/docs/cli.rst index 731e93b..c446a72 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -2615,6 +2615,19 @@ If your column names are already prefixed with a hyphen you'll need to manually Add the ``--analyze`` option to run ``ANALYZE`` against the index after it has been created. +.. _cli_drop_index: + +Dropping indexes +================ + +You can drop an index from an existing table using the ``drop-index`` command: + +.. code-block:: bash + + sqlite-utils drop-index mydb.db mytable idx_mytable_col1 + +Use ``--ignore`` to ignore the error if the index does not exist on that table. + .. _cli_fts: Configuring full-text search diff --git a/docs/python-api.rst b/docs/python-api.rst index f8e0787..fed617e 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -2877,6 +2877,14 @@ Use ``if_not_exists=True`` to do nothing if an index with that name already exis Pass ``analyze=True`` to run ``ANALYZE`` against the new index after creating it. +You can drop an index from a table using ``.drop_index(index_name)``: + +.. code-block:: python + + db.table("dogs").drop_index("idx_dogs_name") + +Use ``ignore=True`` to ignore the error if the index does not exist. + .. _python_api_analyze: Optimizing index usage with ANALYZE diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index cf39ff8..7fab72b 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -692,6 +692,34 @@ def create_index( ) +@cli.command(name="drop-index") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("table") +@click.argument("index") +@click.option("--ignore", help="Ignore if index does not exist", is_flag=True) +@load_extension_option +def drop_index(path, table, index, ignore, load_extension): + """ + Drop an index by index name from the specified table + + Example: + + \b + sqlite-utils drop-index chickens.db chickens idx_chickens_name + """ + db = sqlite_utils.Database(path) + _register_db_for_cleanup(db) + _load_extensions(db, load_extension) + try: + db.table(table).drop_index(index, ignore=ignore) + except OperationalError as ex: + raise click.ClickException(str(ex)) + + @cli.command(name="enable-fts") @click.argument( "path", diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 3033a36..62e3656 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -3080,6 +3080,22 @@ class Table(Queryable): self.db.analyze(created_index_name) return self + def drop_index(self, index_name: str, ignore: bool = False): + """ + Drop an index on this table. + + :param index_name: Name of the index to drop + :param ignore: Set to ``True`` to ignore the error if the index does not exist + """ + if index_name not in {index.name for index in self.indexes}: + if ignore: + return self + raise OperationalError( + "No index named {} on table {}".format(index_name, self.name) + ) + self.db.execute("DROP INDEX {}".format(quote_identifier(index_name))) + return self + def add_column( self, col_name: str, diff --git a/tests/test_cli.py b/tests/test_cli.py index bc5d492..a828c70 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -291,6 +291,24 @@ def test_create_index(db_path): ) +def test_drop_index(db_path): + db = Database(db_path) + db["Gosh"].create_index(["c1"]) + assert [index.name for index in db["Gosh"].indexes] == ["idx_Gosh_c1"] + result = CliRunner().invoke(cli.cli, ["drop-index", db_path, "Gosh", "idx_Gosh_c1"]) + assert result.exit_code == 0 + assert db["Gosh"].indexes == [] + + result = CliRunner().invoke(cli.cli, ["drop-index", db_path, "Gosh", "idx_Gosh_c1"]) + assert result.exit_code == 1 + assert "No index named idx_Gosh_c1" in result.output + + result = CliRunner().invoke( + cli.cli, ["drop-index", db_path, "Gosh", "idx_Gosh_c1", "--ignore"] + ) + assert result.exit_code == 0 + + def test_create_index_analyze(db_path): db = Database(db_path) assert "sqlite_stat1" not in db.table_names() diff --git a/tests/test_create.py b/tests/test_create.py index 7fab5e6..d281eb4 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -809,6 +809,34 @@ def test_create_index_if_not_exists(fresh_db): dogs.create_index(["name"], if_not_exists=True) +def test_drop_index(fresh_db): + dogs = fresh_db["dogs"] + dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is_good_dog": True}) + dogs.create_index(["name"]) + assert [index.name for index in dogs.indexes] == ["idx_dogs_name"] + dogs.drop_index("idx_dogs_name") + assert dogs.indexes == [] + + +def test_drop_index_ignore(fresh_db): + dogs = fresh_db["dogs"] + dogs.insert({"name": "Cleo"}) + with pytest.raises(OperationalError, match="No index named idx_dogs_name"): + dogs.drop_index("idx_dogs_name") + dogs.drop_index("idx_dogs_name", ignore=True) + + +def test_drop_index_wrong_table(fresh_db): + dogs = fresh_db["dogs"] + cats = fresh_db["cats"] + dogs.insert({"name": "Cleo"}) + cats.insert({"name": "Misty"}) + dogs.create_index(["name"]) + with pytest.raises(OperationalError, match="No index named idx_dogs_name"): + cats.drop_index("idx_dogs_name") + assert [index.name for index in dogs.indexes] == ["idx_dogs_name"] + + def test_create_index_desc(fresh_db): dogs = fresh_db["dogs"] dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is good dog": True})