diff options
author | Aaron Patterson <aaron.patterson@gmail.com> | 2012-12-21 13:22:52 -0800 |
---|---|---|
committer | Aaron Patterson <aaron.patterson@gmail.com> | 2012-12-21 13:22:52 -0800 |
commit | 68e91da765565f0c473463b0b47814592dea5de3 (patch) | |
tree | 7b97a7d3ac2788c3acc2da90245208e844621710 | |
parent | 59ea907a30438a3aa458ef18f0ccb6ceadb8322d (diff) | |
parent | a81845f26864d076970e706863c766aead432672 (diff) | |
download | rails-68e91da765565f0c473463b0b47814592dea5de3.tar.gz rails-68e91da765565f0c473463b0b47814592dea5de3.tar.bz2 rails-68e91da765565f0c473463b0b47814592dea5de3.zip |
Merge pull request #8267 from marcandre/reversible_drop_table_etc
Reversible commands
-rw-r--r-- | activerecord/CHANGELOG.md | 17 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb | 18 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb | 40 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb | 10 | ||||
-rw-r--r-- | activerecord/lib/active_record/migration.rb | 162 | ||||
-rw-r--r-- | activerecord/lib/active_record/migration/command_recorder.rb | 122 | ||||
-rw-r--r-- | activerecord/lib/rails/generators/active_record/migration/templates/migration.rb | 20 | ||||
-rw-r--r-- | activerecord/test/cases/invertible_migration_test.rb | 125 | ||||
-rw-r--r-- | activerecord/test/cases/migration/change_table_test.rb | 35 | ||||
-rw-r--r-- | activerecord/test/cases/migration/command_recorder_test.rb | 208 | ||||
-rw-r--r-- | activerecord/test/cases/migration/create_join_table_test.rb | 42 | ||||
-rw-r--r-- | guides/source/4_0_release_notes.md | 13 | ||||
-rw-r--r-- | guides/source/migrations.md | 225 | ||||
-rw-r--r-- | railties/CHANGELOG.md | 4 | ||||
-rw-r--r-- | railties/test/generators/migration_generator_test.rb | 107 |
15 files changed, 841 insertions, 307 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 442b11dad9..272cef6fcf 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -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. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 73012834c9..b1ec33d06c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -423,7 +423,7 @@ module ActiveRecord # 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 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index f1e42dfbbe..f2d6eb9d27 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -214,6 +214,17 @@ module ActiveRecord 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 @@ module ActiveRecord 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 @@ module ActiveRecord execute(add_column_sql) end - # Removes the column(s) from the table definition. + # Removes the given columns 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}" } + 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) + # + # 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 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 8aa5707959..11e8197293 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -444,15 +444,11 @@ module ActiveRecord 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| diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index ef2107ad24..806b367c6b 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -373,22 +373,129 @@ module ActiveRecord @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 @@ module ActiveRecord 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 @@ module ActiveRecord 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 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 95f4360578..13ce28330a 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -16,69 +16,116 @@ module ActiveRecord 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 @@ module ActiveRecord 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] - end + 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] - def invert_add_timestamps(args) - [:remove_timestamps, args] + options = options.dup + [:add_index, [table, options.delete(:column), options]] 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. diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index d5c07aecd3..ae9c74fd05 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -21,28 +21,16 @@ class <%= migration_class_name %> < ActiveRecord::Migration 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 -%> diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 8f1cdd47ea..cf4e39c4ef 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -17,6 +17,37 @@ module ActiveRecord 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 @@ module ActiveRecord 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 @@ module ActiveRecord 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" diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 8fb03cdee0..ac1ad176db 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -3,21 +3,8 @@ require "cases/migration/helper" 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 @@ module ActiveRecord 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 @@ module ActiveRecord 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 diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index f2213ee6aa..2cad8a6d96 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -26,7 +26,7 @@ module ActiveRecord }.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 @@ module ActiveRecord 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 @@ module ActiveRecord 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 + 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_inverted_commands_are_reveresed - @recorder.record :create_table, [:hello] - @recorder.record :create_table, [:world] - tables = @recorder.inverse.map(&:last) - assert_equal [[:world], [:hello]], tables + 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_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.record :create_table, [:system_settings] - drop_table = @recorder.inverse.first - assert_equal [:drop_table, [:system_settings]], drop_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_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_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 diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index f262bbaad7..c099854ad8 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -78,6 +78,48 @@ module ActiveRecord 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 diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index dd57787111..55ac4bca87 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -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` diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 9840e7694f..62b70b5571 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -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 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.string :price, null: false + 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 diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 2965ce9ffc..961d3dc450 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -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. diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 15e5a0b92b..62d9d1f06a 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -28,7 +28,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 @@ class MigrationGeneratorTest < Rails::Generators::TestCase 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 |