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>
63 lines
2.0 KiB
Python
63 lines
2.0 KiB
Python
"""Database connection helpers.
|
|
|
|
Provides:
|
|
- :func:`get_conn` — a context manager that yields a psycopg connection.
|
|
- :func:`apply_schema` — idempotently applies ``infra/schema.sql``.
|
|
|
|
The connection pool (psycopg_pool) is intentionally deferred to F-05
|
|
(ingest); here we expose a simple per-call ``psycopg.connect`` wrapper
|
|
that is sufficient for schema migration and unit-testing without requiring
|
|
an additional ``psycopg[pool]`` dependency at scaffold stage.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Generator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
import psycopg
|
|
import psycopg.rows
|
|
|
|
from codex.config import get_settings
|
|
|
|
|
|
@contextmanager
|
|
def get_conn() -> Generator[psycopg.Connection[psycopg.rows.DictRow], None, None]:
|
|
"""Yield a psycopg connection with dict-row factory.
|
|
|
|
The connection is opened from :func:`codex.config.get_settings`
|
|
``database_url`` and closed automatically when the context exits.
|
|
|
|
Usage::
|
|
|
|
with get_conn() as conn:
|
|
conn.execute("SELECT 1")
|
|
"""
|
|
settings = get_settings()
|
|
with psycopg.connect(
|
|
settings.database_url,
|
|
row_factory=psycopg.rows.dict_row,
|
|
) as conn:
|
|
yield conn
|
|
|
|
|
|
def apply_schema(conn: psycopg.Connection[psycopg.rows.DictRow]) -> None:
|
|
"""Idempotently apply ``infra/schema.sql`` to the connected database.
|
|
|
|
Safe to call repeatedly — every statement is ``CREATE … IF NOT EXISTS`` or
|
|
``ADD COLUMN IF NOT EXISTS``, so re-application is a no-op.
|
|
|
|
Requires a connection whose role can run DDL (owns / can CREATE + ALTER the
|
|
tables). The least-privilege application role is typically DML-only and will
|
|
raise ``InsufficientPrivilege``; use a privileged connection — see
|
|
``codex migrate`` and ``MIGRATION_DATABASE_URL`` (audit M-1).
|
|
|
|
Args:
|
|
conn: An open psycopg connection with DDL privileges.
|
|
"""
|
|
schema_path = Path(__file__).parent.parent / "infra" / "schema.sql"
|
|
sql = schema_path.read_text(encoding="utf-8")
|
|
conn.execute(sql)
|
|
conn.commit()
|