Home

Feb 28

List pending production migrations in Rails

  • ruby
  • rails
  • scripts

Typically, you’ll know what migrations need to be run on the production database on deploy. You can check on a production server with rake db:migrate:status | grep down, but that requires the server to have been deployed with the migrations that need to be run. It’s much more convenient to be able to see what migrations in your local project have yet to be run on the production database.

Logger output

I’ve found it very useful to run this script before - or after - a production deploy, so I don’t miss any migrations that need to be run. You’ll probably need to customize some of the code for your environment, since it depends on how your production environment is set up, but any such adjustments should be relatively minor.

The basic idea

The script is set up in two pieces. The first piece runs on a remote (production) server, and acts as a tap into the remote database - returning a list of migrations that have been run there. The second piece runs on the local machine, where it compares the list from the remote server to the local migration files. The filenames of any of the local migration files that aren’t in the remote list will be printed to the console.

A fast script

This script should execute quickly - both on the remove server and locally. Loading a large Rails environment can add significant overhead - particularly since it would have to be loaded twice - so the script loads the smallest portion of Rails that it needs and avoids loading anything else. In this case, all that’s needed in the remote script is ActiveRecord:

require 'active_record'

In my environment, I also need to require 'seamless_database_pool', since that’s the database adapter I’m using. You may need to require whatever database adapter your production database.yml specifies.

Listing migrations that have been run

Migration version numbers are stored in a schema_migrations table as the values in the version column. There’s a convenience method to get all the versions recorded in the schema_migrations table: ActiveRecord::Migrator.get_all_versions.

In an executable file named script/deploy/list_migrations, add:

#!/usr/bin/env ruby

require 'active_record'

rails_root = File.expand_path "#{ File.dirname __FILE__ }/../../"
database_config = YAML::load(IO.read("#{ rails_root }/config/database.yml"))
config = database_config[ENV['RAILS_ENV'] || 'development']
ActiveRecord::Base.establish_connection(config)

puts ActiveRecord::Migrator.get_all_versions.join '|'

This script is very simple: connect to the database and print all the migration versions to STDOUT. You can test this by running it on a server as RAILS_ENV=production script/deploy/list_migrations, which should output a long string of pipe-delimited numbers.

Listing local migrations

The script that runs locally is also pretty straightforward: get a list of remote migrations; get a list of local migrations; compare the lists. Local migrations can be enumerated by listing files in the db/migrate directory. By looking at this directory instead of the local database, the script will list all the migrations that would be executed if the local directory were deployed - regardless of whether those migrations have been run locally.

def list_local_migrations
  rails_root = File.expand_path "#{ File.dirname __FILE__ }/../"

  file_list = []
  Dir.foreach(File.join(rails_root, 'db', 'migrate')) do |file|
    # only files matching "20091231235959_some_name.rb" pattern
    if match_data = /(\d{14})_(.+)\.rb/.match(file)
      file_list << [ match_data[1], match_data[2], file ]
    end
  end

  file_list
end

There’s a bit of trickiness here to ensure only files that match the standard migration pattern are included in the list. My db/migrate directory contains some roll-up migration data files that don’t include a version in the filename, so those files will be ignored.

Listing remote migrations

Retrieving the list of remote migrations output by script/deploy/list_migrations relies on code that is specific to your particular server setup. I can SSH to a server with the application deployed on it, so I can execute the remote script using SSH:

def execute_on_server(server, command)
  command = "cd #{ ServerDefines::REMOTE_ROOT } && source .rvmrc && #{ command }"
  `ssh -o ConnectTimeout=10 -o BatchMode=yes deployuser@#{ server } '#{ command }'`
end

You’ll need to customize this for your deployed environment - at the very least by specifying a remote directory where the application can be found.

The local script

In an executable file named script/deploy/list_pending_migrations, add:

#!/usr/bin/env ruby

def execute_on_server(server, command)
  # ...
end

def list_local_migrations
  rails_root = File.expand_path "#{ File.dirname __FILE__ }/../"

  file_list = []
  Dir.foreach(File.join(rails_root, 'db', 'migrate')) do |file|
    # only files matching "20091231235959_some_name.rb" pattern
    if match_data = /(\d{14})_(.+)\.rb/.match(file)
      file_list << [ match_data[1], match_data[2], file ]
    end
  end

  file_list
end

def list_pending_migrations
  delimiter = '-' * 84
  puts delimiter

  # Get list of all migrations run on the remote database
  remote_migrations_text = execute_on_server(
    'app027.example.com', 'RAILS_ENV=production script/deploy/list_migrations'
  )
  remote_migrations = remote_migrations_text.strip.split('|')

  # Find local migration files that haven't been run remotely
  local_migrations = list_local_migrations
  local_migrations.reject! do |(version, filename, file)|
    remote_migrations.delete(version)
  end

  if local_migrations.size > 0
    puts "The following #{ local_migrations.size } migrations have not yet been run on the remote database:"
    puts delimiter
    local_migrations.each do |(version, filename, file)|
      puts "db/migrate/#{ filename }"
    end
  else
    puts 'All local migration files have been run on the remote database'
  end

  puts delimiter
end

list_pending_migrations

When I’m about to deploy, I’ll typically run script/deploy/list_pending_migrations to make sure that I know what migrations will need to be run. All the deploy scripts in our project also run it as the last step, just to make sure whomever is deploying doesn’t forget to run a migration.

blog comments powered by Disqus