How to Recover From Rails Database Schema Conflicts When Rebasing


This is something which comes up every so often when working on Rails projects with a branching git workflow.

I’ll assume you’re rebasing a feature branch onto develop.

The issue occurs when both develop and your branch contain migrations, leading to conflicts in the schema file - at least one conflict over the ‘version’ declared at the top of the file, and possibly other conflicts.

This ‘version’ number is a timestamp, and will correspond to the filename of the last migration which was added at the time that the schema was generated.

That’s the property that we need to preserve when rebasing:

The version number in the schema file needs to correspond to the migration which was generated last - i.e. the migration with the highest number at the beginning of its filename.

How not to fix it

The temptation is to manually resolve the conflicts by editing the schema.rb file. This risks getting into a state where the committed schema is different to the ‘schema that would be generated by migrating again’.

If that ever happens then at some point someone will run rake db:migrate and get a different schema file. When this happens it’s hard to know what the correct way to resolve this is.

How to fix it

The robust approach is to rebuild the schema at each stage as follows:

  1. Before rebasing, checkout (and pull!) develop and ensure that the database agrees with the version of the schema file on develop. You can do this with rake db:migrate:status. Ideally you should see everything as ‘up’

    • if anything is ‘down’ then most likely you haven’t migrated everything on develop yet, so running rake db:migrate should sort this out.
    • if you see any lines like the following, this means that a migration which has been applied to your database doesn’t exist on develop and is presumably on one of the feature branches. Rails has no way of knowing how to rollback this migration, so you need to switch to whichever branch added that migration and rollback from there.

      up     20170210155150  ********** NO FILE ***********
      
  2. You can now switch to your branch and rebase.

  3. At the first conflict, unstage the schema diff from your new branch:

     git reset HEAD db/schema.rb
    
  4. Discard the schema diff from your new branch:

    git checkout db/schema.rb
    
  5. Rebuild the schema:

    rake db:migrate
    
  6. Check the resulting schema diff against the original diff on your branch (e.g. by looking at the commit on the pull request) - it should be identical

    • …with one exception: if the migration you’re adding was created before any of the migrations on develop, then the version in the schema file won’t change. This is expected: that version should always relate to the most recently generated migration.
  7. That’s it! You can now continue your rebase.

Background

To understand why the above approach works, the following insights might be useful:

  • The schema file is constructed from the database, not directly from the migrations
  • When rake db:migrate is run, checks a schema_migrations table in the database (which contains a list of the timestamps of migrations which have been run on this database) and compares that against the db/migrations table:. It then runs any migrations which haven’t been run yet, add adds the timestamps of those migrations into the schema_migrations table, and rebuilds the schema
  • rake db:migrate:status shows the difference between schema_migrations and the db/migrations directory, but without running anything.
  • Schemas in general are mostly reliably built in the same way even if some migrations end up being run out of order, because e.g new indexes are added in alphabetical order, not in migration order.