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:
@@ -33,3 +33,40 @@ def test_apply_schema_executes_sql() -> None:
|
||||
|
||||
mock_conn.execute.assert_called_once()
|
||||
mock_conn.commit.assert_called_once()
|
||||
|
||||
|
||||
def test_schema_sql_is_idempotent() -> None:
|
||||
"""Every CREATE TABLE/INDEX in infra/schema.sql uses IF NOT EXISTS (audit M-1, T-2).
|
||||
|
||||
Re-applying schema.sql must be a safe no-op so the migration can run
|
||||
repeatedly against an existing database.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
schema = (Path(__file__).resolve().parents[2] / "infra" / "schema.sql").read_text()
|
||||
# Drop line comments so commented-out example queries don't count.
|
||||
code = "\n".join(line.split("--", 1)[0] for line in schema.splitlines())
|
||||
non_idempotent = re.findall(
|
||||
r"CREATE\s+(?:UNIQUE\s+)?(?:TABLE|INDEX)\s+(?!IF\s+NOT\s+EXISTS)(\w+)",
|
||||
code,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
assert not non_idempotent, f"schema.sql has non-idempotent CREATE statements: {non_idempotent}"
|
||||
|
||||
|
||||
def test_migrate_command_reports_privilege_error() -> None:
|
||||
"""`codex migrate` surfaces InsufficientPrivilege with guidance and exits 1 (M-1)."""
|
||||
import psycopg
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from codex.cli import app
|
||||
|
||||
def boom(*args: object, **kwargs: object) -> object:
|
||||
raise psycopg.errors.InsufficientPrivilege("must be owner of table chunks")
|
||||
|
||||
with patch("psycopg.connect", boom):
|
||||
result = CliRunner().invoke(app, ["migrate"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "MIGRATION_DATABASE_URL" in result.output
|
||||
|
||||
Reference in New Issue
Block a user