Home >

Fix Partial Rails Migration

23 Dec 2014

Using Rails migrations with databases that do not support DDL Transactions like Oracle and MySQL can be a pain. Recently, on project where we are using Postgres for development and Oracle in production, I ran into a case where I needed to run a conditional migration to fix schema for production.

The Problem

While using ActiveRecord does help to make switching databases easier there are some database specific constraints that it doesn't completely solve.

For example, Oracle has a limit on the number of characters used for an index name, 30, and limits you to one index per column.

Because we have to manually specify index names in migrations to get them under 30 characters it's possible to make mistakes like the following, specifying two indexes for the same column.

class AddPostIndexes < ActiveRecord::Migration
  def change
    add_index :posts, :author_id, { name: 'post_author_idx' }
    add_index :posts, :author_id, { name: 'post_editor_idx' }
  end
end

In Postgres this is fine as you can have multiple indexes for the same column. But in Oracle the migration will fail when it tries to add the second index.

Solution

To fix this problem from happening in the future we can edit the migration to be correct.

class AddPostIndexes < ActiveRecord::Migration
  def change
    add_index :posts, :author_id, { name: 'post_author_idx' }
    add_index :posts, :editor_id, { name: 'post_editor_idx' }
  end
end

However, that doesn't solve the issue of the local development databases that are already incorrect. To solve this we'll need to use a conditional migration to remove the old index.

Conditional Migration

Thankfully ActiveRecord gives us a way to query the indexes of each table and we can use this information to write a migration that will remove the incorrect post_editor_idx if it exists and allow us to add the right index.

class FixPostIndexes < ActiveRecord::Migration
  def up
    indexes = ActiveRecord::Base.connection.indexes(:posts)
    if indexes.map { |i| i.name }.include?('post_editor_idx')
      remove_index :posts, { name: 'post_editor_idx' }
    end
    add_index :posts, :editor_id, { name: 'post_editor_idx' }
  end

  def down
    remove_index :posts, { name: 'post_editor_idx' }
  end
end

With this solution we can now have other developers run the new migration and their local development database will be up to date. If we had just changed the already run migration AddPostIndexes then other developers would have needed to drop and recreate their databases from scratch.

ActiveRecord also gives a method to query the table columns in the same way as indexes.