Master database schema changes with Alembic. Learn to upgrade, downgrade, and debug migrations.
Database migrations are one of the riskiest parts of deployment. A bad migration can take down production. A missing downgrade can block rollbacks.
This exercise builds muscle memory for safe schema changes. You'll create migrations, test rollbacks, and handle common pitfalls.
| Command | What it does |
|---|---|
alembic revision --autogenerate -m "msg" |
Generate migration from model changes |
alembic revision -m "msg" |
Create empty migration (manual edits) |
alembic upgrade head |
Apply all pending migrations |
alembic downgrade -1 |
Rollback one migration |
alembic history |
Show migration history |
alembic current |
Show current database revision |
alembic heads |
Show latest revision(s) |
Add a performance index to an existing column.
The finding table gets slow when filtering by severity. Add an index.
# Create an empty migration (we'll write it manually)
alembic revision -m "add_index_finding_severity"
"""add_index_finding_severity
Revision ID: abc123
Revises: previous_revision
Create Date: 2025-01-27
"""
from alembic import op
# revision identifiers
revision = 'abc123'
down_revision = 'previous_revision'
def upgrade() -> None:
# CONCURRENTLY prevents table locks in production
# Note: CONCURRENTLY cannot run inside a transaction
op.execute(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS "
"ix_finding_severity ON finding (severity)"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_finding_severity")
CREATE INDEX CONCURRENTLY to avoid locking the table during index creation. This requires running outside a transaction.
Add a new column to an existing table with existing data.
Add a priority column to the lead table. Existing leads should default to 'medium'.
def upgrade() -> None:
op.add_column(
'lead',
sa.Column(
'priority',
sa.String(20),
nullable=False,
server_default='medium'
)
)
def downgrade() -> None:
op.drop_column('lead', 'priority')
Transform existing data as part of a migration.
Rename severity values: 'info' → 'informational'
def upgrade() -> None:
# Use raw SQL for data migrations
op.execute(
"UPDATE finding SET severity = 'informational' "
"WHERE severity = 'info'"
)
def downgrade() -> None:
op.execute(
"UPDATE finding SET severity = 'info' "
"WHERE severity = 'informational'"
)
Ensure your migration is safe and complete.
# 1. Apply the migration
alembic upgrade head
# 2. Verify the change
# (connect to DB and check schema)
# 3. Test the rollback
alembic downgrade -1
# 4. Verify the rollback
# (schema should be back to original)
# 5. Re-apply for final state
alembic upgrade head
alembic upgrade head succeeds from clean databasealembic downgrade -1 succeeds for each migration