Writing migrations¶
Alembic is configured with two branches — schema and general (data). See Architecture → Migrations for why, and alembic.ini for the configuration.
Apply migrations¶
The plural heads is important — it advances both branches. The running server does this automatically on startup via the lifespan hook in server/main.py.
Create a schema migration¶
For DDL changes (new tables, columns, indexes, FKs):
PYTHONPATH=. alembic revision --autogenerate \
-m "Add foo column to bar" \
--head=schema@head \
--version-path=migrations/versions/schema
Alembic diffs the SQLAlchemy models in server/db/models.py against the live schema to produce an autogenerated migration. Always read the generated file before committing — autogenerate misses enum changes, check constraints, and server defaults, and can produce destructive ops that need manual review.
Create a data migration¶
For seed data, backfills, and enum fixes:
Without the --head / --version-path flags this lands in migrations/versions/general/ on the data branch. The generated file is effectively empty — you fill in upgrade() and downgrade() by hand. Use raw SQL via op.get_bind() when SQLAlchemy ORM churn would bloat the migration:
from alembic import op
def upgrade() -> None:
conn = op.get_bind()
conn.execute("UPDATE orders SET language = 'en' WHERE language IS NULL")
def downgrade() -> None:
# Irreversible — document why.
pass
Review checklist¶
Before merging a migration:
- Ran locally against a populated
shopdatabase (not just a fresh one). - Verified
alembic upgrade headssucceeds from the previousheads. - Verified
alembic downgradeto the prior revision if the change is reversible. - Checked for accidental cross-branch dependencies (schema depending on data, or vice versa).
- Added a corresponding SQLAlchemy model change if it's a schema migration.