Vellum/migrations/0003_search.sql
mithral a2c2f7a7c1 Phase 2b: FTS5 search
- migration 0003: an FTS5 index over media_items as an external-content table,
  so the index stores no copy of the text. Triggers keep it in sync for every
  write path, including the scanner's bulk inserts.
- migrate: splitStatements now tracks BEGIN...END depth, so CREATE TRIGGER
  bodies survive as one statement. Without this, the trigger's internal
  semicolons would tear it apart.
- search: every whitespace term is quoted before it reaches FTS5. Unquoted
  input like a bare quote, a hyphen, or the word OR is valid FTS5 syntax and
  would either error or silently change the query's meaning. Tested against a
  list of hostile inputs, including a DROP TABLE attempt.
- scoped through the same VisibleLibraryIDs chokepoint; an empty library set
  returns nothing, never everything.
- web: search box in the topbar and a results view.

Verified live: finds items across libraries, hostile input returns 200 with no
results, index stays in sync on insert/update/delete.
2026-10-09 21:10:44 -04:00

32 lines
1.2 KiB
SQL

-- 0003_search.sql — full-text search over items.
--
-- FTS5 with an external content table: the index stores no copy of the text,
-- it points back at media_items, so the database does not double in size.
-- Triggers keep the index in sync for every write path, including the scanner's
-- bulk inserts and any future module's.
CREATE VIRTUAL TABLE media_search USING fts5(
title,
path,
content='media_items',
content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
-- Populate the index from any rows that already exist.
INSERT INTO media_search(media_search) VALUES ('rebuild');
CREATE TRIGGER media_items_ai AFTER INSERT ON media_items BEGIN
INSERT INTO media_search(rowid, title, path) VALUES (new.id, new.title, new.path);
END;
CREATE TRIGGER media_items_ad AFTER DELETE ON media_items BEGIN
INSERT INTO media_search(media_search, rowid, title, path)
VALUES ('delete', old.id, old.title, old.path);
END;
CREATE TRIGGER media_items_au AFTER UPDATE ON media_items BEGIN
INSERT INTO media_search(media_search, rowid, title, path)
VALUES ('delete', old.id, old.title, old.path);
INSERT INTO media_search(rowid, title, path) VALUES (new.id, new.title, new.path);
END;