aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/migration.rb
blob: 4c10cd806c486831e41df070b9cbaa50db6ba2f5 (plain) (tree)
1
2
3
4
5
6
7
8
9
                   
                                                         
     
 




                                                                     
 





                                                                 





                                                                                                              











                                                                                                                            
   




                                               
                                                                                                                      
                                                                                                                               
                                                                                                                                






                                                                                              




                                
             
   

                                                                                      
   










                                                                                                                              

                                                                                                                          

                                                                                              
                                                                                                   
                                                                                                                     
                                                                     

                                                                               

                                                                                                                            


                                                                                                                            
                                                                                                                                 
                                                                                                                                
                                                                                
                                                                                                 



                                                                                                                  
                                                                                     
   

                                           

                                                                            


                                              
                                                                          
                                                                                               
                                                                      
   
                                                                            
                   
   













                                                                                                   
                                                                    
                                                                                 


                                                                           
                                                                               
                                                            
                                                                                          
                                
   

                       
                                                                    
                                                                        








                                                                       
   

                                                      
                                                                                     















                                                                     
   
                                                                                        









                                                                                                                     
   
                                             
   
                                                                                                                     
                                                                                                                     

                                            
                                                     

                                               
                                         
                                       
                                                                   

             
         















                                                                                    
                                                                   






                                                                           
                 


                           
                 
                                     


                    
                                       







                                                     

                                              
           
 
                    
                                                                                     

                      

                                                                        
           
 






                                                                              
                                                                      
 

                                    
 
                  

                                                
                                                                




                                     
 




                             
                                                

                                               










                                                      
                                                                


              
                           
                                           




                           
                                                    





                                                                      

                                                                        
         


       
                        
                 









                                                                    









                                                                           
 
                                                   
                                                              
         
 
                                                     
                                                                
         

                                                         
                                                                
         
 



                                                                       
                         
                                                                                          
         




                                                                                                                       
       
 
                                                                    
                                                                                                                          
                                                   
                                                                                                      




                                









                                                                            

               






















                                                                              
 























                                                                     


         
                          
                                                           

       
           





                                                             
           

                                                                                                      
         
 


                         
 


                           
     
   
module ActiveRecord
  class IrreversibleMigration < ActiveRecordError#:nodoc:
  end

  class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
    def initialize(version)
      super("Multiple migrations have the version number #{version}")
    end
  end

  class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
    def initialize(version)
      super("No migration with version number #{version}")
    end
  end

  class IllegalMigrationNameError < ActiveRecordError#:nodoc:
    def initialize(name)
      super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
    end
  end

  # 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 migrations, you can describe the transformations
  # in self-contained classes 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
  #       add_column :accounts, :ssl_enabled, :boolean, :default => 1
  #     end
  #
  #     def self.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 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 transformations.
  #
  # Example of a more complex migration that also needs to initialize data:
  #
  #   class AddSystemSettings < ActiveRecord::Migration
  #     def self.up
  #       create_table :system_settings do |t|
  #         t.string  :name
  #         t.string  :label
  #         t.text  :value
  #         t.string  :type
  #         t.integer  :position
  #       end
  #
  #       SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
  #     end
  #
  #     def self.down
  #       drop_table :system_settings
  #     end
  #   end
  #
  # 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 complete table schema
  # in one block call.
  #
  # == Available transformations
  #
  # * <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 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+ to +new_name+.
  # * <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:
  #   :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time,
  #   :date, :binary, :boolean. A default value can be specified by passing an
  #   +options+ hash like { :default => 11 }. Other options include :limit and :null (e.g. { :limit => 50, :null => false })
  #   -- 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 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 +column_name+ from the table called +table_name+.
  # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index with the name of the column. Other options include
  #   :name and :unique (e.g. { :name => "users_name_index", :unique => true }).
  # * <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> exception in their +down+ method.
  #
  # == Running migrations from within Rails
  #
  # The Rails package has several tools to help create and apply migrations.
  #
  # To generate a new migration, you can use 
  #   script/generate migration MyNewMigration
  #
  # where MyNewMigration is the name of your migration. The generator will
  # create an empty migration file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
  # directory where <tt>nnn</tt> is the next largest migration number.
  #
  # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
  # MyNewMigration.
  #
  # There is a special syntactic shortcut to generate migrations that add fields to a table.
  #   script/generate migration add_fieldname_to_tablename fieldname:string
  #
  # This will generate the file <tt>nnn_add_fieldname_to_tablename</tt>, which will look like this:
  #   class AddFieldnameToTablename < ActiveRecord::Migration
  #     def self.up
  #       add_column :tablenames, :fieldname, :string
  #     end
  # 
  #     def self.down
  #       remove_column :tablenames, :fieldname
  #     end
  #   end
  # 
  # To run migrations against the currently configured database, use
  # <tt>rake db:migrate</tt>. This will update the database by running all of the
  # pending migrations, creating the <tt>schema_info</tt> table if missing.
  #
  # To roll the database back to a previous migration version, use
  # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
  # you wish to downgrade. If any of the migrations throw an
  # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
  # have some manual work to do.
  #
  # == Database support
  #
  # Migrations are currently supported in MySQL, PostgreSQL, SQLite,
  # SQL Server, Sybase, and Oracle (all supported databases except DB2).
  #
  # == More examples
  #
  # Not all migrations change the schema. Some just fix the data:
  #
  #   class RemoveEmptyTags < ActiveRecord::Migration
  #     def self.up
  #       Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
  #     end
  #
  #     def self.down
  #       # not much we can do to restore deleted data
  #       raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
  #     end
  #   end
  #
  # Others remove columns when they migrate up instead of down:
  #
  #   class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
  #     def self.up
  #       remove_column :items, :incomplete_items_count
  #       remove_column :items, :completed_items_count
  #     end
  #
  #     def self.down
  #       add_column :items, :incomplete_items_count
  #       add_column :items, :completed_items_count
  #     end
  #   end
  #
  # And sometimes you need to do something in SQL not abstracted directly by migrations:
  #
  #   class MakeJoinUnique < ActiveRecord::Migration
  #     def self.up
  #       execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
  #     end
  #
  #     def self.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 Base#reset_column_information 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
  #       add_column :people, :salary, :integer
  #       Person.reset_column_information
  #       Person.find(:all).each do |p|
  #         p.update_attribute :salary, SalaryCalculator.compute(p)
  #       end
  #     end
  #   end
  #
  # == Controlling verbosity
  #
  # By default, migrations will describe the actions they are taking, writing
  # them to the console as they happen, along with benchmarks describing how
  # long each step took.
  #
  # You can quiet them down by setting ActiveRecord::Migration.verbose = false.
  #
  # You can also insert your own messages and benchmarks by using the #say_with_time
  # method:
  #
  #   def self.up
  #     ...
  #     say_with_time "Updating salaries..." do
  #       Person.find(:all).each do |p|
  #         p.update_attribute :salary, SalaryCalculator.compute(p)
  #       end
  #     end
  #     ...
  #   end
  #
  # The phrase "Updating salaries..." would then be printed, along with the
  # benchmark for the block when the block completes.
  class Migration
    @@verbose = true
    cattr_accessor :verbose

    class << self
      def up_with_benchmarks #:nodoc:
        migrate(:up)
      end

      def down_with_benchmarks #:nodoc:
        migrate(:down)
      end

      # 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

        result = nil
        time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }

        case direction
          when :up   then announce "migrated (%.4fs)" % time.real; write
          when :down then announce "reverted (%.4fs)" % time.real; write
        end

        result
      end

      # 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

        begin
          @ignore_new_methods = true

          case sym
            when :up, :down
              klass = (class << self; self; end)
              klass.send(:alias_method_chain, sym, "benchmarks")
          end
        ensure
          @ignore_new_methods = false
        end
      end

      def write(text="")
        puts(text) if verbose
      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_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 method_missing(method, *arguments, &block)
        arg_list = arguments.map(&:inspect) * ', '

        say_with_time "#{method}(#{arg_list})" do
          unless arguments.empty? || method == :execute
            arguments[0] = Migrator.proper_table_name(arguments.first)
          end
          ActiveRecord::Base.connection.send(method, *arguments, &block)
        end
      end
    end
  end

  class Migrator#:nodoc:
    class << self
      def migrate(migrations_path, target_version = nil)
        case
          when target_version.nil?, current_version < target_version
            up(migrations_path, target_version)
          when current_version > target_version
            down(migrations_path, target_version)
          when current_version == target_version
            return # You're on the right version
        end
      end
      
      def rollback(migrations_path, steps=1)
        migrator = self.new(:down, migrations_path)
        start_index = migrator.migrations.index(migrator.current_migration)
        
        return unless start_index
        
        finish = migrator.migrations[start_index + steps]
        down(migrations_path, finish ? finish.version : 0)
      end

      def up(migrations_path, target_version = nil)
        self.new(:up, migrations_path, target_version).migrate
      end

      def down(migrations_path, target_version = nil)
        self.new(:down, migrations_path, target_version).migrate
      end
      
      def run(direction, migrations_path, target_version)
        self.new(direction, migrations_path, target_version).run
      end

      def schema_info_table_name
        Base.table_name_prefix + "schema_info" + Base.table_name_suffix
      end

      def current_version
        Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").to_i
      end

      def proper_table_name(name)
        # Use the ActiveRecord objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
        name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
      end
    end

    def initialize(direction, migrations_path, target_version = nil)
      raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
      Base.connection.initialize_schema_information
      @direction, @migrations_path, @target_version = direction, migrations_path, target_version      
    end

    def current_version
      self.class.current_version
    end
    
    def current_migration
      migrations.detect { |m| m.version == current_version }
    end
    
    def run
      target = migrations.detect { |m| m.version == @target_version }
      raise UnknownMigrationVersionError.new(@target_version) if target.nil?
      target.migrate(@direction)
    end

    def migrate
      current = migrations.detect { |m| m.version == current_version }
      target = migrations.detect { |m| m.version == @target_version }
            
      if target.nil? && !@target_version.nil? && @target_version > 0
        raise UnknownMigrationVersionError.new(@target_version)
      end
      
      start = migrations.index(current) || 0
      finish = migrations.index(target) || migrations.size - 1      
      runnable = migrations[start..finish]
      
      # skip the current migration if we're heading upwards
      runnable.shift if up? && runnable.first == current
      
      # skip the last migration if we're headed down, but not ALL the way down
      runnable.pop if down? && !target.nil?
      
      runnable.each do |migration|
        Base.logger.info "Migrating to #{migration} (#{migration.version})"
        migration.migrate(@direction)
        set_schema_version_after_migrating(migration)
      end
    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(f) unless version
          version = version.to_i
          
          if klasses.detect { |m| m.version == version }
            raise DuplicateMigrationVersionError.new(version) 
          end
          
          load(file)
          
          klasses << returning(name.camelize.constantize) do |klass|
            class << klass; attr_accessor :version end
            klass.version = version
          end
        end
        
        migrations = migrations.sort_by(&:version)
        down? ? migrations.reverse : migrations
      end
    end

    def pending_migrations
      migrations.select { |m| m.version > current_version }
    end

    private
      def set_schema_version_after_migrating(migration)
        version = migration.version
            
        if down?
          after = migrations[migrations.index(migration) + 1]
          version = after ? after.version : 0
        end
            
        Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{version}")
      end

      def up?
        @direction == :up
      end

      def down?
        @direction == :down
      end
  end
end