Skip to content

Commit

Permalink
Add support for interleaving migrations by storing which migrations h…
Browse files Browse the repository at this point in the history
…ave 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
  • Loading branch information
technoweenie committed Apr 9, 2008
1 parent 78c2d9f commit 8a5a9dc
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 80 deletions.
2 changes: 2 additions & 0 deletions 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]
Expand Down
Expand Up @@ -232,33 +232,41 @@ def structure_dump

# 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:

This comment has been minimized.

Copy link
@pd

pd Apr 19, 2008

Contributor

Removing this method breaks db:structure:dump which breaks db:test:clone_structure; see http://rails.lighthouseapp.com/projects/8994/tickets/21. Not sure how to restore it using the new schema_migrations table.

This comment has been minimized.

Copy link
@robmckinnon

robmckinnon May 4, 2008

A fix is contained in a patch which
restores the dump_schema_information method
, updated to support the timestamped migrations. See the ticket comments for details: http://rails.lighthouseapp.com/projects/8994/tickets/21#ticket-21-12

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]
Expand Down
83 changes: 54 additions & 29 deletions activerecord/lib/active_record/migration.rb
Expand Up @@ -123,7 +123,8 @@ def initialize(name)
#
# 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
Expand Down Expand Up @@ -216,6 +217,21 @@ def initialize(name)
#
# 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
Expand Down Expand Up @@ -315,15 +331,12 @@ class Migrator#:nodoc:
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)
Expand All @@ -346,12 +359,13 @@ def run(direction, migrations_path, target_version)
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)
Expand All @@ -362,7 +376,7 @@ def proper_table_name(name)

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

Expand All @@ -383,25 +397,31 @@ def run
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

Expand All @@ -412,7 +432,7 @@ def migrations
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 }
Expand All @@ -433,19 +453,24 @@ class << klass; attr_accessor :version end
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?
Expand Down
17 changes: 5 additions & 12 deletions activerecord/lib/active_record/schema.rb
Expand Up @@ -34,24 +34,17 @@ class Schema < Migration
# #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
Expand Down
6 changes: 3 additions & 3 deletions activerecord/lib/active_record/schema_dumper.rb
Expand Up @@ -30,11 +30,11 @@ def dump(stream)
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,
Expand All @@ -59,7 +59,7 @@ def trailer(stream)

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
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/cases/ar_schema_test.rb
Expand Up @@ -25,8 +25,8 @@ def test_schema_define
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

Expand Down
52 changes: 43 additions & 9 deletions activerecord/test/cases/migration_test.rb
Expand Up @@ -7,6 +7,7 @@
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
Expand Down Expand Up @@ -34,8 +35,8 @@ def setup
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
Expand Down Expand Up @@ -779,6 +780,39 @@ def test_migrator_one_up_one_down
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
Expand Down Expand Up @@ -817,16 +851,16 @@ def test_migrator_rollback
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
Expand All @@ -839,15 +873,15 @@ def test_migrator_run
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 = ""
Expand Down

0 comments on commit 8a5a9dc

Please sign in to comment.