PlanetScale — Versioned schema migrations¶
Summary¶
Short PlanetScale tutorial post by Brian Morrison II
(originally 2023-04-05, re-surfaced via the 2026-04-21
feed snapshot). The sibling-companion to the same
author's 2023-04-05 declarative-schema-migrations
explainer (sources/2026-04-21-planetscale-declarative-schema-migrations)
— the two posts bracket the schema-migration design
space. Where the declarative post covers the
desired-state / reconcile ceremony (Atlas HCL-style),
this post covers the imperative / versioned / replay-
ordered ceremony (Laravel artisan migrate-style).
The post opens with the load-bearing framing:
"versioned schema migrations consist of multiple files
or scripts that iterate on each other to describe the
database as it moves through time … It works very
similarly to a system you may already be familiar with:
git." Canonicalises the migrations tracking table
+ monotonic file naming + batch-number ledger +
reversible up()/down() mechanism, then composes
it with PlanetScale's native branching + deploy-request
workflow — including a previously-undisclosed
automatic migration-data copy between branches
feature that keeps the ledger in sync when deploy
requests only merge schema, not data.
Key takeaways¶
-
Versioned schema management is git by direct analogy. Verbatim opening: "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." The database is the checked-out working tree; the ordered migration files are the commit history; applying unapplied migrations is fast-forwarding. Canonicalised as patterns/versioned-schema-migration. (Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
-
The
migrationstracking table is the mechanism. Canonical verbatim: "The system will use a dedicated table within your database to track which scripts have been applied, and which ones still need to be applied." The worked Laravel example shows the table shape:id(surrogate PK),migration(the filename slug, e.g.2014_10_12_000000_create_users_table),batch(monotonically-increasing integer marking whichmigrateinvocation applied the row). Onmigrate, the tool lists the migration files on disk, diffs against themigrationstable, and runs the unrun files in file-name order. Canonicalised as concepts/migration-tracking-table. -
Monotonic file-name ordering. "Those files are usually numbered in the order they need to be applied." Laravel uses timestamp-prefix filenames (
2014_10_12_000000_create_users_table.php,2019_08_19_000000_create_failed_jobs_table.php,2023_01_13_000001_add_new_column.php) so filesystem sort order equals application order. Other ecosystems use integer prefixes (FlywayV1__,V2__…) or content-hash prefixes. The property that matters is total ordering by filename — the migration runner makes no attempt to understand content dependencies; it relies on operators naming files in a monotonically-increasing scheme. -
Reversible migrations via paired
up()/down(). Every Laravel migration file defines two methods:up()does the change,down()undoes it. The worked example canonicalises both directions:up()=Schema::table('users', fn($t) => $t->string('nickname'));down()=Schema::table('users', fn($t) => $t->dropColumn('nickname')). Running./vendor/bin/sail artisan migrate:rollback --step=1executes the last batch'sdown()methods in reverse. This gives versioned migrations a mechanism-level rollback story that purely declarative tools lack — but it is operator-authored, not tool-inferred: the operator must remember to write thedown()method, and many destructive operations (dropping a column with production data in it) are not genuinely reversible even with adown()method that re-creates the column shape. -
Benefits framed by the post (three): (1) Familiarity — "versioned schema migrations have been around for much longer than declarative migrations. This means developers are likely more familiar with how they work and may be more comfortable working in this environment." (2) Bidirectional support — "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." The "assuming" clause is load-bearing — the reversibility is a convention, not a guarantee. (3) Incremental diagnosis without Git — "it's easier to track incremental changes without using a version control system. Since all of the migration scripts are stored alongside each other, diagnosing migration issues may be a bit more straightforward when compared to the declarative approach."
-
Drawbacks canonicalised by the post (two): (1) No snapshot of the current schema — "Since the schema is managed incrementally via scripts, 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." Declarative tools give you the whole-schema file; versioned tools give you a delta history. (2) Drift blindness — "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." The
migrationstable tracks what files ran, not what the resulting schema looks like — if an operator manually altered a table between migrations, the versioned tool has no mechanism to detect or reconcile the drift. -
PlanetScale safe-migrations as gate — the post frames versioned migrations as perfectly fine on PlanetScale when safe-migrations is not enabled: "If safe migrations is not enabled for your production branch, versioned migrations would work with PlanetScale branches just as they would with any other MySQL environment." When safe-migrations is enabled on a production branch, direct DDL is blocked — the operator must run migrations against a development branch first, then merge via PlanetScale's native deploy request workflow. This mirrors the declarative-schema-migrations post's framing: the two approaches are rivals of PlanetScale's vendor-managed ceremony, not substrates underneath it.
-
Automatic copy migration data between branches — canonical new mechanism disclosure. Because PlanetScale deploy requests only merge schema, not data, the
migrationstracking table (a data table holding ledger rows) would get lost on each merge — breaking the versioned-migration tool's ability to know what has been applied in production. The post discloses PlanetScale's mitigation: "PlanetScale offers a setting in every Vitess database to automatically copy migration data between branches. This can be set to several preconfigured ORMs, or you can provide a custom table name to sync between database branches." First canonical wiki disclosure of this feature. Canonicalised as concepts/migration-data-copy-between-branches. The feature is structurally necessary for versioned-migration tools to compose with PlanetScale's branching workflow — without it, the ledger is dropped on every production merge and the tool mis-re-runs already-applied migrations.
Worked example — Laravel artisan migrate on PlanetScale¶
The post's running example is the default Laravel scaffolding in a Sail container:
./vendor/bin/sail artisan migrate
# Output:
INFO Preparing database.
Creating migration table .............................. 45ms DONE
INFO Running migrations.
2014_10_12_000000_create_users_table .................. 45ms DONE
2014_10_12_100000_create_password_resets_table ........ 64ms DONE
2019_08_19_000000_create_failed_jobs_table ............ 38ms DONE
2019_12_14_000001_create_personal_access_tokens_table . 44ms DONE
After which the migrations table contains four rows
all marked batch = 1 — four files applied in one
invocation. Subsequent invocations with new files
(e.g. 2023_01_13_000001_add_new_column.php adding a
nickname column to users) get the next batch
number (batch = 2), and migrate:rollback --step=1
rolls back the most recent batch by executing its
files' down() methods in reverse order.
Systems / concepts / patterns extracted¶
- Systems: systems/laravel (worked-example framework — PHP 8.x, artisan CLI, Eloquent schema builder), systems/mysql (target engine), systems/planetscale (vendor layer running the Vitess-backed MySQL underneath), systems/git (explicit analogy in the opening paragraph: versioned migrations are "very similar to a system you may already be familiar with: git").
- Concepts: concepts/schema-evolution (meta-
category), concepts/schema-as-code (versioned
files are the code), concepts/migration-tracking-table
(new — the
migrationsledger table), concepts/migration-data-copy-between-branches (new — PlanetScale's mechanism for preserving the ledger across deploy requests), concepts/coupled-vs-decoupled-database-schema-app-deploy (versioned migrations default to coupled-deploy when run as part of the application deploy script), concepts/database-branching (PlanetScale composition surface), concepts/deploy-request (PlanetScale production-merge gate). - Patterns: patterns/versioned-schema-migration (new — canonical pattern this post is a framing post for, the imperative sibling to patterns/declarative-schema-management), patterns/branch-based-schema-change-workflow (PlanetScale composition), patterns/expand-migrate-contract (complementary sequencing discipline).
Operational numbers / diagrams¶
- Per-migration latencies in the
migrateoutput: 38–64 ms for trivialCREATE TABLEmigrations on a fresh MySQL; 32 ms for anADD COLUMN; 41 ms for the pairedDROP COLUMNrollback. These are not production numbers — they are Laravel Sail container + local MySQL latencies — but they canonicalise the sub-100-ms per-trivial-migration range for online-DDL-eligible changes on a small table. - Two screenshots (not captured in the raw markdown):
the Laravel default
database/migrations/folder listing, and PlanetScale's auto-migration-data-copy settings screen.
Caveats¶
- Tutorial voice, not retrospective. Brian Morrison II is writing conceptual developer- education content, not a PlanetScale production war story. No data on how PlanetScale customers actually use versioned tools in practice; no adoption numbers; no breakdown of Laravel vs Rails vs Django vs Flyway vs Liquibase market share.
- Laravel-only worked example. The taxonomy
(tracking table + monotonic filename + up/down) is
ecosystem-agnostic but the worked example shows
exclusively Laravel. Rails
rake db:migrate(ActiveRecord), Djangopython manage.py migrate, FlywayV1__Create_Users.sql, Liquibase XML/YAML changelogs, Alembic (SQLAlchemy), golang-migrate — all follow the same ledger pattern with minor variations, but none are named. - Reversibility-as-convention understated. The
post says reversibility is "easier" via
down()methods but does not canonicalise the two load- bearing caveats: (a)down()is operator-authored — some operators skip it; (b) truly destructive operations (column drop with production data, irreversible type narrowing, constraint enforcement that filtered rows) cannot be reversed by adown()method even if one exists. The concepts/non-revertible-schema-change concept page on the wiki canonicalises this at depth; this post elides it. - Drift-blindness understated. The post flags
drift blindness as a drawback but does not explain
the mechanism — the
migrationstable tracks which files ran, not what schema resulted, so out-of-band DDL silently de-syncs the ledger from reality without any tooling signal. - Concurrent-migration race elided. Unlike the
declarative-migrations sibling post, which flags
"multiple developers may be making changes to the
schema definition files at the same time on
separate machines, you may run into a scenario
where one developer's changes will overwrite
another's", this post does not discuss the
versioned-migration equivalent: timestamp
collision (two engineers branching from the same
commit and authoring a migration with timestamp
2023_01_13_000001_…produces a merge conflict at the filename level), and out-of-order apply (if engineer A's2023_01_13_…migration is applied to production before engineer B's2023_01_12_…migration lands, the ordering on disk no longer matches the ordering in the ledger — most versioned tools simply don't detect or handle this). - No declarative/versioned hybrid coverage. The
post frames the two approaches as a binary choice.
In practice, tools like Atlas
support both modes (declarative
schema apply+ amigratesub-command for versioned files) and migrations-as-versioned-files-generated-from-diff workflows (Railsgenerate migrationinferring the delta) blur the distinction. This post stays on the pedagogical binary. - Automatic-copy-migration-data mechanism unexplained. The feature is disclosed and its purpose is clear, but the mechanism is hand-waved — the post does not explain how the Vitess substrate replicates this specific data table during a deploy-request cutover (whether it uses VReplication with a custom-table allowlist, or some other mechanism). First-party disclosure of the feature without first-party disclosure of its mechanism.
- Published 2023-04-05 with PlanetScale's pre- discontinuation branching + deploy-request workflow intact — post references live PlanetScale docs links for safe-migrations, branching, deploy-requests, and the automatic-copy-migration-data setting, all load-bearing product features at publication time.
- Raw filename carries truncated-slug suffix
(
…-migrations-e1764932.md) — the URL field in the raw frontmatter is authoritative: verbatimhttps://planetscale.com/blog/versioned-schema-migrations.
Source¶
- Original: https://planetscale.com/blog/versioned-schema-migrations
- Raw markdown:
raw/planetscale/2026-04-21-versioned-schema-migrations-e1764932.md
Related¶
- sources/2026-04-21-planetscale-declarative-schema-migrations — direct sibling-companion post by the same author published the same day. Two halves of a declarative-vs-versioned comparison pair.
- sources/2026-04-21-planetscale-declarative-mysql-schemas-with-atlas-cli — the same author's earlier 2022-09-16 tutorial on the Atlas declarative mechanism. Together with the 2023-04-05 pair they form Morrison II's schema-migration trilogy.
- sources/2026-04-21-planetscale-the-operational-relational-schema-paradigm — Noach's 2022-05-09 ten-tenet manifesto. Tenet 10 (declarative) frames declarative as the canonically-preferred mechanism; this post canonicalises versioned as the pragmatically-more- familiar mechanism. The two pedagogies sit in productive tension.
- systems/laravel · systems/mysql · systems/planetscale · systems/git
- concepts/schema-evolution · concepts/schema-as-code · concepts/migration-tracking-table · concepts/migration-data-copy-between-branches · concepts/coupled-vs-decoupled-database-schema-app-deploy · concepts/database-branching · concepts/deploy-request
- patterns/versioned-schema-migration · patterns/declarative-schema-management · patterns/branch-based-schema-change-workflow · patterns/expand-migrate-contract
- companies/planetscale