feat(db): idempotent schema + 'codex migrate' command (audit M-1, T-2)

M-1: schema.sql could not be re-applied (leading CREATE TABLE/INDEX lacked
IF NOT EXISTS), apply_schema was never called, and the live migration just
failed on a missing chunks.section column. Fixes:

- schema.sql: all CREATE TABLE/INDEX now use IF NOT EXISTS — the whole file is
  re-applyable as a no-op.
- codex migrate: new CLI command that applies schema.sql. Connects via
  MIGRATION_DATABASE_URL (falls back to DATABASE_URL) and catches
  InsufficientPrivilege with guidance — because the app role is DML-only and
  core tables are owned by 'postgres' (the privilege dimension found during the
  live migration incident).
- config: migration_database_url (optional privileged connection).
- db: apply_schema docstring corrected (idempotency now true) + privilege note.
- T-2: static test that schema.sql is fully idempotent; migrate privilege-error
  test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-15 15:53:25 +02:00
parent 9f172f5c68
commit cdb7d254df
5 changed files with 98 additions and 14 deletions

View File

@@ -13,7 +13,7 @@ CREATE EXTENSION IF NOT EXISTS vector;
-- ---------------------------------------------------------------------
-- Layer 1: Papers + full-text chunks
-- ---------------------------------------------------------------------
CREATE TABLE papers (
CREATE TABLE IF NOT EXISTS papers (
id TEXT PRIMARY KEY, -- canonical ID: arXiv ID or DOI
openalex_id TEXT UNIQUE, -- e.g. W2741809807
bibkey TEXT UNIQUE, -- BibTeX key -> .bib + Doxygen @cite
@@ -27,10 +27,10 @@ CREATE TABLE papers (
);
-- HNSW index for fast approximate nearest-neighbour search on abstracts.
CREATE INDEX papers_abstract_emb_idx
CREATE INDEX IF NOT EXISTS papers_abstract_emb_idx
ON papers USING hnsw (abstract_emb vector_cosine_ops);
CREATE TABLE chunks (
CREATE TABLE IF NOT EXISTS chunks (
id BIGSERIAL PRIMARY KEY,
paper_id TEXT REFERENCES papers(id) ON DELETE CASCADE,
ord INT NOT NULL, -- position within the paper
@@ -38,13 +38,13 @@ CREATE TABLE chunks (
embedding vector(1024)
);
CREATE INDEX chunks_emb_idx
CREATE INDEX IF NOT EXISTS chunks_emb_idx
ON chunks USING hnsw (embedding vector_cosine_ops);
CREATE INDEX chunks_paper_idx ON chunks (paper_id);
CREATE INDEX IF NOT EXISTS chunks_paper_idx ON chunks (paper_id);
-- Sparse / keyword hits for exact mathematical terminology (hybrid search).
-- Dense (above) + full-text (below) combined = robust against math terms.
CREATE INDEX chunks_fts_idx
CREATE INDEX IF NOT EXISTS chunks_fts_idx
ON chunks USING gin (to_tsvector('english', content));
-- ---------------------------------------------------------------------
@@ -53,19 +53,19 @@ CREATE INDEX chunks_fts_idx
-- edges to not-yet-ingested papers are intentionally preserved —
-- those are your discovery leads.
-- ---------------------------------------------------------------------
CREATE TABLE citations (
CREATE TABLE IF NOT EXISTS citations (
citing_id TEXT REFERENCES papers(id) ON DELETE CASCADE,
cited_id TEXT NOT NULL, -- arXiv/DOI/OpenAlex ID of the target
context TEXT, -- optional: citation context (S2)
PRIMARY KEY (citing_id, cited_id)
);
CREATE INDEX citations_cited_idx ON citations (cited_id);
CREATE INDEX IF NOT EXISTS citations_cited_idx ON citations (cited_id);
-- ---------------------------------------------------------------------
-- Layer 3: Provenance (the "cleanly couple" goal)
-- ---------------------------------------------------------------------
CREATE TABLE code_links (
CREATE TABLE IF NOT EXISTS code_links (
id BIGSERIAL PRIMARY KEY,
symbol TEXT NOT NULL, -- e.g. 'dec::hodge_star' or 'file.cpp:120'
paper_id TEXT REFERENCES papers(id) ON DELETE SET NULL,
@@ -74,8 +74,8 @@ CREATE TABLE code_links (
added_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX code_links_symbol_idx ON code_links (symbol);
CREATE INDEX code_links_paper_idx ON code_links (paper_id);
CREATE INDEX IF NOT EXISTS code_links_symbol_idx ON code_links (symbol);
CREATE INDEX IF NOT EXISTS code_links_paper_idx ON code_links (paper_id);
-- ---------------------------------------------------------------------
-- Example query: discovery leads