diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
commit | 9887f238871bb2dd73de6ce8855615bcc5d8d079 (patch) | |
tree | 74fa9ff9524a51701cfa23f708b3f777c65b7fe5 /activerecord/lib/active_record/migration.rb | |
parent | aff821508a16245ebc03510ba29c70379718dfb7 (diff) | |
parent | 5214e73850916de3c9127d35a4ecee0424d364a3 (diff) | |
download | rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.gz rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.bz2 rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.zip |
Merge branch 'master' of https://github.com/rails/rails
Diffstat (limited to 'activerecord/lib/active_record/migration.rb')
-rw-r--r-- | activerecord/lib/active_record/migration.rb | 475 |
1 files changed, 299 insertions, 176 deletions
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5e272f0ba4..640111096d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,4 @@ -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/aliasing' +require "active_support/core_ext/array/wrap" module ActiveRecord # Exception that can be raised to stop migrations from going backwards. @@ -31,39 +30,39 @@ module ActiveRecord end # = Active Record Migrations - # - # Migrations can manage the evolution of a schema used by several physical + # + # Migrations can manage the evolution of a schema used by several physical # databases. It's a solution to the common problem of adding a field to make # a new feature work in your local database, but being unsure of how to - # push that change to other developers and to the production server. With + # push that change to other developers and to the production server. With # migrations, you can describe the transformations in self-contained classes - # that can be checked into version control systems and executed against + # that can be checked into version control systems and executed against # another database that might be one, two, or five versions behind. # # Example of a simple migration: # # class AddSsl < ActiveRecord::Migration - # def self.up + # def up # add_column :accounts, :ssl_enabled, :boolean, :default => 1 # end # - # def self.down + # def down # remove_column :accounts, :ssl_enabled # end # end # - # This migration will add a boolean flag to the accounts table and remove it - # if you're backing out of the migration. It shows how all migrations have - # two class methods +up+ and +down+ that describes the transformations + # This migration will add a boolean flag to the accounts table and remove it + # if you're backing out of the migration. It shows how all migrations have + # two class methods +up+ and +down+ that describes the transformations # required to implement or remove the migration. These methods can consist # of both the migration specific methods like add_column and remove_column, - # but may also contain regular Ruby code for generating data needed for the + # but may also contain regular Ruby code for generating data needed for the # transformations. # # Example of a more complex migration that also needs to initialize data: # # class AddSystemSettings < ActiveRecord::Migration - # def self.up + # def up # create_table :system_settings do |t| # t.string :name # t.string :label @@ -72,58 +71,58 @@ module ActiveRecord # t.integer :position # end # - # SystemSetting.create :name => "notice", - # :label => "Use notice?", + # SystemSetting.create :name => "notice", + # :label => "Use notice?", # :value => 1 # end # - # def self.down + # def down # drop_table :system_settings # end # end # - # This migration first adds the system_settings table, then creates the very + # This migration first adds the system_settings table, then creates the very # first row in it using the Active Record model that relies on the table. It - # also uses the more advanced create_table syntax where you can specify a + # also uses the more advanced create_table syntax where you can specify a # complete table schema in one block call. # # == Available transformations # - # * <tt>create_table(name, options)</tt> Creates a table called +name+ and + # * <tt>create_table(name, options)</tt> Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, - # following the same format as add_column. See example above. The options hash - # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create + # following the same format as add_column. See example above. The options hash + # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. # * <tt>drop_table(name)</tt>: Drops the table called +name+. - # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ # to +new_name+. - # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column + # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column # to the table called +table_name+ # named +column_name+ specified to be one of the following types: - # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, + # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be - # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. - # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. - # <tt>{ :limit => 50, :null => false }</tt>) -- see + # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. + # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. + # <tt>{ :limit => 50, :null => false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames # a column but keeps the type and content. - # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named + # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named # +column_name+ from the table called +table_name+. - # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index + # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include - # <tt>:name</tt> and <tt>:unique</tt> (e.g. + # <tt>:name</tt> and <tt>:unique</tt> (e.g. # <tt>{ :name => "users_name_index", :unique => true }</tt>). - # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified + # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified # by +index_name+. # # == Irreversible transformations # - # Some transformations are destructive in a manner that cannot be reversed. - # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt> + # Some transformations are destructive in a manner that cannot be reversed. + # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt> # exception in their +down+ method. # # == Running migrations from within Rails @@ -134,11 +133,11 @@ module ActiveRecord # rails generate migration MyNewMigration # # where MyNewMigration is the name of your migration. The generator will - # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> - # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the + # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> + # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the # UTC formatted date and time that the migration was generated. # - # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of + # You may then edit the <tt>up</tt> and <tt>down</tt> methods of # MyNewMigration. # # There is a special syntactic shortcut to generate migrations that add fields to a table. @@ -147,11 +146,11 @@ module ActiveRecord # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration - # def self.up + # def up # add_column :tablenames, :fieldname, :string # end # - # def self.down + # def down # remove_column :tablenames, :fieldname # end # end @@ -179,11 +178,11 @@ module ActiveRecord # Not all migrations change the schema. Some just fix the data: # # class RemoveEmptyTags < ActiveRecord::Migration - # def self.up + # def up # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? } # end # - # def self.down + # def down # # not much we can do to restore deleted data # raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags" # end @@ -192,12 +191,12 @@ module ActiveRecord # Others remove columns when they migrate up instead of down: # # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration - # def self.up + # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count # end # - # def self.down + # def down # add_column :items, :incomplete_items_count # add_column :items, :completed_items_count # end @@ -206,24 +205,24 @@ module ActiveRecord # And sometimes you need to do something in SQL not abstracted directly by migrations: # # class MakeJoinUnique < ActiveRecord::Migration - # def self.up + # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end # - # def self.down + # def down # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`" # end # end # # == Using a model after changing its table # - # Sometimes you'll want to add a column in a migration and populate it - # immediately after. In that case, you'll need to make a call to - # <tt>Base#reset_column_information</tt> in order to ensure that the model has the + # Sometimes you'll want to add a column in a migration and populate it + # immediately after. In that case, you'll need to make a call to + # <tt>Base#reset_column_information</tt> in order to ensure that the model has the # latest column data from after the new column was added. Example: # # class AddPeopleSalary < ActiveRecord::Migration - # def self.up + # def up # add_column :people, :salary, :integer # Person.reset_column_information # Person.find(:all).each do |p| @@ -243,7 +242,7 @@ module ActiveRecord # You can also insert your own messages and benchmarks by using the +say_with_time+ # method: # - # def self.up + # def up # ... # say_with_time "Updating salaries..." do # Person.find(:all).each do |p| @@ -286,111 +285,216 @@ module ActiveRecord # # In application.rb. # + # == Reversible Migrations + # + # Starting with Rails 3.1, you will be able to define reversible migrations. + # Reversible migrations are migrations that know how to go +down+ for you. + # You simply supply the +up+ logic, and the Migration system will figure out + # how to execute the down commands for you. + # + # To define a reversible migration, define the +change+ method in your + # migration like this: + # + # class TenderloveMigration < ActiveRecord::Migration + # def change + # create_table(:horses) do + # t.column :content, :text + # t.column :remind_at, :datetime + # end + # end + # end + # + # This migration will create the horses table for you on the way up, and + # automatically figure out how to drop the table on the way down. + # + # Some commands like +remove_column+ cannot be reversed. If you care to + # define how to move up and down in these cases, you should define the +up+ + # and +down+ methods as before. + # + # If a command cannot be reversed, an + # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when + # the migration is moving down. + # + # For a list of commands that are reversible, please see + # <tt>ActiveRecord::Migration::CommandRecorder</tt>. class Migration - @@verbose = true - cattr_accessor :verbose + autoload :CommandRecorder, 'active_record/migration/command_recorder' class << self - def up_with_benchmarks #:nodoc: - migrate(:up) - end - - def down_with_benchmarks #:nodoc: - migrate(:down) - end + attr_accessor :delegate # :nodoc: + end - # Execute this migration in the named direction - def migrate(direction) - return unless respond_to?(direction) + def self.method_missing(name, *args, &block) # :nodoc: + (delegate || superclass.delegate).send(name, *args, &block) + end - case direction - when :up then announce "migrating" - when :down then announce "reverting" - end + cattr_accessor :verbose - result = nil - time = Benchmark.measure { result = send("#{direction}_without_benchmarks") } + attr_accessor :name, :version - case direction - when :up then announce "migrated (%.4fs)" % time.real; write - when :down then announce "reverted (%.4fs)" % time.real; write - end + def initialize + @name = self.class.name + @version = nil + @connection = nil + end - result - end + # instantiate the delegate object after initialize is defined + self.verbose = true + self.delegate = new - # Because the method added may do an alias_method, it can be invoked - # recursively. We use @ignore_new_methods as a guard to indicate whether - # it is safe for the call to proceed. - def singleton_method_added(sym) #:nodoc: - return if defined?(@ignore_new_methods) && @ignore_new_methods + def up + self.class.delegate = self + return unless self.class.respond_to?(:up) + self.class.up + end - begin - @ignore_new_methods = true + def down + self.class.delegate = self + return unless self.class.respond_to?(:down) + self.class.down + end - case sym - when :up, :down - singleton_class.send(:alias_method_chain, sym, "benchmarks") + # Execute this migration in the named direction + def migrate(direction) + return unless respond_to?(direction) + + case direction + when :up then announce "migrating" + when :down then announce "reverting" + end + + 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 { + recorder.inverse.each do |cmd, args| + send(cmd, *args) + end + } + else + time = Benchmark.measure { change } end - ensure - @ignore_new_methods = false + else + time = Benchmark.measure { send(direction) } end + @connection = nil end - def write(text="") - puts(text) if verbose + case direction + when :up then announce "migrated (%.4fs)" % time.real; write + when :down then announce "reverted (%.4fs)" % time.real; write end + end - def announce(message) - version = defined?(@version) ? @version : nil + def write(text="") + puts(text) if verbose + end - text = "#{version} #{name}: #{message}" - length = [0, 75 - text.length].max - write "== %s %s" % [text, "=" * length] - end + def announce(message) + text = "#{version} #{name}: #{message}" + length = [0, 75 - text.length].max + write "== %s %s" % [text, "=" * length] + end - def say(message, subitem=false) - write "#{subitem ? " ->" : "--"} #{message}" - end + def say(message, subitem=false) + write "#{subitem ? " ->" : "--"} #{message}" + end - def say_with_time(message) - say(message) - result = nil - time = Benchmark.measure { result = yield } - say "%.4fs" % time.real, :subitem - say("#{result} rows", :subitem) if result.is_a?(Integer) - result - end + def say_with_time(message) + say(message) + result = nil + time = Benchmark.measure { result = yield } + say "%.4fs" % time.real, :subitem + say("#{result} rows", :subitem) if result.is_a?(Integer) + result + end - def suppress_messages - save, self.verbose = verbose, false - yield - ensure - self.verbose = save - end + def suppress_messages + save, self.verbose = verbose, false + yield + ensure + self.verbose = save + end + + def connection + @connection || ActiveRecord::Base.connection + end + + def method_missing(method, *arguments, &block) + arg_list = arguments.map{ |a| a.inspect } * ', ' - def connection - ActiveRecord::Base.connection + say_with_time "#{method}(#{arg_list})" do + unless arguments.empty? || method == :execute + arguments[0] = Migrator.proper_table_name(arguments.first) + end + return super unless connection.respond_to?(method) + connection.send(method, *arguments, &block) end + end + + def copy(destination, sources, options = {}) + copied = [] + + FileUtils.mkdir_p(destination) unless File.exists?(destination) + + destination_migrations = ActiveRecord::Migrator.migrations(destination) + last = destination_migrations.last + sources.each do |name, path| + source_migrations = ActiveRecord::Migrator.migrations(path) - def method_missing(method, *arguments, &block) - arg_list = arguments.map(&:inspect) * ', ' + source_migrations.each do |migration| + source = File.read(migration.filename) + source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" - say_with_time "#{method}(#{arg_list})" do - unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) + if duplicate = destination_migrations.detect { |m| m.name == migration.name } + options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] + next end - connection.send(method, *arguments, &block) + + migration.version = next_migration_number(last ? last.version + 1 : 0).to_i + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + old_path, migration.filename = migration.filename, new_path + last = migration + + FileUtils.cp(old_path, migration.filename) + copied << migration + options[:on_copy].call(name, migration, old_path) if options[:on_copy] + destination_migrations << migration end end + + copied + end + + def next_migration_number(number) + if ActiveRecord::Base.timestamped_migrations + [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max + else + "%.3d" % number + end end end # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy + class MigrationProxy < Struct.new(:name, :version, :filename) - attr_accessor :name, :version, :filename + def initialize(name, version, filename) + super + @migration = nil + end + + def basename + File.basename(filename) + end delegate :migrate, :announce, :write, :to=>:migration @@ -402,47 +506,47 @@ module ActiveRecord def load_migration require(File.expand_path(filename)) - name.constantize + name.constantize.new end end class Migrator#:nodoc: class << self - def migrate(migrations_path, target_version = nil) + attr_writer :migrations_paths + alias :migrations_path= :migrations_paths= + + def migrate(migrations_paths, target_version = nil) case when target_version.nil? - up(migrations_path, target_version) + up(migrations_paths, target_version) when current_version == 0 && target_version == 0 + [] when current_version > target_version - down(migrations_path, target_version) + down(migrations_paths, target_version) else - up(migrations_path, target_version) + up(migrations_paths, target_version) end end - def rollback(migrations_path, steps=1) - move(:down, migrations_path, steps) + def rollback(migrations_paths, steps=1) + move(:down, migrations_paths, steps) end - def forward(migrations_path, steps=1) - move(:up, migrations_path, steps) + def forward(migrations_paths, steps=1) + move(:up, migrations_paths, steps) end - def up(migrations_path, target_version = nil) - self.new(:up, migrations_path, target_version).migrate + def up(migrations_paths, target_version = nil) + self.new(:up, migrations_paths, target_version).migrate end - def down(migrations_path, target_version = nil) - self.new(:down, migrations_path, target_version).migrate + def down(migrations_paths, target_version = nil) + self.new(:down, migrations_paths, target_version).migrate end - def run(direction, migrations_path, target_version) - self.new(direction, migrations_path, target_version).run - end - - def migrations_path - 'db/migrate' + def run(direction, migrations_paths, target_version) + self.new(direction, migrations_paths, target_version).run end def schema_migrations_table_name @@ -451,7 +555,7 @@ module ActiveRecord def get_all_versions table = Arel::Table.new(schema_migrations_table_name) - Base.connection.select_values(table.project(table['version']).to_sql).map(&:to_i).sort + Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort end def current_version @@ -468,24 +572,59 @@ module ActiveRecord name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" end + def migrations_paths + @migrations_paths ||= ['db/migrate'] + # just to not break things if someone uses: migration_path = some_string + Array.wrap(@migrations_paths) + end + + def migrations_path + migrations_paths.first + end + + def migrations(paths) + paths = Array.wrap(paths) + + files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }] + + seen = Hash.new false + + migrations = files.map do |file| + version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first + + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + raise DuplicateMigrationVersionError.new(version) if seen[version] + raise DuplicateMigrationNameError.new(name) if seen[name] + + seen[version] = seen[name] = true + + MigrationProxy.new(name, version, file) + end + + migrations.sort_by(&:version) + end + private - def move(direction, migrations_path, steps) - migrator = self.new(direction, migrations_path) + def move(direction, migrations_paths, steps) + migrator = self.new(direction, migrations_paths) start_index = migrator.migrations.index(migrator.current_migration) if start_index finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_path, version) + send(direction, migrations_paths, version) end end end - def initialize(direction, migrations_path, target_version = nil) + def initialize(direction, migrations_paths, target_version = nil) raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? Base.connection.initialize_schema_migrations_table - @direction, @migrations_path, @target_version = direction, migrations_path, target_version + @direction, @migrations_paths, @target_version = direction, migrations_paths, target_version end def current_version @@ -509,7 +648,7 @@ module ActiveRecord current = migrations.detect { |m| m.version == current_version } target = migrations.detect { |m| m.version == @target_version } - if target.nil? && !@target_version.nil? && @target_version > 0 + if target.nil? && @target_version && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end @@ -518,16 +657,19 @@ module ActiveRecord runnable = migrations[start..finish] # skip the last migration if we're headed down, but not ALL the way down - runnable.pop if down? && !target.nil? + runnable.pop if down? && target + ran = [] runnable.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + seen = migrated.include?(migration.version.to_i) + # On our way up, we skip migrating the ones we've already migrated - next if up? && migrated.include?(migration.version.to_i) + next if up? && seen # On our way down, we skip reverting the ones we've never migrated - if down? && !migrated.include?(migration.version.to_i) + if down? && !seen migration.announce 'never migrated, skipping'; migration.write next end @@ -537,39 +679,18 @@ module ActiveRecord migration.migrate(@direction) record_version_state_after_migrating(migration.version) end + ran << migration rescue => e canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end + ran end def migrations @migrations ||= begin - files = Dir["#{@migrations_path}/[0-9]*_*.rb"] - - migrations = files.inject([]) do |klasses, file| - version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first - - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - - if klasses.detect { |m| m.version == version } - raise DuplicateMigrationVersionError.new(version) - end - - if klasses.detect { |m| m.name == name.camelize } - raise DuplicateMigrationNameError.new(name.camelize) - end - - migration = MigrationProxy.new - migration.name = name.camelize - migration.version = version - migration.filename = file - klasses << migration - end - - migrations = migrations.sort_by(&:version) + migrations = self.class.migrations(@migrations_paths) down? ? migrations.reverse : migrations end end @@ -590,10 +711,12 @@ module ActiveRecord @migrated_versions ||= [] if down? @migrated_versions.delete(version) - table.where(table["version"].eq(version.to_s)).delete + stmt = table.where(table["version"].eq(version.to_s)).compile_delete + Base.connection.delete stmt.to_sql else @migrated_versions.push(version).sort! - table.insert table["version"] => version.to_s + stmt = table.compile_insert table["version"] => version.to_s + Base.connection.insert stmt.to_sql end end |