Skip to content

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 migrations table exists now and it contains the name of each of the migration scripts, along with a batch number stored in the batch column to signal to artisan that 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 migrate after 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 migrate once 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 the down() order mirrors up() order. But if the developer authored the down()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

Last updated · 550 distilled / 1,221 read