"""sources_meta + saved_chats.sources_version: the KB-generation stamp (phase 53) Revision ID: 0010 Revises: 0009 Create Date: 2026-08-30 Phase 53 (invalidate saved chats on sources sync, TODO-derived 2026-08-30): a saved answer can silently predate the current index, because neither sync path (the admin Sync button, ``scripts/import_docs.py``) records *when the KB last changed*. ONE migration carries the whole feature (A13 — one feature, one atomic, reversible schema change): * ``sources_meta`` — a **single-row** counter (``id INTEGER PK DEFAULT 1``, the ``kb_overview`` phase-31 precedent) holding the current **generation** of the knowledge base. The seed row (id 1, version 0) is inserted here — not lazily on first bump — so ``app.rag.sources_meta.current_sources_version`` is a plain PK read. ``version`` is bumped exactly once per KB-changing sync (task 02, change-gated on ``added + updated + pruned > 0``); a failed sync never bumps. * ``saved_chats.sources_version`` — ``INTEGER NOT NULL DEFAULT 0``: the generation the conversation was saved against, stamped by the chats API at save time (task 03). Pre-0010 rows come back stamped 0 (the pre-counter KB) and go stale on the first bump. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "0010" down_revision = "0009" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "sources_meta", sa.Column("id", sa.Integer(), primary_key=True, server_default=sa.text("1")), sa.Column("version", sa.Integer(), nullable=False, server_default=sa.text("0")), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), ) # Seed the single row (id 1, version 0): "the pre-counter KB". op.execute( "INSERT INTO sources_meta (id, version, updated_at) VALUES (1, 0, now())" ) op.add_column( "saved_chats", sa.Column( "sources_version", sa.Integer(), nullable=False, server_default=sa.text("0") ), ) def downgrade() -> None: # Safe order: drop the stamp column first, then the counter table. op.drop_column("saved_chats", "sources_version") op.drop_table("sources_meta")