CONCEPT Cited by 1 source
Up/down migration pair¶
Definition¶
An up/down migration pair is the authoring
discipline, native to most
versioned schema
migration tools, where every migration file exports
two symmetric closures: an up() that advances
the schema one version forward, and a down() that
inverts up() to restore the prior state. The tool's
migrate command runs up()s in order; the tool's
rollback command runs the most recently applied
down()s in reverse.
Canonical framing (Morrison II, 2023-04-05, PlanetScale):
"Many tools that support versioned migrations support going both directions, upgrading and/or downgrading the schema. This makes reverting changes simpler since a single script will have instructions on performing a downgrade, assuming the developers or database administrators include those details in the migration scripts."
(Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
The emphasis on "assuming" is load-bearing: the reversibility is aspirational, not mechanical. The tool cannot infer the inverse of an arbitrary DDL statement; the inverse is whatever the developer writes. Correctness of rollback depends entirely on author discipline.
Canonical worked example — Laravel Blueprint¶
Verbatim from the post:
return new class extends Migration
{
public function up() {
Schema::table('users', function (Blueprint $table) {
$table->string('nickname');
});
}
public function down() {
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('nickname');
});
}
};
up() adds a nickname column; down() drops it.
Running artisan migrate invokes the up() — new
column appears, row written to
migrations
table with the next batch number. Running
artisan migrate:rollback --step=1 invokes down() —
column dropped, row removed from migrations.
The reversibility discipline¶
Authoring a correct down() requires:
- Knowing the DDL inverse —
DROP COLUMNforADD COLUMN,DROP INDEXforADD INDEX,ALTER … TYPE old_typeforALTER … TYPE new_type. - Handling data-loss asymmetries — dropping a
column loses its data. The inverse cannot restore
it.
up()adds column, populates via backfill;down()drops column, data is gone. The reversibility is structural, not semantic. - Preserving order-dependence — if
up()adds column A then backfills from column B then drops column B,down()must add column B back, but the B→A backfill is unrecoverable. - Handling rename ambiguity — a rename is
syntactically indistinguishable from a
drop-plus-add. If
up()doesALTER … RENAME COLUMN old TO new,down()must know the old name to emitRENAME new TO old.
Per-tool variants of the pair¶
- Laravel (Eloquent Blueprint) — explicit
up()anddown()methods on every migration class. Canonical instance. - Rails (ActiveRecord) — either explicit
def up; end/def down; end, or a singledef change; endblock that ActiveRecord can reverse automatically for a subset of "reversible" primitives (add_column,create_table, etc.). For irreversible operations (raw SQL,remove_columnwith lossy type coercion), the developer must fall back to explicitup/down. - Django —
operations = [...]list on each migration class. Django generates reverse operations automatically for its built-in operation types; forRunSQL/RunPython, the developer must supply areverse_sql/reverse_codeargument or the operation is marked irreversible. - Alembic — explicit
upgrade()anddowngrade()functions; Alembic's autogenerate tool produces both but the developer is expected to review and edit, especially for data-migration operations. - Flyway — no built-in downgrade support. Flyway's philosophy is forward-only; teams that want rollback must author a new forward migration that inverses the bad one. The paired-reversibility discipline is explicitly rejected.
When down() is commonly wrong or empty¶
Real-world failure modes of the discipline:
- Empty
down()for production migrations. Teams that never plan to rollback simply commit empty bodies. Works until a production incident requires one. - Incorrect
down()that compiles but leaves the database in a different state than pre-up()(forgotten default value, forgotten index, forgotten FK constraint). down()that lose data silently. Dropping a populated column indown()works but loses all values; the schema rolls back, the data does not.- Non-commutative
up()sequences that break when batch-rolled-back in reverse. - Environment-dependent
down()that works in dev (empty table) and fails in prod (FK constraints on rows the drop would orphan).
Contrast with schema-revert¶
PlanetScale's schema-revert primitive is a different mechanism for the same goal (undoing a schema change). Revert works by:
- Capturing the pre-migration snapshot of the shadow table at cutover time.
- On revert request, replaying the inverse of the shadow-table diff against a new shadow table and cutting over back to the original.
Revert does not require a dev-authored down().
The inverse is computed from the forward diff + the
captured snapshot. Caveat: revert has a time bound
(the snapshot retention window) and a data
semantics — rows written after the migration are lost
on revert unless specifically handled.
The up/down pair is author-authored reversibility; revert is snapshot-computed reversibility. They target the same user need from opposite ends.
See patterns/instant-schema-revert-via-inverse-replication.
Canonical wiki caveat — reversibility-as-aspiration¶
The load-bearing wiki framing is Morrison II's
"assuming" — the tool gives you the scaffolding, but
correctness is a human discipline. This is the
asymmetric failure mode versioned-paradigm shops
discover at their first production incident: the
down() was never tested, was wrong, or didn't exist.
The declarative-paradigm sibling post's "DDL-as-crutch" drawback is the symmetric human failure on the other side — engineers who don't know DDL can't read the emitted diff. Both paradigms push a cognitive burden onto humans that tooling cannot absorb; they're just different burdens.
Seen in¶
- Laravel (artisan migrate) —
canonical worked example. Every Blueprint migration
exports
up()anddown()closures; rollback viaartisan migrate:rollback --step=N. (Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
Related¶
- concepts/versioned-schema-migration — the paradigm
- concepts/migration-tracking-table — where applied rows live
- concepts/schema-revert — snapshot-based inverse, alternative to author-authored inverse
- patterns/batch-number-for-rollback-grouping
- patterns/sequential-numbered-migration-files
- patterns/instant-schema-revert-via-inverse-replication
- systems/laravel
- sources/2026-04-21-planetscale-versioned-schema-migrations