"""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")
