Alembic Migrations — Evolving the Schema Safely
IntermediateAlembic versions your schema like git versions code: autogenerate diffs models against the DB, you review the script, alembic upgrade head applies it — and create_all never touches production.
Overview
Base.metadata.create_all() creates missing tables but will never alter an existing one — add a column to a model and production silently doesn't have it. Alembic (SQLAlchemy's migration tool) fixes this with versioned migration scripts: each has upgrade() and downgrade(), forming a chain the alembic_version table tracks per database. The daily loop is change model → alembic revision --autogenerate → review the script → alembic upgrade head. Review is not optional: autogenerate sees add/drop but misreads renames as drop+add (data loss) and misses server defaults for NOT NULL backfills. In deployment, migrations run as a release step before new code starts, and risky changes ship in two deploys — expand, migrate data, then contract.
Setup and the Daily Loop
Point env.py at your models' metadata and your real database URL; from then on it is revision → review → upgrade. The migration scripts are code — committed, reviewed in PRs, never edited after they've run anywhere shared.
pip install alembic
alembic init alembic # creates alembic/ + alembic.ini
# ── alembic/env.py — the two lines that matter ──
from app.core.db import Base
from app.models import student, offer # import ALL model modules (registers tables)
target_metadata = Base.metadata
# and set the URL from settings, not alembic.ini:
# config.set_main_option("sqlalchemy.url", get_settings().database_url)
# ── the daily loop ──
# 1. Edit the model: add backlogs: Mapped[int] = mapped_column(default=0)
# 2. Generate:
alembic revision --autogenerate -m "add backlogs to students"
# 3. REVIEW alembic/versions/9f2c_add_backlogs_to_students.py:
def upgrade():
op.add_column("students",
sa.Column("backlogs", sa.Integer(), nullable=False,
server_default="0")) # ← you often ADD this by hand:
# NOT NULL on a full table needs a default
def downgrade():
op.drop_column("students", "backlogs")
# 4. Apply:
alembic upgrade head
# Everyday commands:
alembic current # what version is this DB on?
alembic history # the chain of migrations
alembic downgrade -1 # step back one (dev only, usually)What Autogenerate Gets Wrong + Production Discipline
Autogenerate is a diff, not a mind reader. Renames, data backfills, and some type changes need hand-written operations — and production migrations have their own rules of engagement.
# 1. RENAME: you renamed cgpa → gpa. Autogenerate emits:
# op.drop_column("students", "cgpa") ← DATA GONE
# op.add_column("students", sa.Column("gpa", sa.Float()))
# Fix by hand:
def upgrade():
op.alter_column("students", "cgpa", new_column_name="gpa")
# 2. DATA MIGRATION: new column derived from old data —
def upgrade():
op.add_column("students", sa.Column("email_domain", sa.String(80)))
op.execute(
"UPDATE students SET email_domain = split_part(email, '@', 2)"
)
op.alter_column("students", "email_domain", nullable=False)
# 3. Production rules:
# - migrations run BEFORE new code serves traffic:
# fly.toml → [deploy] release_command = "alembic upgrade head"
# - never edit a migration that has run on any shared DB — add a new one
# - big/risky changes = expand → migrate → contract across TWO deploys:
# deploy 1: add nullable column, write to both old+new
# backfill: batched UPDATEs (not one giant locking UPDATE)
# deploy 2: drop the old column once nothing reads it
# - index on a busy Postgres table: CREATE INDEX CONCURRENTLY
# (needs op.execute + autocommit block — plain create_index locks writes)Key Points to Remember
- 1create_all never ALTERs — schema changes go through Alembic, always
- 2Loop: model change → revision --autogenerate → REVIEW → upgrade head
- 3Autogen misreads renames as drop+add and forgets NOT NULL server defaults
- 4Run migrations as a release step; risky changes = expand → migrate → contract
Interview Questions
Sign in to ask AriaWhy is Base.metadata.create_all() insufficient once you are in production?
You renamed a column — what does autogenerate produce and why is it dangerous?
Add a NOT NULL column to a table with 50 million rows, zero downtime — your plan?
Ask Aria about Alembic Migrations — Evolving the Schema Safely
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.