Alembic Branching: Managing Parallel Migration Paths
In teams where multiple developers create migrations on different branches, you face a problem: developer A creates migration 003_add_admin_role.py on branch feature/admin, while developer B creates 003_add_audit_logging.py on branch feature/audit. When you merge both into main, you have two 003_* files with different content. Alembic's branching feature lets you manage these scenarios safely using branch_labels and explicit merge points.
The Branching Problem
Without branching, sequential revisions look like:
001_initial_schema -> 002_add_users -> 003_add_posts -> head
With parallel development, you get a divergence:
-> 003_add_admin_role -> 003_add_admin_role
/
001_initial -> 002_add_users
\
-> 003_add_audit_logging -> 003_add_audit_logging
Both 003_* revisions have the same parent (002_add_users) but different content. Alembic doesn't know which should run first or how to order them.
Creating a Branched Migration
When you branch off main to develop a feature, create a new migration on your branch:
# On feature/admin branch
alembic revision --autogenerate -m "Add admin role table"
This creates, e.g., 003_add_admin_role.py with down_revision = '002_add_users...'.
Meanwhile, on main, someone else creates:
# On main branch (or different feature branch)
alembic revision --autogenerate -m "Add audit logging"
Creating 003_add_audit_logging.py with the same parent. When you try to merge:
git merge feature/admin
You get both migrations in your versions/ folder. Running alembic upgrade head fails because Alembic can't determine the order.
Using Branch Labels to Resolve Conflicts
Alembic's solution: mark revisions with branch_labels. For example:
# migrations/versions/003_add_admin_role.py
revision = '003a_admin'
down_revision = '002_base'
branch_labels = ('admin_branch',) # Mark this as part of admin_branch
# migrations/versions/003_add_audit_logging.py
revision = '003b_audit'
down_revision = '002_base'
branch_labels = ('audit_branch',) # Mark this as part of audit_branch
Now Alembic knows these are on different branches. You can selectively apply one branch:
alembic upgrade admin_branch
# Applies only the admin_branch revisions
alembic upgrade audit_branch
# Applies only the audit_branch revisions
Merging Branches
To merge both branches, create a new migration that depends on both:
alembic revision -m "Merge admin and audit branches"
Edit the generated file:
# migrations/versions/004_merge_admin_audit.py
revision = '004_merge'
down_revision = ('003a_admin', '003b_audit') # Depends on BOTH parents
branch_labels = None # Back on main trunk
depends_on = None
The down_revision is now a tuple instead of a string, indicating a merge commit (like Git). When you run:
alembic upgrade head
Alembic applies both 003a_admin and 003b_audit in any order (they're independent), then applies 004_merge.
After the merge, the migration graph looks like:
-> 003a_admin --------\
/ \-> 004_merge -> head
001_initial -> 002_base /
\ /
-> 003b_audit --------/
Conditional Upgrades by Branch
For testing, you might want to upgrade only specific branches:
# Apply all admin-related migrations only
alembic upgrade admin_branch
# Apply all audit-related migrations only
alembic upgrade audit_branch
# Apply everything (all branches + merges)
alembic upgrade head
This is useful if your app supports feature flags and needs to run migrations selectively.
Handling Conflicting Column Names
If both branches add a column with the same name, merging becomes problematic. After merging, you'd have two migrations trying to add the same column:
# 003a_admin.py
op.add_column('users', sa.Column('is_admin', sa.Boolean))
# 003b_audit.py
op.add_column('users', sa.Column('is_admin', sa.Boolean)) # Conflict!
The second migration fails. To resolve:
- Pick one migration to keep the column definition.
- Remove it from the other. Edit
003b_audit.pyto not addis_admin. - Add the column in the merge commit if neither had it.
Example fix:
# 003b_audit.py (modified to avoid conflict)
def upgrade():
op.add_column('audit_logs', sa.Column('table_name', sa.String(100)))
# Don't add is_admin; 003a_admin already does it
def downgrade():
op.drop_column('audit_logs', 'table_name')
Best Practices for Branching
- Create a feature-specific branch label for long-lived feature branches:
# On feature/payments branch
revision = '003_add_payment_tables'
down_revision = '002_base'
branch_labels = ('payments',)
- Test both branches independently before merging:
# Test admin branch on a copy of your database
alembic stamp 002_base # Reset to base
alembic upgrade admin_branch
# Run tests
alembic downgrade base
# Test audit branch
alembic stamp 002_base
alembic upgrade audit_branch
# Run tests
alembic downgrade base
# Test merged state
alembic stamp 002_base
alembic upgrade head # Both branches + merge
# Run all tests
- Use descriptive revision IDs to track branches:
# Good: clearly identifies the branch
revision = '003_admin_add_roles'
branch_labels = ('admin_feature',)
# Avoid: ambiguous or auto-generated
revision = 'a1b2c3d4e5f6'
-
Avoid long-lived branches. Merge frequently to reduce conflicts. If two branches both modify the same table, conflicts are likely.
-
Document branch intent. In the migration docstring, explain why the branch exists:
"""Add admin role and permissions tables
This migration is part of the admin_feature branch.
Merge point: must be merged with any audit_feature
changes before deploying to production.
"""
Advanced: Conditional Branch Selection
In CI/CD, you might apply different migrations based on configuration:
# env.py
import os
target_branch = os.getenv('ALEMBIC_BRANCH', 'head')
# Use this in your deployment script:
# ALEMBIC_BRANCH=payments alembic upgrade payments
# ALEMBIC_BRANCH=head alembic upgrade head
Viewing the Branch Graph
Alembic doesn't have a built-in graph visualization, but you can see the structure with:
alembic history -v
For complex branch structures, export to a tool like Graphviz:
alembic history --rev-range=base:head -v > migrations.txt
# Manually convert to Graphviz or use a custom script
Key Takeaways
- Use
branch_labelsto mark revisions that are part of parallel development efforts - Create merge commits with
down_revision = (parent1, parent2)to join branches - Test each branch independently before merging to ensure no conflicts
- Resolve naming conflicts in the merge commit by picking one definition and removing duplicates
- Keep branches short-lived; merge frequently to reduce conflict complexity
- Document branch purpose in migration docstrings for team clarity
Frequently Asked Questions
Can I have more than two parent revisions in a merge?
Yes. The down_revision tuple can contain any number of parents: down_revision = ('001', '002', '003'). Alembic applies all parents before the merge commit, in any order (they're assumed independent).
What if I forget to create a merge commit?
Alembic will detect multiple heads (end points of branches) when you run alembic upgrade head and fail with an error listing the ambiguous heads. Create a merge commit immediately to resolve.
Can I downgrade from a merged state back to one branch?
Partially. alembic downgrade base_revision will roll back to a specific revision, but if that revision is behind the merge point, you'll lose migrations from the other branch. Plan your merges carefully or test rollback scenarios locally first.
Should I merge branches before or after deployment?
Merge on a feature branch, test the merged migrations thoroughly on staging, deploy the merged migrations. Never merge to main, deploy, then merge in production—that risks applying untested merged migrations in production.