Rails 2.2.2 Chicken and Egg Migrations Headache

For this upcoming March release, we plan to upgrade from Rails 2.1 to Rails 2.2.2. When testing bootstrapping fresh instances of our app, we ran across an annoying migrations problem. Read on to see how we resolved it.

The bootstrap process for our app is pretty straightforward. We prepare the new empty database by loading db/schema.rb from our last stable release. db/schema.rb is kept in sync as we move from one release to the next. Once the structure of the database is created with db:schema:load, we setup initial defaults in the app by loading yaml fixtures via a custom db:seed:load rake task.

desc "Initialize database"
task :seed, :roles => :app do
  run <<-CMD
    cd #{current_path} &&
    rake db:schema:load &&
    rake db:seed:load SRC=db/blank SEEDS=all
  CMD
end

Easy peasey right? I was very annoyed when db:schema:load stopped dead in it's page long backtrace. The offending error was:

Mysql::Error: Table 'coupa_blah.setup doesn't exist: SHOW FIELDS FROM `setup`

Looking a little deeper, it became apparent that models and controllers assumes the existence of the 'setup' table, and reference it when they are loaded for configuration settings. However, the 'setup' table is created and populated by db:schema:load and db:seed:load. Chicken and egg, you have foiled me again!

I knew it was a problem with class caching because when I tried running db:schema:load in development mode, it worked without a hiccup. Rails::Configuration told me that there is a variable eager_load_paths that determines where to eager load. In our environment.rb, I added this to the Rails::Initializer block:

# Hint: this doesn't fix our problem!!
config.eager_load_paths = []

At first glance, this not only fixed the db:schema:load problem, but had the added side benefit of speeding up start up times of Passenger app servers, and script/console. Of course, it was too good to be true. I soon realized that weird things in our app were broken. We were getting method undefined errors on classes that obviously had them. It was frustrating, but at least I know I'm not alone. It turns out that we need our model classes to be eager loaded. I ended up turning eager loading back on and adding this hack to environments/production.rb:

# kids, this is what an ugly hack looks like. Don't worry, Rails
# 2.3 will fix this.
config.cache_classes = (File.basename($0) == "rake" && !ARGV.grep(/db:/).empty?) ? false : true

If cache_classes is false, then the paths in eager_load_paths is ignored, and no eager loading is done. The conditional basically turns off eager loading for any rake task within the 'db' namespace. The hack was already there, but the previous version specifically looked for 'db:migrate', whereas this one will exempt db:schema:load and db:seed:load as well.

Fortunately, according to the lighthouse ticket on this issue, it looks like Rails 2.3 will default to turning off eager loading of classes for rake tasks. Until then, this hack will have to do.