Applying and Rolling Back Migrations

Applying migrations and rolling them back are the two most frequent Alembic operations. alembic upgrade head applies all pending migrations in order; alembic downgrade -1 rolls back the most recent one. Understanding the migration chain, how Alembic determines the current state, and how to safely handle rollbacks โ€” including the case where downgrade is difficult or impossible โ€” gives you the confidence to evolve your schema without fear.

Applying Migrations

# โ”€โ”€ Apply all pending migrations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic upgrade head
# "head" means the latest migration in the chain
# Runs all unapplied migrations in order from current version to head

# โ”€โ”€ Apply a specific number of steps โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic upgrade +1    # apply the next one migration
alembic upgrade +2    # apply the next two migrations

# โ”€โ”€ Apply up to a specific revision โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic upgrade abc123def456   # apply up to and including this revision

# โ”€โ”€ Preview the SQL without executing it โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic upgrade head --sql     # prints SQL to stdout, does not run it

# โ”€โ”€ Check what would be applied โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic current         # show current applied revision
alembic history         # show all revisions with applied marker
alembic show abc123     # show details of a specific revision
Note: Alembic stores the current migration version in a table called alembic_version in your database. When you run alembic upgrade head, Alembic reads the current version from this table, finds all migrations with a higher version (i.e., those after the current one in the chain), and applies them in order. If the alembic_version table does not exist (fresh database), Alembic creates it and runs all migrations from the beginning. This makes migrations idempotent โ€” safe to run on any database at any state.
Tip: Use alembic upgrade head --sql to preview the exact SQL that will run before applying it. This is especially useful before production deployments โ€” you can review the SQL, estimate how long it will take (a large table ADD COLUMN might take seconds or minutes), and decide whether to schedule a maintenance window. The SQL output also helps you verify that generated migrations are correct before touching the production database.
Warning: Running alembic downgrade base rolls back ALL migrations โ€” essentially dropping all tables and recreating from scratch. This is appropriate for test environments but catastrophically destructive in production. Always specify an exact revision for production rollbacks: alembic downgrade -1 (one step back) or alembic downgrade abc123 (to a specific known-good revision). Never use base in production.

Rolling Back

# โ”€โ”€ Roll back the most recent migration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic downgrade -1

# โ”€โ”€ Roll back two migrations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic downgrade -2

# โ”€โ”€ Roll back to a specific revision โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic downgrade abc123def456   # reverts to state at abc123def456

# โ”€โ”€ Roll back ALL migrations (development/test only!) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic downgrade base   # NEVER in production โ€” drops everything!

# โ”€โ”€ Preview what downgrade SQL would run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
alembic downgrade -1 --sql

The Migration Chain

Migration chain (each revision points to its parent):

base โ†’ rev001 โ†’ rev002 โ†’ rev003 โ†’ rev004 (head)
                                          โ†‘ current (alembic_version table)

alembic upgrade head:   applies nothing (already at head)
alembic downgrade -1:   reverts rev004, current becomes rev003
alembic downgrade -1:   reverts rev003, current becomes rev002
alembic upgrade head:   applies rev003 then rev004

# After a bad migration was applied:
# 1. alembic downgrade -1    โ† revert the bad migration
# 2. Fix the migration script (or write a new one)
# 3. alembic upgrade head    โ† reapply

Handling Irreversible Migrations

from alembic.operations import MigrationScript

def upgrade() -> None:
    # Add a NOT NULL column with a default โ€” fine to run
    op.add_column("posts", sa.Column("is_featured", sa.Boolean(),
                                     nullable=False, server_default="false"))
    # Remove server_default after data is populated (now truly NOT NULL)
    op.alter_column("posts", "is_featured", server_default=None)

def downgrade() -> None:
    op.drop_column("posts", "is_featured")

# โ”€โ”€ Migration that loses data on downgrade โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def upgrade() -> None:
    # Archive and then delete old data
    op.execute(
        "INSERT INTO post_archive SELECT * FROM posts WHERE deleted_at IS NOT NULL"
    )
    op.execute("DELETE FROM posts WHERE deleted_at IS NOT NULL")
    op.drop_column("posts", "deleted_at")

def downgrade() -> None:
    # Cannot restore deleted data โ€” document this explicitly
    op.add_column("posts", sa.Column("deleted_at", sa.TIMESTAMP(timezone=True),
                                     nullable=True))
    # NOTE: data in post_archive could be restored here, but deleted rows are gone
    # This downgrade restores the column structure but NOT the deleted data

Common Mistakes

Mistake 1 โ€” Running downgrade base in the wrong environment

โŒ Wrong โ€” drops all tables in production:

DATABASE_URL=postgresql://prod-db... alembic downgrade base   # destroys everything!

โœ… Correct โ€” always use specific revisions in production:

DATABASE_URL=postgresql://prod-db... alembic downgrade -1   # โœ“ controlled rollback

Mistake 2 โ€” Editing an already-applied migration

โŒ Wrong โ€” changing a migration that has already run:

Editing alembic/versions/abc123_add_column.py after it was applied.
Alembic will not re-run it โ€” the database now differs from the script!

โœ… Correct โ€” create a new migration to fix the issue. Never edit applied migrations.

Mistake 3 โ€” Forgetting to write the downgrade function

โŒ Wrong โ€” cannot roll back:

def downgrade() -> None:
    pass   # no rollback possible โ€” dangerous in production!

โœ… Correct โ€” always implement a meaningful downgrade, even if it just logs a warning about data loss.

Quick Reference

Task Command
Apply all alembic upgrade head
Apply one alembic upgrade +1
Preview SQL alembic upgrade head --sql
Roll back one alembic downgrade -1
Roll back to revision alembic downgrade abc123
Current version alembic current
Full history alembic history --verbose

🧠 Test Yourself

You applied a migration to production that added a NOT NULL column with wrong logic. You need to roll it back. What is the safest command?