diff options
author | Rick Olson <technoweenie@gmail.com> | 2008-04-09 16:20:15 +0000 |
---|---|---|
committer | Rick Olson <technoweenie@gmail.com> | 2008-04-09 16:20:15 +0000 |
commit | 8a5a9dcbf64843f064b6e8a0b9c6eea8f0b8536e (patch) | |
tree | 27064d2d5ebccdd40bf5c4f43fcf5b2e3e7e57f3 /activerecord/lib/active_record | |
parent | 78c2d9fc223e7a9945aee65c838f7ce78e9ddb3e (diff) | |
download | rails-8a5a9dcbf64843f064b6e8a0b9c6eea8f0b8536e.tar.gz rails-8a5a9dcbf64843f064b6e8a0b9c6eea8f0b8536e.tar.bz2 rails-8a5a9dcbf64843f064b6e8a0b9c6eea8f0b8536e.zip |
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
Diffstat (limited to 'activerecord/lib/active_record')
4 files changed, 91 insertions, 65 deletions
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 # <tt>rake db:migrate</tt>. This will update the database by running all of the - # pending migrations, creating the <tt>schema_info</tt> table if missing. + # pending migrations, creating the <tt>schema_migrations</tt> table + # (see "About the schema_migrations table" section below) if missing. # # To roll the database back to a previous migration version, use # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> 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 + # <tt>schema_info</tt> when using migrations. This table contained the + # version of the schema as of the last applied migration. + # + # Starting with Rails 2.1, the <tt>schema_info</tt> table is + # (automatically) replaced by the <tt>schema_migrations</tt> 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 <<HEADER # This file is auto-generated from the current state of the database. Instead of editing this file, @@ -59,7 +59,7 @@ HEADER def tables(stream) @connection.tables.sort.each do |tbl| - next if ["schema_info", ignore_tables].flatten.any? do |ignored| + next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored when String; tbl == ignored when Regexp; tbl =~ ignored |