mirror of
https://github.com/github/rails.git
synced 2026-01-10 07:07:54 -05:00
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
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -232,34 +232,42 @@ 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
|
||||
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 }
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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})"
|
||||
end
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
# No Schema Info
|
||||
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]
|
||||
column_type_sql = native.is_a?(Hash) ? native[:name] : native
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,8 +25,8 @@ if ActiveRecord::Base.connection.supports_migrations?
|
||||
end
|
||||
|
||||
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
|
||||
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_info" }
|
||||
assert_equal 7, @connection.select_one("SELECT version FROM schema_info")['version'].to_i
|
||||
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
|
||||
assert_equal 7, ActiveRecord::Migrator::current_version
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ require 'models/topic'
|
||||
require MIGRATIONS_ROOT + "/valid/1_people_have_last_names"
|
||||
require MIGRATIONS_ROOT + "/valid/2_we_need_reminders"
|
||||
require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
|
||||
require MIGRATIONS_ROOT + "/interleaved/pass_3/2_i_raise_on_down"
|
||||
|
||||
if ActiveRecord::Base.connection.supports_migrations?
|
||||
class BigNumber < ActiveRecord::Base; end
|
||||
@@ -34,8 +35,8 @@ if ActiveRecord::Base.connection.supports_migrations?
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveRecord::Base.connection.initialize_schema_information
|
||||
ActiveRecord::Base.connection.update "UPDATE #{ActiveRecord::Migrator.schema_info_table_name} SET version = 0"
|
||||
ActiveRecord::Base.connection.initialize_schema_migrations_table
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
|
||||
|
||||
%w(reminders people_reminders prefix_reminders_suffix).each do |table|
|
||||
Reminder.connection.drop_table(table) rescue nil
|
||||
@@ -779,6 +780,39 @@ if ActiveRecord::Base.connection.supports_migrations?
|
||||
assert !Reminder.table_exists?
|
||||
end
|
||||
|
||||
def test_finds_migrations
|
||||
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
|
||||
[['1', 'people_have_last_names'],
|
||||
['2', 'we_need_reminders'],
|
||||
['3', 'innocent_jointable']].each_with_index do |pair, i|
|
||||
migrations[i].version == pair.first
|
||||
migrations[1].name == pair.last
|
||||
end
|
||||
end
|
||||
|
||||
def test_finds_pending_migrations
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1)
|
||||
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations
|
||||
assert_equal 1, migrations.size
|
||||
migrations[0].version == '3'
|
||||
migrations[0].name == 'innocent_jointable'
|
||||
end
|
||||
|
||||
def test_migrator_interleaved_migrations
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1")
|
||||
|
||||
assert_nothing_raised do
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2")
|
||||
end
|
||||
|
||||
Person.reset_column_information
|
||||
assert Person.column_methods_hash.include?(:last_name)
|
||||
|
||||
assert_nothing_raised do
|
||||
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/interleaved/pass_3")
|
||||
end
|
||||
end
|
||||
|
||||
def test_migrator_verbosity
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
|
||||
assert PeopleHaveLastNames.message_count > 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 = ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
class IRaiseOnDown < ActiveRecord::Migration
|
||||
def self.up
|
||||
end
|
||||
|
||||
def self.down
|
||||
raise
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user