"""add notes.data JSONB — queryable structured fields for typed records Revision ID: 0070 Revises: 0069 Create Date: 2026-07-26 Snippets (note_type='snippet') carry structured fields — name, language, signature, and a list of canonical locations (repo · path · symbol). Those were stored as a markdown body-convention, which reads well and feeds the embedding but cannot be QUERIED: answering "which snippets live in this file?" meant scanning every snippet and regexing its body. This adds a general `data` JSONB column plus a GIN index, so those fields become indexable. The body stays exactly as it was — it is still the human-readable form and still what gets embedded. `data` is a queryable mirror of the same facts, not a replacement, and the code itself is deliberately NOT copied into it (the body already holds it; duplicating a blob to index fields around it would be waste). Relationship to 0069: that migration DROPPED `notes.metadata`, a JSONB column which only ever held person/place/list entity fields, when those surfaces were removed. This is not a revival of it — different name, different purpose, and nothing reads the old shape. The column is named `data` rather than `metadata` because `metadata` collides with SQLAlchemy's declarative `Base.metadata`, which is why the old model had to map an awkward `entity_metadata` attribute onto it. Nullable with no backfill, deliberately: rows written before this migration keep working because the service falls back to parsing the body when `data` is absent. That means no migration deadline and no risk of a backfill mangling a hand-edited body. Downgrade drops the index and the column. Any structured fields it held remain recoverable from the body convention, which is the same source they mirror. """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB revision = "0070" down_revision = "0069" branch_labels = None depends_on = None def upgrade() -> None: op.add_column("notes", sa.Column("data", JSONB, nullable=True)) # GIN supports containment (`data @> '{"locations":[{"repo":"x"}]}'`), which # covers exact repo/path/symbol and language lookups. Path PREFIX matching # ("everything under frontend/src/") is not an index-served operation here # and still filters after the fact — acceptable while snippet counts are # small, and a generated column is the escape hatch if that changes. op.create_index( "ix_notes_data_gin", "notes", ["data"], postgresql_using="gin", ) def downgrade() -> None: op.drop_index("ix_notes_data_gin", table_name="notes") op.drop_column("notes", "data")