Migration tracking & DB-test CI (#1327)
Two related problems, one now addressed in CI, one still requiring an ops step.
1. DB-test harness β built (manual trigger for now)
.github/workflows/db-tests.yml boots a fresh Postgres with the Supabase CLI
(supabase db start, which reads supabase/migrations/ β the repo source of
truth), then runs scripts/run-db-tests.sh over supabase/tests/*.test.sql.
Itβs the first environment that can actually validate clean apply (needs Docker + the Supabase CLI, which CI has but local dev here did not). The #1325 rename (unique 14-digit version prefixes) fixed the version-collision that previously stopped a from-scratch apply after the first same-day migration β the CLI now applies migrations in correct order.
It now GATES PRs (issue #1998, 2026-08-24). A from-scratch apply
(supabase db start) completes cleanly end-to-end and all
supabase/tests/*.test.sql pass. The remaining latent issues surfaced on clean
apply were fixed IN PLACE (prod-safe: prod wonβt re-run already-recorded
migrations, and every fix is idempotent so re-running is a no-op):
- Invalid SQL β
20250222β¦_004_multi_tenancy.sqlALTER COLUMN β¦ SET DEFAULT (SELECT β¦)(a column DEFAULT canβt be a subquery). Fixed earlier by pinning the literaldefaulttenant id. - Cross-migration-set / ordering β
20260326β¦_002_music_scores_lyrics.sqlsorts beforeβ¦_043_music_scores.sqlcreates the table. Fixed: the ALTER is guarded to a no-op when the table is absent, and thelyricscolumn is created idempotently inβ¦_043. - Function-not-yet-created β
20260407β¦_064_security_advisor_fixes.sqlpinnedsearch_pathondeduct_credits(uuid,integer), an overload not created until migration 070. Fixed: theALTER FUNCTIONblock now skips functions that donβt yet exist. - ON CONFLICT vs partial index β
20260517β¦_113_org_chart_annual_plan.sqlusedON CONFLICT (slug)against a partial unique index. Fixed: spelled out the matchingWHERE slug IS NOT NULLpredicate. - Storage-trigger delete β
20260605β¦_146_drop_vpat_source_file_url.sqldeleted fromstorage.objects, blocked by the local storage trigger. Fixed: the deletes are wrapped and the trigger error swallowed (bucket is empty on a fresh DB anyway). - Broken dollar-quote β
20260816β¦_249_workshop_category_and_trainings.sqlnested$$string literals inside aDO $$block. Fixed: the block now uses a distinct$do$tag. - Duplicate version prefixes β
20260816000000and20260816020000were each used by two files (PK collision inschema_migrationson fresh apply). Fixed: the later-committed file of each pair was renamed to the next free version (β¦000001/β¦020001); both renamed files are idempotent (CREATE OR REPLACE/ADD COLUMN IF NOT EXISTS/ guarded), so if prodβs ledger lacks the new version and re-applies it, it is a safe no-op.
The PR gate runs on changes under supabase/migrations/**, supabase/tests/**,
supabase/seed.sql, supabase/config.toml, and the runner script. The runner
uses psql -v ON_ERROR_STOP=1 and asserts on each testβs exit code β critical,
because the tests print βALL TESTS PASSEDβ from inside their own transaction even
when a statement errored, so a naive psql -f would exit 0 on a broken test.
2. Prod migration tracking β still broken (ops step needed)
Finding (2026-06-27): Supabaseβs hosted migration history is decoupled
from the repo. Creating a fresh dev branch replays Supabaseβs own recorded
migrations, not the repo files β and it still records only the first
migration under the old name (20250213_001_credits_system.sql), failing
immediately afterward. So:
- The renamed repo files (#1325) are the source of truth for
supabase db */CI, but Supabaseβs stored migration history predates the rename and is incomplete/colliding. - Prod schema itself is fine (applied out-of-band over time); itβs the ledger
thatβs wrong. There is no reliable automated record of which migrations are
applied to prod β reconciliation today is the manual
scripts/audit-migrations.py+scripts/migration-reconciliation-overlay.json(see migration-ledger-reconciliation.md).
Reconciliation plan (do deliberately, against prod β not blind)
- Snapshot the live prod schema (
scripts/snapshot-prod-schema.sqlvia the Supabase MCP/psql) and runscripts/audit-migrations.pyto get the current applied/not-applied verdict per (renamed) file. - Re-baseline Supabaseβs hosted migration history to match the repo: mark every
already-applied migration as applied at its new version
(
supabase migration repair --status applied <version>for each), so the ledger lines up withsupabase/migrations/. - Confirm a fresh dev branch then comes up
β¦HEALTHYwith all migrations recorded (notMIGRATIONS_FAILED). - From then on, treat
supabase_migrations.schema_migrationsas the source of truth and retire the manual overlay.
Recommended: squash to a prod-captured baseline
Given the historical migrations donβt faithfully reproduce prod (invalid SQL that never ran, cross-set dependencies β see Β§1), repairing all ~200 individually is costly and low-value. The cleaner path:
pg_dump --schema-onlythe live prod DB βsupabase/migrations/<ts>_baseline.sql.- Move the existing 202 files to an
archive/(kept for history, not applied). - The baseline becomes migration #1; new migrations stack on top. From-scratch apply == prod by construction, so the Β§1 harness can flip to a PR gate.
- Re-baseline the hosted ledger to the single baseline version.
This makes the ledger and from-scratch apply correct in one move instead of
chasing latent issues through 200 files. Requires a prod schema dump (DB
credentials / direct psql β not available via the MCP), so itβs an explicit ops
task. The apps/music and other app-specific migration sets should be scoped to
their own projects as part of this.
Until then, the Β§1 harness runs on demand (workflow_dispatch) and locally; it
is not yet a PR gate.