Skip to main content

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:

  1. Pick one migration to keep the column definition.
  2. Remove it from the other. Edit 003b_audit.py to not add is_admin.
  3. 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

  1. 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',)
  1. 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
  1. 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'
  1. Avoid long-lived branches. Merge frequently to reduce conflicts. If two branches both modify the same table, conflicts are likely.

  2. 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_labels to 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.

Further Reading