test_pg_migration_quiet_log.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """Startup migrations leave no ERRORs in the PostgreSQL server log.
  2. Nearly every ADD COLUMN in run_migrations() is a re-run on every start, because
  3. create_all() has already made the column. _safe_execute swallowed the duplicate,
  4. but PostgreSQL had written it to its own log first: ~320 ERROR + STATEMENT pairs
  5. per start on every PostgreSQL install, measured on postgres:16. On PostgreSQL the
  6. DDL is now made a no-op when already applied (IF NOT EXISTS, or a catalog check
  7. where no IF NOT EXISTS exists), so the server skips it with a NOTICE instead.
  8. Measured against a real postgres:16 after the change: 0 ERRORs on a fresh
  9. database, 0 on a re-run, 0 on an older schema whose columns, rename and
  10. constraint were then applied, and pg_dump -s identical to the schema before.
  11. """
  12. from __future__ import annotations
  13. from types import SimpleNamespace
  14. import pytest
  15. from backend.app.core import database
  16. from backend.app.core.database import _pg_already_applied, _pg_if_not_exists
  17. class TestTheRewrite:
  18. @pytest.mark.parametrize(
  19. "sql,expected",
  20. [
  21. (
  22. "ALTER TABLE spool ADD COLUMN tag_type VARCHAR(20)",
  23. "ALTER TABLE spool ADD COLUMN IF NOT EXISTS tag_type VARCHAR(20)",
  24. ),
  25. (
  26. "ALTER TABLE print_queue ADD COLUMN batch_id INTEGER REFERENCES print_batches(id) ON DELETE SET NULL",
  27. "ALTER TABLE print_queue ADD COLUMN IF NOT EXISTS batch_id INTEGER REFERENCES print_batches(id) "
  28. "ON DELETE SET NULL",
  29. ),
  30. ("alter table t add column c int", "alter table t add column IF NOT EXISTS c int"),
  31. ("CREATE INDEX ix_a ON t (a)", "CREATE INDEX IF NOT EXISTS ix_a ON t (a)"),
  32. ("CREATE UNIQUE INDEX ux_a ON t (a)", "CREATE UNIQUE INDEX IF NOT EXISTS ux_a ON t (a)"),
  33. ("CREATE TABLE t (id INTEGER)", "CREATE TABLE IF NOT EXISTS t (id INTEGER)"),
  34. ],
  35. )
  36. def test_already_applied_ddl_becomes_a_no_op(self, sql, expected):
  37. assert _pg_if_not_exists(sql) == expected
  38. @pytest.mark.parametrize(
  39. "sql",
  40. [
  41. "ALTER TABLE t ADD COLUMN IF NOT EXISTS c INT",
  42. "CREATE INDEX IF NOT EXISTS ix_a ON t (a)",
  43. "CREATE TABLE IF NOT EXISTS t (id INTEGER)",
  44. # CONCURRENTLY has its own syntax position; leave it alone.
  45. "CREATE INDEX CONCURRENTLY ix_a ON t (a)",
  46. "ALTER TABLE t ALTER COLUMN c SET DEFAULT 0",
  47. "ALTER TABLE t DROP COLUMN c",
  48. "ALTER TABLE t ADD CONSTRAINT ck CHECK (a > 0)",
  49. "UPDATE t SET c = 1 WHERE c IS NULL",
  50. "SELECT 1",
  51. ],
  52. )
  53. def test_everything_else_is_untouched(self, sql):
  54. assert _pg_if_not_exists(sql) == sql
  55. class _CatalogConn:
  56. """Answers the catalog query with a canned result and records it."""
  57. def __init__(self, result):
  58. self.result = result
  59. self.asked = []
  60. async def scalar(self, stmt, params):
  61. self.asked.append((str(stmt), params))
  62. return self.result
  63. class TestTheCatalogCheck:
  64. ADD_CONSTRAINT = "ALTER TABLE oidc_providers ADD CONSTRAINT ck_x CHECK (a = FALSE OR b = TRUE)"
  65. RENAME = "ALTER TABLE project_bom_items RENAME COLUMN notes TO remarks"
  66. @pytest.mark.asyncio
  67. async def test_an_existing_constraint_is_skipped(self):
  68. conn = _CatalogConn(1)
  69. assert await _pg_already_applied(conn, self.ADD_CONSTRAINT) is True
  70. assert conn.asked[0][1] == {"name": "ck_x", "table": "oidc_providers"}
  71. @pytest.mark.asyncio
  72. async def test_a_missing_constraint_is_added(self):
  73. assert await _pg_already_applied(_CatalogConn(None), self.ADD_CONSTRAINT) is False
  74. @pytest.mark.asyncio
  75. async def test_a_rename_whose_old_column_is_gone_already_ran(self):
  76. conn = _CatalogConn(None)
  77. assert await _pg_already_applied(conn, self.RENAME) is True
  78. assert conn.asked[0][1] == {"table": "project_bom_items", "column": "notes"}
  79. @pytest.mark.asyncio
  80. async def test_a_rename_whose_old_column_is_there_still_runs(self):
  81. assert await _pg_already_applied(_CatalogConn(1), self.RENAME) is False
  82. @pytest.mark.asyncio
  83. async def test_other_ddl_is_not_asked_about(self):
  84. conn = _CatalogConn(1)
  85. assert await _pg_already_applied(conn, "ALTER TABLE t ADD COLUMN c INT") is False
  86. assert conn.asked == []
  87. class TestTheHookIsScoped:
  88. @pytest.mark.asyncio
  89. async def test_sqlite_gets_no_hook(self, monkeypatch):
  90. listened = []
  91. monkeypatch.setattr(database.event, "listen", lambda *a, **k: listened.append(a))
  92. async def body(conn):
  93. return None
  94. monkeypatch.setattr(database, "_run_migrations", body)
  95. await database.run_migrations(SimpleNamespace(dialect=SimpleNamespace(name="sqlite")))
  96. assert listened == []
  97. @pytest.mark.asyncio
  98. async def test_postgres_hook_is_removed_even_when_a_migration_fails(self, monkeypatch):
  99. """Only the migration run is rewritten, never a runtime statement."""
  100. calls = []
  101. monkeypatch.setattr(database.event, "listen", lambda target, name, fn, **k: calls.append(("listen", fn)))
  102. monkeypatch.setattr(database.event, "remove", lambda target, name, fn: calls.append(("remove", fn)))
  103. async def boom(conn):
  104. raise RuntimeError("migration failed")
  105. monkeypatch.setattr(database, "_run_migrations", boom)
  106. conn = SimpleNamespace(dialect=SimpleNamespace(name="postgresql"), sync_connection=object())
  107. with pytest.raises(RuntimeError):
  108. await database.run_migrations(conn)
  109. assert calls == [
  110. ("listen", database._pg_ddl_if_not_exists),
  111. ("remove", database._pg_ddl_if_not_exists),
  112. ]
  113. def test_the_hook_rewrites_and_keeps_the_parameters(self):
  114. params = {"a": 1}
  115. stmt, out = database._pg_ddl_if_not_exists(None, None, "ALTER TABLE t ADD COLUMN c INT", params, None, False)
  116. assert stmt == "ALTER TABLE t ADD COLUMN IF NOT EXISTS c INT"
  117. assert out is params