Rails migration rollback scope: restoring schema without expecting deleted data back
0 reputation · 14 Mar 2026, 20:50 UTC
0 reputation · 14 Mar 2026, 20:50 UTC
I am defining a rollback policy for a Rails application (assume a current Rails 7.x line with PostgreSQL) and need to pin down exactly what rails db:rollback is allowed to promise. Our migrations are written with change where possible, but some use remove_column, change_column, and occasionally execute for data cleanup.
My understanding is that reversibility is structural only: Rails can recreate a dropped column if enough type information is present, but rows deleted or transformed by the migration are not restored. Operations like raw SQL or ambiguous column changes raise ActiveRecord::IrreversibleMigration unless I write explicit up/down or reversible blocks. What I cannot settle is the policy boundary: should rollback be defined as "schema compatible with the previous code version" or as "full pre-migration state," given that the latter seems unachievable for destructive changes without separate backups?
db:rollback, with data recovery delegated to backups?remove_column without options, change_column, execute) must be rewritten as up/down to avoid an irreversible rollback at runtime?29275 reputation · 15 Mar 2026, 02:54 UTC
Yes—define your policy boundary as schema compatible with the previous code version. rails db:rollback is a structural operation: it runs the migration's down method (or the inverse of change) and moves the version pointer in schema_migrations back. It never snapshots, copies, or restores row data. "Full pre-migration state" is unachievable through migrations alone, so your policy should state that data recovery is delegated to backups, point-in-time recovery, or application-level soft deletes—not to rollback.
Assuming a current Rails 7.x line with PostgreSQL:
remove_column without options: irreversible in change. Active Record cannot guess the dropped column's type, so rollback raises ActiveRecord::IrreversibleMigration. You can make it reversible in place by passing the type explicitly—remove_column :users, :legacy_flag, :boolean—but note the recreated column comes back empty (or with a default), never with the old values.change_column: not invertible from change because the old type isn't recorded in the call. Rewrite as up/down, or use change_column inside reversible blocks. Even when reversed, values truncated or coerced by the type change (e.g., string to integer) are not restored.execute (raw SQL): always irreversible from change. Wrap it in up/down with a hand-written inverse, or in a reversible block. If the SQL deletes or transforms rows, your down can only undo structure—it cannot reconstruct deleted rows.Expand/contract (add new structures, migrate reads/writes, remove old structures in a later deploy) narrows the window where destructive rollback matters, but does not eliminate it. During the expand phase, rollback is safe because nothing was dropped. Once you ship the contract phase—the migration that actually drops the old column—rollback of that step is again schema-only. The pattern's real benefit is that by the time you drop anything, the data is usually already duplicated or obsolete, so the missing data restoration stops mattering. It is a deployment-safety strategy, not a data-recovery mechanism.
These checks are cheap on a staging or development copy and confirm the behavior for your exact Rails version:
# 1. Confirm structural reversal with no data recovery
bin/rails db:migrate
bin/rails db:rollback
bin/rails db:migrate:status # version pointer moved back
# 2. Drop a populated table, roll back, inspect
bin/rails runner 'puts User.count' # after rollback: table exists, 0 rows
# 3. Confirm irreversibility of raw SQL in change
bin/rails db:rollback # => ActiveRecord::IrreversibleMigrationAlso diff db/schema.rb before and after rollback to confirm the structure returned to the prior version.
Write the policy as: rollback guarantees the schema matches the previous deployable code version; every destructive migration must be reversible (up/down or reversible) and must be preceded by a verified backup or table-level copy of affected data; recovery of deleted rows is a restore-from-backup runbook, not a rollback runbook. One caveat to flag for review: exact error text and STEP option behavior can vary slightly across Rails minor versions, so pin the verification steps above to the version you actually run in production.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.