Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
# Generate from model changes
alembic revision --autogenerate -m "add user preferences"# Apply migrations
alembic upgrade head# Rollback one step
alembic downgrade -1
# Generate SQL for review (production)
alembic upgrade head --sql > migration.sql
# Check current revision
alembic current
# Show migration history
alembic history --verbose
Running Async Code in Migrations
"""Migration with async operation.
NOTE: Alembic upgrade/downgrade cannot be async, but you can
run async code using sqlalchemy.util.await_only workaround.
"""from alembic import op
from sqlalchemy import text
from sqlalchemy.util import await_only
defupgrade() -> None:
# Get connection (works with async dialect)
connection = op.get_bind()
# For async-only operations, use await_only# This works because Alembic runs in greenlet context
result = await_only(
connection.execute(text("SELECT count(*) FROM users"))
)
# Standard operations work normally with async engine
op.execute("""
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_org
ON users (organization_id, created_at DESC)
""")
Concurrent Index (Zero-Downtime)
defupgrade() -> None:
# CONCURRENTLY avoids table locks on large tables# IMPORTANT: Cannot run inside transaction block
op.execute("""
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_org
ON users (organization_id, created_at DESC)
""")
defdowngrade() -> None:
op.execute("DROP INDEX CONCURRENTLY IF EXISTS idx_users_org")
# In alembic.ini or env.py, disable transaction for this migration:# Set transaction_per_migration = false for CONCURRENTLY operations
Two-Phase NOT NULL Migration
"""Add org_id column (phase 1 - nullable).
Phase 1: Add nullable column
Phase 2: Backfill data
Phase 3: Add NOT NULL (separate migration after verification)
"""defupgrade() -> None:
# Phase 1: Add as nullable first
op.add_column('users', sa.Column('org_id', UUID(as_uuid=True), nullable=True))
# Phase 2: Backfill with default org
op.execute("""
UPDATE users
SET org_id = 'default-org-uuid'
WHERE org_id IS NULL
""")
# Phase 3 in SEPARATE migration after app updated:# op.alter_column('users', 'org_id', nullable=False)defdowngrade() -> None:
op.drop_column('users', 'org_id')
Key Decisions
Decision
Recommendation
Rationale
Async dialect
Use postgresql+asyncpg
Native async support
NOT NULL column
Two-phase: nullable first, then alter
Avoids locking, backward compatible
Large table index
CREATE INDEX CONCURRENTLY
Zero-downtime, no table locks
Column rename
4-phase expand/contract
Safe migration without downtime
Autogenerate review
Always review generated SQL
May miss custom constraints
Migration granularity
One logical change per file
Easier rollback and debugging
Production deployment
Generate SQL, review, then apply
Never auto-run in production
Downgrade function
Always implement properly
Ensures reversibility
Transaction mode
Default on, disable for CONCURRENTLY
CONCURRENTLY requires no transaction
Anti-Patterns (FORBIDDEN)
# NEVER: Add NOT NULL without default or two-phase approach
op.add_column('users', sa.Column('org_id', UUID, nullable=False)) # LOCKS TABLE, FAILS!# NEVER: Use blocking index creation on large tables
op.create_index('idx_large', 'big_table', ['col']) # LOCKS TABLE - use CONCURRENTLY# NEVER: Skip downgrade implementationdefdowngrade():
pass# WRONG - implement proper rollback# NEVER: Modify migration after deployment# Create a new migration instead!# NEVER: Run migrations automatically in production# Use: alembic upgrade head --sql > review.sql# NEVER: Use asyncio.run() in env.py if loop exists# Already handled by async template, but check for FastAPI lifespan conflicts# NEVER: Run CONCURRENTLY inside transaction
op.execute("BEGIN; CREATE INDEX CONCURRENTLY ...; COMMIT;") # FAILS
Alembic with FastAPI Lifespan
# When running migrations during FastAPI startup (advanced)# Issue: Event loop already running# Solution 1: Run migrations before app starts (recommended)# In entrypoint.sh:# alembic upgrade head && uvicorn app.main:app# Solution 2: Use run_sync for programmatic migrationsfrom sqlalchemy import Connection
from alembic import command
from alembic.config import Config
asyncdefrun_migrations(connection: Connection) -> None:
"""Run migrations programmatically within existing async context."""defdo_upgrade(connection: Connection):
config = Config("alembic.ini")
config.attributes["connection"] = connection
command.upgrade(config, "head")
await connection.run_sync(do_upgrade)
Related Skills
database-schema-designer - Schema design and normalization patterns
database-versioning - Version control and change management
zero-downtime-migration - Expand/contract patterns for safe migrations