Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 9244

Show
Ignore:
Timestamp:
04/09/08 16:20:15 (8 months ago)
Author:
rick
Message:

Add support for interleaving migrations by storing which migrations have run in the new schema_migrations table. Closes #11493 [jordi]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/CHANGELOG

    r9243 r9244  
    11*SVN* 
     2 
     3* Add support for interleaving migrations by storing which migrations have run in the new schema_migrations table. Closes #11493 [jordi] 
    24 
    35* ActiveRecord::Base#sum defaults to 0 if no rows are returned.  Closes #11550 [kamal] 
  • trunk/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb

    r9226 r9244  
    233233      # Should not be called normally, but this operation is non-destructive. 
    234234      # The migrations module handles this automatically. 
    235       def initialize_schema_information(current_version=0) 
    236         begin 
    237           execute "CREATE TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version #{type_to_sql(:string)})" 
    238           execute "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES(#{current_version})" 
    239         rescue ActiveRecord::StatementInvalid 
    240           # Schema has been initialized, make sure version is a string 
    241           version_column = columns(:schema_info).detect { |c| c.name == "version" } 
    242            
    243           # can't just alter the table, since SQLite can't deal 
    244           unless version_column.type == :string 
    245             version = ActiveRecord::Migrator.current_version 
    246             execute "DROP TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)}" 
    247             initialize_schema_information(version) 
     235      def initialize_schema_migrations_table 
     236        sm_table = ActiveRecord::Migrator.schema_migrations_table_name 
     237 
     238        unless tables.detect { |t| t == sm_table } 
     239          create_table(sm_table, :id => false) do |schema_migrations_table| 
     240            schema_migrations_table.column :version, :string, :null => false 
    248241          end 
    249         end 
    250       end 
    251  
    252       def dump_schema_information #:nodoc: 
    253         begin 
    254           if (current_schema = ActiveRecord::Migrator.current_version) > 0 
    255             return "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES (#{current_schema})"  
     242          add_index sm_table, :version, :unique => true, 
     243            :name => 'unique_schema_migrations' 
     244 
     245          # Backwards-compatibility: if we find schema_info, assume we've 
     246          # migrated up to that point: 
     247          si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix 
     248 
     249          if tables.detect { |t| t == si_table } 
     250 
     251            old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i 
     252            assume_migrated_upto_version(old_version) 
     253            drop_table(si_table) 
    256254          end 
    257         rescue ActiveRecord::StatementInvalid  
    258           # No Schema Info 
    259         end 
    260       end 
    261  
     255        end 
     256      end 
     257 
     258      def assume_migrated_upto_version(version) 
     259        sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) 
     260        migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) 
     261        versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename| 
     262          filename.split('/').last.split('_').first.to_i 
     263        end 
     264 
     265        execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" unless migrated.include?(version.to_i) 
     266        (versions - migrated).select { |v| v < version.to_i }.each do |v| 
     267          execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" 
     268        end 
     269      end 
    262270 
    263271      def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: 
  • trunk/activerecord/lib/active_record/migration.rb

    r9126 r9244  
    124124  # To run migrations against the currently configured database, use 
    125125  # <tt>rake db:migrate</tt>. This will update the database by running all of the 
    126   # pending migrations, creating the <tt>schema_info</tt> table if missing. 
     126  # pending migrations, creating the <tt>schema_migrations</tt> table 
     127  # (see "About the schema_migrations table" section below) if missing. 
    127128  # 
    128129  # To roll the database back to a previous migration version, use 
     
    217218  # The phrase "Updating salaries..." would then be printed, along with the 
    218219  # benchmark for the block when the block completes. 
     220  # 
     221  # == About the schema_migrations table 
     222  # 
     223  # Rails versions 2.0 and prior used to create a table called 
     224  # <tt>schema_info</tt> when using migrations. This table contained the 
     225  # version of the schema as of the last applied migration. 
     226  # 
     227  # Starting with Rails 2.1, the <tt>schema_info</tt> table is 
     228  # (automatically) replaced by the <tt>schema_migrations</tt> table, which 
     229  # contains the version numbers of all the migrations applied. 
     230  # 
     231  # As a result, it is now possible to add migration files that are numbered 
     232  # lower than the current schema version: when migrating up, those 
     233  # never-applied "interleaved" migrations will be automatically applied, and 
     234  # when migrating down, never-applied "interleaved" migrations will be skipped. 
    219235  class Migration 
    220236    @@verbose = true 
     
    316332      def migrate(migrations_path, target_version = nil) 
    317333        case 
    318           when target_version.nil?, current_version < target_version 
    319             up(migrations_path, target_version) 
    320           when current_version > target_version 
    321             down(migrations_path, target_version) 
    322           when current_version == target_version 
    323             return # You're on the right version 
    324         end 
    325       end 
    326        
     334          when target_version.nil?              then up(migrations_path, target_version) 
     335          when current_version > target_version then down(migrations_path, target_version) 
     336          else                                       up(migrations_path, target_version) 
     337        end 
     338      end 
     339 
    327340      def rollback(migrations_path, steps=1) 
    328341        migrator = self.new(:down, migrations_path) 
     
    347360      end 
    348361 
    349       def schema_info_table_name 
    350         Base.table_name_prefix + "schema_info" + Base.table_name_suffix 
     362      def schema_migrations_table_name 
     363        Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix 
    351364      end 
    352365 
    353366      def current_version 
    354         Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").to_i 
     367        Base.connection.select_values( 
     368          "SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).max || 0 
    355369      end 
    356370 
     
    363377    def initialize(direction, migrations_path, target_version = nil) 
    364378      raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? 
    365       Base.connection.initialize_schema_information 
     379      Base.connection.initialize_schema_migrations_table 
    366380      @direction, @migrations_path, @target_version = direction, migrations_path, target_version       
    367381    end 
     
    384398      current = migrations.detect { |m| m.version == current_version } 
    385399      target = migrations.detect { |m| m.version == @target_version } 
    386              
     400 
    387401      if target.nil? && !@target_version.nil? && @target_version > 0 
    388402        raise UnknownMigrationVersionError.new(@target_version) 
    389403      end 
    390404       
    391       start = migrations.index(current) || 0 
    392       finish = migrations.index(target) || migrations.size - 1       
     405      start = up? ? 0 : (migrations.index(current) || 0) 
     406      finish = migrations.index(target) || migrations.size - 1 
    393407      runnable = migrations[start..finish] 
    394        
    395       # skip the current migration if we're heading upwards 
    396       runnable.shift if up? && runnable.first == current 
    397408       
    398409      # skip the last migration if we're headed down, but not ALL the way down 
     
    401412      runnable.each do |migration| 
    402413        Base.logger.info "Migrating to #{migration} (#{migration.version})" 
    403         migration.migrate(@direction) 
    404         set_schema_version_after_migrating(migration) 
     414 
     415        # On our way up, we skip migrating the ones we've already migrated 
     416        # On our way down, we skip reverting the ones we've never migrated 
     417        next if up? && migrated.include?(migration.version.to_i) 
     418 
     419        if down? && !migrated.include?(migration.version.to_i) 
     420          migration.announce 'never migrated, skipping'; migration.write 
     421        else 
     422          migration.migrate(@direction) 
     423          record_version_state_after_migrating(migration.version) 
     424        end 
    405425      end 
    406426    end 
     
    413433          version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first 
    414434           
    415           raise IllegalMigrationNameError.new(f) unless version 
     435          raise IllegalMigrationNameError.new(file) unless version 
    416436          version = version.to_i 
    417437           
     
    434454 
    435455    def pending_migrations 
    436       migrations.select { |m| m.version > current_version } 
     456      already_migrated = migrated 
     457      migrations.reject { |m| already_migrated.include?(m.version.to_i) } 
     458    end 
     459 
     460    def migrated 
     461      sm_table = self.class.schema_migrations_table_name 
     462      Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort 
    437463    end 
    438464 
    439465    private 
    440       def set_schema_version_after_migrating(migration) 
    441         version = migration.version 
    442              
     466      def record_version_state_after_migrating(version) 
     467        sm_table = self.class.schema_migrations_table_name 
     468 
    443469        if down? 
    444           after = migrations[migrations.index(migration) + 1] 
    445           version = after ? after.version : 0 
    446         end 
    447              
    448         Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{version}") 
     470          Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'") 
     471        else 
     472          Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") 
     473        end 
    449474      end 
    450475 
  • trunk/activerecord/lib/active_record/schema_dumper.rb

    r8124 r9244  
    3131        @connection = connection 
    3232        @types = @connection.native_database_types 
    33         @info = @connection.select_one("SELECT * FROM schema_info") rescue nil 
     33        @version = Migrator::current_version rescue nil 
    3434      end 
    3535 
    3636      def header(stream) 
    37         define_params = @info ? ":version => #{@info['version']}" : "" 
     37        define_params = @version ? ":version => #{@version}" : "" 
    3838 
    3939        stream.puts <<HEADER 
     
    6060      def tables(stream) 
    6161        @connection.tables.sort.each do |tbl| 
    62           next if ["schema_info", ignore_tables].flatten.any? do |ignored| 
     62          next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| 
    6363            case ignored 
    6464            when String; tbl == ignored 
  • trunk/activerecord/lib/active_record/schema.rb

    r8089 r9244  
    3535    # 
    3636    # The +info+ hash is optional, and if given is used to define metadata 
    37     # about the current schema (like the schema's version): 
     37    # about the current schema (currently, only the schema's version): 
    3838    # 
    39     #   ActiveRecord::Schema.define(:version => 15) do 
     39    #   ActiveRecord::Schema.define(:version => 20380119000001) do 
    4040    #     ... 
    4141    #   end 
     
    4343      instance_eval(&block) 
    4444 
    45       unless info.empty? 
    46         initialize_schema_information 
    47         cols = columns('schema_info') 
    48  
    49         info = info.map do |k,v| 
    50           v = Base.connection.quote(v, cols.detect { |c| c.name == k.to_s }) 
    51           "#{k} = #{v}" 
    52         end 
    53  
    54         Base.connection.update "UPDATE #{Migrator.schema_info_table_name} SET #{info.join(", ")}" 
     45      unless info[:version].blank? 
     46        initialize_schema_migrations_table 
     47        assume_migrated_upto_version info[:version] 
    5548      end 
    5649    end 
  • trunk/activerecord/test/cases/ar_schema_test.rb

    r8681 r9244  
    2626 
    2727      assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } 
    28       assert_nothing_raised { @connection.select_all "SELECT * FROM schema_info" } 
    29       assert_equal 7, @connection.select_one("SELECT version FROM schema_info")['version'].to_i 
     28      assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } 
     29      assert_equal 7, ActiveRecord::Migrator::current_version 
    3030    end 
    3131  end 
  • trunk/activerecord/test/cases/migration_test.rb

    r9175 r9244  
    88require MIGRATIONS_ROOT + "/valid/2_we_need_reminders" 
    99require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" 
     10require MIGRATIONS_ROOT + "/interleaved/pass_3/2_i_raise_on_down" 
    1011 
    1112if ActiveRecord::Base.connection.supports_migrations? 
     
    3536 
    3637    def teardown 
    37       ActiveRecord::Base.connection.initialize_schema_information 
    38       ActiveRecord::Base.connection.update "UPDATE #{ActiveRecord::Migrator.schema_info_table_name} SET version = 0
     38      ActiveRecord::Base.connection.initialize_schema_migrations_table 
     39      ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}
    3940 
    4041      %w(reminders people_reminders prefix_reminders_suffix).each do |table| 
     
    780781    end 
    781782 
     783    def test_finds_migrations 
     784      migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations 
     785      [['1', 'people_have_last_names'], 
     786       ['2', 'we_need_reminders'], 
     787       ['3', 'innocent_jointable']].each_with_index do |pair, i| 
     788        migrations[i].version == pair.first 
     789        migrations[1].name    == pair.last 
     790      end 
     791    end 
     792 
     793    def test_finds_pending_migrations 
     794      ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1) 
     795      migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations 
     796      assert_equal 1, migrations.size 
     797      migrations[0].version == '3' 
     798      migrations[0].name    == 'innocent_jointable' 
     799    end 
     800 
     801    def test_migrator_interleaved_migrations 
     802      ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1") 
     803 
     804      assert_nothing_raised do 
     805        ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2") 
     806      end 
     807 
     808      Person.reset_column_information 
     809      assert Person.column_methods_hash.include?(:last_name) 
     810 
     811      assert_nothing_raised do 
     812        ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/interleaved/pass_3") 
     813      end 
     814    end 
     815 
    782816    def test_migrator_verbosity 
    783817      ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) 
     
    818852       
    819853      ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") 
    820       assert_equal(2, ActiveRecord::Migrator.current_version)             
     854      assert_equal(2, ActiveRecord::Migrator.current_version) 
    821855       
    822856      ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") 
    823       assert_equal(1, ActiveRecord::Migrator.current_version)             
     857      assert_equal(1, ActiveRecord::Migrator.current_version) 
    824858       
    825859      ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") 
    826       assert_equal(0, ActiveRecord::Migrator.current_version)             
     860      assert_equal(0, ActiveRecord::Migrator.current_version) 
    827861       
    828862      ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") 
    829       assert_equal(0, ActiveRecord::Migrator.current_version)             
     863      assert_equal(0, ActiveRecord::Migrator.current_version) 
    830864    end 
    831865     
     
    840874    end 
    841875 
    842     def test_schema_info_table_name 
     876    def test_schema_migrations_table_name 
    843877      ActiveRecord::Base.table_name_prefix = "prefix_" 
    844878      ActiveRecord::Base.table_name_suffix = "_suffix" 
    845879      Reminder.reset_table_name 
    846       assert_equal "prefix_schema_info_suffix", ActiveRecord::Migrator.schema_info_table_name 
     880      assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name 
    847881      ActiveRecord::Base.table_name_prefix = "" 
    848882      ActiveRecord::Base.table_name_suffix = "" 
    849883      Reminder.reset_table_name 
    850       assert_equal "schema_info", ActiveRecord::Migrator.schema_info_table_name 
     884      assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name 
    851885    ensure 
    852886      ActiveRecord::Base.table_name_prefix = "" 
  • trunk/activerecord/test/cases/schema_dumper_test.rb

    r8969 r9244  
    1717      assert_match %r{create_table "accounts"}, output 
    1818      assert_match %r{create_table "authors"}, output 
    19       assert_no_match %r{create_table "schema_info"}, output 
     19      assert_no_match %r{create_table "schema_migrations"}, output 
    2020    end 
    2121 
     
    8282      assert_no_match %r{create_table "accounts"}, output 
    8383      assert_match %r{create_table "authors"}, output 
    84       assert_no_match %r{create_table "schema_info"}, output 
     84      assert_no_match %r{create_table "schema_migrations"}, output 
    8585    end 
    8686 
     
    9393      assert_no_match %r{create_table "accounts"}, output 
    9494      assert_match %r{create_table "authors"}, output 
    95       assert_no_match %r{create_table "schema_info"}, output 
     95      assert_no_match %r{create_table "schema_migrations"}, output 
    9696    end 
    9797 
  • trunk/activerecord/test/schema/sybase.drop.sql

    r8659 r9244  
    3232DROP TABLE mixed_case_monkeys 
    3333DROP TABLE minimalistics 
    34 DROP TABLE schema_info 
     34DROP TABLE schema_migrations 
    3535go