Cheatsheet for Alembic setup. Long-form: textbook .
Install + init
uv add alembic
alembic init -t async migrations # async template
# or
alembic init migrations # sync
Layout:
migrations/
├── env.py
├── README
├── script.py.mako
└── versions/
alembic.ini
alembic.ini key settings
[alembic]
script_location = migrations
sqlalchemy.url = # set from env.py
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
transaction_per_migration = false # CONCURRENTLY ops won't be in transaction
env.py (async, SQLAlchemy 2.0)
import asyncio
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
# Import models so Base.metadata is populated
import src.app.models # noqa
from src.app.db import Base
from src.app.settings import settings
config = context.config
config.set_main_option("sqlalchemy.url", settings.database_url)
target_metadata = Base.metadata
def do_run_migrations(connection):
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
include_schemas=False,
)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
connectable = async_engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
)
async with connectable.connect() as conn:
await conn.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
raise NotImplementedError("offline mode not supported")
else:
asyncio.run(run_migrations_online())
env.py (sync)
from alembic import context
from sqlalchemy import engine_from_config, pool
from src.app.db import Base
import src.app.models # noqa
config = context.config
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as conn:
context.configure(connection=conn, target_metadata=target_metadata, compare_type=True)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Naming conventions (in Base.metadata)
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Without these: ck_1-style names.
URL from env
config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
Or from Settings (preferred).
Verify setup
alembic current # current revision
alembic heads # head revisions
alembic history # all revisions
First migration
alembic revision --autogenerate -m "initial schema"
# review the file
alembic upgrade head
Common mistakes
- Models not imported in env.py — empty autogenerate.
compare_type=False(default) — type changes missed.- Sync env.py for async engine — fails.
- Hard-coded URL in alembic.ini instead of from env.
Read this next
If you want my async env.py + naming convention template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .