CONCEPT Cited by 1 source
Migration tracking table¶
Definition¶
A migration tracking table is a database table, inside the managed schema itself, where a versioned schema migration tool records which migration scripts have already been applied. It is the tool's source of truth for "where am I in the chain?" and the idempotency guard that prevents the same script from running twice.
Canonical framing verbatim (Morrison II, 2023-04-05, PlanetScale):
"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."
(Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
Schema shape¶
The table schema varies per tool but the minimum useful set of columns is:
- A primary key (usually auto-increment integer) for insertion ordering and row uniqueness.
- A migration identifier — typically the filename or a derived version string — that uniquely names the applied script.
- Optionally, a batch or version number grouping scripts applied in the same invocation (Laravel).
- Optionally, an applied-at timestamp.
- Optionally, a checksum of the script contents (Flyway) for tamper detection.
Canonical worked example — Laravel migrations table¶
Verbatim from Morrison II's MySQL session output:
mysql> select * from migrations;
+----+-------------------------------------------------------+-------+
| id | migration | batch |
+----+-------------------------------------------------------+-------+
| 1 | 2014_10_12_000000_create_users_table | 1 |
| 2 | 2014_10_12_100000_create_password_resets_table | 1 |
| 3 | 2019_08_19_000000_create_failed_jobs_table | 1 |
| 4 | 2019_12_14_000001_create_personal_access_tokens_table | 1 |
+----+-------------------------------------------------------+-------+
Three columns: id (PK), migration (filename without
.php), batch (invocation counter).
After a subsequent artisan migrate run that applies
one new file:
+----+-------------------------------------------------------+-------+
| id | migration | batch |
+----+-------------------------------------------------------+-------+
| ... |
| 5 | 2023_01_13_000001_add_new_column | 2 |
+----+-------------------------------------------------------+-------+
The batch of the new row = 2 because this is the
second migrate invocation to apply at least one file.
See patterns/batch-number-for-rollback-grouping.
Per-tool variants¶
- Laravel (Eloquent) —
migrations (id, migration, batch). Documented verbatim in this post. - Rails (ActiveRecord) —
schema_migrations (version). No batch concept; each migration is identified by its timestamp alone. Rollback in Rails israke db:rollback STEP=Nwhich pops theNmost recent rows and runs theirdown()methods in reverse; the unit of rollback is the individual migration, not a batch. - Django —
django_migrations (id, app, name, applied). Per-app namespaced so each Django app tracks its own migration chain independently. - Flyway —
flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success). The checksum column lets Flyway detect a migration whose file was edited after being applied — a common antipattern the tool treats as an error. - Alembic (Python) —
alembic_version (version_num)— only one row, storing the current HEAD revision. Alembic's chain is a DAG of revisions rather than a linear list, so the table is a pointer rather than a log.
Why it lives inside the managed database¶
Canonical wiki rationale: the tracking table must be
transactionally consistent with the schema it tracks.
If the tracking table lived outside the database, the
tool would have a two-phase-commit problem: apply the
DDL, then mark the tracking row — a failure between the
two steps produces a database in a state the tool can't
recognise (DDL applied, tracking not updated → next run
re-applies, fails on duplicate-column / duplicate-index).
Co-locating the tracking table means the INSERT INTO
migrations VALUES (...) can run in the same
transaction as the DDL (for dialects that support
transactional DDL — Postgres does, MySQL mostly does
not, which is why MySQL-targeted tools tend to accept
non-atomic apply-then-mark sequencing with careful
ordering).
The migration-table-is-data-not-schema problem on¶
PlanetScale
PlanetScale's deploy request workflow merges schema from a development branch to production — schema-only by default, no data. This means the tracking table's rows (which are data) are not copied on merge. The tool, looking at the production branch's tracking table after merge, finds zero applied migrations and would try to re-apply every script, which at best fails on existing objects and at worst corrupts data.
PlanetScale's workaround, canonicalised in this post:
an opt-in setting "automatically copy migration data
between branches" with preconfigured recognisers for
Laravel (migrations) / Rails (schema_migrations) /
Django (django_migrations) / custom-table-name
override. The named table is copied as data during
deploy-request promotion, even though the rest of the
merge is schema-only. First wiki capture of this
schema-only-merge-plus-whitelisted-data-copy
composite primitive.
Drift detection via the tracking table¶
Some tools cross-check the tracking table against the live schema:
- Flyway's
validaterecomputes each applied migration's checksum against the filesystem; mismatch → error. - Alembic's
currentreports the tracked revision; doesn't validate the DB matches. - Laravel / Rails — no built-in drift detection. If someone runs manual DDL, the tracking table is oblivious.
Canonical wiki caveat: the tracking table knows "what the tool thinks it has applied", not "what the database actually looks like." This is the load-bearing asymmetry that declarative tools avoid by always diffing against live state.
Seen in¶
-
Laravel (artisan migrate) — canonical worked example.
migrations (id, migration, batch), inserted row per applied file,batchincremented permigrateinvocation. (Source: sources/2026-04-21-planetscale-versioned-schema-migrations) -
PlanetScale — branching/deploy-request interaction with the tracking table motivates the "automatically copy migration data between branches" setting. Canonical framing: the tracking table is data, and schema-only merges need a whitelist for it.
Related¶
- concepts/versioned-schema-migration — the paradigm
- concepts/up-down-migration-pair — author discipline
- patterns/sequential-numbered-migration-files
- patterns/batch-number-for-rollback-grouping
- concepts/database-branching
- concepts/deploy-request
- systems/laravel
- systems/mysql
- sources/2026-04-21-planetscale-versioned-schema-migrations