Repository navigation
Squash the migration history into one baseline so it replays from an empty database #877
Description
Summary
We can't build a MaveDB database from scratch by running our migrations: alembic upgrade head fails on an empty database, and some migrations don't apply all-or-nothing. That blocks CI from testing migrations, blocks the new test setup, and makes the RLS rollout risky. This issue replaces the whole migration history up to the 2026.3.0 release with one starting migration built from production's schema, and adds rules and a CI check so the history stays replayable.
RT and 754 are branch names, explained in the glossary on #808.
What's broken
| Migration | Problem |
|---|---|
22e2d92d602e |
Opens an ORM session, calls PubMed and bioRxiv, and commits in the middle of the upgrade. It imports today's mavedb.lib and models, so it breaks as they change |
398067c53257 |
Uses autocommit_block(), which commits everything before it |
a7c3e5f9b1d2 |
Uses autocommit_block() to build uq_alleles_hgvs concurrently |
b8f2c5a1d3e4 |
Imports its view definition from mavedb.models, so replaying it builds today's view, not the one it shipped with |
alembic/manual_migrations/ |
One-off data scripts run by hand; not runnable as migrations |
Why this matters for RLS: the policy rollout needs upgrades that apply fully or not at all. A half-applied migration can leave a table with RLS enabled and no policy, and Postgres then hides every row.
Plan
Replace every revision up to the head prod runs after 2026.3.0 (c5e1a9d4b7f2, unless a 2026.3.x patch adds a migration first) with one migration that keeps that revision ID. Prod and staging need no restamp. Do the work on the 2026.4 branch now; merge it once 2026.3.0 has been stable in prod for a few days, since the squash removes Alembic downgrades across that release. The rollback for 2026.3.0 is the pre-deploy RDS snapshot either way.
Build the baseline
- Confirm prod and staging report the squash revision in
alembic_version. - Take a schema-only dump of prod:
Ownership and grants stay with the bootstrap script (Split database roles into owner, migrate, worker and api #827).
pg_dump "host=<rds endpoint> dbname=mavedb user=postgres sslmode=require" --schema-only --no-owner --no-privileges -f prod-schema-<revision>.sql
- Replace the head revision's file with a migration that has the same revision ID,
down_revision = None, the dump as literal SQL, and adowngrade()that raises. - Seed the fixed vocabulary an empty database needs, from a data-only dump of prod:
controlled_keywords,licenses, and the genericmondo_termsrow. Add any other table the replay smoke test (acceptance criteria) shows the API can't run without. - Delete every older revision. Git keeps them.
Retire alembic/manual_migrations/
- Move
migrate_mapped_variants_to_allele_substrate.pytosrc/mavedb/scripts/. The RT backfill (feat: Retroactive backfill — migrate MappedVariant data and run reverse translation for existing score sets #747) and the 2026.3.0 runbook still run it, andbackfill_campaign.pyandaudit_allele_identifiers.pyreference its path. Update those references. - Delete the rest of the directory, including its README.
Re-parent in-flight branches
- Re-parent the first migration on each open branch onto the baseline:
feature/estelle/721/supersedingCalibration(offa7f3c2e9b104) andfeature/estelle/795/premappedVRS(offc4b18d0f7a92).
Keep it replayable
- Add these rules for new migrations:
- No imports from
mavedb.*. Write views and functions as literal SQL. - No network calls, ORM sessions or commits.
- Nothing that can't run inside a transaction, such as
CREATE INDEX CONCURRENTLYorautocommit_block(). For a large index, build it by hand before the deploy, and have the migration create the same index withoutCONCURRENTLYand withIF NOT EXISTS. In prod that does nothing; on an empty database it builds the index.
- No imports from
- Add a CI job that runs
alembic upgrade headon an empty database for every PR, and a check that fails on the forbidden patterns in migration code. The check ignores docstrings, which describe the build-by-hand step.
Document it
- In a new
alembic/README, describe the squash: at each cycle boundary, once the last release is stable in prod, squash up to its head revision and keep that revision ID. Keep a log there of each squash's revision, release and date. - Add a release checklist to mavedb-deployment as
docs/releasing.md, linked from its README, with the squash as a cycle-boundary step. - In
.github/instructions/database.instructions.md, replace the Manual Migrations section, which tells authors to import scripts intoupgrade(), with the rules from step 9. Remove themanual_migrationsreference in.github/instructions/copilot-instructions.md. - In mavedb-deployment's
docs/database-dumps.md, note that a dump loads against a checkout of the tag it was made from.
Acceptance criteria
-
alembic upgrade headon an empty database produces a schema that matches a schema-only dump of prod. - On a freshly replayed database, the API starts and can create and publish a score set.
-
alembic/versions/holds only the baseline and migrations added after it. -
alembic/manual_migrations/is gone, and the allele substrate backfill runs fromsrc/mavedb/scripts/. - CI fails when a migration imports from
mavedb.*, usesautocommit_block()or usesCONCURRENTLYoutside a docstring. -
alembic/READMEdescribes the squash and logs this one. -
database.instructions.md,copilot-instructions.mdanddatabase-dumps.mdmatch the new procedure. - mavedb-deployment's
docs/releasing.mdincludes the squash step.
Background
- Squashing to the release head rather than one release earlier gives up Alembic downgrades across 2026.3.0. RT reshapes data, so a downgrade wouldn't restore it; the real rollback is the snapshot.
- Squashing everything also absorbs
a7c3e5f9b1d2andb8f2c5a1d3e4, so neither needs a rewrite. 15c40c367731,45e8ec1db15eandf6752571d14dmentionCONCURRENTLYonly in their docstrings, as the build-by-hand instruction; their migration code is already transaction-safe.- Alembic has no squash command, and autogenerate misses views, functions, policies and seed data, so the baseline comes from a dump.
- Tests build their schema with
Base.metadata.create_all(tests/conftest.py), not migrations, so a migration that fails on an empty database isn't caught today.
Activity
[-]Replace the migration chain with a baseline built from prod's schema[/-][+]Make the migration history replayable from an empty database[/+]fix(db): chain the variant-number index after #874 and follow the mig…
fix(migrations): build functional classification variant index withou…
[-]Make the migration history replayable from an empty database[/-][+]Squash the migration history into one baseline so it replays from an empty database[/+]