Skip to content

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 is rake db:rollback STEP=N which pops the N most recent rows and runs their down() methods in reverse; the unit of rollback is the individual migration, not a batch.
  • Djangodjango_migrations (id, app, name, applied). Per-app namespaced so each Django app tracks its own migration chain independently.
  • Flywayflyway_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 validate recomputes each applied migration's checksum against the filesystem; mismatch → error.
  • Alembic's current reports 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, batch incremented per migrate invocation. (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.

Last updated · 550 distilled / 1,221 read