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.
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.