Option to add triggers when enabling FTS (#57)

--create-triggers CLI option and create_triggers=True in the Python library

* Add an option to create triggers for fts table.
* Add cli option for the create-update-trigger.
* Add tests for the create-update-trigger option.
* Change FTS table escaping to square brackets.
This commit is contained in:
Amjith Ramanujam 2019-09-02 16:42:28 -07:00 committed by Simon Willison
commit 405e092d59
4 changed files with 128 additions and 17 deletions

View file

@ -646,7 +646,7 @@ class Table(Queryable):
return self
def drop(self):
self.db.conn.execute("DROP TABLE {}".format(self.name))
self.db.conn.execute("DROP TABLE [{}]".format(self.name))
def guess_foreign_table(self, column):
column = column.lower()
@ -710,12 +710,12 @@ class Table(Queryable):
)
self.db.add_foreign_keys([(self.name, column, other_table, other_column)])
def enable_fts(self, columns, fts_version="FTS5"):
"Enables FTS on the specified columns"
def enable_fts(self, columns, fts_version="FTS5", create_triggers=False):
"Enables FTS on the specified columns."
sql = """
CREATE VIRTUAL TABLE "{table}_fts" USING {fts_version} (
CREATE VIRTUAL TABLE [{table}_fts] USING {fts_version} (
{columns},
content="{table}"
content=[{table}]
);
""".format(
table=self.name,
@ -724,12 +724,34 @@ class Table(Queryable):
)
self.db.conn.executescript(sql)
self.populate_fts(columns)
if create_triggers:
old_cols = ", ".join("old.[{}]".format(c) for c in columns)
new_cols = ", ".join("new.[{}]".format(c) for c in columns)
triggers = """
CREATE TRIGGER [{table}_ai] AFTER INSERT ON [{table}] BEGIN
INSERT INTO [{table}_fts] (rowid, {columns}) VALUES (new.rowid, {new_cols});
END;
CREATE TRIGGER [{table}_ad] AFTER DELETE ON [{table}] BEGIN
INSERT INTO [{table}_fts] ([{table}_fts], rowid, {columns}) VALUES('delete', old.rowid, {old_cols});
END;
CREATE TRIGGER [{table}_au] AFTER UPDATE ON [{table}] BEGIN
INSERT INTO [{table}_fts] ([{table}_fts], rowid, {columns}) VALUES('delete', old.rowid, {old_cols});
INSERT INTO [{table}_fts] (rowid, {columns}) VALUES (new.rowid, {new_cols});
END;
""".format(
table=self.name,
columns=", ".join("[{}]".format(c) for c in columns),
old_cols=old_cols,
new_cols=new_cols,
)
self.db.conn.executescript(triggers)
return self
def populate_fts(self, columns):
sql = """
INSERT INTO "{table}_fts" (rowid, {columns})
SELECT rowid, {columns} FROM {table};
INSERT INTO [{table}_fts] (rowid, {columns})
SELECT rowid, {columns} FROM [{table}];
""".format(
table=self.name, columns=", ".join(columns)
)
@ -738,21 +760,20 @@ class Table(Queryable):
def detect_fts(self):
"Detect if table has a corresponding FTS virtual table and return it"
rows = self.db.conn.execute(
"""
sql = """
SELECT name FROM sqlite_master
WHERE rootpage = 0
AND (
sql LIKE '%VIRTUAL TABLE%USING FTS%content="{table}"%'
sql LIKE '%VIRTUAL TABLE%USING FTS%content=%{table}%'
OR (
tbl_name = "{table}"
AND sql LIKE '%VIRTUAL TABLE%USING FTS%'
)
)
""".format(
table=self.name
)
).fetchall()
table=self.name
)
rows = self.db.conn.execute(sql).fetchall()
if len(rows) == 0:
return None
else:
@ -796,7 +817,7 @@ class Table(Queryable):
def search(self, q):
sql = """
select * from {table} where rowid in (
select * from "{table}" where rowid in (
select rowid from [{table}_fts]
where [{table}_fts] match :search
)
@ -1144,7 +1165,7 @@ class View(Queryable):
)
def drop(self):
self.db.conn.execute("DROP VIEW {}".format(self.name))
self.db.conn.execute("DROP VIEW [{}]".format(self.name))
def chunks(sequence, size):