diff --git a/docs/cli.rst b/docs/cli.rst index 8590456..35e34b2 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -411,6 +411,10 @@ A better solution here is to use database triggers. You can set up database trig $ sqlite-utils enable-fts mydb.db documents title summary --create-triggers +To remove the FTS tables and triggers you created, use ``disable-fts``:: + + $ sqlite-utils disable-fts mydb.db documents + Vacuum ====== diff --git a/docs/python-api.rst b/docs/python-api.rst index 14de937..878752a 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -1052,6 +1052,12 @@ A better solution is to use database triggers. You can set up database triggers dogs.enable_fts(["name", "twitter"], fts_version="FTS4") +To remove the FTS tables and triggers you created, use the ``disable_fts()`` table method: + +.. code-block:: python + + dogs.disable_fts() + Optimizing a full-text search table =================================== diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index ba55e61..54667e5 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -300,6 +300,19 @@ def populate_fts(path, table, column): db[table].populate_fts(column) +@cli.command(name="disable-fts") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False), + required=True, +) +@click.argument("table") +def disable_fts(path, table): + "Disable FTS for specific table" + db = sqlite_utils.Database(path) + db[table].disable_fts() + + def insert_upsert_options(fn): for decorator in reversed( ( diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 2078525..40b734c 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -794,6 +794,25 @@ class Table(Queryable): self.db.conn.executescript(sql) return self + def disable_fts(self): + fts_table = self.detect_fts() + if fts_table: + self.db[fts_table].drop() + # Now delete the triggers that related to that table + sql = """ + SELECT name FROM sqlite_master + WHERE type = 'trigger' + AND sql LIKE '% INSERT INTO [{}]%' + """.format( + fts_table + ) + trigger_names = [] + for row in self.db.conn.execute(sql).fetchall(): + trigger_names.append(row[0]) + with self.db.conn: + for trigger_name in trigger_names: + self.db.conn.execute("DROP TRIGGER IF EXISTS [{}]".format(trigger_name)) + def detect_fts(self): "Detect if table has a corresponding FTS virtual table and return it" sql = """ diff --git a/tests/test_cli.py b/tests/test_cli.py index 1f24090..52c50da 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -393,6 +393,24 @@ def test_populate_fts(db_path): assert [("martha",)] == search("martha") +def test_disable_fts(db_path): + db = Database(db_path) + assert {"Gosh", "Gosh2"} == set(db.table_names()) + db["Gosh"].enable_fts(["c1"], create_triggers=True) + assert { + "Gosh_fts", + "Gosh_fts_idx", + "Gosh_fts_data", + "Gosh2", + "Gosh_fts_config", + "Gosh", + "Gosh_fts_docsize", + } == set(db.table_names()) + exit_code = CliRunner().invoke(cli.cli, ["disable-fts", db_path, "Gosh"]).exit_code + assert 0 == exit_code + assert {"Gosh", "Gosh2"} == set(db.table_names()) + + def test_vacuum(db_path): result = CliRunner().invoke(cli.cli, ["vacuum", db_path]) assert 0 == result.exit_code diff --git a/tests/test_enable_fts.py b/tests/test_fts.py similarity index 77% rename from tests/test_enable_fts.py rename to tests/test_fts.py index 7f58916..134e0e7 100644 --- a/tests/test_enable_fts.py +++ b/tests/test_fts.py @@ -1,3 +1,5 @@ +import pytest + search_records = [ {"text": "tanuki are tricksters", "country": "Japan", "not_searchable": "foo"}, {"text": "racoons are trash pandas", "country": "USA", "not_searchable": "bar"}, @@ -92,3 +94,37 @@ def test_enable_fts_w_triggers(fresh_db): # Triggers will auto-populate FTS virtual table, not need to call populate_fts() assert [("racoons are trash pandas", "USA", "bar")] == table.search("usa") assert [] == table.search("bar") + + +@pytest.mark.parametrize("create_triggers", [True, False]) +def test_disable_fts(fresh_db, create_triggers): + table = fresh_db["searchable"] + table.insert(search_records[0]) + table.enable_fts(["text", "country"], create_triggers=create_triggers) + assert { + "searchable", + "searchable_fts", + "searchable_fts_data", + "searchable_fts_idx", + "searchable_fts_docsize", + "searchable_fts_config", + } == set(fresh_db.table_names()) + if create_triggers: + expected_triggers = {"searchable_ai", "searchable_ad", "searchable_au"} + else: + expected_triggers = set() + assert expected_triggers == set( + r[0] + for r in fresh_db.conn.execute( + "select name from sqlite_master where type = 'trigger'" + ).fetchall() + ) + # Now run .disable_fts() and confirm it worked + table.disable_fts() + assert ( + 0 + == fresh_db.conn.execute( + "select count(*) from sqlite_master where type = 'trigger'" + ).fetchone()[0] + ) + assert ["searchable"] == fresh_db.table_names()