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:
31
codex/cli.py
31
codex/cli.py
@@ -29,6 +29,37 @@ app.add_typer(graph_app, name="graph")
|
||||
app.add_typer(search_app, name="search")
|
||||
|
||||
|
||||
@app.command()
|
||||
def migrate() -> None:
|
||||
"""Apply infra/schema.sql to bring the database schema up to date (idempotent).
|
||||
|
||||
Must connect as a role that can run DDL (owns / can CREATE + ALTER the tables).
|
||||
The application's DATABASE_URL is usually a least-privilege DML role and will
|
||||
fail with InsufficientPrivilege; set MIGRATION_DATABASE_URL to a privileged
|
||||
(owner/superuser) connection (audit M-1).
|
||||
"""
|
||||
import psycopg
|
||||
import psycopg.rows
|
||||
|
||||
from codex.config import get_settings
|
||||
from codex.db import apply_schema
|
||||
|
||||
settings = get_settings()
|
||||
url = settings.migration_database_url or settings.database_url
|
||||
try:
|
||||
with psycopg.connect(url, row_factory=psycopg.rows.dict_row) as conn:
|
||||
apply_schema(conn)
|
||||
except psycopg.errors.InsufficientPrivilege as exc:
|
||||
typer.echo(
|
||||
"ERROR: schema migration needs a role that owns the tables "
|
||||
"(CREATE/ALTER). Set MIGRATION_DATABASE_URL to a privileged connection "
|
||||
f"and retry. ({exc})",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1) from exc
|
||||
typer.echo("Schema applied — database is up to date.")
|
||||
|
||||
|
||||
@app.command()
|
||||
def ingest(
|
||||
paper_id: str = typer.Argument(..., help="arXiv ID, DOI, or OpenAlex W-ID"),
|
||||
|
||||
Reference in New Issue
Block a user