"""trigger_leaves_the_stored_title — a snippet's and lesson's title is its name (milestone 427) Revision ID: 0108 Revises: 0107 Create Date: 2026-09-23 Snippets and lessons stored their title as `subject — trigger`, because the join is what makes these kinds rank on the situation they apply to (#2485) and the stored title WAS the embedded one. Every surface that shows a title then showed the trigger too: menu lines, lists and search rows ran to 1,500–3,000 characters, and an injected repeat spent all of it again. The trigger's home is `data` (decision #4157), and since this milestone the join happens at embed time (`embeddings.document_title`). This revision rewrites the rows already stored. EXACT-SUFFIX, READ FROM THE ROW'S OWN MIRROR. A title is only rewritten when it ends with `' — ' || `, so a subject that legitimately contains an em dash is never cut, and a row whose title and mirror disagree — hand-edited through the generic note door — is left alone. Such a row still embeds correctly (`document_title` is idempotent) and merely shows its old title; that is the right way round for a data migration to fail. NO RE-EMBED, AND `updated_at` IS NOT TOUCHED. The embedded text is identical before and after, so the vectors are already right. The startup backfill re-embeds any row whose `updated_at` is newer than its vectors, and a raw UPDATE that leaves `updated_at` alone is what keeps this from queueing the whole snippet corpus for work that would produce the same numbers. There is no database trigger on `updated_at`; it is set by the ORM only. """ from alembic import op revision = "0108" down_revision = "0107" branch_labels = None depends_on = None # kind → the `data` key its trigger is mirrored under (embeddings._TRIGGER_DATA_KEYS). _KINDS = (("snippet", "when_to_use"), ("lesson", "when_to_apply")) _SEP = " — " def upgrade() -> None: for note_type, key in _KINDS: op.execute(f""" UPDATE notes SET title = btrim(left(title, length(title) - length('{_SEP}' || (data->>'{key}')))) WHERE note_type = '{note_type}' AND coalesce(btrim(data->>'{key}'), '') <> '' AND right(title, length('{_SEP}' || (data->>'{key}'))) = '{_SEP}' || (data->>'{key}') """) def downgrade() -> None: # Recompose, on the same condition inverted: only where the trigger is not # already on the end, so a downgrade run twice cannot double it. for note_type, key in _KINDS: op.execute(f""" UPDATE notes SET title = title || '{_SEP}' || (data->>'{key}') WHERE note_type = '{note_type}' AND coalesce(btrim(data->>'{key}'), '') <> '' AND right(title, length('{_SEP}' || (data->>'{key}'))) <> '{_SEP}' || (data->>'{key}') """)