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>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
"""Smoke tests for codex.db — get_conn and apply_schema."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def test_get_conn_context_manager() -> None:
|
|
"""get_conn() yields a context manager that calls psycopg.connect and cleans up."""
|
|
from codex.db import get_conn
|
|
|
|
mock_conn = MagicMock()
|
|
mock_connect = MagicMock()
|
|
# psycopg.connect is itself used as a context manager inside get_conn
|
|
mock_connect.return_value.__enter__ = MagicMock(return_value=mock_conn)
|
|
mock_connect.return_value.__exit__ = MagicMock(return_value=False)
|
|
|
|
with patch("codex.db.psycopg.connect", mock_connect), get_conn() as conn:
|
|
assert conn is mock_conn
|
|
|
|
mock_connect.assert_called_once()
|
|
|
|
|
|
def test_apply_schema_executes_sql() -> None:
|
|
"""apply_schema(conn) calls conn.execute and conn.commit."""
|
|
from codex.db import apply_schema
|
|
|
|
mock_conn = MagicMock()
|
|
|
|
# Provide a fake schema.sql so Path.read_text succeeds
|
|
with patch("codex.db.Path.read_text", return_value="CREATE TABLE IF NOT EXISTS test ();"):
|
|
apply_schema(mock_conn)
|
|
|
|
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
|