# bare_migration.rb # Scott Bronson # 26 Jan 2006 # # This module provides bare ActiveRecord::Base objects for migration # scripts. # # Most migration scripts use model classes to move data. This is # fine when the schema you're migrating from is compatible with your # current models. It's disastrous when your models have changed # significantly. # # This class allows you to use bare ActiveRecord classes in your migrate # scripts instead of your models so there is no chance of compatibility # problems. # # TODO: Make associations work (has_a, etc.) without requiring legacy calls # TODO: If a class already exists when defining it then Ruby just adds to # the existing class. This is why migration objects need to be # named "BareArticle12" instead of just "BareArticle". This stinks # and should be fixed. It won't be an easy fix though. # TODO: I know that STI (inheritance) won't work, but it should be # simple enough to change instantiate_without_callbacks to make # it work. # TODO: Create a test framework. # To use: # # - Create bare inner classes instead of models in your migration script. # For instance, this would declare a bare object that matches an Article # model: # # class Migration < ActiveRecord::Migration # class BareArticle # include BareMigration # end # ... # end # # Don't forget to nest the declaration of the bare model in order to ensure # that the class name is unique. Another option is to declare it outside # the scope of the migration with a number based on the migration # number, say Bare3Article # # Use set_table_name if the object doesn't calculate its table name # properly. # # - Then, use the bare objects to migrate the data: # # BareArticle.find(:all).each do |a| # a.published = 1 # end # # - Now your Article module can change all it wants and your migration # script will still work. module BareMigration def self.append_features(base) base.extend(ClassMethods) # Set the table name by eradicating "Bare" from the calculated table name. # You can still use set_table_name if this is wrong. base.set_table_name(base.table_name.sub(/.*?bare\d*_?/, '')) end module ClassMethods # The problem with STI is that it causes AR to instantiate and # return classes DIFFERENT from the class that called find() # (classes that come from your app/models dir, a no-no when # migrating). For now, this routine overrides that behavior # so that you will ONLY get the same class as the one that called # find. (todo: make STI work again) def instantiate_without_callbacks(record) object = allocate object.instance_variable_set("@attributes", record) object end def find_and_update(find_type=:all, *rest, &update_block) self.transaction do self.find(find_type, *rest).each do |item| update_block[item] item.save! end end end end end class ActiveRecord::Migration def self.opposite_of(method) method = method.to_s method.sub!(/^add_/, 'remove_') || method.sub!(/^remove_/, 'add_') || method.sub!(/^create/, 'drop') || method.sub!(/^drop/, 'create') || method.sub!(/^rename_column/, 'reverse_columns') end def self.modify_schema(method, *args) case method.to_s when 'create_table' block = args.last.is_a?(Proc) ? args.pop : Proc.new {|t| nil} create_table *args, &block when 'remove_column' remove_column args[0], args[1] when 'reverse_columns' (table, to_col, from_col) = args rename_column table, from_col, to_col else send(method, *args) end end def self.modify_tables_and_update(*colspecs, &block) unless colspecs.first.is_a?(Array) colspecs = [colspecs] end begin updated_classes = [] colspecs.each do |spec| if spec[1].is_a?(Class) updated_classes << spec[1] spec[1] = spec[1].table_name.to_sym end end colspecs.each {|spec| modify_schema(*spec) } updated_classes.uniq! if updated_classes.size == 1 && block && block.arity == 1 say "About to call find_and_update" updated_classes.first.find_and_update(:all, &block) else block.call if block end rescue Exception => e colspecs.reverse.each do |(method, table, column, *rest)| modify_schema(opposite_of(method), table, column, *rest) rescue nil end raise e end end end