From 8a5a9dcbf64843f064b6e8a0b9c6eea8f0b8536e Mon Sep 17 00:00:00 2001 From: Rick Olson Date: Wed, 9 Apr 2008 16:20:15 +0000 Subject: Add support for interleaving migrations by storing which migrations have run in the new schema_migrations table. Closes #11493 [jordi] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@9244 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 2 + .../abstract/schema_statements.rb | 50 +++++++------ activerecord/lib/active_record/migration.rb | 83 ++++++++++++++-------- activerecord/lib/active_record/schema.rb | 17 ++--- activerecord/lib/active_record/schema_dumper.rb | 6 +- activerecord/test/cases/ar_schema_test.rb | 4 +- activerecord/test/cases/migration_test.rb | 52 +++++++++++--- activerecord/test/cases/schema_dumper_test.rb | 6 +- .../interleaved/pass_1/3_innocent_jointable.rb | 12 ++++ .../interleaved/pass_2/1_people_have_last_names.rb | 9 +++ .../interleaved/pass_2/3_innocent_jointable.rb | 12 ++++ .../interleaved/pass_3/1_people_have_last_names.rb | 9 +++ .../interleaved/pass_3/2_i_raise_on_down.rb | 8 +++ .../interleaved/pass_3/3_innocent_jointable.rb | 12 ++++ activerecord/test/schema/sybase.drop.sql | 2 +- 15 files changed, 204 insertions(+), 80 deletions(-) create mode 100644 activerecord/test/migrations/interleaved/pass_1/3_innocent_jointable.rb create mode 100644 activerecord/test/migrations/interleaved/pass_2/1_people_have_last_names.rb create mode 100644 activerecord/test/migrations/interleaved/pass_2/3_innocent_jointable.rb create mode 100644 activerecord/test/migrations/interleaved/pass_3/1_people_have_last_names.rb create mode 100644 activerecord/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb create mode 100644 activerecord/test/migrations/interleaved/pass_3/3_innocent_jointable.rb diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index b4ebf3de48..dfe6f988c2 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Add support for interleaving migrations by storing which migrations have run in the new schema_migrations table. Closes #11493 [jordi] + * ActiveRecord::Base#sum defaults to 0 if no rows are returned. Closes #11550 [kamal] * Ensure that respond_to? considers dynamic finder methods. Closes #11538. [floehopper] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 393d5c130e..c986f0c6f1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -232,33 +232,41 @@ module ActiveRecord # Should not be called normally, but this operation is non-destructive. # The migrations module handles this automatically. - def initialize_schema_information(current_version=0) - begin - execute "CREATE TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version #{type_to_sql(:string)})" - execute "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES(#{current_version})" - rescue ActiveRecord::StatementInvalid - # Schema has been initialized, make sure version is a string - version_column = columns(:schema_info).detect { |c| c.name == "version" } - - # can't just alter the table, since SQLite can't deal - unless version_column.type == :string - version = ActiveRecord::Migrator.current_version - execute "DROP TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)}" - initialize_schema_information(version) + def initialize_schema_migrations_table + sm_table = ActiveRecord::Migrator.schema_migrations_table_name + + unless tables.detect { |t| t == sm_table } + create_table(sm_table, :id => false) do |schema_migrations_table| + schema_migrations_table.column :version, :string, :null => false end - end - end + add_index sm_table, :version, :unique => true, + :name => 'unique_schema_migrations' + + # Backwards-compatibility: if we find schema_info, assume we've + # migrated up to that point: + si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix + + if tables.detect { |t| t == si_table } - def dump_schema_information #:nodoc: - begin - if (current_schema = ActiveRecord::Migrator.current_version) > 0 - return "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES (#{current_schema})" + old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i + assume_migrated_upto_version(old_version) + drop_table(si_table) end - rescue ActiveRecord::StatementInvalid - # No Schema Info end end + def assume_migrated_upto_version(version) + sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) + migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) + versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename| + filename.split('/').last.split('_').first.to_i + end + + execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" unless migrated.include?(version.to_i) + (versions - migrated).select { |v| v < version.to_i }.each do |v| + execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" + end + end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: if native = native_database_types[type] diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4c10cd806c..573c6a4f02 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -123,7 +123,8 @@ module ActiveRecord # # To run migrations against the currently configured database, use # rake db:migrate. This will update the database by running all of the - # pending migrations, creating the schema_info table if missing. + # pending migrations, creating the schema_migrations table + # (see "About the schema_migrations table" section below) if missing. # # To roll the database back to a previous migration version, use # rake db:migrate VERSION=X where X is the version to which @@ -216,6 +217,21 @@ module ActiveRecord # # The phrase "Updating salaries..." would then be printed, along with the # benchmark for the block when the block completes. + # + # == About the schema_migrations table + # + # Rails versions 2.0 and prior used to create a table called + # schema_info when using migrations. This table contained the + # version of the schema as of the last applied migration. + # + # Starting with Rails 2.1, the schema_info table is + # (automatically) replaced by the schema_migrations table, which + # contains the version numbers of all the migrations applied. + # + # As a result, it is now possible to add migration files that are numbered + # lower than the current schema version: when migrating up, those + # never-applied "interleaved" migrations will be automatically applied, and + # when migrating down, never-applied "interleaved" migrations will be skipped. class Migration @@verbose = true cattr_accessor :verbose @@ -315,15 +331,12 @@ module ActiveRecord class << self def migrate(migrations_path, target_version = nil) case - when target_version.nil?, current_version < target_version - up(migrations_path, target_version) - when current_version > target_version - down(migrations_path, target_version) - when current_version == target_version - return # You're on the right version + when target_version.nil? then up(migrations_path, target_version) + when current_version > target_version then down(migrations_path, target_version) + else up(migrations_path, target_version) end end - + def rollback(migrations_path, steps=1) migrator = self.new(:down, migrations_path) start_index = migrator.migrations.index(migrator.current_migration) @@ -346,12 +359,13 @@ module ActiveRecord self.new(direction, migrations_path, target_version).run end - def schema_info_table_name - Base.table_name_prefix + "schema_info" + Base.table_name_suffix + def schema_migrations_table_name + Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix end def current_version - Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").to_i + Base.connection.select_values( + "SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).max || 0 end def proper_table_name(name) @@ -362,7 +376,7 @@ module ActiveRecord def initialize(direction, migrations_path, target_version = nil) raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? - Base.connection.initialize_schema_information + Base.connection.initialize_schema_migrations_table @direction, @migrations_path, @target_version = direction, migrations_path, target_version end @@ -383,25 +397,31 @@ module ActiveRecord def migrate current = migrations.detect { |m| m.version == current_version } target = migrations.detect { |m| m.version == @target_version } - + if target.nil? && !@target_version.nil? && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end - start = migrations.index(current) || 0 - finish = migrations.index(target) || migrations.size - 1 + start = up? ? 0 : (migrations.index(current) || 0) + finish = migrations.index(target) || migrations.size - 1 runnable = migrations[start..finish] - # skip the current migration if we're heading upwards - runnable.shift if up? && runnable.first == current - # skip the last migration if we're headed down, but not ALL the way down runnable.pop if down? && !target.nil? runnable.each do |migration| Base.logger.info "Migrating to #{migration} (#{migration.version})" - migration.migrate(@direction) - set_schema_version_after_migrating(migration) + + # On our way up, we skip migrating the ones we've already migrated + # On our way down, we skip reverting the ones we've never migrated + next if up? && migrated.include?(migration.version.to_i) + + if down? && !migrated.include?(migration.version.to_i) + migration.announce 'never migrated, skipping'; migration.write + else + migration.migrate(@direction) + record_version_state_after_migrating(migration.version) + end end end @@ -412,7 +432,7 @@ module ActiveRecord migrations = files.inject([]) do |klasses, file| version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first - raise IllegalMigrationNameError.new(f) unless version + raise IllegalMigrationNameError.new(file) unless version version = version.to_i if klasses.detect { |m| m.version == version } @@ -433,19 +453,24 @@ module ActiveRecord end def pending_migrations - migrations.select { |m| m.version > current_version } + already_migrated = migrated + migrations.reject { |m| already_migrated.include?(m.version.to_i) } + end + + def migrated + sm_table = self.class.schema_migrations_table_name + Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort end private - def set_schema_version_after_migrating(migration) - version = migration.version - + def record_version_state_after_migrating(version) + sm_table = self.class.schema_migrations_table_name + if down? - after = migrations[migrations.index(migration) + 1] - version = after ? after.version : 0 + Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'") + else + Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") end - - Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{version}") end def up? diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 9d50efb74f..d6b254fcf9 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -34,24 +34,17 @@ module ActiveRecord # #add_index, etc.). # # The +info+ hash is optional, and if given is used to define metadata - # about the current schema (like the schema's version): + # about the current schema (currently, only the schema's version): # - # ActiveRecord::Schema.define(:version => 15) do + # ActiveRecord::Schema.define(:version => 20380119000001) do # ... # end def self.define(info={}, &block) instance_eval(&block) - unless info.empty? - initialize_schema_information - cols = columns('schema_info') - - info = info.map do |k,v| - v = Base.connection.quote(v, cols.detect { |c| c.name == k.to_s }) - "#{k} = #{v}" - end - - Base.connection.update "UPDATE #{Migrator.schema_info_table_name} SET #{info.join(", ")}" + unless info[:version].blank? + initialize_schema_migrations_table + assume_migrated_upto_version info[:version] end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 286306874e..826662d3ee 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -30,11 +30,11 @@ module ActiveRecord def initialize(connection) @connection = connection @types = @connection.native_database_types - @info = @connection.select_one("SELECT * FROM schema_info") rescue nil + @version = Migrator::current_version rescue nil end def header(stream) - define_params = @info ? ":version => #{@info['version']}" : "" + define_params = @version ? ":version => #{@version}" : "" stream.puts <
0 @@ -817,16 +851,16 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal(3, ActiveRecord::Migrator.current_version) ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(2, ActiveRecord::Migrator.current_version) + assert_equal(2, ActiveRecord::Migrator.current_version) ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(1, ActiveRecord::Migrator.current_version) + assert_equal(1, ActiveRecord::Migrator.current_version) ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal(0, ActiveRecord::Migrator.current_version) ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal(0, ActiveRecord::Migrator.current_version) end def test_migrator_run @@ -839,15 +873,15 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal(0, ActiveRecord::Migrator.current_version) end - def test_schema_info_table_name + def test_schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name - assert_equal "prefix_schema_info_suffix", ActiveRecord::Migrator.schema_info_table_name + assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" Reminder.reset_table_name - assert_equal "schema_info", ActiveRecord::Migrator.schema_info_table_name + assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name ensure ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index d96bd62e56..ba8bff3b44 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -16,7 +16,7 @@ if ActiveRecord::Base.connection.respond_to?(:tables) output = standard_dump assert_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output - assert_no_match %r{create_table "schema_info"}, output + assert_no_match %r{create_table "schema_migrations"}, output end def test_schema_dump_excludes_sqlite_sequence @@ -81,7 +81,7 @@ if ActiveRecord::Base.connection.respond_to?(:tables) output = stream.string assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output - assert_no_match %r{create_table "schema_info"}, output + assert_no_match %r{create_table "schema_migrations"}, output end def test_schema_dump_with_regexp_ignored_table @@ -92,7 +92,7 @@ if ActiveRecord::Base.connection.respond_to?(:tables) output = stream.string assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output - assert_no_match %r{create_table "schema_info"}, output + assert_no_match %r{create_table "schema_migrations"}, output end def test_schema_dump_illegal_ignored_table_value diff --git a/activerecord/test/migrations/interleaved/pass_1/3_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_1/3_innocent_jointable.rb new file mode 100644 index 0000000000..21c9ca5328 --- /dev/null +++ b/activerecord/test/migrations/interleaved/pass_1/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end \ No newline at end of file diff --git a/activerecord/test/migrations/interleaved/pass_2/1_people_have_last_names.rb b/activerecord/test/migrations/interleaved/pass_2/1_people_have_last_names.rb new file mode 100644 index 0000000000..81af5fef5e --- /dev/null +++ b/activerecord/test/migrations/interleaved/pass_2/1_people_have_last_names.rb @@ -0,0 +1,9 @@ +class PeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "last_name", :string + end + + def self.down + remove_column "people", "last_name" + end +end \ No newline at end of file diff --git a/activerecord/test/migrations/interleaved/pass_2/3_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_2/3_innocent_jointable.rb new file mode 100644 index 0000000000..21c9ca5328 --- /dev/null +++ b/activerecord/test/migrations/interleaved/pass_2/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end \ No newline at end of file diff --git a/activerecord/test/migrations/interleaved/pass_3/1_people_have_last_names.rb b/activerecord/test/migrations/interleaved/pass_3/1_people_have_last_names.rb new file mode 100644 index 0000000000..81af5fef5e --- /dev/null +++ b/activerecord/test/migrations/interleaved/pass_3/1_people_have_last_names.rb @@ -0,0 +1,9 @@ +class PeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "last_name", :string + end + + def self.down + remove_column "people", "last_name" + end +end \ No newline at end of file diff --git a/activerecord/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb b/activerecord/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb new file mode 100644 index 0000000000..9b1ce9f017 --- /dev/null +++ b/activerecord/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb @@ -0,0 +1,8 @@ +class IRaiseOnDown < ActiveRecord::Migration + def self.up + end + + def self.down + raise + end +end \ No newline at end of file diff --git a/activerecord/test/migrations/interleaved/pass_3/3_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_3/3_innocent_jointable.rb new file mode 100644 index 0000000000..21c9ca5328 --- /dev/null +++ b/activerecord/test/migrations/interleaved/pass_3/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end \ No newline at end of file diff --git a/activerecord/test/schema/sybase.drop.sql b/activerecord/test/schema/sybase.drop.sql index ebb91931fb..1a2cc9ff56 100644 --- a/activerecord/test/schema/sybase.drop.sql +++ b/activerecord/test/schema/sybase.drop.sql @@ -31,5 +31,5 @@ DROP TABLE legacy_things DROP TABLE numeric_data DROP TABLE mixed_case_monkeys DROP TABLE minimalistics -DROP TABLE schema_info +DROP TABLE schema_migrations go -- cgit v1.2.3