Resolving schema.rb Merge Conflicts

Two pull requests each added a migration, each regenerated db/schema.rb, and the second one to merge now has a conflict on the very first line — ActiveRecord::Schema[7.1].define(version: 2026_09_18_101500) versus version: 2026_09_17_164200 — plus conflicting hunks inside create_table "orders". It is tempting to resolve it by hand, picking lines until it parses. That is how a schema.rb ends up describing a database no migration ever produced: a column from one branch missing, an index from the other duplicated, a version that makes db:schema:load skip a migration. Because new environments and CI load the schema dump rather than replaying migrations, a wrong dump silently diverges every fresh database from production. This guide shows the correct resolution, and how to make the dump impossible to get wrong. It rounds out Rails Active Record Migrations.

Hand-Edited vs Regenerated Dump Two panels. Hand-edited resolution: pick the higher version, merge create_table hunks manually, risk of missing or duplicated columns and indexes, dump no longer matches what migrations produce. Regenerated: take either side, load the pre-merge schema, run db:migrate so all migrations apply, and let Rails rewrite schema.rb. Hand-Edited vs Regenerated Dump Hand-merged pick the higher version line merge create_table hunks by eye column order and indexes guessed dump ≠ what migrations produce silent divergence in new environments Regenerated resolve to the main-branch version db:schema:load, then db:migrate Rails rewrites schema.rb CI checks dump is unchanged dump always matches migrations
The dump is a build artifact of the migrations; regenerate it instead of merging it by hand.

Symptom / Error Signatures

The conflict itself is obvious:

<<<<<<< HEAD
ActiveRecord::Schema[7.1].define(version: 2026_09_18_101500) do
=======
ActiveRecord::Schema[7.1].define(version: 2026_09_17_164200) do
>>>>>>> feature/order-region

The consequences of resolving it badly are less obvious and appear later:

  • bin/rails db:schema:load produces a database that lacks a column or index that exists in production, and tests pass locally but fail in production (or the reverse).
  • After a fresh setup, bin/rails db:migrate:status shows a migration as down that production has applied — or up although its changes are absent — because the version in the dump was set wrongly.
  • The CI step that regenerates the dump produces a diff on every build.

Root Cause Analysis

db/schema.rb is generated by bin/rails db:schema:dump (run automatically after db:migrate) by inspecting the current database. Its version argument is the latest migration version applied, and when the dump is loaded Rails inserts all migration versions up to that one into schema_migrations, assuming their effects are in the dump. Two branches that each ran migrations produce two dumps, each reflecting only its own branch’s changes plus the common base. Neither is correct after the merge: the correct dump reflects all migrations.

Hand-merging tries to compute that union by reading two text files. It fails on details — column order within create_table, index names, check_constraint blocks, foreign keys — and it fails silently, because schema.rb stays valid Ruby. Regenerating computes the union by actually running the migrations, which is the only reliable way. With structure.sql (config.active_record.schema_format = :sql) the same applies, and conflicts are worse because the file is a raw pg_dump, including the INSERT INTO schema_migrations list at the end.

Artifact Conflicts on Correct resolution
schema.rb define(version:) latest version per branch regenerate; result is the highest version
schema.rb table blocks columns and indexes per branch regenerate
structure.sql body DDL per branch regenerate
structure.sql version list INSERT INTO schema_migrations rows regenerate
migration files never, unless same file name keep both
Regenerating the Dump After a Merge Five steps. Resolve the conflict by taking the main branch's schema.rb; load it into a clean local database; run db:migrate so this branch's migrations apply; Rails rewrites schema.rb including both branches' changes; commit and let CI verify the dump is stable. Regenerating the Dump After a Merge STEP 1 Take main's dump git checkout --theirs STEP 2 Load clean DB db:schema:load STEP 3 Run migrations db:migrate (this branch) STEP 4 Dump rewritten schema.rb has both STEP 5 CI stability check git diff --exit-code
Running the migrations is what computes the true union of both branches — the text merge never can.

Immediate Mitigation

1. Resolve the conflict by taking the target branch’s dump. During a rebase or merge onto main, take main’s version of the file — which includes everything already merged — and discard your branch’s.

# Shell · feature branch, mid-merge from main · no database changes yet
# WARNING: in a rebase, --ours and --theirs are swapped; check which side is main before choosing.
git checkout --theirs db/schema.rb      # during `git merge main`: take main's dump
git add db/schema.rb

2. Rebuild a local database from that dump and apply your branch’s migrations. Rails re-dumps automatically after db:migrate.

# Shell · local development database only · drops and recreates it
# WARNING: destroys local development data.
bin/rails db:drop db:create db:schema:load
bin/rails db:migrate
git diff --stat db/schema.rb   # should show only your branch's additions and the new version

3. Commit the regenerated dump. Review the diff: it should add exactly the columns, indexes and constraints your migrations create, and the version should be your newest migration’s timestamp (or main’s, if it is higher).

4. If a wrong dump was already merged, fix it the same way. Check out main, load the dump into a clean database, run db:migrate, and compare with a database built by running all migrations from scratch; commit the corrected dump.

Permanent Fix / Long-Term Pattern

Treat the dump as a generated artifact with a check, not as source code. Add a CI step that rebuilds the dump from migrations and fails if it differs from the committed file, so a hand-edited or stale dump can never merge:

# YAML · CI job · PostgreSQL service container matching production's version
- name: Verify schema dump matches migrations
  run: |
    bin/rails db:create
    bin/rails db:schema:load
    bin/rails db:migrate
    git diff --exit-code db/schema.rb db/structure.sql
  env:
    RAILS_ENV: test

Reduce the frequency of conflicts, too. Timestamped migration names already avoid file-name collisions; the remaining conflict is the version line, which is unavoidable but trivial once regeneration is the habit. Some teams add a .gitattributes merge driver for db/schema.rb that always takes one side and relies on the CI check plus a local db:migrate to correct it. Periodically compare a database built from all migrations with one loaded from the dump, and with production itself, to catch drift that slipped past review — the approach in detecting production schema drift against a desired state. The version-ordering side of the same problem, for tools with sequential numbers, is covered in resolving migration version conflicts during merges.

Keeping the Dump Honest in CI Pipeline. Load the committed dump; run all pending migrations; a gate fails if the regenerated dump differs from the committed one; a scheduled job compares a from-scratch migration build with the dump; a gate fails on differences. Keeping the Dump Honest in CI Load dump db:schema:load Run migrations db:migrate diff dump unchanged? Nightly rebuild all migrations from zero equal matches dump? commit regenerated dump fix drift fail
One check on every pull request and one scheduled comparison keep the dump, the migrations and production in agreement.

Verification Checklist

Frequently Asked Questions

Can I just keep the higher version number and merge the rest by hand? You can, but it is error-prone and the mistakes are silent. The version line is only one part of the conflict; the table blocks must also reflect both branches exactly. Regenerating by running migrations produces the correct union every time.

Why does db:schema:load skip migrations? Loading the dump inserts every migration version up to the dump’s version into schema_migrations, on the assumption that their effects are in the dump. If the version is too high or the dump is missing a change, those migrations are marked applied without their effects existing.

Should I switch to structure.sql to avoid this? Not for conflicts — structure.sql conflicts are usually larger. Switch to it when you need database features schema.rb cannot express, such as triggers, custom types or complex partial indexes, and apply the same regenerate-and-check discipline.

What about conflicts in schema_migrations in production? There are none: production’s schema_migrations table records what actually ran. Merge conflicts only affect the committed dump, which is why the dump must be regenerated rather than trusted as a merge result.