Merge pull request #8267 from marcandre/reversible_drop_table_etc

Reversible commands
This commit is contained in:
Aaron Patterson 2012-12-21 13:22:52 -08:00
commit 68e91da765
15 changed files with 846 additions and 312 deletions

@ -1,5 +1,22 @@
## Rails 4.0.0 (unreleased) ##
* Improve ways to write `change` migrations, making the old `up` & `down` methods no longer necessary.
* The methods `drop_table` and `remove_column` are now reversible, as long as the necessary information is given.
The method `remove_column` used to accept multiple column names; instead use `remove_columns` (which is not revertible).
The method `change_table` is also reversible, as long as its block doesn't call `remove`, `change` or `change_default`
* New method `reversible` makes it possible to specify code to be run when migrating up or down.
See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#using-the-reversible-method)
* New method `revert` will revert a whole migration or the given block.
If migrating down, the given migration / block is run normally.
See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations)
Attempting to revert the methods `execute`, `remove_columns` and `change_column` will now raise an IrreversibleMigration instead of actually executing them without any output.
*Marc-André Lafortune*
* Serialized attributes can be serialized in integer columns.
Fix #8575.

@ -423,7 +423,7 @@ def change_default(column_name, default)
# t.remove(:qualification)
# t.remove(:qualification, :experience)
def remove(*column_names)
@base.remove_column(@table_name, *column_names)
@base.remove_columns(@table_name, *column_names)
end
# Removes the given index from the table.
@ -490,20 +490,8 @@ def remove_references(*args)
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{column_type}(*args) # def string(*args)
options = args.extract_options! # options = args.extract_options!
column_names = args # column_names = args
type = :'#{column_type}' # type = :string
column_names.each do |name| # column_names.each do |name|
column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type)
if options[:limit] # if options[:limit]
column.limit = options[:limit] # column.limit = options[:limit]
elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash)
column.limit = native[type][:limit] # column.limit = native[type][:limit]
end # end
column.precision = options[:precision] # column.precision = options[:precision]
column.scale = options[:scale] # column.scale = options[:scale]
column.default = options[:default] # column.default = options[:default]
column.null = options[:null] # column.null = options[:null]
@base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
args.each do |name| # column_names.each do |name|
@base.add_column(@table_name, name, :#{column_type}, options) # @base.add_column(@table_name, name, :string, options)
end # end
end # end
EOV

@ -214,6 +214,17 @@ def create_join_table(table_1, table_2, options = {})
end
end
# Drops the join table specified by the given arguments.
# See create_join_table for details.
#
# Although this command ignores the block if one is given, it can be helpful
# to provide one in a migration's +change+ method so it can be reverted.
# In that case, the block will be used by create_join_table.
def drop_join_table(table_1, table_2, options = {})
join_table_name = find_join_table_name(table_1, table_2, options)
drop_table(join_table_name)
end
# A block for changing columns in +table+.
#
# # change_table() yields a Table instance
@ -294,6 +305,10 @@ def rename_table(table_name, new_name)
end
# Drops a table from the database.
#
# Although this command ignores +options+ and the block if one is given, it can be helpful
# to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
execute "DROP TABLE #{quote_table_name(table_name)}"
end
@ -306,14 +321,26 @@ def add_column(table_name, column_name, type, options = {})
execute(add_column_sql)
end
# Removes the column(s) from the table definition.
# Removes the given columns from the table definition.
#
# remove_columns(:suppliers, :qualification, :experience)
def remove_columns(table_name, *column_names)
raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty?
column_names.each do |column_name|
remove_column(table_name, column_name)
end
end
# Removes the column from the table definition.
#
# remove_column(:suppliers, :qualification)
# remove_columns(:suppliers, :qualification, :experience)
def remove_column(table_name, *column_names)
columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
#
# The +type+ and +options+ parameters will be ignored if present. It can be helpful
# to provide these in a migration's +change+ method so it can be reverted.
# In that case, +type+ and +options+ will be used by add_column.
def remove_column(table_name, column_name, type = nil, options = {})
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name{column_name}}"
end
alias :remove_columns :remove_column
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
@ -662,7 +689,8 @@ def index_name_for_remove(table_name, options = {})
end
def columns_for_remove(table_name, *column_names)
raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future")
raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank?
column_names.map {|column_name| quote_column_name(column_name) }
end

@ -444,15 +444,11 @@ def add_column(table_name, column_name, type, options = {}) #:nodoc:
end
end
def remove_column(table_name, *column_names) #:nodoc:
raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
column_names.each do |column_name|
alter_table(table_name) do |definition|
definition.columns.delete(definition[column_name])
end
def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.columns.delete(definition[column_name])
end
end
alias :remove_columns :remove_column
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|

@ -373,22 +373,129 @@ def initialize(name = self.class.name, version = nil)
@name = name
@version = version
@connection = nil
@reverting = false
end
# instantiate the delegate object after initialize is defined
self.verbose = true
self.delegate = new
def revert
@reverting = true
yield
ensure
@reverting = false
# Reverses the migration commands for the given block and
# the given migrations.
#
# 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
#
# Or equivalently, if +TenderloveMigration+ is defined as in the
# documentation for Migration:
#
# require_relative '2012121212_tenderlove_migration'
#
# class FixupTLMigration < ActiveRecord::Migration
# def change
# revert TenderloveMigration
#
# create_table(:apples) do |t|
# t.string :variety
# end
# end
# end
#
# This command can be nested.
def revert(*migration_classes)
run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
if block_given?
if @connection.respond_to? :revert
@connection.revert { yield }
else
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?
@reverting
@connection.respond_to?(:reverting) && @connection.reverting
end
class ReversibleBlockHelper < Struct.new(:reverting)
def up
yield unless reverting
end
def down
yield if reverting
end
end
# Used to specify an operation that can be run in one direction or another.
# Call the methods +up+ and +down+ of the yielded object to run a block
# only in one given direction.
# The whole block will be called in the right order within the migration.
#
# In the following example, the looping on users will always be done
# when the three columns 'first_name', 'last_name' and 'full_name' exist,
# even when migrating down:
#
# class SplitNameMigration < ActiveRecord::Migration
# def change
# add_column :users, :first_name, :string
# add_column :users, :last_name, :string
#
# reversible do |dir|
# User.reset_column_information
# User.all.each do |u|
# dir.up { u.first_name, u.last_name = u.full_name.split(' ') }
# dir.down { u.full_name = "#{u.first_name} #{u.last_name}" }
# u.save
# end
# end
#
# revert { add_column :users, :full_name, :string }
# end
# end
def reversible
helper = ReversibleBlockHelper.new(reverting?)
transaction{ yield helper }
end
# Runs the given migration classes.
# Last argument can specify options:
# - :direction (default is :up)
# - :revert (default is false)
def run(*migration_classes)
opts = migration_classes.extract_options!
dir = opts[:direction] || :up
dir = (dir == :down ? :up : :down) if opts[:revert]
if reverting?
# If in revert and going :up, say, we want to execute :down without reverting, so
revert { run(*migration_classes, direction: dir, revert: true) }
else
migration_classes.each do |migration_class|
migration_class.new.exec_migration(@connection, dir)
end
end
end
def up
@ -414,29 +521,9 @@ def migrate(direction)
time = nil
ActiveRecord::Base.connection_pool.with_connection do |conn|
@connection = conn
if respond_to?(:change)
if direction == :down
recorder = CommandRecorder.new(@connection)
suppress_messages do
@connection = recorder
change
end
@connection = conn
time = Benchmark.measure {
self.revert {
recorder.inverse.each do |cmd, args|
send(cmd, *args)
end
}
}
else
time = Benchmark.measure { change }
end
else
time = Benchmark.measure { send(direction) }
time = Benchmark.measure do
exec_migration(conn, direction)
end
@connection = nil
end
case direction
@ -445,6 +532,21 @@ def migrate(direction)
end
end
def exec_migration(conn, direction)
@connection = conn
if respond_to?(:change)
if direction == :down
revert { change }
else
change
end
else
send(direction)
end
ensure
@connection = nil
end
def write(text="")
puts(text) if verbose
end
@ -483,7 +585,7 @@ def method_missing(method, *arguments, &block)
arg_list = arguments.map{ |a| a.inspect } * ', '
say_with_time "#{method}(#{arg_list})" do
unless reverting?
unless @connection.respond_to? :revert
unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first)
arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table

@ -16,69 +16,116 @@ class Migration
class CommandRecorder
include JoinTable
attr_accessor :commands, :delegate
attr_accessor :commands, :delegate, :reverting
def initialize(delegate = nil)
@commands = []
@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
# record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
def record(*command)
@commands << command
def record(*command, &block)
if @reverting
@commands << inverse_of(*command, &block)
else
@commands << (command << block)
end
end
# Returns a list that represents commands that are the inverse of the
# commands stored in +commands+. For example:
# Returns the inverse of the given command. For example:
#
# recorder.record(:rename_table, [:old, :new])
# recorder.inverse # => [:rename_table, [:new, :old]]
# recorder.inverse_of(:rename_table, [:old, :new])
# # => [:rename_table, [:new, :old]]
#
# This method will raise an +IrreversibleMigration+ exception if it cannot
# invert the +commands+.
def inverse
@commands.reverse.map { |name, args|
method = :"invert_#{name}"
raise IrreversibleMigration unless respond_to?(method, true)
send(method, args)
}
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
raise IrreversibleMigration unless respond_to?(method, true)
send(method, args, &block)
end
def respond_to?(*args) # :nodoc:
super || delegate.respond_to?(*args)
end
[:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default, :add_reference, :remove_reference].each do |method|
[:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column, :change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :remove_index,
:change_column, :execute, :remove_columns, # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args) # def create_table(*args)
record(:"#{method}", args) # record(:create_table, args)
end # end
def #{method}(*args, &block) # def create_table(*args, &block)
record(:"#{method}", args, &block) # record(:create_table, args, &block)
end # end
EOV
end
alias :add_belongs_to :add_reference
alias :remove_belongs_to :remove_reference
private
def invert_create_table(args)
[:drop_table, [args.first]]
def change_table(table_name, options = {})
yield ConnectionAdapters::Table.new(table_name, self)
end
def invert_create_join_table(args)
table_name = find_join_table_name(*args)
private
[:drop_table, [table_name]]
module StraightReversions
private
{ transaction: :transaction,
create_table: :drop_table,
create_join_table: :drop_join_table,
add_column: :remove_column,
add_timestamps: :remove_timestamps,
add_reference: :remove_reference,
}.each do |cmd, inv|
[[inv, cmd], [cmd, inv]].each do |method, inverse|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def invert_#{method}(args, &block) # def invert_create_table(args, &block)
[:#{inverse}, args, block] # [:drop_table, args, block]
end # end
EOV
end
end
end
include StraightReversions
def invert_drop_table(args, &block)
if args.size == 1 && block == nil
raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
end
super
end
def invert_rename_table(args)
[:rename_table, args.reverse]
end
def invert_add_column(args)
[:remove_column, args.first(2)]
def invert_remove_column(args)
raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
super
end
def invert_rename_index(args)
@ -91,27 +138,18 @@ def invert_rename_column(args)
def invert_add_index(args)
table, columns, options = *args
index_name = options.try(:[], :name)
options_hash = index_name ? {:name => index_name} : {:column => columns}
[:remove_index, [table, options_hash]]
[:remove_index, [table, (options || {}).merge(column: columns)]]
end
def invert_remove_timestamps(args)
[:add_timestamps, args]
def invert_remove_index(args)
table, options = *args
raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." unless options && options[:column]
options = options.dup
[:add_index, [table, options.delete(:column), options]]
end
def invert_add_timestamps(args)
[:remove_timestamps, args]
end
def invert_add_reference(args)
[:remove_reference, args]
end
alias :invert_add_belongs_to :invert_add_reference
def invert_remove_reference(args)
[:add_reference, args]
end
alias :invert_remove_belongs_to :invert_remove_reference
# Forwards any missing method call to the \target.

@ -21,28 +21,16 @@ def change
end
end
<%- else -%>
def up
def change
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
<%- if attribute.reference? -%>
remove_reference :<%= table_name %>, :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
<%- else -%>
remove_column :<%= table_name %>, :<%= attribute.name %>
<%- end -%>
<%- end -%>
<%- end -%>
end
def down
<% attributes.reverse.each do |attribute| -%>
<%- if migration_action -%>
<%- if attribute.reference? -%>
add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
<%- else -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- end -%>
<%- end -%>
<%- end -%>

@ -17,6 +17,37 @@ def change
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 InvertibleByPartsMigration < SilentMigration
attr_writer :test
def change
create_table("new_horses") do |t|
t.column :breed, :string
end
reversible do |dir|
@test.yield :both
dir.up { @test.yield :up }
dir.down { @test.yield :down }
end
revert do
create_table("horses") do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
end
end
class NonInvertibleMigration < SilentMigration
def change
create_table("horses") do |t|
@ -40,6 +71,23 @@ def self.down
end
end
class RevertWholeMigration < SilentMigration
def initialize(name = self.class.name, version = nil, migration)
@migration = migration
super(name, version)
end
def change
revert @migration
end
end
class NestedRevertWholeMigration < RevertWholeMigration
def change
revert { super }
end
end
def teardown
if ActiveRecord::Base.connection.table_exists?("horses")
ActiveRecord::Base.connection.drop_table("horses")
@ -67,6 +115,83 @@ def test_migrate_down
assert !migration.connection.table_exists?("horses")
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_migrate_revert_by_part
InvertibleMigration.new.migrate :up
received = []
migration = InvertibleByPartsMigration.new
migration.test = ->(dir){
assert migration.connection.table_exists?("horses")
assert migration.connection.table_exists?("new_horses")
received << dir
}
migration.migrate :up
assert_equal [:both, :up], received
assert !migration.connection.table_exists?("horses")
assert migration.connection.table_exists?("new_horses")
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
assert migration.connection.table_exists?("horses")
assert !migration.connection.table_exists?("new_horses")
end
def test_migrate_revert_whole_migration
migration = InvertibleMigration.new
[LegacyMigration, InvertibleMigration].each do |klass|
revert = RevertWholeMigration.new(klass)
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
end
def test_migrate_nested_revert_whole_migration
revert = NestedRevertWholeMigration.new(InvertibleRevertMigration)
revert.migrate :down
assert revert.connection.table_exists?("horses")
revert.migrate :up
assert !revert.connection.table_exists?("horses")
end
def test_revert_order
block = Proc.new{|t| t.string :name }
recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection)
recorder.instance_eval do
create_table("apples", &block)
revert do
create_table("bananas", &block)
revert do
create_table("clementines")
create_table("dates")
end
create_table("elderberries")
end
revert do
create_table("figs")
create_table("grapes")
end
end
assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil],
[:create_table, ["clementines"], nil], [:create_table, ["dates"], nil],
[:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil],
[:drop_table, ["figs"], nil]], recorder.commands
end
def test_legacy_up
LegacyMigration.migrate :up
assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"

@ -3,21 +3,8 @@
module ActiveRecord
class Migration
class TableTest < ActiveRecord::TestCase
class MockConnection < MiniTest::Mock
def native_database_types
{
:string => 'varchar(255)',
:integer => 'integer',
}
end
def type_to_sql(type, limit, precision, scale)
native_database_types[type]
end
end
def setup
@connection = MockConnection.new
@connection = MiniTest::Mock.new
end
def teardown
@ -98,26 +85,18 @@ def test_remove_timestamps_creates_updated_at_and_created_at
end
end
def string_column
@connection.native_database_types[:string]
end
def integer_column
@connection.native_database_types[:integer]
end
def test_integer_creates_integer_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :foo, integer_column, {}]
@connection.expect :add_column, nil, [:delete_me, :bar, integer_column, {}]
@connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}]
@connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
t.integer :foo, :bar
end
end
def test_string_creates_string_column
with_change_table do |t|
@connection.expect :add_column, nil, [:delete_me, :foo, string_column, {}]
@connection.expect :add_column, nil, [:delete_me, :bar, string_column, {}]
@connection.expect :add_column, nil, [:delete_me, :foo, :string, {}]
@connection.expect :add_column, nil, [:delete_me, :bar, :string, {}]
t.string :foo, :bar
end
end
@ -194,14 +173,14 @@ def test_change_default_changes_column
def test_remove_drops_single_column
with_change_table do |t|
@connection.expect :remove_column, nil, [:delete_me, :bar]
@connection.expect :remove_columns, nil, [:delete_me, :bar]
t.remove :bar
end
end
def test_remove_drops_multiple_columns
with_change_table do |t|
@connection.expect :remove_column, nil, [:delete_me, :bar, :baz]
@connection.expect :remove_columns, nil, [:delete_me, :bar, :baz]
t.remove :bar, :baz
end
end

@ -26,7 +26,7 @@ def create_table(name); end
}.new)
assert recorder.respond_to?(:create_table), 'respond_to? create_table'
recorder.send(:create_table, :horses)
assert_equal [[:create_table, [:horses]]], recorder.commands
assert_equal [[:create_table, [:horses], nil]], recorder.commands
end
def test_unknown_commands_delegate
@ -34,10 +34,15 @@ def test_unknown_commands_delegate
assert_equal 'bar', recorder.foo
end
def test_unknown_commands_raise_exception_if_they_cannot_delegate
@recorder.record :execute, ['some sql']
def test_inverse_of_raise_exception_on_unknown_commands
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse
@recorder.inverse_of :execute, ['some sql']
end
end
def test_irreversible_commands_raise_exception
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.revert{ @recorder.execute 'some sql' }
end
end
@ -46,121 +51,196 @@ def test_record
assert_equal 1, @recorder.commands.length
end
def test_inverse
@recorder.record :create_table, [:system_settings]
assert_equal 1, @recorder.inverse.length
@recorder.record :rename_table, [:old, :new]
assert_equal 2, @recorder.inverse.length
end
def test_inverted_commands_are_reveresed
@recorder.record :create_table, [:hello]
@recorder.record :create_table, [:world]
tables = @recorder.inverse.map(&:last)
def test_inverted_commands_are_reversed
@recorder.revert do
@recorder.record :create_table, [:hello]
@recorder.record :create_table, [:world]
end
tables = @recorder.commands.map{|_cmd, args, _block| args}
assert_equal [[:world], [:hello]], tables
end
def test_invert_create_table
@recorder.record :create_table, [:system_settings]
drop_table = @recorder.inverse.first
assert_equal [:drop_table, [:system_settings]], drop_table
def test_revert_order
block = Proc.new{|t| t.string :name }
@recorder.instance_eval do
create_table("apples", &block)
revert do
create_table("bananas", &block)
revert do
create_table("clementines", &block)
create_table("dates")
end
create_table("elderberries")
end
revert do
create_table("figs", &block)
create_table("grapes")
end
end
assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil],
[:create_table, ["clementines"], block], [:create_table, ["dates"], nil],
[:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil],
[:drop_table, ["figs"], block]], @recorder.commands
end
def test_invert_create_table_with_options
@recorder.record :create_table, [:people_reminders, {:id => false}]
drop_table = @recorder.inverse.first
assert_equal [:drop_table, [:people_reminders]], drop_table
def test_invert_change_table
@recorder.revert do
@recorder.change_table :fruits do |t|
t.string :name
t.rename :kind, :cultivar
end
end
assert_equal [
[:rename_column, [:fruits, :cultivar, :kind]],
[:remove_column, [:fruits, :name, :string, {}], nil],
], @recorder.commands
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.revert do
@recorder.change_table :fruits do |t|
t.remove :kind
end
end
end
end
def test_invert_create_table
@recorder.revert do
@recorder.record :create_table, [:system_settings]
end
drop_table = @recorder.commands.first
assert_equal [:drop_table, [:system_settings], nil], drop_table
end
def test_invert_create_table_with_options_and_block
block = Proc.new{}
drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block
assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table
end
def test_invert_drop_table
block = Proc.new{}
create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block
assert_equal [:create_table, [:people_reminders, id: false], block], create_table
end
def test_invert_drop_table_without_a_block_nor_option
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :drop_table, [:people_reminders]
end
end
def test_invert_create_join_table
@recorder.record :create_join_table, [:musics, :artists]
drop_table = @recorder.inverse.first
assert_equal [:drop_table, [:artists_musics]], drop_table
drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists]
assert_equal [:drop_join_table, [:musics, :artists], nil], drop_join_table
end
def test_invert_create_join_table_with_table_name
@recorder.record :create_join_table, [:musics, :artists, {:table_name => :catalog}]
drop_table = @recorder.inverse.first
assert_equal [:drop_table, [:catalog]], drop_table
drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists, table_name: :catalog]
assert_equal [:drop_join_table, [:musics, :artists, table_name: :catalog], nil], drop_join_table
end
def test_invert_drop_join_table
block = Proc.new{}
create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block
assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table
end
def test_invert_rename_table
@recorder.record :rename_table, [:old, :new]
rename = @recorder.inverse.first
rename = @recorder.inverse_of :rename_table, [:old, :new]
assert_equal [:rename_table, [:new, :old]], rename
end
def test_invert_add_column
@recorder.record :add_column, [:table, :column, :type, {}]
remove = @recorder.inverse.first
assert_equal [:remove_column, [:table, :column]], remove
remove = @recorder.inverse_of :add_column, [:table, :column, :type, {}]
assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
end
def test_invert_remove_column
add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
assert_equal [:add_column, [:table, :column, :type, {}], nil], add
end
def test_invert_remove_column_without_type
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :remove_column, [:table, :column]
end
end
def test_invert_rename_column
@recorder.record :rename_column, [:table, :old, :new]
rename = @recorder.inverse.first
rename = @recorder.inverse_of :rename_column, [:table, :old, :new]
assert_equal [:rename_column, [:table, :new, :old]], rename
end
def test_invert_add_index
@recorder.record :add_index, [:table, [:one, :two], {:options => true}]
remove = @recorder.inverse.first
assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
remove = @recorder.inverse_of :add_index, [:table, [:one, :two], options: true]
assert_equal [:remove_index, [:table, {column: [:one, :two], options: true}]], remove
end
def test_invert_add_index_with_name
@recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}]
remove = @recorder.inverse.first
assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove
remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"]
assert_equal [:remove_index, [:table, {column: [:one, :two], name: "new_index"}]], remove
end
def test_invert_add_index_with_no_options
@recorder.record :add_index, [:table, [:one, :two]]
remove = @recorder.inverse.first
assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
remove = @recorder.inverse_of :add_index, [:table, [:one, :two]]
assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove
end
def test_invert_remove_index
add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}]
assert_equal [:add_index, [:table, [:one, :two], options: true]], add
end
def test_invert_remove_index_with_name
add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], name: "new_index"}]
assert_equal [:add_index, [:table, [:one, :two], name: "new_index"]], add
end
def test_invert_remove_index_with_no_special_options
add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two]}]
assert_equal [:add_index, [:table, [:one, :two], {}]], add
end
def test_invert_remove_index_with_no_column
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :remove_index, [:table, name: "new_index"]
end
end
def test_invert_rename_index
@recorder.record :rename_index, [:table, :old, :new]
rename = @recorder.inverse.first
rename = @recorder.inverse_of :rename_index, [:table, :old, :new]
assert_equal [:rename_index, [:table, :new, :old]], rename
end
def test_invert_add_timestamps
@recorder.record :add_timestamps, [:table]
remove = @recorder.inverse.first
assert_equal [:remove_timestamps, [:table]], remove
remove = @recorder.inverse_of :add_timestamps, [:table]
assert_equal [:remove_timestamps, [:table], nil], remove
end
def test_invert_remove_timestamps
@recorder.record :remove_timestamps, [:table]
add = @recorder.inverse.first
assert_equal [:add_timestamps, [:table]], add
add = @recorder.inverse_of :remove_timestamps, [:table]
assert_equal [:add_timestamps, [:table], nil], add
end
def test_invert_add_reference
@recorder.record :add_reference, [:table, :taggable, { polymorphic: true }]
remove = @recorder.inverse.first
assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }]], remove
remove = @recorder.inverse_of :add_reference, [:table, :taggable, { polymorphic: true }]
assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }], nil], remove
end
def test_invert_add_belongs_to_alias
@recorder.record :add_belongs_to, [:table, :user]
remove = @recorder.inverse.first
assert_equal [:remove_reference, [:table, :user]], remove
remove = @recorder.inverse_of :add_belongs_to, [:table, :user]
assert_equal [:remove_reference, [:table, :user], nil], remove
end
def test_invert_remove_reference
@recorder.record :remove_reference, [:table, :taggable, { polymorphic: true }]
add = @recorder.inverse.first
assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }]], add
add = @recorder.inverse_of :remove_reference, [:table, :taggable, { polymorphic: true }]
assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add
end
def test_invert_remove_belongs_to_alias
@recorder.record :remove_belongs_to, [:table, :user]
add = @recorder.inverse.first
assert_equal [:add_reference, [:table, :user]], add
add = @recorder.inverse_of :remove_belongs_to, [:table, :user]
assert_equal [:add_reference, [:table, :user], nil], add
end
end
end

@ -78,6 +78,48 @@ def test_create_join_table_with_index
assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns)
end
def test_drop_join_table
connection.create_join_table :artists, :musics
connection.drop_join_table :artists, :musics
assert !connection.tables.include?('artists_musics')
end
def test_drop_join_table_with_strings
connection.create_join_table :artists, :musics
connection.drop_join_table 'artists', 'musics'
assert !connection.tables.include?('artists_musics')
end
def test_drop_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
connection.drop_join_table :videos, :musics
assert !connection.tables.include?('musics_videos')
end
def test_drop_join_table_with_the_table_name
connection.create_join_table :artists, :musics, table_name: :catalog
connection.drop_join_table :artists, :musics, table_name: :catalog
assert !connection.tables.include?('catalog')
end
def test_drop_join_table_with_the_table_name_as_string
connection.create_join_table :artists, :musics, table_name: 'catalog'
connection.drop_join_table :artists, :musics, table_name: 'catalog'
assert !connection.tables.include?('catalog')
end
def test_create_join_table_with_column_options
connection.create_join_table :artists, :musics, column_options: {null: true}
connection.drop_join_table :artists, :musics, column_options: {null: true}
assert !connection.tables.include?('artists_musics')
end
end
end
end

@ -165,6 +165,19 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railt
### Notable changes
* Improve ways to write `change` migrations, making the old `up` & `down` methods no longer necessary.
* The methods `drop_table` and `remove_column` are now reversible, as long as the necessary information is given.
The method `remove_column` used to accept multiple column names; instead use `remove_columns` (which is not revertible).
The method `change_table` is also reversible, as long as its block doesn't call `remove`, `change` or `change_default`
* New method `reversible` makes it possible to specify code to be run when migrating up or down.
See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#using-the-reversible-method)
* New method `revert` will revert a whole migration or the given block.
If migrating down, the given migration / block is run normally.
See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations)
* Adds some metadata columns to `schema_migrations` table.
* `migrated_at`

@ -56,25 +56,40 @@ Before this migration is run, there will be no table. After, the table will
exist. Active Record knows how to reverse this migration as well: if we roll
this migration back, it will remove the table.
On databases that support transactions with statements that change the schema ,
On databases that support transactions with statements that change the schema,
migrations are wrapped in a transaction. If the database does not support this
then when a migration fails the parts of it that succeeded will not be rolled
back. You will have to rollback the changes that were made by hand.
If you wish for a migration to do something that Active Record doesn't know how
to reverse, you can use `up` and `down` instead of `change`:
to reverse, you can use `reversible`:
```ruby
class ChangeProductsPrice < ActiveRecord::Migration
def up
change_table :products do |t|
t.string :price, null: false
def change
reversible do |dir|
change_table :products do |t|
dir.up { t.change :price, :string }
dir.down { t.change :price, :integer }
end
end
end
end
```
Alternatively, you can use `up` and `down` instead of `change`:
``ruby
class ChangeProductsPrice < ActiveRecord::Migration
def up
change_table :products do |t|
t.change :price, :string
end
end
def down
change_table :products do |t|
t.integer :price, null: false
t.change :price, :integer
end
end
end
@ -93,7 +108,7 @@ of the migration. The name of the migration class (CamelCased version)
should match the latter part of the file name. For example
`20080906120000_create_products.rb` should define class `CreateProducts` and
`20080906120001_add_details_to_products.rb` should define
`AddDetailsToProducts`.
`AddDetailsToProducts`.
Of course, calculating timestamps is no fun, so Active Record provides a
generator to handle making it for you:
@ -139,12 +154,8 @@ generates
```ruby
class RemovePartNumberFromProducts < ActiveRecord::Migration
def up
remove_column :products, :part_number
end
def down
add_column :products, :part_number, :string
def change
remove_column :products, :part_number, :string
end
end
```
@ -170,10 +181,6 @@ As always, what has been generated for you is just a starting point. You can add
or remove from it as you see fit by editing the
`db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file.
NOTE: The generated migration file for destructive migrations will still be
old-style using the `up` and `down` methods. This is because Rails needs to
know the original data types defined when you made the original changes.
Also, the generator accepts column type as `references`(also available as
`belongs_to`). For instance
@ -346,7 +353,7 @@ Products.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
For more details and examples of individual methods, check the API documentation.
In particular the documentation for
[`ActiveRecord::ConnectionAdapters::SchemaStatements`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html)
(which provides the methods available in the `up` and `down` methods),
(which provides the methods available in the `change`, `up` and `down` methods),
[`ActiveRecord::ConnectionAdapters::TableDefinition`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html)
(which provides the methods available on the object yielded by `create_table`)
and
@ -362,25 +369,82 @@ definitions:
* `add_column`
* `add_index`
* `add_reference`
* `add_timestamps`
* `create_table`
* `create_join_table`
* `drop_table` (must supply a block)
* `drop_join_table` (must supply a block)
* `remove_timestamps`
* `rename_column`
* `rename_index`
* `remove_reference`
* `rename_table`
If you're going to need to use any other methods, you'll have to write the
`up` and `down` methods instead of using the `change` method.
`change_table` is also reversible, as long as the block does not call `change`,
`change_default` or `remove`.
If you're going to need to use any other methods, you should use `reversible`
or write the `up` and `down` methods instead of using the `change` method.
### Using `reversible`
Complex migrations may require processing that Active Record doesn't know how
to reverse. You can use `reversible` to specify what to do when running a
migration what else to do when reverting it. For example,
```ruby
class ExampleMigration < ActiveRecord::Migration
def change
create_table :products do |t|
t.references :category
end
reversible do |dir|
dir.up do
#add a foreign key
execute <<-SQL
ALTER TABLE products
ADD CONSTRAINT fk_products_categories
FOREIGN KEY (category_id)
REFERENCES categories(id)
SQL
end
dir.down do
execute <<-SQL
ALTER TABLE products
DROP FOREIGN KEY fk_products_categories
SQL
end
end
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
```
Using `reversible` will insure that the instructions are executed in the
right order too. If the previous example migration is reverted,
the `down` block will be run after the `home_page_url` column is removed and
right before the table `products` is dropped.
Sometimes your migration will do something which is just plain irreversible; for
example, it might destroy some data. In such cases, you can raise
`ActiveRecord::IrreversibleMigration` in your `down` block. If someone tries
to revert your migration, an error message will be displayed saying that it
can't be done.
### Using the `up`/`down` Methods
You can also use the old style of migration using `up` and `down` methods
instead of the `change` method.
The `up` method should describe the transformation you'd like to make to your
schema, and the `down` method of your migration should revert the
transformations done by the `up` method. In other words, the database schema
should be unchanged if you do an `up` followed by a `down`. For example, if you
create a table in the `up` method, you should drop it in the `down` method. It
is wise to reverse the transformations in precisely the reverse order they were
made in the `up` method. For example,
made in the `up` method. The example in the `reversible` section is equivalent to:
```ruby
class ExampleMigration < ActiveRecord::Migration
@ -415,19 +479,92 @@ class ExampleMigration < ActiveRecord::Migration
end
```
Sometimes your migration will do something which is just plain irreversible; for
example, it might destroy some data. In such cases, you can raise
If your migration is irreversible, you should raise
`ActiveRecord::IrreversibleMigration` from your `down` method. If someone tries
to revert your migration, an error message will be displayed saying that it
can't be done.
### Reverting Previous Migrations
You can use Active Record's ability to rollback migrations using the `revert` method:
```ruby
require_relative '2012121212_example_migration'
class FixupExampleMigration < ActiveRecord::Migration
def change
revert ExampleMigration
create_table(:apples) do |t|
t.string :variety
end
end
end
```
The `revert` method also accepts a block of instructions to reverse.
This could be useful to revert selected parts of previous migrations.
For example, let's imagine that `ExampleMigration` is committed and it
is later decided it would be best to serialize the product list instead.
One could write:
```ruby
class SerializeProductListMigration < ActiveRecord::Migration
def change
add_column :categories, :product_list
reversible do |dir|
dir.up do
# transfer data from Products to Category#product_list
end
dir.down do
# create Products from Category#product_list
end
end
revert do
# copy-pasted code from ExampleMigration
create_table :products do |t|
t.references :category
end
reversible do |dir|
dir.up do
#add a foreign key
execute <<-SQL
ALTER TABLE products
ADD CONSTRAINT fk_products_categories
FOREIGN KEY (category_id)
REFERENCES categories(id)
SQL
end
dir.down do
execute <<-SQL
ALTER TABLE products
DROP FOREIGN KEY fk_products_categories
SQL
end
end
# The rest of the migration was ok
end
end
end
```
The same migration could also have been written without using `revert`
but this would have involved a few more steps: reversing the order
of `create_table` and `reversible`, replacing `create_table`
by `drop_table`, and finally replacing `up` by `down` and vice-versa.
This is all taken care of by `revert`.
Running Migrations
------------------
Rails provides a set of Rake tasks to run certain sets of migrations.
The very first migration related Rake task you will use will probably be
`rake db:migrate`. In its most basic form it just runs the `up` or `change`
`rake db:migrate`. In its most basic form it just runs the `change` or `up`
method for all the migrations that have not yet been run. If there are
no such migrations, it exits. It will run these migrations in order based
on the date of the migration.
@ -436,7 +573,7 @@ Note that running the `db:migrate` also invokes the `db:schema:dump` task, which
will update your `db/schema.rb` file to match the structure of your database.
If you specify a target version, Active Record will run the required migrations
(up, down or change) until it has reached the specified version. The version
(change, up, down) until it has reached the specified version. The version
is the numerical prefix on the migration's filename. For example, to migrate
to version 20080906120000 run
@ -445,7 +582,8 @@ $ rake db:migrate VERSION=20080906120000
```
If version 20080906120000 is greater than the current version (i.e., it is
migrating upwards), this will run the `up` method on all migrations up to and
migrating upwards), this will run the `change` (or `up`) method
on all migrations up to and
including 20080906120000, and will not execute any later migrations. If
migrating downwards, this will run the `down` method on all the migrations
down to, but not including, 20080906120000.
@ -460,14 +598,15 @@ number associated with the previous migration you can run
$ rake db:rollback
```
This will run the `down` method from the latest migration. If you need to undo
This will rollback the latest migration, either by reverting the `change`
method or by running the `down` method. If you need to undo
several migrations you can provide a `STEP` parameter:
```bash
$ rake db:rollback STEP=3
```
will run the `down` method from the last 3 migrations.
will revert the last 3 migrations.
The `db:migrate:redo` task is a shortcut for doing a rollback and then migrating
back up again. As with the `db:rollback` task, you can use the `STEP` parameter
@ -495,14 +634,15 @@ contents of the current schema.rb file. If a migration can't be rolled back,
If you need to run a specific migration up or down, the `db:migrate:up` and
`db:migrate:down` tasks will do that. Just specify the appropriate version and
the corresponding migration will have its `up` or `down` method invoked, for
example,
the corresponding migration will have its `change`, `up` or `down` method
invoked, for example,
```bash
$ rake db:migrate:up VERSION=20080906120000
```
will run the `up` method from the 20080906120000 migration. This task will
will run the 20080906120000 migration by running the `change` method (or the
`up` method). This task will
first check whether the migration is already performed and will do nothing if
Active Record believes that it has already been run.
@ -596,6 +736,10 @@ you require. Editing a freshly generated migration that has not yet been
committed to source control (or, more generally, which has not been propagated
beyond your development machine) is relatively harmless.
The `revert` method can be helpful when writing a new migration to undo
previous migrations in whole or in part
(see [Reverting Previous Migrations](#reverting-previous-migrations) above).
Using Models in Your Migrations
-------------------------------
@ -622,6 +766,9 @@ column.
class AddFlagToProduct < ActiveRecord::Migration
def change
add_column :products, :flag, :boolean
reversible do |dir|
dir.up { Product.update_all flag: false }
end
Product.update_all flag: false
end
end
@ -645,7 +792,9 @@ column.
class AddFuzzToProduct < ActiveRecord::Migration
def change
add_column :products, :fuzz, :string
Product.update_all fuzz: 'fuzzy'
reversible do |dir|
dir.up { Product.update_all fuzz: 'fuzzy' }
end
end
end
```
@ -697,7 +846,9 @@ class AddFlagToProduct < ActiveRecord::Migration
def change
add_column :products, :flag, :boolean
Product.reset_column_information
Product.update_all flag: false
reversible do |dir|
dir.up { Product.update_all flag: false }
end
end
end
```
@ -712,7 +863,9 @@ class AddFuzzToProduct < ActiveRecord::Migration
def change
add_column :products, :fuzz, :string
Product.reset_column_information
Product.update_all fuzz: 'fuzzy'
reversible do |dir|
dir.up { Product.update_all fuzz: 'fuzzy' }
end
end
end
```
@ -810,9 +963,9 @@ Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump`
utility is used. For MySQL, this file will contain the output of `SHOW CREATE
TABLE` for the various tables.
Loading these schemas is simply a question of executing the SQL statements they
contain. By definition, this will create a perfect copy of the database's
structure. Using the `:sql` schema format will, however, prevent loading the
Loading these schemas is simply a question of executing the SQL statements they
contain. By definition, this will create a perfect copy of the database's
structure. Using the `:sql` schema format will, however, prevent loading the
schema into a RDBMS other than the one used to create it.
### Schema Dumps and Source Control

@ -1,5 +1,9 @@
## Rails 4.0.0 (unreleased) ##
* Generated migrations now always use the `change` method.
*Marc-André Lafortune*
* Add `app/models/concerns` and `app/controllers/concerns` to the default directory structure and load path.
See http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns for usage instructions.

@ -28,7 +28,7 @@ def test_migration_with_class_name
run_generator [migration]
assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/
end
def test_migration_with_invalid_file_name
migration = "add_something:datetime"
assert_raise ActiveRecord::IllegalMigrationNameError do
@ -41,9 +41,9 @@ def test_add_migration_with_attributes
run_generator [migration, "title:string", "body:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/add_column :posts, :title, :string/, up)
assert_match(/add_column :posts, :body, :text/, up)
assert_method :change, content do |change|
assert_match(/add_column :posts, :title, :string/, change)
assert_match(/add_column :posts, :body, :text/, change)
end
end
end
@ -53,15 +53,10 @@ def test_remove_migration_with_indexed_attribute
run_generator [migration, "title:string:index", "body:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :up, content do |up|
assert_match(/remove_column :posts, :title/, up)
assert_match(/remove_column :posts, :body/, up)
end
assert_method :down, content do |down|
assert_match(/add_column :posts, :title, :string/, down)
assert_match(/add_column :posts, :body, :text/, down)
assert_match(/add_index :posts, :title/, down)
assert_method :change, content do |change|
assert_match(/remove_column :posts, :title, :string/, change)
assert_match(/remove_column :posts, :body, :text/, change)
assert_match(/remove_index :posts, :title/, change)
end
end
end
@ -71,14 +66,9 @@ def test_remove_migration_with_attributes
run_generator [migration, "title:string", "body:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :up, content do |up|
assert_match(/remove_column :posts, :title/, up)
assert_match(/remove_column :posts, :body/, up)
end
assert_method :down, content do |down|
assert_match(/add_column :posts, :title, :string/, down)
assert_match(/add_column :posts, :body, :text/, down)
assert_method :change, content do |change|
assert_match(/remove_column :posts, :title, :string/, change)
assert_match(/remove_column :posts, :body, :text/, change)
end
end
end
@ -88,14 +78,9 @@ def test_remove_migration_with_references_options
run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :up, content do |up|
assert_match(/remove_reference :books, :author/, up)
assert_match(/remove_reference :books, :distributor, polymorphic: true/, up)
end
assert_method :down, content do |down|
assert_match(/add_reference :books, :author, index: true/, down)
assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, down)
assert_method :change, content do |change|
assert_match(/remove_reference :books, :author, index: true/, change)
assert_match(/remove_reference :books, :distributor, polymorphic: true, index: true/, change)
end
end
end
@ -105,13 +90,13 @@ def test_add_migration_with_attributes_and_indices
run_generator [migration, "title:string:index", "body:text", "user_id:integer:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/add_column :posts, :title, :string/, up)
assert_match(/add_column :posts, :body, :text/, up)
assert_match(/add_column :posts, :user_id, :integer/, up)
assert_method :change, content do |change|
assert_match(/add_column :posts, :title, :string/, change)
assert_match(/add_column :posts, :body, :text/, change)
assert_match(/add_column :posts, :user_id, :integer/, change)
assert_match(/add_index :posts, :title/, change)
assert_match(/add_index :posts, :user_id, unique: true/, change)
end
assert_match(/add_index :posts, :title/, content)
assert_match(/add_index :posts, :user_id, unique: true/, content)
end
end
@ -120,10 +105,10 @@ def test_add_migration_with_attributes_and_wrong_index_declaration
run_generator [migration, "title:string:inex", "content:text", "user_id:integer:unik"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/add_column :books, :title, :string/, up)
assert_match(/add_column :books, :content, :text/, up)
assert_match(/add_column :books, :user_id, :integer/, up)
assert_method :change, content do |change|
assert_match(/add_column :books, :title, :string/, change)
assert_match(/add_column :books, :content, :text/, change)
assert_match(/add_column :books, :user_id, :integer/, change)
end
assert_no_match(/add_index :books, :title/, content)
assert_no_match(/add_index :books, :user_id/, content)
@ -135,13 +120,13 @@ def test_add_migration_with_attributes_without_type_and_index
run_generator [migration, "title:index", "body:text", "user_uuid:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/add_column :posts, :title, :string/, up)
assert_match(/add_column :posts, :body, :text/, up)
assert_match(/add_column :posts, :user_uuid, :string/, up)
assert_method :change, content do |change|
assert_match(/add_column :posts, :title, :string/, change)
assert_match(/add_column :posts, :body, :text/, change)
assert_match(/add_column :posts, :user_uuid, :string/, change)
assert_match(/add_index :posts, :title/, change)
assert_match(/add_index :posts, :user_uuid, unique: true/, change)
end
assert_match(/add_index :posts, :title/, content)
assert_match(/add_index :posts, :user_uuid, unique: true/, content)
end
end
@ -150,11 +135,11 @@ def test_add_migration_with_attributes_index_declaration_and_attribute_options
run_generator [migration, "title:string{40}:index", "content:string{255}", "price:decimal{1,2}:index", "discount:decimal{3.4}:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/add_column :books, :title, :string, limit: 40/, up)
assert_match(/add_column :books, :content, :string, limit: 255/, up)
assert_match(/add_column :books, :price, :decimal, precision: 1, scale: 2/, up)
assert_match(/add_column :books, :discount, :decimal, precision: 3, scale: 4/, up)
assert_method :change, content do |change|
assert_match(/add_column :books, :title, :string, limit: 40/, change)
assert_match(/add_column :books, :content, :string, limit: 255/, change)
assert_match(/add_column :books, :price, :decimal, precision: 1, scale: 2/, change)
assert_match(/add_column :books, :discount, :decimal, precision: 3, scale: 4/, change)
end
assert_match(/add_index :books, :title/, content)
assert_match(/add_index :books, :price/, content)
@ -167,9 +152,9 @@ def test_add_migration_with_references_options
run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/add_reference :books, :author, index: true/, up)
assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, up)
assert_method :change, content do |change|
assert_match(/add_reference :books, :author, index: true/, change)
assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, change)
end
end
end
@ -179,10 +164,10 @@ def test_create_join_table_migration
run_generator [migration, "artist_id", "musics:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |up|
assert_match(/create_join_table :artists, :musics/, up)
assert_match(/# t.index \[:artist_id, :music_id\]/, up)
assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, up)
assert_method :change, content do |change|
assert_match(/create_join_table :artists, :musics/, change)
assert_match(/# t.index \[:artist_id, :music_id\]/, change)
assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change)
end
end
end
@ -192,12 +177,8 @@ def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove
run_generator [migration, "title:string", "content:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :up, content do |up|
assert_match(/^\s*$/, up)
end
assert_method :down, content do |down|
assert_match(/^\s*$/, down)
assert_method :change, content do |change|
assert_match(/^\s*$/, change)
end
end
end