"""Startup migrations leave no ERRORs in the PostgreSQL server log. Nearly every ADD COLUMN in run_migrations() is a re-run on every start, because create_all() has already made the column. _safe_execute swallowed the duplicate, but PostgreSQL had written it to its own log first: ~320 ERROR + STATEMENT pairs per start on every PostgreSQL install, measured on postgres:16. On PostgreSQL the DDL is now made a no-op when already applied (IF NOT EXISTS, or a catalog check where no IF NOT EXISTS exists), so the server skips it with a NOTICE instead. Measured against a real postgres:16 after the change: 0 ERRORs on a fresh database, 0 on a re-run, 0 on an older schema whose columns, rename and constraint were then applied, and pg_dump -s identical to the schema before. """ from __future__ import annotations from types import SimpleNamespace import pytest from backend.app.core import database from backend.app.core.database import _pg_already_applied, _pg_if_not_exists class TestTheRewrite: @pytest.mark.parametrize( "sql,expected", [ ( "ALTER TABLE spool ADD COLUMN tag_type VARCHAR(20)", "ALTER TABLE spool ADD COLUMN IF NOT EXISTS tag_type VARCHAR(20)", ), ( "ALTER TABLE print_queue ADD COLUMN batch_id INTEGER REFERENCES print_batches(id) ON DELETE SET NULL", "ALTER TABLE print_queue ADD COLUMN IF NOT EXISTS batch_id INTEGER REFERENCES print_batches(id) " "ON DELETE SET NULL", ), ("alter table t add column c int", "alter table t add column IF NOT EXISTS c int"), ("CREATE INDEX ix_a ON t (a)", "CREATE INDEX IF NOT EXISTS ix_a ON t (a)"), ("CREATE UNIQUE INDEX ux_a ON t (a)", "CREATE UNIQUE INDEX IF NOT EXISTS ux_a ON t (a)"), ("CREATE TABLE t (id INTEGER)", "CREATE TABLE IF NOT EXISTS t (id INTEGER)"), ], ) def test_already_applied_ddl_becomes_a_no_op(self, sql, expected): assert _pg_if_not_exists(sql) == expected @pytest.mark.parametrize( "sql", [ "ALTER TABLE t ADD COLUMN IF NOT EXISTS c INT", "CREATE INDEX IF NOT EXISTS ix_a ON t (a)", "CREATE TABLE IF NOT EXISTS t (id INTEGER)", # CONCURRENTLY has its own syntax position; leave it alone. "CREATE INDEX CONCURRENTLY ix_a ON t (a)", "ALTER TABLE t ALTER COLUMN c SET DEFAULT 0", "ALTER TABLE t DROP COLUMN c", "ALTER TABLE t ADD CONSTRAINT ck CHECK (a > 0)", "UPDATE t SET c = 1 WHERE c IS NULL", "SELECT 1", ], ) def test_everything_else_is_untouched(self, sql): assert _pg_if_not_exists(sql) == sql class _CatalogConn: """Answers the catalog query with a canned result and records it.""" def __init__(self, result): self.result = result self.asked = [] async def scalar(self, stmt, params): self.asked.append((str(stmt), params)) return self.result class TestTheCatalogCheck: ADD_CONSTRAINT = "ALTER TABLE oidc_providers ADD CONSTRAINT ck_x CHECK (a = FALSE OR b = TRUE)" RENAME = "ALTER TABLE project_bom_items RENAME COLUMN notes TO remarks" @pytest.mark.asyncio async def test_an_existing_constraint_is_skipped(self): conn = _CatalogConn(1) assert await _pg_already_applied(conn, self.ADD_CONSTRAINT) is True assert conn.asked[0][1] == {"name": "ck_x", "table": "oidc_providers"} @pytest.mark.asyncio async def test_a_missing_constraint_is_added(self): assert await _pg_already_applied(_CatalogConn(None), self.ADD_CONSTRAINT) is False @pytest.mark.asyncio async def test_a_rename_whose_old_column_is_gone_already_ran(self): conn = _CatalogConn(None) assert await _pg_already_applied(conn, self.RENAME) is True assert conn.asked[0][1] == {"table": "project_bom_items", "column": "notes"} @pytest.mark.asyncio async def test_a_rename_whose_old_column_is_there_still_runs(self): assert await _pg_already_applied(_CatalogConn(1), self.RENAME) is False @pytest.mark.asyncio async def test_other_ddl_is_not_asked_about(self): conn = _CatalogConn(1) assert await _pg_already_applied(conn, "ALTER TABLE t ADD COLUMN c INT") is False assert conn.asked == [] class TestTheHookIsScoped: @pytest.mark.asyncio async def test_sqlite_gets_no_hook(self, monkeypatch): listened = [] monkeypatch.setattr(database.event, "listen", lambda *a, **k: listened.append(a)) async def body(conn): return None monkeypatch.setattr(database, "_run_migrations", body) await database.run_migrations(SimpleNamespace(dialect=SimpleNamespace(name="sqlite"))) assert listened == [] @pytest.mark.asyncio async def test_postgres_hook_is_removed_even_when_a_migration_fails(self, monkeypatch): """Only the migration run is rewritten, never a runtime statement.""" calls = [] monkeypatch.setattr(database.event, "listen", lambda target, name, fn, **k: calls.append(("listen", fn))) monkeypatch.setattr(database.event, "remove", lambda target, name, fn: calls.append(("remove", fn))) async def boom(conn): raise RuntimeError("migration failed") monkeypatch.setattr(database, "_run_migrations", boom) conn = SimpleNamespace(dialect=SimpleNamespace(name="postgresql"), sync_connection=object()) with pytest.raises(RuntimeError): await database.run_migrations(conn) assert calls == [ ("listen", database._pg_ddl_if_not_exists), ("remove", database._pg_ddl_if_not_exists), ] def test_the_hook_rewrites_and_keeps_the_parameters(self): params = {"a": 1} stmt, out = database._pg_ddl_if_not_exists(None, None, "ALTER TABLE t ADD COLUMN c INT", params, None, False) assert stmt == "ALTER TABLE t ADD COLUMN IF NOT EXISTS c INT" assert out is params