I got sucked into a trap. In a Rails migration it’s so easy to use an ActiveRecord::Base model object to update some stuff, and it looks nice and DRY. But it is extremely fragile. For example suppose your domain has a user and sponsor model. Each user has a sponsor. You start out this way:

   def self.up
      create table :users do |t|
         t.string :name
         t.string :password
         t.string :sponsor_name
         t.timestamps
      end
   end

Later you want to insist that every user in the system have a sponsor. You already have a bunch of test data that you are intellectually invested in, so you whip up a quick migration to make sure.

  def self.up
     User.find(:all, :conditions => 'sponsor_name IS NULL').each do |u|
         u.sponsor_name = "The Default"
         u.save!
      end
  end

Cool. Works good. You can go up and down. But some time later you realize the supreme error of your ways. You should have had a sponsor model and the user should have a sponsor_id, not a sponsor_name. Doh! What was I thinking. Time for another migration:

   def self.up
      add_column :users, :sponsor_id, :integer
      drop_column :users, :sponsor_name
   end

and the corresponding self.down method too, and the necessary belongs_to, has_one in the user and sponsor model classes. But now you can’t migrate the database up from bootstrap any more because your use of the the User model with u.sponsor_name above will not work. The user no longer has a sponsor_name.

The root of the issue is that the model objects are not versioned along with the database as it goes up and down. Since fixtures just use the same model objects that doesn’t help one bit.

The only reliable way we have come up with is to inject data using raw sql. So step two above
could change to

  def self.up
     execute 'update users set sponsor_name = "The Default" where sponsor_name IS NULL'
  end

That works, up and down the line, because the database sql IS in sync with the database at every step.

Have a better way to write less brittle migrations? Let me know in the comments.