rails ActiveRecord::IrreversibleMigration - how to write a reversible down migration
How to write a reversible down migration in rails when ActiveRecord raises IrreversibleMigration: replacing change with explicit up and down methods, or wrapping the tricky part in a reversible block. Use when db:migrate:redo or db:rollback fails because change cannot be inverted. Not for migrations that fail on the way up.
TL;DR
The change method can only auto-invert operations rails knows how to reverse; removecolumn without a type, changecolumn, and raw execute are not invertible. Rewrite the migration with explicit up and down methods that spell out both directions, or keep change and wrap just the tricky call in a reversible block. Prove it with db:migrate:redo before merging.
The query
rails ActiveRecord::IrreversibleMigration - how to write a reversible down migrationUse this when
- rails raises ActiveRecord::IrreversibleMigration on rollback
- db:migrate:redo fails on a migration using change
- A migration calls remove_column without a type or uses execute
- An agent generated a migration that only works in one direction
Not for
- Migrations that fail while applying (fix the up direction first)
- Data migrations with no schema change (make them idempotent instead)
- Irreversible by design (document it and provide a compensating migration)
Steps
1. Identify the irreversible call
Read the migration and find the operation rails cannot invert. The usual suspects: removecolumn without a column type, changecolumn, change_table with a block rails cannot reverse, and execute with raw SQL.
rails db:migrate:redoExpected output: IrreversibleMigration naming the migration. The failing operation is the one to isolate.
2. Option A: rewrite with explicit up and down
Replace def change with def up and def down, spelling out both directions:
def up
remove_column :users, :legacy_token, :string
end
def down
add_column :users, :legacy_token, :string
endExpected output: rollback now has explicit instructions. This is the clearest form for anything non-trivial.
3. Option B: keep change and wrap the tricky part
def change
reversible do |dir|
dir.up { execute "[your up SQL]" }
dir.down { execute "[your down SQL]" }
end
endExpected output: the rest of change still auto-inverts, and only the wrapped part uses your explicit SQL. Good for one raw statement inside an otherwise reversible migration.
4. Prove reversibility before merging
rails db:migrate:redoExpected output: the migration rolls back and re-applies with no IrreversibleMigration. Run it twice: redo exercises down then up, which is exactly what a deploy rollback would do.
5. Add the check to the agent's migration procedure
Every generated migration gets a db:migrate:redo on a scratch database before it is committed. Migrations that cannot roll back never reach the main branch.
Expected output: irreversible migrations fail in development, not during a production rollback at 2am.
Tool compatibility
rails 6.1 through 8.x with ActiveRecord. reversible and up/down semantics are stable across these versions.
Variant phrasings
rails ActiveRecord IrreversibleMigration how to fix
Steps 1 and 2: find the non-invertible call and rewrite with up/down.
rails change method cannot be reversed
Step 3: wrap the tricky call in reversible so the rest of change keeps auto-inverting.
rails remove_column irreversible migration
Pass the type: removecolumn :users, :legacytoken, :string is reversible, without the type it is not. Step 2 covers the general fix.
Why it happens
The change method is syntactic sugar: rails records each call and replays its inverse on rollback. Calls with no known inverse (a removed column whose type rails never learned, arbitrary SQL) have no replay, so rails raises instead of guessing. Explicit up/down removes the need to guess.
Edge cases
- down methods that lose data (re-adding a dropped column comes back empty) are "reversible" but destructive: note the data loss in the migration comment.
- execute with database-specific SQL in down must be tested on the same database family as production; sqlite reversibility proves nothing about postgres.
- Some teams ban change entirely for non-trivial migrations and require up/down always: a fine policy, and it makes this error impossible.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_Bqml9xOjE3fL16XXUhosHg