Monday, September 11, 2006

Rails: Loading fixtures to development with a specified order

Background
My development style is heavily test driven. One of the very first things I do write are fixtures, in order to write tests on them. For this reason the very first database I use in a Rails projects is the test database, rather than the development one.
When I have to populate the development db, I already have plenty of fixtures: if they aren't enough to try the user interface (that is what is left to do), they weren't enough to test the models and the controllers.
Of course this should be no problem, there is the rake task db:load:fixtures that plays well with this kind of issues. However, I tend to use db constraints rules such as
ALTER TABLE bars ADD FOREIGN KEY(`foo_id`)  REFERENCES foos  (`id`) ON DELETE CASCADE;
if you use MySQL or
ALTER TABLE bars ADD CONSTRAINT valid_foo FOREIGN KEY (foo_id) REFERENCES foos (id) MATCH FULL;
if you use Postgres.
Solution
Of course, in this case you can't load fixtures in any order and you must supply correct fixtures loading order.
After some searching I found this solution.
However, it does not work. In fact it builds a Hash inserting fixture names in the specified order, than builds an array with other fixtures that had not been specified as needing some particular order.
Eventually, it sums the keys of the hash and the array, and loads fixtures in this order. I don't understand how the poster got this code working, since Ruby Hashes are not ordered in any way and the documentation explicitly says that iterators and methods return values in arbitrary order and do not rely on insertion order.
This meant I had to change the code. Now it uses arrays everywhere (which seems the most reasonable thing to do, since this rake task it's about order). I also added namespaces.
You still have to define something like

ENV["FIXTURE_ORDER"] = 
    %w( entities interaction_sources interaction_devices 
        payments interactions accounts employments 
        enrollments payables receivables tenures wards ).join(' ')

My code is:

require File.expand_path(File.dirname(__FILE__) + "/../../config/environment")
ENV["FIXTURE_ORDER"] ||= ""

def print_separator_line(n=80)
    puts "\n" + '='*n
end

desc "Load fixtures into #{ENV['RAILS_ENV']} database"

namespace :db do
    namespace :fixtures do
        task :load_ordered => :environment do
            require 'active_record/fixtures'
            print_separator_line
            puts "Collecting specified ordered fixtures\n"
            ordered_fixtures = ENV["FIXTURE_ORDER"].split
            fixture_files = Dir.glob(
                File.join(RAILS_ROOT, 'test', 'fixtures', '*.{yml,csv}'))
            other_fixtures = fixture_files.collect
                { |file| File.basename(file, '.*') }.reject 
                    {|fx| ordered_fixtures.include? fx }
            ActiveRecord::Base.establish_connection(ENV['RAILS_ENV'])
            all_fixtures = ordered_fixtures + other_fixtures
            print_separator_line
            puts "Fixtures will be loaded in following order\n"
            all_fixtures.each_with_index do |fx, i|
                puts "#{i+1}. #{fx}"
            end
        
            print_separator_line
            puts "Actually loading fixtures to #{ENV['RAILS_ENV']} db..."
    
            all_fixtures.each do |fixture|
                puts "Loading #{fixture}..."
                Fixtures.create_fixtures('test/fixtures',  fixture)
            end unless :environment == 'production' 
            # You really don't want to load your *fixtures* 
            # into your production database, do you?  
        end
    end
end