Skip to main content

## MIGRATION_021

# Constant MIGRATION_021

Source

    const MIGRATION_021: &str = r#"
    INSERT INTO items_fts(items_fts) VALUES('rebuild');
    "#;

Expand description

Discard and rebuild the FTS index from the `items` table.

Until DR-110, `save_to_cache` used `INSERT OR REPLACE INTO items`. REPLACE deletes the conflicting row and inserts a new one, but SQLite only fires `AFTER DELETE` triggers on that implicit delete when `recursive_triggers` is enabled — it is not (storage/mod.rs sets only `foreign_keys` and `journal_mode`), so `items_ad` never ran and the old index row was orphaned. Worse, `items.id` is a `TEXT PRIMARY KEY`, so the replacement row also took a fresh rowid and `items_ai` appended a second entry. Every catalog pass therefore left another duplicate behind, and existing installs carry one stale entry per item per sync since the database was created.

This was invisible in results — the `JOIN items_fts fts ON fts.rowid = i.rowid` drops rowids that no longer exist — but it degrades `MATCH` permanently, and it becomes a correctness problem the moment rowids are freed and reused: a new item landing on a freed rowid inherits the orphan’s index entry and matches queries for the deleted item’s title. The DR-110 deletion sweep frees rowids, so this rebuild must run before it.

`'rebuild'` is the FTS5 command for exactly this: it truncates the index and repopulates it from the external content table.

TRACES: UR-065 | DR-110