---
title: Configure Alembic autogenerate for safe schema migration from model changes
category: alembic
impact: CRITICAL
impactDescription: Auto-generating migrations from model changes is the primary workflow for schema evolution. Misconfigured autogenerate leads to missed changes or destructive operations.
tags: [alembic, autogenerate, sqlalchemy, async, migrations]
---

# Alembic Autogenerate Migrations

## Async env.py Configuration

```python
# migrations/env.py
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context

# Import your models' Base for autogenerate
from app.models.base import Base

config = context.config
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

target_metadata = Base.metadata

def run_migrations_offline() -> None:
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url, target_metadata=target_metadata,
        literal_binds=True, dialect_opts={"paramstyle": "named"},
    )
    with context.begin_transaction():
        context.run_migrations()

def do_run_migrations(connection: Connection) -> None:
    context.configure(connection=connection, target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()

async def run_async_migrations() -> None:
    connectable = async_engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.", poolclass=pool.NullPool,
    )
    async with connectable.connect() as connection:
        await connection.run_sync(do_run_migrations)
    await connectable.dispose()

def run_migrations_online() -> None:
    asyncio.run(run_async_migrations())

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()
```

## Generate and Apply

```bash
# 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
```

## Migration Template

```python
"""Add users table.

Revision ID: abc123
Revises: None
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID

revision = 'abc123'
down_revision = None

def upgrade() -> None:
    op.create_table('users',
        sa.Column('id', UUID(as_uuid=True), primary_key=True),
        sa.Column('email', sa.String(255), nullable=False),
        sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
    )
    op.create_index('idx_users_email', 'users', ['email'], unique=True)

def downgrade() -> None:
    op.drop_index('idx_users_email', table_name='users')
    op.drop_table('users')
```

## Key Decisions

| Decision | Recommendation | Rationale |
|----------|----------------|-----------|
| Always review generated SQL | May miss custom constraints | Autogenerate is a starting point |
| One logical change per file | Easier rollback and debugging | Atomic migration units |
| Always implement downgrade | Ensures reversibility | Required for safe rollback |
| Use `postgresql+asyncpg` | Native async support | SQLAlchemy 2.0 standard |

**Incorrect — Missing target_metadata:**
```python
# env.py without target_metadata
from alembic import context

target_metadata = None  # Autogenerate won't work!

def run_migrations_online():
    context.configure(target_metadata=target_metadata)
```

**Correct — Import all models via Base:**
```python
# env.py with proper metadata import
from app.models.base import Base  # Imports all SQLAlchemy models
from alembic import context

target_metadata = Base.metadata  # Autogenerate detects changes

def run_migrations_online():
    context.configure(target_metadata=target_metadata)
```

## Common Mistakes

- Skipping review of autogenerated migrations
- Running autogenerate without importing all models
- Using `asyncio.run()` when event loop already exists (FastAPI lifespan)
- Missing `target_metadata` configuration
