Fix migration ordering across databases

Previously if there were 2 migrations in one db and 1 migration in the
other db all the migrations for db one would run and then all migrations
for db two would run. If a migration in one database depended on a
migration in another database then it could fail. This is probably
pretty rare, however in a multi-db application that's moving tables from
one db to another, running them out of order could result in a migration
error.

In this this change we collect all the versions for each migration and
the corresponding db_config so we can run them in the order they are
created rather than per-db.

Closes #41664
Related #41538

Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
Co-authored-by: Kiril Dokh <dsounded@gmail.com>
This commit is contained in:
eileencodes 2021-03-01 13:31:50 -05:00
parent 6ae78e964d
commit 45eb0f3bec
No known key found for this signature in database
GPG Key ID: BA5C575120BBE8DF
4 changed files with 61 additions and 7 deletions

View File

@ -1140,7 +1140,11 @@ module ActiveRecord
end
def needs_migration? # :nodoc:
(migrations.collect(&:version) - get_all_versions).size > 0
pending_migration_versions.size > 0
end
def pending_migration_versions # :nodoc:
migrations.collect(&:version) - get_all_versions
end
def migrations # :nodoc:

View File

@ -86,14 +86,23 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
original_db_config = ActiveRecord::Base.connection_db_config
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config)
db_configs = ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
if db_configs.size == 1
ActiveRecord::Tasks::DatabaseTasks.migrate
else
original_db_config = ActiveRecord::Base.connection_db_config
mapped_versions = ActiveRecord::Tasks::DatabaseTasks.db_configs_with_versions(db_configs)
mapped_versions.sort.each do |version, db_config|
ActiveRecord::Base.establish_connection(db_config)
ActiveRecord::Tasks::DatabaseTasks.migrate(version)
end
end
db_namespace["_dump"].invoke
ensure
ActiveRecord::Base.establish_connection(original_db_config)
ActiveRecord::Base.establish_connection(original_db_config) if original_db_config
end
# IMPORTANT: This task won't dump the schema if ActiveRecord.dump_schema_after_migration is set to false

View File

@ -268,13 +268,13 @@ module ActiveRecord
end
end
def migrate
def migrate(version = nil)
check_target_version
scope = ENV["SCOPE"]
verbose_was, Migration.verbose = Migration.verbose, verbose?
Base.connection.migration_context.migrate(target_version) do |migration|
Base.connection.migration_context.migrate(target_version || version) do |migration|
scope.blank? || scope == migration.scope
end.tap do |migrations_ran|
Migration.write("No migrations ran. (using #{scope} scope)") if scope.present? && migrations_ran.empty?
@ -285,6 +285,23 @@ module ActiveRecord
Migration.verbose = verbose_was
end
def db_configs_with_versions(db_configs) # :nodoc:
db_configs_with_versions = {}
db_configs.each do |db_config|
ActiveRecord::Base.establish_connection(db_config)
versions_to_run = ActiveRecord::Base.connection.migration_context.pending_migration_versions
target_version = ActiveRecord::Tasks::DatabaseTasks.target_version
versions_to_run.each do |version|
next if target_version && target_version != version
db_configs_with_versions[version] = db_config
end
end
db_configs_with_versions
end
def migrate_status
unless ActiveRecord::Base.connection.schema_migration.table_exists?
Kernel.abort "Schema migrations table does not exist yet."

View File

@ -427,6 +427,30 @@ module ApplicationTests
end
end
test "db:migrate respects timestamp ordering across databases" do
require "#{app_path}/config/environment"
app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
class OneMigration < ActiveRecord::Migration::Current
end
MIGRATION
app_file "db/animals_migrate/02_two_migration.rb", <<-MIGRATION
class TwoMigration < ActiveRecord::Migration::Current
end
MIGRATION
app_file "db/migrate/03_three_migration.rb", <<-MIGRATION
class ThreeMigration < ActiveRecord::Migration::Current
end
MIGRATION
Dir.chdir(app_path) do
output = rails "db:migrate"
entries = output.scan(/^== (\d+).+migrated/).map(&:first).map(&:to_i)
assert_equal [1, 2, 3], entries
end
end
test "db:migrate and db:schema:dump and db:schema:load works on all databases" do
db_migrate_and_schema_dump_and_load
end