Merge branch 'joshsusser-master' into merge

* joshsusser-master:
  style cleanup
  Add migration history to schema.rb dump
  Add metadata to schema_migrations

Conflicts:
	activerecord/CHANGELOG.md
	activerecord/lib/active_record/schema.rb
This commit is contained in:
Aaron Patterson 2012-12-05 12:13:49 -08:00
commit 0c692f4d12
15 changed files with 268 additions and 58 deletions

@ -1,5 +1,23 @@
## Rails 4.0.0 (unreleased) ##
* Add migration history to schema.rb dump.
Loading schema.rb with full migration history
restores the exact list of migrations that created
that schema (including names and fingerprints). This
avoids possible mistakes caused by assuming all
migrations with a lower version have been run when
loading schema.rb. Old schema.rb files without migration
history but with the :version setting still work as before.
*Josh Susser*
* 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*
* Fix performance problem with primary_key method in PostgreSQL adapter when having many schemas.
Uses pg_constraint table instead of pg_depend table which has many records in general.
Fix #8414

@ -490,8 +490,8 @@ 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}');"
}.join "\n\n"
"INSERT INTO #{sm_table} (version, migrated_at, fingerprint, name) VALUES ('#{sm.version}',LOCALTIMESTAMP,'#{sm.fingerprint}','#{sm.name}');"
}.join("\n\n")
end
# Should not be called normally, but this operation is non-destructive.
@ -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

@ -39,27 +39,45 @@ def migrations_paths
end
def define(info, &block) # :nodoc:
@using_deprecated_version_setting = info[:version].present?
SchemaMigration.drop_table
initialize_schema_migrations_table
instance_eval(&block)
unless info[:version].blank?
initialize_schema_migrations_table
assume_migrated_upto_version(info[:version], migrations_paths)
end
# handle files from pre-4.0 that used :version option instead of dumping migration table
assume_migrated_upto_version(info[:version], migrations_paths) if @using_deprecated_version_setting
end
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
# database definition DSL to build up your schema (+create_table+,
# +add_index+, etc.).
#
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
#
# ActiveRecord::Schema.define(version: 20380119000001) do
# ...
# end
def self.define(info={}, &block)
new.define(info, &block)
end
# Create schema migration history. Include migration statements in a block to this method.
#
# migrations do
# migration 20121128235959, "44f1397e3b92442ca7488a029068a5ad", "add_horn_color_to_unicorns"
# migration 20121129235959, "4a1eb3965d94406b00002b370854eae8", "add_magic_power_to_unicorns"
# end
def migrations
raise(ArgumentError, "Can't set migrations while using :version option") if @using_deprecated_version_setting
yield
end
# Add a migration to the ActiveRecord::SchemaMigration table.
#
# The +version+ argument is an integer.
# The +fingerprint+ and +name+ arguments are required but may be empty strings.
# The migration's +migrated_at+ attribute is set to the current time,
# instead of being set explicitly as an argument to the method.
#
# migration 20121129235959, "4a1eb3965d94406b00002b370854eae8", "add_magic_power_to_unicorns"
def migration(version, fingerprint, name)
SchemaMigration.create!(version: version, migrated_at: Time.now, fingerprint: fingerprint, name: name)
end
end
end

@ -24,6 +24,7 @@ def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
def dump(stream)
header(stream)
migrations(stream)
tables(stream)
trailer(stream)
stream
@ -44,7 +45,7 @@ def header(stream)
stream.puts "# encoding: #{stream.external_encoding.name}"
end
stream.puts <<HEADER
header_text = <<HEADER_RUBY
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@ -59,13 +60,25 @@ def header(stream)
ActiveRecord::Schema.define(#{define_params}) do
HEADER
HEADER_RUBY
stream.puts header_text
end
def trailer(stream)
stream.puts "end"
end
def migrations(stream)
all_migrations = ActiveRecord::SchemaMigration.all.to_a
if all_migrations.any?
stream.puts(" migrations do")
all_migrations.each do |migration|
stream.puts(migration.schema_line(" "))
end
stream.puts(" end")
end
end
def tables(stream)
@connection.tables.sort.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|

@ -14,17 +14,38 @@ def self.index_name
end
def self.create_table
unless connection.table_exists?(table_name)
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
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
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
@ -32,5 +53,17 @@ def self.drop_table
def version
super.to_i
end
# Construct ruby source to include in schema.rb dump for this migration.
# Pass a string of spaces as +indent+ to allow calling code to control how deeply indented the line is.
# The generated line includes the migration version, fingerprint, and name. Either fingerprint or name
# can be an empty string.
#
# Example output:
#
# migration 20121129235959, "ee4be703f9e6e2fc0f4baddebe6eb8f7", "add_magic_power_to_unicorns"
def schema_line(indent)
%Q(#{indent}migration %s, "%s", "%s") % [version, fingerprint, name]
end
end
end

@ -46,4 +46,55 @@ def test_schema_subclass
end
end
class ActiveRecordSchemaMigrationsTest < ActiveRecordSchemaTest
def setup
super
ActiveRecord::SchemaMigration.delete_all
end
def test_migration_adds_row_to_migrations_table
schema = ActiveRecord::Schema.new
schema.migration(1001, "", "")
schema.migration(1002, "123456789012345678901234567890ab", "add_magic_power_to_unicorns")
migrations = ActiveRecord::SchemaMigration.all.to_a
assert_equal 2, migrations.length
assert_equal 1001, migrations[0].version
assert_match %r{^2\d\d\d-}, migrations[0].migrated_at.to_s(:db)
assert_equal "", migrations[0].fingerprint
assert_equal "", migrations[0].name
assert_equal 1002, migrations[1].version
assert_match %r{^2\d\d\d-}, migrations[1].migrated_at.to_s(:db)
assert_equal "123456789012345678901234567890ab", migrations[1].fingerprint
assert_equal "add_magic_power_to_unicorns", migrations[1].name
end
def test_define_clears_schema_migrations
assert_nothing_raised do
ActiveRecord::Schema.define do
migrations do
migration(123001, "", "")
end
end
ActiveRecord::Schema.define do
migrations do
migration(123001, "", "")
end
end
end
end
def test_define_raises_if_both_version_and_explicit_migrations
assert_raise(ArgumentError) do
ActiveRecord::Schema.define(version: 123001) do
migrations do
migration(123001, "", "")
end
end
end
end
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
@ -337,7 +346,7 @@ def test_create_table_with_binary_column
assert_nothing_raised {
Person.connection.create_table :binary_testings do |t|
t.column "data", :binary, :null => false
t.column :data, :binary, :null => false
end
}

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

@ -1,6 +1,5 @@
require "cases/helper"
class SchemaDumperTest < ActiveRecord::TestCase
def setup
super
@ -18,11 +17,15 @@ 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, :migrated_at => Time.now,
:fingerprint => "123456789012345678901234567890ab", :name => "anon")
end
schema_info = ActiveRecord::Base.connection.dump_schema_information
assert_match(/20100201010101.*20100301010101/m, schema_info)
target_line = %q{INSERT INTO schema_migrations (version, migrated_at, fingerprint, name) VALUES ('20100101010101',LOCALTIMESTAMP,'123456789012345678901234567890ab','anon');}
assert_match target_line, schema_info
end
def test_magic_comment
@ -36,6 +39,16 @@ def test_schema_dump
assert_no_match %r{create_table "schema_migrations"}, output
end
def test_schema_dump_includes_migrations
ActiveRecord::SchemaMigration.delete_all
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/always_safe")
output = standard_dump
assert_match %r{migrations do}, output, "Missing migrations block"
assert_match %r{migration 1001, "[0-9a-f]{32}", "always_safe"}, output, "Missing migration line"
assert_match %r{migration 1002, "[0-9a-f]{32}", "still_safe"}, output, "Missing migration line"
end
def test_schema_dump_excludes_sqlite_sequence
output = standard_dump
assert_no_match %r{create_table "sqlite_sequence"}, output

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

@ -0,0 +1,5 @@
class AlwaysSafe < ActiveRecord::Migration
def change
# do nothing to avoid side-effect conflicts from running multiple times
end
end

@ -0,0 +1,5 @@
class StillSafe < ActiveRecord::Migration
def change
# do nothing to avoid side-effect conflicts from running multiple times
end
end