# DuplicateMigrations module DuplicateMigrations module ClassMethods def convert_from_non_duplicates(migrations_path) self.new(:up, migrations_path, nil, true).convert_from_non_duplicates end def convert_from_duplicates(migrations_path) self.new(:up, migrations_path, nil, true).convert_from_duplicates end def migrate_with_duplicates(migrations_path, target_version = nil) self.new(:up, migrations_path, target_version, true).migrate_with_duplicates end def using_duplicate_migrations? tables = ActiveRecord::Base.connection.select_all("SHOW TABLES") tables.collect(&:values).flatten.include?("#{ActiveRecord::Migrator.schema_info_table_name}s") end def using_non_duplicate_migrations? tables = ActiveRecord::Base.connection.select_all("SHOW TABLES") tables.collect(&:values).flatten.include?("#{ActiveRecord::Migrator.schema_info_table_name}") end def delete_schema_with_duplicates ActiveRecord::Base.connection.execute("DROP TABLE #{ActiveRecord::Migrator.schema_info_table_name}s") puts "Dropped table #{ActiveRecord::Migrator.schema_info_table_name}s" end def delete_schema_without_duplicates ActiveRecord::Base.connection.execute("DROP TABLE #{ActiveRecord::Migrator.schema_info_table_name}") puts "Dropped table #{ActiveRecord::Migrator.schema_info_table_name}" end def self.included(base) base.send(:alias_method_chain, :migrate, :duplicates) end end module InstanceMethods def initialize_with_duplicates(direction, migrations_path, target_version = nil, duplicates = false) if duplicates raise StandardError.new("This database does not yet support migrations") unless ActiveRecord::Base.connection.supports_migrations? @direction, @migrations_path, @target_version = direction, migrations_path, target_version ActiveRecord::Base.connection.initialize_schema_information_with_duplicates else initialize_without_duplicates(direction, migrations_path, target_version) end end def migrate_with_duplicates migration_classes_before(@target_version).each do |(version, migration_class)| next if schema_information_contains?(migration_class) ActiveRecord::Base.logger.info "Migrating up #{migration_class} (#{version})" migration_class.migrate(:up) insert_schema_information(migration_class) end ActiveRecord::Base.logger.info("Reached target version: #{@target_version}") migration_classes_after(@target_version).each do |(version, migration_class)| next if !schema_information_contains?(migration_class) ActiveRecord::Base.logger.info "Migrating down #{migration_class} (#{version})" migration_class.migrate(:down) remove_schema_information(migration_class) end end def convert_from_non_duplicates puts "===========================" puts " Converting migration system to allow duplicates, using version #{current_version} from #{self.class.schema_info_table_name}" puts " Deleting all existing rows in #{self.class.schema_info_table_name}s" clear_schema_information! migration_classes_before(current_version).each do |(version, migration_class)| puts " Version #{version}: Inserting row #{normalize_migration_name migration_class} into #{self.class.schema_info_table_name}s" insert_schema_information(migration_class) end puts " Conversion Complete!" puts "===========================" end def convert_from_duplicates puts "===========================" puts " Converting migration system to traditional rails, using version data from #{self.class.schema_info_table_name}s" puts " Scanning migrations for current version number" largest = 0 gap = 0 migration_classes_before(nil, true).each do |(version, migration_class)| if schema_information_contains?(migration_class) raise "Found a gap starting at version #{gap}, wasn't expecting #{normalize_migration_name migration_class} to have been run!" if gap > 0 largest = version else gap = version if gap.zero? end end puts " Initializing #{self.class.schema_info_table_name} table" ActiveRecord::Base.connection.initialize_schema_information puts " Setting version to #{largest}" set_schema_version(largest) puts " Conversion Complete!" puts "===========================" end def self.included(base) base.alias_method_chain :initialize, :duplicates end private def migration_classes_before(target_version, check_duplicates = false) migrations = migration_files.inject([]) do |migs, migration_file| version, name = migration_version_and_name(migration_file) assert_unique_migration_version(migs, version.to_i) if check_duplicates if (!target_version || version.to_i <= target_version.to_i) load(migration_file) migs << [ version.to_i, migration_class_with_duplicates(name, version.to_i) ] end migs end migrations.sort{|a,b| a.first <=> b.first } end def migration_classes_after(target_version) migrations = migration_files.inject([]) do |migs, migration_file| version, name = migration_version_and_name(migration_file) if (target_version && version.to_i > target_version.to_i) load(migration_file) migs << [ version.to_i, migration_class_with_duplicates(name, version.to_i) ] end migs end migrations.sort{|a,b| a.first <=> b.first }.reverse end def migration_class_with_duplicates(migration_name, version) klass = migration_name.camelize.constantize class << klass; attr_accessor :version end klass.version = version klass end def insert_schema_information(migration_name) migration_name = normalize_migration_name(migration_name) ActiveRecord::Base.connection.execute("INSERT INTO #{self.class.schema_info_table_name}s (migration,run_at) VALUES(#{ActiveRecord::Base.connection.quote migration_name},'#{ActiveRecord::Base.connection.quoted_date(Time.now)}')") end def remove_schema_information(migration_name) migration_name = normalize_migration_name(migration_name) ActiveRecord::Base.connection.execute("DELETE FROM #{self.class.schema_info_table_name}s WHERE migration = #{ActiveRecord::Base.connection.quote migration_name}") end def clear_schema_information! ActiveRecord::Base.connection.execute("DELETE FROM #{self.class.schema_info_table_name}s") end def schema_information_contains?(migration_name) migration_name = normalize_migration_name(migration_name) cnt = ActiveRecord::Base.connection.select_one("SELECT COUNT(*) FROM #{self.class.schema_info_table_name}s WHERE migration = #{ActiveRecord::Base.connection.quote migration_name}").values.first cnt.to_i != 0 end def normalize_migration_name(migration_name) migration_name = migration_name.name if migration_name.is_a?(Class) migration_name = migration_name.to_s migration_name = migration_name.underscore if migration_name.downcase != migration_name end end module SchemaStatements # Should not be called normally, but this operation is non-destructive. # The migrations module handles this automatically. def initialize_schema_information_with_duplicates begin execute "CREATE TABLE #{ActiveRecord::Migrator.schema_info_table_name}s (migration #{type_to_sql(:string)}, run_at #{type_to_sql(:timestamp)})" rescue ActiveRecord::StatementInvalid # Schema has been intialized end end def dump_schema_information_with_duplicates #:nodoc: end end end