PATTERN Cited by 1 source
Batch number for rollback grouping¶
Problem¶
A versioned schema migration tool with up/down reversibility needs a named unit of rollback. The finest-grained unit — one migration file — is too fine: if a developer applied three related migrations in a single deploy ("add column, add index, backfill data"), rolling back only the third leaves an orphan index and a populated column, which is usually not what the operator wants. The coarsest-grained unit — "everything applied historically" — is too coarse: rolling all the way back to a fresh schema is never the desired outcome either.
What the operator usually wants is "undo the last deploy" — the set of migrations that landed together as a logical unit.
Shape¶
The tracking table gets a batch column (or
equivalent). Each invocation of the migrate command
that applies ≥1 new migration increments a
batch-wide counter and stamps every file applied
during that invocation with the new value. The
migration
tracking table now records not just which files
ran but which files ran together.
The rollback command is parameterised by a step
count — "undo the last N batches" — and replays
the down() methods of every file in the last N
batches in reverse order. One batch = one invocation
= one logical deploy; rolling back 1 step undoes the
last deploy, regardless of whether that deploy applied
1 or 20 files.
Canonical framing verbatim (Morrison II, 2023-04-05, PlanetScale):
"Notice how a
migrationstable exists now and it contains the name of each of the migration scripts, along with a batch number stored in thebatchcolumn to signal toartisanthat it's been run previously."(Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
Worked example — Laravel¶
First migrate run (new project with 4 scaffolded migrations):
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 |
+----+-------------------------------------------------------+-------+
All four migrations applied in the first invocation →
batch = 1 on every row.
Later, a developer adds 2023_01_13_000001_add_new_column.php
and re-runs artisan migrate:
~❯ ./vendor/bin/sail artisan migrate
INFO Running migrations.
2023_01_13_000001_add_new_column ...................... 32ms DONE
Now:
+----+-------------------------------------------------------+-------+
| id | migration | batch |
+----+-------------------------------------------------------+-------+
| ... |
| 5 | 2023_01_13_000001_add_new_column | 2 |
+----+-------------------------------------------------------+-------+
Row 5's batch = 2 because this is the second
invocation that applied at least one file.
Rollback the last deploy:
~ ❯ ./vendor/bin/sail artisan migrate:rollback --step=1
INFO Rolling back migrations.
2023_01_13_000001_add_new_column ...................... 41ms DONE
Row 5 is deleted from migrations; the nickname
column is dropped from users. The first four rows
(batch 1) remain untouched.
Why not just use per-file rollback?¶
A tool could offer "undo the last migration"
(rolling back exactly one file) without a batch
concept — and some tools do (Rails' default
rake db:rollback pops one migration per invocation,
with STEP=N for "undo N individual migrations").
The trade-off:
- Per-file rollback is simpler and gives finer granularity, but makes the common case ("undo yesterday's deploy that added a column + its index") require knowing exactly how many individual migrations were in that deploy.
- Per-batch rollback maps the rollback unit onto
the invocation, which for most teams aligns with
the deploy unit. The common case is one command:
"rollback last deploy" =
migrate:rollback --step=1.
The batch concept trades a state column (one integer per migration row) for alignment with the operator's mental model of "a deploy."
When the pattern fails¶
- Manual invocation during development. A
developer who runs
artisan migrateafter adding each migration individually produces a batch per file — now each batch is a single migration, and "rollback last batch" reverts only one, which may leave the schema in a half-applied state. Resolved by applying whole PR-worth of migrations in one invocation. - CI re-applies per-file. If a CI system runs
migrations as part of test setup by invoking
artisan migrateonce per known file, each file becomes its own batch. Usually not a concern because CI environments are ephemeral, but can be confusing when inspecting a dev-shared database. - Batch-spanning downtime. If two migrations are
applied in the same batch but one depends on the
other's effect (column A added, then column A index
created), rolling back the batch runs both
down()s — but in reverse order, drop-index-then-drop-column. Works because thedown()order mirrorsup()order. But if the developer authored thedown()s in the wrong order or with cross-file assumptions, the reverse-order replay can fail. - Tools without the batch column (Rails, Alembic) can't offer per-batch rollback at all. They pop one migration per rollback invocation; the "deploy unit" concept lives in the deployment system, not the migration tool.
Canonical wiki contribution¶
The batch column is a first-class example of naming the invocation as the rollback unit. It bridges the per-file granularity of the file-based pattern and the all-history granularity of the tracking table. Without it, rollback is either too fine or too coarse to match operator intent.
The generalisation: when a tool provides a reversible operation over a forward-only log, grouping entries by the invocation that produced them gives a natural unit-of-undo that matches how humans think about deploys. Applies beyond schema migrations — any tool with a reversible replayable log can adopt batch- style grouping.
Seen in¶
- Laravel artisan migrate /
migrate:rollback — canonical implementation.
batchcolumn onmigrations,--step=Non rollback. (Source: sources/2026-04-21-planetscale-versioned-schema-migrations)
Related¶
- concepts/migration-tracking-table — where the batch column lives
- concepts/versioned-schema-migration — the paradigm
- concepts/up-down-migration-pair — what gets run on rollback
- patterns/sequential-numbered-migration-files
- concepts/schema-revert — the non-author-based alternative
- systems/laravel
- sources/2026-04-21-planetscale-versioned-schema-migrations