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:
- Communicate when adding migrations.
- Pull main before generating.
- Rebase if your branch is behind.
- 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 .