BranchDb

Gem Version Build Status License: MIT

Automatic per-branch PostgreSQL databases for Rails development.

BranchDb eliminates database migration conflicts by giving each git branch its own isolated database. Switch branches freely without worrying about schema mismatches or losing development data.

Table of Contents

The Problem

Working on multiple feature branches with different migrations causes pain:

# On feature-a branch: Add a 'status' column
rails generate migration AddStatusToUsers status:string
rails db:migrate

# Switch to feature-b branch
git checkout feature-b
git status
# => modified: db/schema.rb   <- Contains 'status' column from feature-a!

# Now your schema.rb has changes that don't belong to this branch
# Accidentally commit it? You've just mixed schema changes across branches
# Run db:migrate? Schema.rb still shows the foreign column

More complex migrations could cause the code to stop working like renaming tables, adding foreign keys, or removing columns!

The Solution

BranchDb automatically manages separate databases for each branch:

main branch         → myapp_development_main
feature-auth        → myapp_development_feature_auth
feature-payments    → myapp_development_feature_payments
bugfix-login        → myapp_development_bugfix_login

Each branch has its own isolated database with its own schema and data. Switch branches, restart your server, and you're working with the right database automatically.

Installation

Add BranchDb to your Gemfile:

group :development, :test do
  gem 'branch_db'
end

Install and run the generator:

bundle install
rails generate branch_db:install

Update your config/database.yml:

development:
  <<: *default
  database: <%= BranchDb.database_name('myapp_development') rescue 'myapp_development' %>

test:
  <<: *default
  database: <%= BranchDb.database_name('myapp_test') rescue 'myapp_test' %>

Note: The rescue fallback ensures production/staging environments work correctly since the gem is only loaded in development/test. Replace myapp with your application name.

Initialize your first branch database:

rails db:prepare

Configuration

The generator creates config/initializers/branch_db.rb:

BranchDb.configure do |config|
  # The name of your main/stable branch (default: 'main')
  config.main_branch = 'main'

  # Maximum length for branch name suffix (default: 33)
  # PostgreSQL has a 63 character limit for database names
  # Formula: base_name_length + 1 (underscore) + max_branch_length <= 63
  config.max_branch_length = 33

  # Database name suffixes for cleanup feature (default: '_development', '_test')
  # Customize if your database names use different conventions
  # config.development_suffix = '_development'
  # config.test_suffix = '_test'
end

Configuration Options

Option Default Description
main_branch 'main' Your primary branch name (used as fallback clone source)
max_branch_length 33 Max characters for branch suffix (prevents exceeding PostgreSQL's 63-char limit)
development_suffix '_development' Suffix pattern for development databases
test_suffix '_test' Suffix pattern for test databases

Usage

Daily Workflow

BranchDb enhances Rails' built-in db:prepare command:

# Just use Rails' standard command - now branch-aware!
rails db:prepare

What happens (development only):

  1. Checks if your branch's database exists and has schema
  2. If missing or empty and a parent/main DB exists, clones from the parent branch (or main as fallback)
  3. If on main branch or no source exists: defers to standard Rails behavior
  4. Rails then runs pending migrations and seeds as usual

Test databases use standard Rails behavior (schema load, no cloning):

RAILS_ENV=test rails db:prepare

Note: Cloning only runs in development environment. All commands support Rails' multiple database feature.

Available Commands

Command Description
rails db:prepare Rails' standard command, enhanced with cloning from parent/main
rails db:branch:list List all branch databases
rails db:branch:purge Remove all branch databases except current and main. Use FORCE=1 to skip confirmation.
rails db:branch:prune Remove databases for branches that no longer exist in git. Use FORCE=1 to skip confirmation.

Examples

# Starting work on a new feature branch
git checkout -b feature-new-thing
rails db:prepare  # Clones from parent branch (or main as fallback)
rails server

# Switching to another branch
git checkout feature-other-thing
# Restart your Rails server to connect to the other database
rails server

# Purging all branch databases (keeps current and main only)
rails db:branch:purge
# => Found 5 database(s) to remove:
# =>   - myapp_development_feature_old
# =>   - myapp_test_feature_old
# =>   ...
# => Proceed with deletion? [y/N]

# Pruning databases for deleted git branches only
rails db:branch:prune
# => Found 2 database(s) to remove:
# =>   - myapp_development_merged_feature
# =>   - myapp_test_merged_feature
# => Proceed with deletion? [y/N]

# Skip confirmation prompt (useful for CI/scripts)
FORCE=1 rails db:branch:purge
FORCE=1 rails db:branch:prune

How It Works

Rails Integration

BranchDb enhances Rails' db:prepare task by adding a prerequisite that clones from the parent branch when needed. That means:

  • You keep running rails db:prepare — no new commands to learn.
  • New branch databases are cloned from their parent, falling back to main.
  • Migrations, seeds, and schema dumps work the way Rails normally handles them.

Database Naming

BranchDb generates database names by combining your base name with a sanitized branch name:

Base name:    myapp_development
Branch:       feature/user-auth
Sanitized:    feature_user_auth
Result:       myapp_development_feature_user_auth

Branch names are sanitized: non-alphanumeric characters become underscores, and names are truncated to max_branch_length.

Cloning Process

When db:prepare detects a missing or empty database:

  1. Detect the parent branch to clone from (see below).
  2. Check whether the parent database exists; fall back to main if it doesn't.
  3. If a source exists, create the target database and copy it with pg_dump | psql.
  4. If no source exists, defer to Rails' standard db:prepare (schema load, migrations, seeds).
  5. On the main branch, defer to Rails' standard db:prepare.

Parent Branch Detection

BranchDb figures out which branch you branched from and clones that branch's database, so nested feature branches work:

main → feature-a → feature-a-child

When you create feature-a-child from feature-a, BranchDb will clone from feature-a's database (if it exists), not main.

Detection priority:

  1. BRANCH_DB_PARENT environment variable (explicit override)
  2. Git reflog analysis (finds the last "checkout: moving from X to current-branch")
  3. Configured main_branch (fallback)

Fallback behavior: If the detected parent's database doesn't exist, BranchDb automatically falls back to the main branch database.

Override with environment variable:

# Force cloning from main, even if on a nested feature branch
BRANCH_DB_PARENT=main rails db:prepare

# Clone from a specific branch
BRANCH_DB_PARENT=feature-other rails db:prepare

Overriding the Branch or Database

For cases where you want to point the app at a different database than the auto-derived branch DB (teammate's branch DB, restored dump, shared QA DB, etc.), use one of:

Override the branch — everything else (branch suffix, clone source, purge protection) stays consistent, as if you were literally on that branch:

BRANCH_DB_BRANCH=main bin/rails server

⚠️ Warning: BRANCH_DB_BRANCH makes purge protection follow the override, not your real branch. If you set BRANCH_DB_BRANCH=main and run rails db:branch:purge, your real branch's database will be treated like any other branch DB and dropped. Unset the variable before running destructive tasks.

Override the database name — bypasses branch-based logic entirely. Use one env var per Rails environment:

BRANCH_DB_DATABASE_DEVELOPMENT=shared_dev_db bin/rails server
BRANCH_DB_DATABASE_TEST=shared_test_db      bin/rails test

When BRANCH_DB_DATABASE_<ENV> is set for the current Rails.env, the db:branch:* rake tasks (list, purge, prune, ensure_cloned) are skipped with a log message. BranchDb assumes you're managing the overridden database's lifecycle yourself.

⚠️ Single-database apps only. The database-name override dispatches by base-name suffix (e.g., _development, _test). If your app uses multiple PostgreSQL databases (e.g., primary and analytics), two configs sharing the same suffix will all receive the same override value, which PostgreSQL cannot satisfy. Use BRANCH_DB_BRANCH instead, or wait for per-connection override support.

Precedence: if both BRANCH_DB_BRANCH and BRANCH_DB_DATABASE_<ENV> are set, the database override wins for the connection name; the branch override still affects current_branch for anything else.

Purge Safety

The purge command protects important databases:

  • Current branch's development and test databases
  • Main branch's development and test databases
  • Databases with active connections (skipped with warning)

Requirements

  • Ruby >= 3.2
  • Rails >= 7.0
  • PostgreSQL (any supported version)
  • PostgreSQL client tools in PATH:
    • psql - for database operations
    • pg_dump - for cloning databases
    • dropdb - for purge/prune operations

Verifying PostgreSQL Tools

which psql pg_dump dropdb
# Should output paths for all three tools

If missing, install PostgreSQL client tools:

# macOS
brew install postgresql

# Ubuntu/Debian
sudo apt-get install postgresql-client

# Docker (add to your Dockerfile)
RUN apt-get update && apt-get install -y postgresql-client

Important Notes

Server Restart Required

Database selection happens at Rails boot time (ERB in database.yml is evaluated once). After switching branches, restart your Rails server to connect to the correct database.

git checkout other-branch
# Must restart Rails to use other-branch's database
rails server  # Now connected to myapp_development_other_branch

Detached HEAD State

In detached HEAD state (e.g., git checkout abc123), BranchDb cannot determine a branch name. It falls back to using the base database name without a suffix. All detached HEAD checkouts share this database.

For CI environments, ensure you checkout an actual branch:

# CI script
git checkout $BRANCH_NAME  # Not just the commit SHA
rails db:prepare

Database Name Length

PostgreSQL limits database names to 63 characters. With default settings:

  • Base name: up to 29 characters
  • Underscore: 1 character
  • Branch suffix: up to 33 characters

If your base name is longer, reduce max_branch_length accordingly.

Troubleshooting

"PostgreSQL tool 'X' not found in PATH"

Install PostgreSQL client tools (see Requirements).

"Could not connect to Postgres on port X"

Ensure PostgreSQL is running and accessible:

# Check if PostgreSQL is running
pg_isready -h localhost -p 5432

# For Docker users
docker ps | grep postgres

Database not switching when I change branches

Remember to restart your Rails server after switching branches. The database name is determined at boot time.

Clone is slow for large databases

pg_dump | psql is already efficient, but for very large databases consider:

  • Keeping your main branch database lean
  • Using database-level compression
  • Running cleanup regularly to remove old branch databases

Branch name too long

Long branch names are automatically truncated to max_branch_length (default: 33). Two branches with the same prefix might collide:

feature/very-long-descriptive-name-for-auth    → _feature_very_long_descriptive_na
feature/very-long-descriptive-name-for-payments → _feature_very_long_descriptive_na  # Same!

Use shorter branch names or increase max_branch_length (if your base name is short enough).

Development

Setup

git clone https://github.com/milkstrawai/branch_db.git
cd branch_db
bin/setup

Running Tests

# Run test suite
bundle exec rspec

# Run with coverage report
bundle exec rspec && open coverage/index.html

# Run linter
bundle exec rubocop

# Run both
bundle exec rake

Test Coverage

Coverage thresholds:

  • Line coverage: 100%
  • Branch coverage: 90%

Roadmap

Features we're considering for future releases:

  • [ ] SQLite and MySQL support - Database adapter pattern for non-PostgreSQL databases
  • [ ] Standalone clone task - rails db:branch:clone FROM=branch-name for manual cloning
  • [ ] Post-checkout git hook - Auto-restart Rails server on branch switch (Doable?)
  • [ ] Database info task - rails db:branch:info showing current branch, DB name, size, and parent
  • [ ] Clone progress indicator - Visual feedback for large database clones
  • [ ] Disk usage report - rails db:branch:list --size to show storage per branch

Have a feature request? Open an issue to discuss it!

Contributing

Contributions are welcome. The usual drill:

  1. Fork the repository.
  2. Create a feature branch: git checkout -b feature/amazing-feature.
  3. Commit your changes: git commit -m 'Add amazing feature'.
  4. Push the branch: git push origin feature/amazing-feature.
  5. Open a pull request.

Guidelines

  • Write tests for new features
  • Follow existing code style (RuboCop will help)
  • Update documentation as needed
  • Keep commits focused and atomic

Reporting Issues

Found a bug? Please open an issue with:

  • Ruby and Rails versions
  • PostgreSQL version
  • Steps to reproduce
  • Expected vs actual behavior

License

BranchDb is available as open source under the terms of the MIT License.

Acknowledgments

Inspired by the pain of database migration conflicts and the joy of isolated development environments.