test_db_migration_contract.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. from __future__ import annotations
  2. from pathlib import Path
  3. MIGRATION = Path("db/migrations/001_creation_knowledge_schema.sql")
  4. MIGRATIONS_DIR = Path("db/migrations")
  5. def _migration_sql() -> str:
  6. return MIGRATION.read_text(encoding="utf-8")
  7. def _all_migration_sql() -> str:
  8. return "\n".join(
  9. path.read_text(encoding="utf-8")
  10. for path in sorted(MIGRATIONS_DIR.glob("*.sql"))
  11. )
  12. def test_creation_knowledge_migration_declares_formal_business_tables():
  13. sql = _migration_sql()
  14. expected_tables = [
  15. "query_batches",
  16. "queries",
  17. "acquisition_runs",
  18. "acquisition_jobs",
  19. "candidate_items",
  20. "media_assets",
  21. "item_classifications",
  22. "decode_jobs",
  23. "decode_results",
  24. "knowledge_particles",
  25. "scope_results",
  26. "payload_drafts",
  27. "ingest_records",
  28. "pipeline_runs",
  29. "pipeline_jobs",
  30. "pipeline_run_events",
  31. "candidate_item_hits",
  32. "llm_call_traces",
  33. "contract_artifacts",
  34. "run_contract_artifacts",
  35. ]
  36. assert "CREATE SCHEMA IF NOT EXISTS creation_knowledge" in sql
  37. assert "CREATE EXTENSION IF NOT EXISTS pgcrypto" in sql
  38. for table in expected_tables:
  39. assert f"CREATE TABLE IF NOT EXISTS creation_knowledge.{table}" in sql
  40. def test_migration_is_replayable_and_not_sqlite_shaped():
  41. sql = _migration_sql()
  42. assert "CREATE TABLE IF NOT EXISTS" in sql
  43. assert "CREATE INDEX IF NOT EXISTS" in sql
  44. assert "ON CONFLICT (version) DO UPDATE" in sql
  45. assert "CREATE OR REPLACE FUNCTION creation_knowledge.touch_updated_at" in sql
  46. assert "CREATE TRIGGER" in sql
  47. sqlite_only_tokens = ["AUTOINCREMENT", "PRAGMA", "sqlite_master", "WITHOUT ROWID"]
  48. assert not any(token in sql.upper() for token in sqlite_only_tokens)
  49. def test_migration_keeps_state_rows_traceable_and_resumable():
  50. sql = _all_migration_sql()
  51. for table in [
  52. "acquisition_runs",
  53. "acquisition_jobs",
  54. "candidate_items",
  55. "decode_jobs",
  56. "decode_results",
  57. "payload_drafts",
  58. "ingest_records",
  59. ]:
  60. assert f"creation_knowledge.{table}" in sql
  61. assert "status text NOT NULL" in sql
  62. assert "attempt_count integer NOT NULL DEFAULT 0" in sql
  63. assert "error_message text" in sql
  64. assert "metadata jsonb NOT NULL DEFAULT '{}'::jsonb" in sql
  65. assert "source_payload jsonb NOT NULL DEFAULT '{}'::jsonb" in sql
  66. assert "response_payload jsonb NOT NULL DEFAULT '{}'::jsonb" in sql
  67. assert "UNIQUE (run_id, query_id, platform)" in sql
  68. assert "idx_candidate_items_platform_item" in sql
  69. def test_candidate_items_unique_key_migration_is_declared():
  70. sql = _all_migration_sql()
  71. assert "ADD COLUMN IF NOT EXISTS unique_key text" in sql
  72. assert "idx_candidate_items_unique_key" in sql
  73. assert "WHERE unique_key IS NOT NULL" in sql
  74. assert "'002_candidate_items_unique_key'" in sql
  75. def test_candidate_items_content_mode_migration_is_declared():
  76. sql = _all_migration_sql()
  77. assert "ADD COLUMN IF NOT EXISTS content_mode text" in sql
  78. assert "idx_candidate_items_content_mode" in sql
  79. assert "video_post" in sql
  80. assert "image_post" in sql
  81. assert "article" in sql
  82. assert "unsupported" in sql
  83. assert "'003_candidate_items_content_mode'" in sql
  84. def test_candidate_items_body_text_migration_is_declared():
  85. sql = _all_migration_sql()
  86. assert "ADD COLUMN IF NOT EXISTS body_text text" in sql
  87. assert "source_payload #>> '{detail,data,data,body_text}'" in sql
  88. assert "LEFT(NULLIF(source_payload #>> '{detail,data,data,body_text}', ''), 50000)" in sql
  89. assert "'004_candidate_items_body_text'" in sql