Django JSONStore

Blog · Migrations

How to undo a Django migration

There is no migrate --undo. Name the migration to return to, and migrate reverses every migration after it. The target is the state to arrive at, not the migration to remove.

# migrations 0001, 0002, 0003 are applied; reverse 0003 only
manage.py migrate myapp 0002

# reverse every migration in the app
manage.py migrate myapp zero

# see the result first
manage.py migrate myapp 0002 --plan
manage.py showmigrations myapp

The output of showmigrations marks each applied migration with an [X]. After the command above, 0003 shows [ ] and the file stays on disk. To remove the migration for good, delete the file after the reverse, not before it.

A reverse of AddField drops the column, and the data in that column is gone. Django asks for no confirmation. Before a reverse on a database with real data, take a backup. The reverse of RemoveField is dangerous in the other direction. It restores an empty column, because the old values are not in the migration.

When the reverse is not possible

Django refuses an operation with no reverse and reports IrreversibleError. A RunPython with no second function is the common cause. Add the reverse function, or mark the step as a no-operation to permit the reverse.

from django.db import migrations

def forwards(apps, schema_editor):
    ...

migrations.RunPython(forwards, migrations.RunPython.noop)

noop states that the reverse needs no database work. It does not undo the effect of forwards. For a data change, write the real reverse function.

--fake, and when it is the wrong tool

--fake writes the migration records and runs no SQL. It is correct when the schema is already in the wanted state and the records disagree with it.

manage.py migrate myapp 0002 --fake

A common repair is migrate myapp zero --fake, followed by migrate myapp --fake-initial. The tables stay, and the records restart. Use this after a squash, or after a manual repair of the schema.

A --fake that hides an error moves the error to the next deploy. The records then claim a state that the database does not hold. The next real migration fails on a column that is absent, or on one that is already present. Before a --fake, compare the schema with the models: run manage.py makemigrations --check --dry-run and read the table definition in the database.