Refactoring Rails Migrations
2017-06-07

I ran into an interesting problem at work today.

Before I came onto the current Rails app at my job, the developer before me had
started the app with just one table.

The Users table!

He then deployed, and promptly deleted the migration.

As you may have heard, early students of Ruby on Rails are taught,

"Never delete or change your migrations!"

But do we know why?

It's because changing history of your migration after the database already has
a representation of the SQL needed to run inside the database, running something
other than what it expects can and will throw an error, and create many
headaches.

There's another gotcha with migrations that some people don't think about.

In order to rebuild a schema from scratch using migrations, each migration needs
to be reversible.

Some migrations, such as those that only contain add_index, can prove to be
difficult to reverse.

I had the pleasant task of fixing the migrations without breaking production.
Yay!

I'm not going to go through each case, as it's very specific the schema I was
working on, but I will enumerate some of the steps that I had to go through, as
well the kicker that inspired this post.

Sometimes Rails won't know how to reverse the change function that is now
default since Rails 4.

You can break it up into def up and def down, and tell the migration
exactly what to do in both scenarios.

This is helpful in the scenario of an index that's orphaned from it's table,
because it was deleted or renamed in an earlier migration.

For example

Migration 1


add_index :users, :username, unique: true


Migration 2


remove_index :users, :username, unique: true
add_index :users, :username, unique: true

If you then try to reverse these migrations, the earlier migration will complain
that there is no such index on users#username!

Solution:

def up
  add_index :users, :username, unique: true
end

def down
  remove_index :users, :username if index_exists?(:users, :username)
end

This takes advantage of a neat method provided by the migrations api:
ConnectionAdapter

Now I come back to the problem introduced by my dear prior developer. Deleting
migrations.

I didn't want every new developer to have to get a the old schema state, and
then run rake db:schema:load, which certain works, but doesn't give me the
confidence that the migrations are stable.

I obtained the original schema state by running all the migrations backwards
(Refactoring each one, as above, as needed)
rake db:migrate VERSION=0

(Version is the little version number that corresponds to the date stamp on the
migration, that always causes merge conflicts in the db/schema.rb Setting it to
0 causes it to look for the earliest migration.)

and then looking at the schema for what existed in there at that point in time.

I then wrote the migration I wish I had.

One problem with this.

Production can't roll back the schema and then migration the new one. I needed
to trick Rails into thinking that it had run this particular migration already!

The way Rails determines if a migration is run is by inserting the timestamp
into a special meta-table in the database called schema_migrations.

You can see it by going into the postgres console (assuming you're using
postgres, cuz, why wouldn't you?)

psql

=# \c database_name

SELECT * FROM schema_migrations;

and Bam! you should see the output of all the timestamps of the migrations.

if you run a rollback, all Rails does is execute a delete on that particular
version number.

The solution was to run a simple insert of the desired version, which, I had
given an impossibly early date in order to run first. (Migrations run in
chronological order)

INSERT INTO schema_migrations VALUES("197010100000")

And that's it! Production thought it had run the migration, development
environments had a completely reversible schema migrations table, and our CI
stopped complaining every time it tried to run a test!

Thanks!