Skip to content

[Audit] Add schema.sql<->Alembic parity gate; close gin-index/embedding/nullability drift #1308

Description

@stranske

Why

Prod Postgres bootstraps from schema.sql (compose initdb mount); CI/tests bootstrap from alembic upgrade head on SQLite. There is no gate comparing the two, so CI validates a schema that differs from production. Verified divergences at e5764a5:

  • idx_news_items_topics_gin exists in schema.sql:160 but is absent from every Alembic migration (0 hits) — topic-array query support present in compose Postgres, missing on Alembic-bootstrapped Postgres.
  • documents.embedding is added Postgres-only in alembic/versions/001_canonical_schema.py:170 (ALTER TABLE documents ADD COLUMN ... vector(384)); the SQLite Alembic path lacks it and embeddings.py patches it at runtime (see related [Audit] Remove Postgres DDL from store_document runtime path #1281) — so Alembic-SQLite schema ≠ schema.sql.
  • chat_feedback.created_at: schema.sql declares it NOT NULL DEFAULT now(), but alembic/versions/009_chat_feedback.py:30 sets nullable=True.

Scope

Add a parity gate that fails CI when schema.sql and the Alembic-produced schema diverge on Postgres, and close the verified gaps above.

Non-Goals

  • Do NOT remove dialect-legitimate differences (e.g. GIN/vector are Postgres-only; SQLite equivalents may be skipped — but the gate must record them as intentional).
  • Do NOT change the existing scripts/check_dialect_portability.py token gate; this is a complementary structural parity check.

Tasks

  • Add a parity check (script or test) that bootstraps an ephemeral Postgres two ways — schema.sql vs alembic upgrade head — and diffs tables/columns/indexes/nullability, failing on undocumented divergence.
  • Add idx_news_items_topics_gin via a dialect-guarded Alembic migration (Postgres postgresql_using='gin'; skip/equivalent on SQLite).
  • Set created_at nullable=False in alembic/versions/009_chat_feedback.py:30 to match schema.sql.

Acceptance Criteria

  • New parity gate passes and fails if a column/index/nullability is present in one bootstrap path but not the other (without an allowlist entry).
  • SELECT 1 FROM pg_indexes WHERE indexname='idx_news_items_topics_gin' returns a row on both bootstrap paths.
  • Deliberate-break demonstration: drop the gin index from schema.sql (or revert the migration) → parity gate fails; restore → passes.

Implementation Notes

Audit baseline: main @ e5764a5 on 2026-06-28. All three divergences source-verified. This is the LMS-class "SQLite-CI vs Postgres-prod" risk: tests pass on a schema prod doesn't run.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agents:formattedIssue has been formatted to AGENT_ISSUE_TEMPLATEbugSomething isn't workingpriority:highHigh-priority weekly repo-review workstatus: readyReady for Orchestrator opener pickuptestingTests or coverage work

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions