A migration reached production and should not have.First: can it be rolled back at all?#
That depends on what it did, not on the tooling.| The migration | Reversible? |
|---|
| Added a table, column or index | Yes — dropping it loses nothing that existed before |
| Widened a column, dropped a constraint | Yes |
| Dropped a column or table | No — the data is gone unless you have a backup |
Narrowed a type (TEXT → VARCHAR(50)) | No — truncated values are not recoverable |
| Backfilled or transformed data | Only if you recorded the previous values |
Run down on the first two groups. For the rest, restoring from a backup is
the real remedy and down is theatre.Rolling back#
Check migration:status first. If several migrations shipped together, undoing
one may leave the schema in a state no migration describes.See what shipped together#
$ npx prorm migration:status
Migration Status:
================
Executed Migrations:
[UP] 20260901-add-nickname [Batch 12]
[UP] 20260924-add-orders-status [Batch 13]
[UP] 20260924-index-orders-status [Batch 13]
----------------
Total: 3 (3 executed, 0 pending)
Batch 13 is the deploy: two migrations, and the index depends on the column.Revert in reverse order#
$ npx prorm migration:revert -- --steps 2
Reverting 2 migration(s)...
Reverted 2 migration(s)
--steps reverts the applied migrations sorted by name, descending: newest
first for timestamped names. Here index-… sorts after add-…, so the index
goes before the column it indexes. Two migrations with the same timestamp
are ordered by the rest of the name, not by when they ran, so give each its
own timestamp. Reverting 20260924-add-orders-status alone by --name would
try to drop a column an index still uses.Confirm the schema matches what the code expects#
Run migration:status again, then diff the live schema against the models
(below) before redeploying the previous release.Rolling forward is usually better#
A down that drops a column deletes production data to undo a deploy. A
forward fix does not:// down: drops the column, and everything written to it
await queryInterface.removeColumn('users', 'nickname');
// forward: stop using it, drop it later once you are sure
await queryInterface.changeColumn('users', 'nickname', { allowNull: true });
Prefer forward unless the migration is provably reversible and provably empty
of new data.Why the expand / migrate / contract shape exists#
It makes every step individually reversible:1.
Expand — add the new column, nullable. Reversible: nothing reads it yet.
2.
Migrate — write both, backfill, then read the new one. Reversible: the
old column is still authoritative.
3.
Contract — make it NOT NULL, drop the old column. Not reversible —
which is why it is last, and separate.
Stopping between two and three leaves a working system. That is the point. The
Gantt example draws the shape.If the schema no longer matches the models#
import { createSchemaDiffer } from 'ts-prorm-orm';
const differ = createSchemaDiffer(prorm.getDialectInstance()!, { compareIndexes: true });
const diffs = await differ.diffAll(); // live schema vs your definitions, one per model
Tells you what actually differs before you write a corrective migration. See
Schema diffing.Before the next one#
Take a snapshot before any migration that drops or narrows.
Run it against a restored copy of production first — staging with 40 rows
does not surface a lock held for eight minutes over 40 million.
Keep migrations small: one concern each, so undoing one does not undo four.
Modified at 2026-10-04 00:40:16