Permalink
Browse files

Switched to UTC-timebased version numbers for migrations and the sche…

…ma. This will as good as eliminate the problem of multiple migrations getting the same version assigned in different branches. Also added rake db:migrate:up/down to apply individual migrations that may need to be run when you merge branches (closes #11458) [jbarnette]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@9122 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent ad8df03 commit c00de99f69358b58ca2bd6bc732e2de1b667800e @dhh dhh committed Mar 28, 2008
View
2 activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Switched to UTC-timebased version numbers for migrations and the schema. This will as good as eliminate the problem of multiple migrations getting the same version assigned in different branches. Also added rake db:migrate:up/down to apply individual migrations that may need to be run when you merge branches #11458 [jbarnette]
+
* Fixed that has_many :through would ignore the hash conditions #11447 [miloops]
* Fix issue where the :uniq option of a has_many :through association is ignored when find(:all) is called. Closes #9407 [cavalle]
View
2 activerecord/lib/active_record/base.rb
@@ -431,7 +431,7 @@ def self.reset_subclasses #:nodoc:
# adapters for, e.g., your development and test environments.
cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby
-
+
class << self # Class methods
# Find operates with four different retrieval approaches:
#
View
16 activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -232,12 +232,20 @@ def structure_dump
# Should not be called normally, but this operation is non-destructive.
# The migrations module handles this automatically.
- def initialize_schema_information
+ def initialize_schema_information(current_version=0)
begin
- execute "CREATE TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version #{type_to_sql(:integer)})"
- execute "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES(0)"
+ execute "CREATE TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version #{type_to_sql(:string)})"
+ execute "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES(#{current_version})"
rescue ActiveRecord::StatementInvalid
- # Schema has been initialized
+ # Schema has been initialized, make sure version is a string
+ version_column = columns(:schema_info).detect { |c| c.name == "version" }
+
+ # can't just alter the table, since SQLite can't deal
+ unless version_column.type == :string
+ version = ActiveRecord::Migrator.current_version
+ execute "DROP TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)}"
+ initialize_schema_information(version)
+ end
end
end
View
148 activerecord/lib/active_record/migration.rb
@@ -8,6 +8,12 @@ def initialize(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)")
@@ -308,8 +314,6 @@ def method_missing(method, *arguments, &block)
class Migrator#:nodoc:
class << self
def migrate(migrations_path, target_version = nil)
- Base.connection.initialize_schema_information
-
case
when target_version.nil?, current_version < target_version
up(migrations_path, target_version)
@@ -319,6 +323,16 @@ def migrate(migrations_path, target_version = nil)
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
@@ -327,6 +341,10 @@ def up(migrations_path, target_version = nil)
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)
+ end
def schema_info_table_name
Base.table_name_prefix + "schema_info" + Base.table_name_suffix
@@ -344,73 +362,90 @@ def proper_table_name(name)
def initialize(direction, migrations_path, target_version = nil)
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
- @direction, @migrations_path, @target_version = direction, migrations_path, target_version
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
- migration_classes.each do |migration_class|
- if reached_target_version?(migration_class.version)
- Base.logger.info("Reached target version: #{@target_version}")
- break
- end
-
- next if irrelevant_migration?(migration_class.version)
+ 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
- Base.logger.info "Migrating to #{migration_class} (#{migration_class.version})"
- migration_class.migrate(@direction)
- set_schema_version(migration_class.version)
+ 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
- migration_classes.select { |m| m.version > current_version }
+ migrations.select { |m| m.version > current_version }
end
private
- def migration_classes
- classes = migration_files.inject([]) do |migrations, migration_file|
- load(migration_file)
- version, name = migration_version_and_name(migration_file)
- assert_unique_migration_version(migrations, version.to_i)
- migrations << migration_class(name, version.to_i)
- end.sort_by(&:version)
-
- down? ? classes.reverse : classes
- end
-
- def assert_unique_migration_version(migrations, version)
- if !migrations.empty? && migrations.find { |m| m.version == version }
- raise DuplicateMigrationVersionError.new(version)
- end
- end
-
- def migration_files
- files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f|
- m = migration_version_and_name(f)
- raise IllegalMigrationNameError.new(f) unless m
- m.first.to_i
+ def set_schema_version_after_migrating(migration)
+ version = migration.version
+
+ if down?
+ after = migrations[migrations.index(migration) + 1]
+ version = after ? after.version : 0
end
- down? ? files.reverse : files
- end
-
- def migration_class(migration_name, version)
- klass = migration_name.camelize.constantize
- class << klass; attr_accessor :version end
- klass.version = version
- klass
- end
-
- def migration_version_and_name(migration_file)
- return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
- end
-
- def set_schema_version(version)
- Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}")
+
+ Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{version}")
end
def up?
@@ -420,14 +455,5 @@ def up?
def down?
@direction == :down
end
-
- def reached_target_version?(version)
- return false if @target_version == nil
- (up? && version.to_i - 1 >= @target_version) || (down? && version.to_i <= @target_version)
- end
-
- def irrelevant_migration?(version)
- (up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
- end
end
end
View
43 activerecord/test/cases/migration_test.rb
@@ -757,9 +757,9 @@ def test_migrator_one_up
def test_migrator_one_down
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid")
-
+
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 1)
-
+
Person.reset_column_information
assert Person.column_methods_hash.include?(:last_name)
assert !Reminder.table_exists?
@@ -805,6 +805,33 @@ def test_migrator_going_down_due_to_version_target
assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
assert_equal "hello world", Reminder.find(:first).content
end
+
+ def test_migrator_rollback
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal(2, ActiveRecord::Migrator.current_version)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal(1, ActiveRecord::Migrator.current_version)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
+
+ def test_migrator_run
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
def test_schema_info_table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
@@ -892,15 +919,9 @@ def test_migrator_with_duplicates
end
def test_migrator_with_missing_version_numbers
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/missing", 500)
- assert !Person.column_methods_hash.include?(:middle_name)
- assert_equal 4, ActiveRecord::Migrator.current_version
-
- ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/missing", 2)
- Person.reset_column_information
- assert !Reminder.table_exists?
- assert Person.column_methods_hash.include?(:last_name)
- assert_equal 2, ActiveRecord::Migrator.current_version
+ assert_raise(ActiveRecord::UnknownMigrationVersionError) do
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/missing", 500)
+ end
end
def test_create_table_with_custom_sequence_name
View
13 railties/lib/rails_generator/commands.rb
@@ -69,19 +69,8 @@ def migration_exists?(file_name)
not existing_migrations(file_name).empty?
end
- def current_migration_number
- Dir.glob("#{RAILS_ROOT}/#{@migration_directory}/[0-9]*_*.rb").inject(0) do |max, file_path|
- n = File.basename(file_path).split('_', 2).first.to_i
- if n > max then n else max end
- end
- end
-
- def next_migration_number
- current_migration_number + 1
- end
-
def next_migration_string(padding = 3)
- "%.#{padding}d" % next_migration_number
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
end
def gsub_file(relative_destination, regexp, *args, &block)
View
17 railties/lib/tasks/databases.rake
@@ -98,13 +98,26 @@ namespace :db do
desc 'Resets your database using your migrations for the current environment'
task :reset => ["db:drop", "db:create", "db:migrate"]
+
+ desc 'Runs the "up" for a given migration VERSION.'
+ task :up => :environment do
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ raise "VERSION is required" unless version
+ ActiveRecord::Migrator.run(:up, "db/migrate/", version)
+ end
+
+ desc 'Runs the "down" for a given migration VERSION.'
+ task :down => :environment do
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ raise "VERSION is required" unless version
+ ActiveRecord::Migrator.run(:down, "db/migrate/", version)
+ end
end
desc 'Rolls the schema back to the previous version. Specify the number of steps with STEP=n'
task :rollback => :environment do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- version = ActiveRecord::Migrator.current_version - step
- ActiveRecord::Migrator.migrate('db/migrate/', version)
+ ActiveRecord::Migrator.rollback('db/migrate/', step)
end
desc 'Drops and recreates the database from db/schema.rb for the current environment.'

0 comments on commit c00de99

Please sign in to comment.