Skip to content

Commit

Permalink
Allow reverting of migration commands with Migration#revert [#8267]
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre committed Dec 21, 2012
1 parent 740dbf8 commit d327c18
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 90 deletions.
70 changes: 45 additions & 25 deletions activerecord/lib/active_record/migration.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -373,22 +373,52 @@ def initialize(name = self.class.name, version = nil)
@name = name @name = name
@version = version @version = version
@connection = nil @connection = nil
@reverting = false
end end


# instantiate the delegate object after initialize is defined # instantiate the delegate object after initialize is defined
self.verbose = true self.verbose = true
self.delegate = new self.delegate = new


# Reverses the migration commands for the given block.
#
# The following migration will remove the table 'horses'
# and create the table 'apples' on the way up, and the reverse
# on the way down.
#
# class FixTLMigration < ActiveRecord::Migration
# def change
# revert do
# create_table(:horses) do |t|
# t.text :content
# t.datetime :remind_at
# end
# end
# create_table(:apples) do |t|
# t.string :variety
# end
# end
# end
#
# This command can be nested.
#
def revert def revert
@reverting = true if @connection.respond_to? :revert
yield @connection.revert { yield }
ensure else
@reverting = false recorder = CommandRecorder.new(@connection)
@connection = recorder
suppress_messages do
@connection.revert { yield }
end
@connection = recorder.delegate
recorder.commands.each do |cmd, args, block|
send(cmd, *args, &block)
end
end
end end


def reverting? def reverting?
@reverting @connection.respond_to?(:reverting) && @connection.reverting
end end


def up def up
Expand All @@ -414,29 +444,19 @@ def migrate(direction)


time = nil time = nil
ActiveRecord::Base.connection_pool.with_connection do |conn| ActiveRecord::Base.connection_pool.with_connection do |conn|
@connection = conn time = Benchmark.measure do
if respond_to?(:change) @connection = conn
if direction == :down if respond_to?(:change)
recorder = CommandRecorder.new(@connection) if direction == :down
suppress_messages do revert { change }
@connection = recorder else
change change
end end
@connection = conn
time = Benchmark.measure {
self.revert {
recorder.inverse.each do |cmd, args|
send(cmd, *args)
end
}
}
else else
time = Benchmark.measure { change } send(direction)
end end
else @connection = nil
time = Benchmark.measure { send(direction) }
end end
@connection = nil
end end


case direction case direction
Expand Down Expand Up @@ -483,7 +503,7 @@ def method_missing(method, *arguments, &block)
arg_list = arguments.map{ |a| a.inspect } * ', ' arg_list = arguments.map{ |a| a.inspect } * ', '


say_with_time "#{method}(#{arg_list})" do say_with_time "#{method}(#{arg_list})" do
unless reverting? unless @connection.respond_to? :revert
unless arguments.empty? || method == :execute unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first) arguments[0] = Migrator.proper_table_name(arguments.first)
arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
Expand Down
53 changes: 36 additions & 17 deletions activerecord/lib/active_record/migration/command_recorder.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -16,35 +16,54 @@ class Migration
class CommandRecorder class CommandRecorder
include JoinTable include JoinTable


attr_accessor :commands, :delegate attr_accessor :commands, :delegate, :reverting


def initialize(delegate = nil) def initialize(delegate = nil)
@commands = [] @commands = []
@delegate = delegate @delegate = delegate
@reverting = false
end

# While executing the given block, the recorded will be in reverting mode.
# All commands recorded will end up being recorded reverted
# and in reverse order.
# For example:
#
# recorder.revert{ recorder.record(:rename_table, [:old, :new]) }
# # same effect as recorder.record(:rename_table, [:new, :old])
def revert
@reverting = !@reverting
previous = @commands
@commands = []
yield
ensure
@commands = previous.concat(@commands.reverse)
@reverting = !@reverting
end end


# record +command+. +command+ should be a method name and arguments. # record +command+. +command+ should be a method name and arguments.
# For example: # For example:
# #
# recorder.record(:method_name, [:arg1, :arg2]) # recorder.record(:method_name, [:arg1, :arg2])
def record(*command) def record(*command, &block)
@commands << command if @reverting
@commands << inverse_of(*command, &block)
else
@commands << (command << block)
end
end end


# Returns a list that represents commands that are the inverse of the # Returns the inverse of the given command. For example:
# commands stored in +commands+. For example:
# #
# recorder.record(:rename_table, [:old, :new]) # recorder.inverse_of(:rename_table, [:old, :new])
# recorder.inverse # => [:rename_table, [:new, :old]] # # => [:rename_table, [:new, :old]]
# #
# This method will raise an +IrreversibleMigration+ exception if it cannot # This method will raise an +IrreversibleMigration+ exception if it cannot
# invert the +commands+. # invert the +command+.
def inverse def inverse_of(command, args, &block)
@commands.reverse.map { |name, args| method = :"invert_#{command}"
method = :"invert_#{name}" raise IrreversibleMigration unless respond_to?(method, true)
raise IrreversibleMigration unless respond_to?(method, true) send(method, args, &block)
send(method, args)
}
end end


def respond_to?(*args) # :nodoc: def respond_to?(*args) # :nodoc:
Expand All @@ -56,9 +75,9 @@ def respond_to?(*args) # :nodoc:
:change_column, :change_column_default, :add_reference, :remove_reference, :change_column, :change_column_default, :add_reference, :remove_reference,
].each do |method| ].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1 class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args) # def create_table(*args) def #{method}(*args, &block) # def create_table(*args, &block)
record(:"#{method}", args) # record(:create_table, args) record(:"#{method}", args, &block) # record(:create_table, args, &block)
end # end end # end
EOV EOV
end end
alias :add_belongs_to :add_reference alias :add_belongs_to :add_reference
Expand Down
23 changes: 23 additions & 0 deletions activerecord/test/cases/invertible_migration_test.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ def change
end end
end end


class InvertibleRevertMigration < SilentMigration
def change
revert do
create_table("horses") do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
end
end

class NonInvertibleMigration < SilentMigration class NonInvertibleMigration < SilentMigration
def change def change
create_table("horses") do |t| create_table("horses") do |t|
Expand Down Expand Up @@ -67,6 +78,18 @@ def test_migrate_down
assert !migration.connection.table_exists?("horses") assert !migration.connection.table_exists?("horses")
end end


def test_migrate_revert
migration = InvertibleMigration.new
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
assert !migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
assert !migration.connection.table_exists?("horses")
end

def test_legacy_up def test_legacy_up
LegacyMigration.migrate :up LegacyMigration.migrate :up
assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
Expand Down
Loading

0 comments on commit d327c18

Please sign in to comment.