| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- """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
|