Add metadata to schema_migrations

migrated_at: timestamp when migration run
fingerprint: md5 hash of migration source
name: filename without version or extension
This commit is contained in:
Josh Susser 2012-12-01 18:32:23 -08:00
parent a7776924a6
commit 0a5afa229d
10 changed files with 126 additions and 44 deletions

@ -1,5 +1,12 @@
## Rails 4.0.0 (unreleased) ##
* Add metadata columns to schema_migrations table.
New columns are: migrated_at (timestamp),
fingerprint (md5 hash of migration source), and
name (filename minus version and extension)
*Josh Susser*
* Add STI support to init and building associations.
Allows you to do BaseClass.new(:type => "SubClass") as well as
parent.children.build(:type => "SubClass") or parent.build_child

@ -490,7 +490,7 @@ def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::SchemaMigration.order('version').map { |sm|
"INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');"
"INSERT INTO #{sm_table} (version, migrated_at, name) VALUES ('#{sm.version}',LOCALTIMESTAMP,'#{sm.name}');"
}.join "\n\n"
end
@ -512,7 +512,7 @@ def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migra
end
unless migrated.include?(version)
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
ActiveRecord::SchemaMigration.create!(:version => version, :migrated_at => Time.now)
end
inserted = Set.new
@ -520,7 +520,7 @@ def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migra
if inserted.include?(v)
raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
elsif v < version
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
ActiveRecord::SchemaMigration.create!(:version => v, :migrated_at => Time.now)
inserted << v
end
end

@ -1,5 +1,6 @@
require "active_support/core_ext/class/attribute_accessors"
require 'set'
require 'digest/md5'
module ActiveRecord
# Exception that can be raised to stop migrations from going backwards.
@ -554,6 +555,10 @@ def basename
delegate :migrate, :announce, :write, :to => :migration
def fingerprint
@fingerprint ||= Digest::MD5.hexdigest(File.read(filename))
end
private
def migration
@ -724,7 +729,7 @@ def run
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
target.migrate(@direction)
record_version_state_after_migrating(target.version)
record_version_state_after_migrating(target)
end
end
@ -747,7 +752,7 @@ def migrate
begin
ddl_transaction do
migration.migrate(@direction)
record_version_state_after_migrating(migration.version)
record_version_state_after_migrating(migration)
end
rescue => e
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
@ -805,13 +810,18 @@ def validate(migrations)
raise DuplicateMigrationVersionError.new(version) if version
end
def record_version_state_after_migrating(version)
def record_version_state_after_migrating(target)
if down?
migrated.delete(version)
ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all
migrated.delete(target.version)
ActiveRecord::SchemaMigration.where(:version => target.version.to_s).delete_all
else
migrated << version
ActiveRecord::SchemaMigration.create!(:version => version.to_s)
migrated << target.version
ActiveRecord::SchemaMigration.create!(
:version => target.version.to_s,
:migrated_at => Time.now,
:fingerprint => target.fingerprint,
:name => File.basename(target.filename,'.rb').gsub(/^\d+_/,'')
)
end
end

@ -14,17 +14,38 @@ def self.index_name
end
def self.create_table
unless connection.table_exists?(table_name)
connection.create_table(table_name, :id => false) do |t|
t.column :version, :string, :null => false
if connection.table_exists?(table_name)
cols = connection.columns(table_name).collect { |col| col.name }
unless cols.include?("migrated_at")
connection.add_column(table_name, "migrated_at", :datetime)
q_table_name = connection.quote_table_name(table_name)
q_timestamp = connection.quoted_date(Time.now)
connection.update("UPDATE #{q_table_name} SET migrated_at = '#{q_timestamp}' WHERE migrated_at IS NULL")
connection.change_column(table_name, "migrated_at", :datetime, :null => false)
end
connection.add_index table_name, :version, :unique => true, :name => index_name
unless cols.include?("fingerprint")
connection.add_column(table_name, "fingerprint", :string, :limit => 32)
end
unless cols.include?("name")
connection.add_column(table_name, "name", :string)
end
else
connection.create_table(table_name, :id => false) do |t|
t.column "version", :string, :null => false
t.column "migrated_at", :datetime, :null => false
t.column "fingerprint", :string, :limit => 32
t.column "name", :string
end
connection.add_index(table_name, "version", :unique => true, :name => index_name)
end
reset_column_information
end
def self.drop_table
if connection.index_exists?(table_name, "version", :unique => true, :name => index_name)
connection.remove_index(table_name, :name => index_name)
end
if connection.table_exists?(table_name)
connection.remove_index table_name, :name => index_name
connection.drop_table(table_name)
end
end

@ -6,10 +6,12 @@ class LoggerTest < ActiveRecord::TestCase
# mysql can't roll back ddl changes
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
Migration = Struct.new(:name, :version, :filename, :fingerprint) do
def migrate direction
# do nothing
end
def filename; "anon.rb"; end
def fingerprint; "123456789012345678901234567890ab"; end
end
def setup

@ -1,24 +0,0 @@
require "cases/helper"
module ActiveRecord
class Migration
class TableAndIndexTest < ActiveRecord::TestCase
def test_add_schema_info_respects_prefix_and_suffix
conn = ActiveRecord::Base.connection
conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
# Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
ActiveRecord::Base.table_name_prefix = 'p_'
ActiveRecord::Base.table_name_suffix = '_s'
conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
conn.initialize_schema_migrations_table
assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name]
ensure
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
end
end
end
end

@ -59,12 +59,21 @@ def teardown
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
ActiveRecord::Migrator.migrations_paths = migrations_path
m0_path = File.join(migrations_path, "1_valid_people_have_last_names.rb")
m0_fingerprint = Digest::MD5.hexdigest(File.read(m0_path))
ActiveRecord::Migrator.up(migrations_path)
assert_equal 3, ActiveRecord::Migrator.current_version
assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal false, ActiveRecord::Migrator.needs_migration?
rows = connection.select_all("SELECT * FROM #{connection.quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)}")
assert_equal m0_fingerprint, rows[0]["fingerprint"]
assert_equal "valid_people_have_last_names", rows[0]["name"]
rows.each do |row|
assert_match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, row["migrated_at"], "missing migrated_at")
end
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
assert_equal 0, ActiveRecord::Migrator.current_version
assert_equal 3, ActiveRecord::Migrator.last_version

@ -18,6 +18,9 @@ def initialize name = self.class.name, version = nil
def up; @went_up = true; end
def down; @went_down = true; end
# also used in place of a MigrationProxy
def filename; "anon.rb"; end
def fingerprint; "123456789012345678901234567890ab"; end
end
def setup
@ -102,7 +105,7 @@ def test_relative_migrations
end
def test_finds_pending_migrations
ActiveRecord::SchemaMigration.create!(:version => '1')
ActiveRecord::SchemaMigration.create!(:version => '1', :name => "anon", :migrated_at => Time.now)
migration_list = [ Migration.new('foo', 1), Migration.new('bar', 3) ]
migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
@ -152,7 +155,7 @@ def test_down_calls_down
end
def test_current_version
ActiveRecord::SchemaMigration.create!(:version => '1000')
ActiveRecord::SchemaMigration.create!(:version => '1000', :name => "anon", :migrated_at => Time.now)
assert_equal 1000, ActiveRecord::Migrator.current_version
end
@ -320,7 +323,7 @@ def test_migrator_forward
def test_only_loads_pending_migrations
# migrate up to 1
ActiveRecord::SchemaMigration.create!(:version => '1')
ActiveRecord::SchemaMigration.create!(:version => '1', :name => "anon", :migrated_at => Time.now)
calls, migrator = migrator_class(3)
migrator.migrate("valid", nil)

@ -18,7 +18,7 @@ def standard_dump
def test_dump_schema_information_outputs_lexically_ordered_versions
versions = %w{ 20100101010101 20100201010101 20100301010101 }
versions.reverse.each do |v|
ActiveRecord::SchemaMigration.create!(:version => v)
ActiveRecord::SchemaMigration.create!(:version => v, :name => "anon", :migrated_at => Time.now)
end
schema_info = ActiveRecord::Base.connection.dump_schema_information

@ -0,0 +1,54 @@
require "cases/helper"
class SchemaMigrationTest < ActiveRecord::TestCase
def sm_table_name
ActiveRecord::SchemaMigration.table_name
end
def connection
ActiveRecord::Base.connection
end
def test_add_schema_info_respects_prefix_and_suffix
connection.drop_table(sm_table_name) if connection.table_exists?(sm_table_name)
# Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters
ActiveRecord::Base.table_name_prefix = 'p_'
ActiveRecord::Base.table_name_suffix = '_s'
connection.drop_table(sm_table_name) if connection.table_exists?(sm_table_name)
ActiveRecord::SchemaMigration.create_table
assert_equal "p_unique_schema_migrations_s", connection.indexes(sm_table_name)[0][:name]
ensure
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
end
def test_add_metadata_columns_to_exisiting_schema_migrations
# creates the old table schema from pre-Rails4.0, so we can test adding to it below
if connection.table_exists?(sm_table_name)
connection.drop_table(sm_table_name)
end
connection.create_table(sm_table_name, :id => false) do |schema_migrations_table|
schema_migrations_table.column("version", :string, :null => false)
end
connection.insert "INSERT INTO #{connection.quote_table_name(sm_table_name)} (version) VALUES (100)"
connection.insert "INSERT INTO #{connection.quote_table_name(sm_table_name)} (version) VALUES (200)"
ActiveRecord::SchemaMigration.create_table
rows = connection.select_all("SELECT * FROM #{connection.quote_table_name(sm_table_name)}")
assert rows[0].has_key?("migrated_at"), "missing column `migrated_at`"
assert_match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, rows[0]["migrated_at"])
assert rows[0].has_key?("fingerprint"), "missing column `fingerprint`"
assert rows[0].has_key?("name"), "missing column `name`"
end
def test_schema_migrations_columns
ActiveRecord::SchemaMigration.create_table
columns = connection.columns(sm_table_name).collect(&:name)
%w[version migrated_at fingerprint name].each { |col| assert columns.include?(col), "missing column `#{col}`" }
end
end