CONCEPT Cited by 2 sources
Versioned schema migration¶
Definition¶
A versioned schema migration is a paradigm for managing relational-database schema evolution in which the schema is expressed as an ordered sequence of immutable, incremental DDL scripts — one per change — rather than as a single desired-state file. Each script captures a delta (add this column, drop this index, rename this table) with a conventional ordering (by timestamp or monotonic number in the filename), the tool applies the scripts in order against the live database, and a dedicated tracking table inside the database itself records which scripts have already run.
Canonical framing verbatim (Morrison II, 2023-04-05, PlanetScale):
"Schema versioning tools have existed long before their declarative counterparts. Instead of having a single file describing the state of the database schema, versioned schema migrations consist of multiple files or scripts that iterate on each other to describe the database as it moves through time. As changes are made to the schema, new files are added to describe those changes … It works very similarly to a system you may already be familiar with: git."
(Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
Why the name¶
The schema's version at any moment is identified by
the cumulative set of migration files that have been
applied. New migrations bump that version monotonically
— adding a script = creating a new version of the
schema; replaying from version N to version M
requires applying every intervening script. The tool
reads the tracking table to determine the current
version and then applies the pending suffix of the
ordered chain.
The paradigm's two structural primitives:
- Sequence ordering at the filesystem / filename level. See patterns/sequential-numbered-migration-files.
- State tracking inside the database via a migration-tracking table that records which scripts have been applied.
Without both, the paradigm doesn't work: without (1), the tool can't determine what to apply next; without (2), the tool can't determine what's already been applied and will either skip pending work or re-apply everything.
Contrast with declarative schema management¶
| Axis | Versioned | Declarative |
|---|---|---|
| Representation | Ordered chain of delta scripts | Single desired-state file |
| Source of truth | The chain's cumulative effect | The file |
| Change expression | New file appended | File edited in place |
| Apply mechanism | Replay pending scripts | Diff file vs live DB → emit DDL |
| Current-state readable from | Live DB (or full replay) | The file directly |
| Drift-blindness | Yes by default — tool trusts tracking table | No — tool always diffs against live |
| Rollback | Often built-in (if down() written) |
Requires inverse-tool machinery |
| Adoption age | Pre-2010s ubiquity via Rails, Django, Laravel | 2020s-ish (Atlas, Prisma migrate with dev-shadow) |
Canonical wiki framing: both paradigms are valid
compositional substrates; the right choice depends on
team discipline (versioned requires correct down()
authoring) + tool compatibility (declarative requires a
diff engine that handles your dialect) + platform
constraints (PlanetScale's safe-migrations mode reshapes
both paradigms — see composition note below).
The tracking table at the heart of the paradigm¶
Every versioned tool creates a table inside the target
database to record applied scripts. Laravel's Eloquent
uses migrations (id, migration, batch) where
migration is the script's filename and batch is a
monotonic counter incremented each artisan migrate
invocation. Rails' ActiveRecord uses schema_migrations
(version) (no batch — Rails lacks Laravel's batch
concept). Django uses django_migrations (id, app, name,
applied). Flyway uses flyway_schema_history with
version, description, and install_rank columns.
Canonicalised on the wiki as concepts/migration-tracking-table.
The reversibility discipline¶
Many versioned tools support forward and reverse
scripts — in Laravel's Blueprint idiom, up() and
down() methods on the migration class. Running
artisan migrate invokes up(); artisan
migrate:rollback --step=1 invokes the down() methods
of the most recent batch in reverse order.
Load-bearing caveat verbatim: "assuming the
developers or database administrators include those
details in the migration scripts." The inverse isn't
inferred by the tool — the developer must author a
correct down(), and the tool's rollback is only as
good as the discipline behind the down() author.
Canonicalised as concepts/up-down-migration-pair.
Benefits¶
- Familiarity / low switching cost. "developers are likely more familiar with how they work and may be more comfortable working in this environment" (Morrison II, 2023). The paradigm has existed since the mid-2000s (Rails Migrations shipped 2006); it's the default scaffolding in every major Active-Record- style framework.
- Incremental-diff audit trail. Each change is one file; code review sees "what changed" directly without diffing against a monster desired-state file. "diagnosing migration issues may be a bit more straightforward when compared to the declarative approach" (Morrison II, 2023).
- Built-in rollback as a first-class primitive via
the
down()+ batch column — if discipline is maintained, "reverting changes simpler since a single script will have instructions on performing a downgrade."
Drawbacks¶
- Current-state-not-materialised. Load-bearing verbatim: "it may be hard to get a full picture of what the database schema looks like at any given point in time. You'd essentially have to replay all of the previous scripts against a live system to see the schema in full." The desired-state file in a declarative tool is browseable / greppable / diff-against-anything; the migration chain is not. Consequence: tooling that needs the current schema (ORM scaffolding, codegen, docs) must read from the live DB or run a shadow replay.
- Drift-blindness. "Depending on the tool, it may
not validate the current state of the schema before
attempting to apply changes. This can cause major
issues if the schema was modified outside of the tool
and DDL was issued directly to the database." If
someone ran
ALTER TABLEmanually, the next migration applies as if the schema still matches the chain's cumulative state — producing silent drift or duplicate-column / missing-column errors. Declarative tools reconcile against the live DB on every apply and cannot silently drift. - Inverse-authoring burden. Every destructive
migration needs a matching
down()that restores the prior state — often harder than the forward path (restoring a dropped column loses the data unless backups exist; restoring a renamed column requires remembering the old name). In practice many teams ship migrations with emptydown()methods. - Conflict-on-same-timestamp. Two developers
branching from the same
HEADand authoring new migrations simultaneously can produce two files with adjacent timestamps; on merge the tool may apply them out of authoring order, with subtle semantic effects if they touch the same table.
Composition with PlanetScale-native primitives¶
When PlanetScale's safe-migrations feature is off, versioned tools work unchanged — same apply command, same tracking table. When on, direct DDL is restricted on production branches; the versioned tool must run against a development branch and the resulting schema diff is promoted via a deploy request. The deploy request is schema-only by default, so the migrations table (which is data, not schema) would otherwise be lost on merge; PlanetScale offers an "automatically copy migration data between branches" setting with presets for Laravel / Rails / Django / custom ORM tracking tables.
Canonical wiki framing: the versioned paradigm and PlanetScale's native branching don't conflict — they compose by splitting the ceremony into authoring (via the versioned tool, against a dev branch) and applying (via the deploy request, against production).
See patterns/branch-based-schema-change-workflow.
Seen in¶
-
Laravel (artisan migrate + Eloquent Blueprint) — canonical worked example in this post. Files live in
database/migrations/, namedYYYY_MM_DD_HHMMSS_description.php, each exportingup()+down()closures that build DDL via theBlueprintDSL. Tracking table ismigrations (id, migration, batch). Rollback viaartisan migrate:rollback --step=N. (Source: sources/2026-04-21-planetscale-versioned-schema-migrations) -
MySQL as execution target — Morrison II's worked example uses Laravel to emit MySQL DDL. The paradigm is engine-agnostic: the tool is responsible for translating its DSL / script syntax into the target dialect.
-
PlanetScale as deployment substrate — both paradigms compose with PlanetScale; versioned tools run against development branches when safe-migrations is on.
Related¶
- concepts/migration-tracking-table
- concepts/up-down-migration-pair
- patterns/sequential-numbered-migration-files
- patterns/batch-number-for-rollback-grouping
- patterns/declarative-schema-management — sibling paradigm
- patterns/branch-based-schema-change-workflow — PlanetScale-native rival
- concepts/schema-evolution
- concepts/schema-as-code
- concepts/coupled-vs-decoupled-database-schema-app-deploy
- concepts/database-branching
- concepts/deploy-request
- systems/laravel
- sources/2026-04-21-planetscale-versioned-schema-migrations
- sources/2026-04-21-planetscale-declarative-schema-migrations