Skip to content

Squash the migration history into one baseline so it replays from an empty database #877

Description

@bencap

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

  1. Confirm prod and staging report the squash revision in alembic_version.
  2. Take a schema-only dump of prod:
    pg_dump "host=<rds endpoint> dbname=mavedb user=postgres sslmode=require" --schema-only --no-owner --no-privileges -f prod-schema-<revision>.sql
    Ownership and grants stay with the bootstrap script (Split database roles into owner, migrate, worker and api #827).
  3. Replace the head revision's file with a migration that has the same revision ID, down_revision = None, the dump as literal SQL, and a downgrade() that raises.
  4. Seed the fixed vocabulary an empty database needs, from a data-only dump of prod: controlled_keywords, licenses, and the generic mondo_terms row. Add any other table the replay smoke test (acceptance criteria) shows the API can't run without.
  5. Delete every older revision. Git keeps them.

Retire alembic/manual_migrations/

  1. Move migrate_mapped_variants_to_allele_substrate.py to src/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, and backfill_campaign.py and audit_allele_identifiers.py reference its path. Update those references.
  2. Delete the rest of the directory, including its README.

Re-parent in-flight branches

  1. Re-parent the first migration on each open branch onto the baseline: feature/estelle/721/supersedingCalibration (off a7f3c2e9b104) and feature/estelle/795/premappedVRS (off c4b18d0f7a92).

Keep it replayable

  1. 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 CONCURRENTLY or autocommit_block(). For a large index, build it by hand before the deploy, and have the migration create the same index without CONCURRENTLY and with IF NOT EXISTS. In prod that does nothing; on an empty database it builds the index.
  2. Add a CI job that runs alembic upgrade head on 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

  1. 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.
  2. Add a release checklist to mavedb-deployment as docs/releasing.md, linked from its README, with the squash as a cycle-boundary step.
  3. In .github/instructions/database.instructions.md, replace the Manual Migrations section, which tells authors to import scripts into upgrade(), with the rules from step 9. Remove the manual_migrations reference in .github/instructions/copilot-instructions.md.
  4. 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 head on 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 from src/mavedb/scripts/.
  • CI fails when a migration imports from mavedb.*, uses autocommit_block() or uses CONCURRENTLY outside a docstring.
  • alembic/README describes the squash and logs this one.
  • database.instructions.md, copilot-instructions.md and database-dumps.md match the new procedure.
  • mavedb-deployment's docs/releasing.md includes 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 a7c3e5f9b1d2 and b8f2c5a1d3e4, so neither needs a rewrite.
  • 15c40c367731, 45e8ec1db15e and f6752571d14d mention CONCURRENTLY only 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

  1. added this to the R2026.4 milestone on Sep 24, 2026
  2. added theissue type on Sep 24, 2026
  3. changed the title [-]Replace the migration chain with a baseline built from prod's schema[/-] [+]Make the migration history replayable from an empty database[/+] on Sep 25, 2026
  4. changed the title [-]Make the migration history replayable from an empty database[/-] [+]Squash the migration history into one baseline so it replays from an empty database[/+] on Oct 15, 2026
  5. self-assigned this
    on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app: backendTask implementation touches the backend

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions