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 (closes #11458) [jbarnette]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@9122 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
David Heinemeier Hansson
2008-03-28 21:21:01 +00:00
parent ad8df03f9c
commit c00de99f69
7 changed files with 150 additions and 91 deletions

View File

@@ -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 File

@@ -431,7 +431,7 @@ module ActiveRecord #: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 File

@@ -232,12 +232,20 @@ module ActiveRecord
# 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 File

@@ -8,6 +8,12 @@ module ActiveRecord
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 @@ module ActiveRecord
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 @@ module ActiveRecord
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 @@ module ActiveRecord
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 @@ module ActiveRecord
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
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
next if irrelevant_migration?(migration_class.version)
Base.logger.info "Migrating to #{migration_class} (#{migration_class.version})"
migration_class.migrate(@direction)
set_schema_version(migration_class.version)
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)
def set_schema_version_after_migrating(migration)
version = migration.version
if down?
after = migrations[migrations.index(migration) + 1]
version = after ? after.version : 0
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
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 @@ module ActiveRecord
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 File

@@ -757,9 +757,9 @@ if ActiveRecord::Base.connection.supports_migrations?
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 @@ if ActiveRecord::Base.connection.supports_migrations?
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 @@ if ActiveRecord::Base.connection.supports_migrations?
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 File

@@ -69,19 +69,8 @@ module Rails
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 File

@@ -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.'