home / skills / yonatangross / orchestkit / alembic-migrations

alembic-migrations skill

/plugins/ork/skills/alembic-migrations

This skill helps you manage Alembic migrations for SQLAlchemy 2.0 async apps, enabling zero-downtime changes and safe rollback.

npx playbooks add skill yonatangross/orchestkit --skill alembic-migrations

Review the files below or copy the command above to add this skill to your agents.

Files (7)
SKILL.md
9.8 KB
---
name: alembic-migrations
description: Alembic migration patterns for SQLAlchemy 2.0 async. Use when creating database migrations, managing schema versions, handling zero-downtime deployments, or implementing reversible database changes.
context: fork
agent: database-engineer
version: 2.0.0
tags: [alembic, migrations, sqlalchemy, database, schema, python, async, 2026]
allowed-tools: [Read, Write, Edit, Bash, Grep, Glob]
author: OrchestKit
user-invocable: false
---

# Alembic Migration Patterns (2026)

Database migration management with Alembic for SQLAlchemy 2.0 async applications.

## Overview

- Creating or modifying database tables and columns
- Auto-generating migrations from SQLAlchemy models
- Implementing zero-downtime schema changes
- Rolling back or managing migration history
- Adding indexes on large production tables
- Setting up Alembic with async PostgreSQL (asyncpg)

## Quick Reference

### Initialize Alembic (Async Template)

```bash
# Initialize with async template for asyncpg
alembic init -t async migrations

# Creates:
# - alembic.ini
# - migrations/env.py (async-ready)
# - migrations/script.py.mako
# - migrations/versions/
```

### 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:
    """Run migrations in 'offline' mode - generates SQL."""
    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:
    """Run migrations in 'online' mode with async engine."""
    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:
    """Entry point for online migrations."""
    asyncio.run(run_async_migrations())

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

### Migration Template

```python
"""Add users table.

Revision ID: abc123
Revises: None
Create Date: 2026-01-17 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID

revision = 'abc123'
down_revision = None
branch_labels = None
depends_on = 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')
```

### Autogenerate Migration

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

### Running Async Code in Migrations

```python
"""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

def upgrade() -> 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)

```python
def upgrade() -> 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)
    """)

def downgrade() -> 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

```python
"""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)
"""

def upgrade() -> 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)

def downgrade() -> 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)

```python
# 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 implementation
def downgrade():
    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

```python
# 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 migrations
from sqlalchemy import Connection
from alembic import command
from alembic.config import Config

async def run_migrations(connection: Connection) -> None:
    """Run migrations programmatically within existing async context."""
    def do_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
- `sqlalchemy-2-async` - Async SQLAlchemy session patterns
- `integration-testing` - Testing migrations with test databases

## Capability Details

### autogenerate-migrations
**Keywords:** autogenerate, auto-generate, revision, model sync, compare
**Solves:**
- Auto-generate migrations from SQLAlchemy models
- Sync database with model changes
- Detect schema drift

### revision-management
**Keywords:** upgrade, downgrade, rollback, history, current, revision
**Solves:**
- Apply or rollback migrations
- View migration history
- Check current database version

### zero-downtime-changes
**Keywords:** concurrent, expand contract, online migration, no downtime
**Solves:**
- Add indexes without locking
- Rename columns safely
- Large table migrations

### data-migration
**Keywords:** backfill, data migration, transform, batch update
**Solves:**
- Backfill new columns with data
- Transform existing data
- Migrate between column formats

### async-configuration
**Keywords:** asyncpg, async engine, env.py async, run_async_migrations
**Solves:**
- Configure Alembic for async SQLAlchemy
- Run migrations with asyncpg
- Handle existing event loop conflicts

Overview

This skill provides battle-tested Alembic migration patterns for SQLAlchemy 2.0 async applications. It covers async env setup, autogenerate guidance, zero-downtime index and NOT NULL changes, and practical rollback strategies. Use it to create safe, reversible database migrations that work with asyncpg and FastAPI lifecycles.

How this skill works

It supplies templates and decision patterns: an async-ready env.py, migration templates for create/upgrade/downgrade, and recipes for running async code in migrations. It documents two-phase changes (add nullable, backfill, then enforce NOT NULL), concurrent index creation outside transactions, and programmatic migration techniques for FastAPI. It also explains autogenerate workflows and commands for review and application of migrations.

When to use it

  • Creating or modifying tables and columns for async SQLAlchemy apps
  • Autogenerating revisions from model changes and reviewing produced SQL
  • Implementing zero-downtime changes on large production tables (indexes, renames)
  • Backfilling and transforming data as separate, safe migration steps
  • Running Alembic in environments where an event loop is already present (FastAPI)

Best practices

  • Initialize Alembic with the async template and point target_metadata to your Base
  • Keep one logical change per migration file for easy rollback and debugging
  • Always review autogenerate output before applying in production
  • Use two-phase approach for NOT NULL additions: add nullable, backfill, then alter in a later deploy
  • Create large-table indexes with CREATE INDEX CONCURRENTLY and disable transaction for that migration

Example use cases

  • Add a UUID users table and unique email index with upgrade and downgrade implemented
  • Autogenerate a revision after model changes, inspect SQL, then apply with alembic upgrade head
  • Add org_id column safely: nullable add, backfill script, later alter_column to NOT NULL
  • Create an index concurrently on a multi-tenant users table to avoid locks
  • Run programmatic migrations within a running FastAPI app using connection.run_sync to avoid event loop conflicts

FAQ

Can Alembic run async migrations directly?

Alembic migration functions are synchronous, but you can run async code via provided patterns: use an async env.py that drives an async engine or use sqlalchemy.util.await_only for single async calls in a migration.

How do I avoid locking when adding an index on a large table?

Use CREATE INDEX CONCURRENTLY and ensure the migration runs outside a transaction block. Configure Alembic to disable per-migration transactions for that file.