Why you should (almost) never rename your Rails database migrations

Who hasn’t seen this before?!

Rails database migrations are a common source of confusion when working in large — or multiple — development teams. In particular they are a common source of Git merge conflicts.

How many times have you gone to merge your pull request— approved by a selection of your peers — only to find you have a db/schema.rb conflict? It’s very annoying right?

And how many times has someone snuck in a migration with a version dated later than yours…or you’ve rebased onto the latest version of main only to discover a newer migration than yours?

What do you do? If your answer to this question is ‘rename my migration to use a more recent date’, then please STOP! (And read on…)

ActiveRecord Migrations follows a sophisticated set of protocols that help ensure your databases are as consistent as reasonably possible across multiple environments. Renaming a migration interferes with these protocols.

If you deploy a migration to any environment outside your local machine; a test server for example, or another developer’s machine (perhaps you are pairing, or perhaps another developer runs your migration on their machine to test it), then you should not change the version number of that migration in future.

Because if you do rename your migration you will break all of those environments on which it has already run. The whole point of version numbers in ActiveRecord Migrations is to ensure every environment can determine if a specific migration has been run upon its database or not.

The fact that version numbers are chronological is incidental and of no significance when choosing if a migration should be run or not.

When you run db:migrate, Rails will check every single migration in the db/migrate folder against a schema_migrations table to see if that migration has been run on the target database or not. It doesn’t matter when that migration was created or what version number it has. If the version number is not recorded in schema_migrations then Rails will attempt to run the migration.

When you change a version number you break the system. Because any environment on which your migration has already been run has no way to know it, because the version number on which it depends has changed. It will try to run the migration again and probably break, possibly with undesirable consequences.

But what about the version in db/schema.rb?

ActiveRecord::Schema.define(version: YYYY_MM_DD_HHMMSS)

This is where I think most of the confusion — and indeed merge conflicts — are caused.

This version number has nothing to do with what migrations will be run on a given environment with db:migrate.

The schema.rb file is there to provide a snapshot of the structure of the entire database so that it can be restored using db:schema:load. When a database is created using db:schema:load, Rails will use this version number to make an assumption about what migrations db/schema.rb ‘includes’.

Rails assumes that db/schema.rb describes the state of the database after running all migrations up to version. In this case, the chronological order of migrations is significant. Wherever your migration is in date order, it doesn’t matter as long as its outcome is correctly represented in schema.rb and its version is equal to or before the version defined here. But this only matters when using db:schema:load. If you are iteratively migrating your database using db:migrate then this version number is largely irrelevant.

So how do I handle these merge conflicts?

Summary

If you follow these simple rules, you’ll hardly ever need to think about database migration versions:

  • Never rename (the version number part of) a migration once you’ve committed it to your shared repo.
  • When you commit your migration, make sure its outcome is correctly represented in db/schema.rb (by running db:migrate locally and committing the changes to your repo).
  • Ensure ActiveRecord::Schema.define(version: YYYY_MM_DD_HHMMSS) in db/schema.rb is always set to the the most recent version represented therein.

What can go wrong?

Most of the time, nothing. There are two scenarios that could occur when things go out of sync:

  • You build your database using db:schema:load, but version is set to the wrong migration. If this occurs and you subsequently run db:migrate on that database, Rails may try to run a migration that is already present in the schema.
  • You build your database using db:schema:load, but there is a migration with a version before or equal to the one specified in version whose outcome is not reflected in schema.rb. In this case, running db:migrate will not run the migration, because Rails will have assumed the migration has been run already.

However, if you follow the simple rules above, this will (almost) never happen.

--

--

Tech Lead at Oyster

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store