Chapter 4: branches happen when team members both add migrations from the same parent. Alembic supports merging; ideally avoid the situation.

How branches happen

Two devs check out main; both add migrations from rev abc123:

main:  abc123
        ├── def456 (Alice's migration)
        └── ghi789 (Bob's migration)

After both merge to main, the migration history has two heads.

Detecting

alembic heads

If multiple heads: branched.

alembic upgrade head fails

alembic upgrade head
# error: multiple heads

Can’t pick which to upgrade to.

Merging

alembic merge -m "merge X+Y" def456 ghi789

Creates a merge revision that has both parents. Subsequent migrations descend from the merge.

abc123
├── def456 ─┐
│           ├── merge (j1k2l3)
└── ghi789 ─┘

alembic upgrade head now picks the merge.

When to merge vs rebase-equivalent

You can rewrite (delete one of the conflicting revisions and re-apply on top of the other) before pushing. Once shared / applied to a DB: don’t.

For internal team: rebase locally; merge if shared. For OSS: merge always.

Avoiding branches

In team workflows:

  1. Communicate when adding migrations.
  2. Pull main before generating.
  3. Rebase if your branch is behind.
  4. Quick reviews so migrations land fast.

For frequent migration churn: a small lock is often cheaper than the merge cost.

Down revision

Each migration has down_revision (the parent) and optionally revision:

"""add users table

Revision ID: abc123
Revises: 
"""
revision = "abc123"
down_revision = None  # initial

For a merge:

revision = "j1k2l3"
down_revision = ("def456", "ghi789")  # tuple = merge

Branch labels

# branches=("api",)
revision = "abc123"
down_revision = "previous"
branch_labels = ("api",)

Tag a revision with a label. Useful for multi-app monorepos.

Multiple heads on purpose

For multi-app where each has its own migration tree:

# api branch
revision = "api_001"
down_revision = None
branch_labels = ("api",)

# admin branch
revision = "admin_001"
down_revision = None
branch_labels = ("admin",)

Then alembic upgrade api@head upgrades only the api branch.

For most apps: stay linear. Multi-head intentional only for multi-app monorepos.

Conflict resolution

If both Alice and Bob added email column:

# Alice's migration
op.add_column("users", sa.Column("email", sa.String(255)))

# Bob's migration
op.add_column("users", sa.Column("email", sa.String(255)))

Whichever runs second fails.

Resolution: review both; combine into one (if they’re the same change); rebase one off the other.

Merge migration content

Often merges are no-op:

def upgrade():
    pass

def downgrade():
    pass

The merge is just a structural placeholder.

If the two branches conflict (e.g., both renamed a column to different names), you might need real reconciliation logic in the merge.

Linear history

For most teams, aim for linear:

A -> B -> C -> D -> E

Easy to read; easy to roll back.

For really busy teams: branches are acceptable but communicate.

CI gate

- name: Check single head
  run: |
    if [ $(alembic heads | wc -l) -gt 1 ]; then
      echo "Multiple heads detected; merge required"
      exit 1
    fi

CI fails on multiple heads. Forces explicit merge.

Common mistakes

1. Force-pushing migrations

Already-deployed revisions can’t be reordered. Don’t force-push migration history.

2. Reusing revision IDs

Generate fresh; don’t copy.

3. Manually editing down_revision

Sometimes necessary; usually causes confusion. Let Alembic handle.

4. Merging without understanding both sides

Empty merge when conflict exists. Read both migrations; reconcile.

5. Branches in solo projects

You’re working alone; you have branches. Probably forgot to git pull. Avoid.

What’s next

Chapter 5: online schema changes (Postgres specifics).

Read this next


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 .