← Recipes

阅读中文版本

Gate Deploys on Backward-Compatible Migrations

Lint migrations for destructive changes before they run, and require expand and contract releases so old and new code can coexist.

~30 mindatabasemigrationdeployment

Task

Prevent a migration from breaking the previous version while a rollout is still in progress.

When to use this

A migration drops a column in the same release that stops using it. A rollback restores the old image, which still selects the dropped column, and every request fails.

Prerequisites

  • Atlas 0.28 or newer, or an equivalent migration linter
  • Migrations in version control, applied by a single pipeline
  • A staging database with a production-like schema

Steps

1. Lint the pending migration against a disposable database.

atlas migrate lint --dir file://migrations --dev-url "docker://postgres/16/dev?search_path=public" --latest 1

Expected output: Destructive changes such as DROP COLUMN, DROP TABLE, or a NOT NULL column without a default are reported before merge.

2. Split the change into an additive release and a later cleanup release.

-- release 1: expand, safe for old and new code
ALTER TABLE orders ADD COLUMN status_v2 text;

-- release 2: contract, after the old code is fully retired
ALTER TABLE orders DROP COLUMN status;

Expected output: Release 1 can ship with both versions running; release 2 waits until rollback to the old version is no longer required.

3. Review the SQL that the migration would apply to the current database.

atlas schema diff --from "$DATABASE_URL" --to file://migrations

Expected output: The printed diff contains only additive statements for the current release.

4. Apply to staging and run both the old and the new test suites against it.

atlas migrate apply --dir file://migrations --url "$STAGING_DATABASE_URL"

Expected output: Both versions pass against the migrated schema, which is the practical definition of backward compatible.

Troubleshooting

Problem

Lint blocks a rename during a normal refactor

Solution

A rename is destructive. Do it as add column, backfill, dual write, then drop the old column two releases later.

Problem

The migration locks the table and blocks traffic

Solution

Set a short lock_timeout and retry, or use online DDL options supported by the engine and the table size.

Limitations

  • Linters cannot prove data correctness; backfills still need verification queries.
  • Some engines cannot run online DDL, so large tables need a dedicated migration tool and a maintenance plan.

The schema outlives the release

Code is replaced in minutes; schema changes persist. During a rolling deploy, and during any rollback, two application versions run against one database. A migration that drops or renames a column in the same release violates that coexistence, because the previous version still references the old shape. The safe pattern is expand and contract: add the new structure, deploy code that writes both, backfill, switch reads, then remove the old structure in a later release.

Lint before the pipeline runs

A linter encodes the rules reviewers forget. Atlas inspects the pending migration and flags destructive statements, including column drops and type changes that rewrite the table. Run it on the migration diff in continuous integration, with a disposable development database so the plan is computed against a real engine rather than guessed. Fail the pull request, not the deploy, because the person who can redesign the migration is the author.

-- release 1
ALTER TABLE orders ADD COLUMN status_v2 text;
-- release 2, after old code is retired
ALTER TABLE orders DROP COLUMN status;

Apply, then prove both versions work

Apply the migration to staging before production and run the old test suite as well as the new one. That is the only check that actually demonstrates compatibility. Watch lock waits and replication lag during the production apply; a migration that takes seconds on staging can take minutes on a large table. Keep the backfill in a separate, restartable job so a failed run does not hold a transaction open, and verify row counts before switching reads.