mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Use FileUpdateChecker for Migration::CheckPending
Migration::CheckPending is a rack middleware normally used in development to raise an exception if there pending migrations. This commit replaces the existing caching, which avoided checking all migrations if the newest migration (by version number) hadn't changed. Instead, we now use FileUpdateChecker, which has two advantages: it can detect new migrations which aren't the highest version, and it is faster.
This commit is contained in:
parent
e684f5b090
commit
4705ba82db
3 changed files with 97 additions and 33 deletions
|
@ -3,6 +3,7 @@
|
||||||
require "benchmark"
|
require "benchmark"
|
||||||
require "set"
|
require "set"
|
||||||
require "zlib"
|
require "zlib"
|
||||||
|
require "active_support/core_ext/enumerable"
|
||||||
require "active_support/core_ext/module/attribute_accessors"
|
require "active_support/core_ext/module/attribute_accessors"
|
||||||
require "active_support/actionable_error"
|
require "active_support/actionable_error"
|
||||||
|
|
||||||
|
@ -553,21 +554,37 @@ module ActiveRecord
|
||||||
# This class is used to verify that all migrations have been run before
|
# This class is used to verify that all migrations have been run before
|
||||||
# loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
|
# loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
|
||||||
class CheckPending
|
class CheckPending
|
||||||
def initialize(app)
|
def initialize(app, file_watcher: ActiveSupport::FileUpdateChecker)
|
||||||
@app = app
|
@app = app
|
||||||
@last_check = 0
|
@needs_check = true
|
||||||
|
@mutex = Mutex.new
|
||||||
|
@file_watcher = file_watcher
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i
|
@mutex.synchronize do
|
||||||
if @last_check < mtime
|
@watcher ||= build_watcher do
|
||||||
ActiveRecord::Migration.check_pending!(connection)
|
@needs_check = true
|
||||||
@last_check = mtime
|
ActiveRecord::Migration.check_pending!(connection)
|
||||||
|
@needs_check = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if @needs_check
|
||||||
|
@watcher.execute
|
||||||
|
else
|
||||||
|
@watcher.execute_if_updated
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@app.call(env)
|
@app.call(env)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def build_watcher(&block)
|
||||||
|
paths = Array(connection.migration_context.migrations_paths)
|
||||||
|
@file_watcher.new([], paths.index_with(["rb"]), &block)
|
||||||
|
end
|
||||||
|
|
||||||
def connection
|
def connection
|
||||||
ActiveRecord::Base.connection
|
ActiveRecord::Base.connection
|
||||||
end
|
end
|
||||||
|
@ -993,10 +1010,6 @@ module ActiveRecord
|
||||||
File.basename(filename)
|
File.basename(filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mtime
|
|
||||||
File.mtime filename
|
|
||||||
end
|
|
||||||
|
|
||||||
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
|
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -1010,16 +1023,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class NullMigration < MigrationProxy #:nodoc:
|
|
||||||
def initialize
|
|
||||||
super(nil, 0, nil, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
def mtime
|
|
||||||
0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class MigrationContext #:nodoc:
|
class MigrationContext #:nodoc:
|
||||||
attr_reader :migrations_paths, :schema_migration
|
attr_reader :migrations_paths, :schema_migration
|
||||||
|
|
||||||
|
@ -1098,10 +1101,6 @@ module ActiveRecord
|
||||||
migrations.any?
|
migrations.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_migration #:nodoc:
|
|
||||||
migrations.last || NullMigration.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def migrations
|
def migrations
|
||||||
migrations = migration_files.map do |file|
|
migrations = migration_files.map do |file|
|
||||||
version, name, scope = parse_migration_filename(file)
|
version, name, scope = parse_migration_filename(file)
|
||||||
|
|
|
@ -82,10 +82,11 @@ module ActiveRecord
|
||||||
ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner }
|
ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner }
|
||||||
end
|
end
|
||||||
|
|
||||||
initializer "active_record.migration_error" do
|
initializer "active_record.migration_error" do |app|
|
||||||
if config.active_record.delete(:migration_error) == :page_load
|
if config.active_record.delete(:migration_error) == :page_load
|
||||||
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
|
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
|
||||||
ActiveRecord::Migration::CheckPending
|
ActiveRecord::Migration::CheckPending,
|
||||||
|
file_watcher: app.config.file_watcher
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,34 +7,98 @@ module ActiveRecord
|
||||||
if current_adapter?(:SQLite3Adapter) && !in_memory_db?
|
if current_adapter?(:SQLite3Adapter) && !in_memory_db?
|
||||||
class PendingMigrationsTest < ActiveRecord::TestCase
|
class PendingMigrationsTest < ActiveRecord::TestCase
|
||||||
setup do
|
setup do
|
||||||
|
@migration_dir = Dir.mktmpdir("activerecord-migrations-")
|
||||||
|
|
||||||
file = ActiveRecord::Base.connection.raw_connection.filename
|
file = ActiveRecord::Base.connection.raw_connection.filename
|
||||||
@conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid"
|
@conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: @migration_dir
|
||||||
source_db = SQLite3::Database.new file
|
source_db = SQLite3::Database.new file
|
||||||
dest_db = ActiveRecord::Base.connection.raw_connection
|
dest_db = ActiveRecord::Base.connection.raw_connection
|
||||||
backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
|
backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
|
||||||
backup.step(-1)
|
backup.step(-1)
|
||||||
backup.finish
|
backup.finish
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
|
||||||
|
|
||||||
|
@app = Minitest::Mock.new
|
||||||
end
|
end
|
||||||
|
|
||||||
teardown do
|
teardown do
|
||||||
@conn.release_connection if @conn
|
@conn.release_connection if @conn
|
||||||
ActiveRecord::Base.establish_connection :arunit
|
ActiveRecord::Base.establish_connection :arunit
|
||||||
|
FileUtils.rm_rf(@migration_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_migrations
|
||||||
|
migrator = Base.connection.migration_context
|
||||||
|
capture(:stdout) { migrator.migrate }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_migration(number, name)
|
||||||
|
filename = "#{number}_#{name.underscore}.rb"
|
||||||
|
File.write(File.join(@migration_dir, filename), <<~RUBY)
|
||||||
|
class #{name.classify} < ActiveRecord::Migration::Current
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_errors_if_pending
|
def test_errors_if_pending
|
||||||
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
|
create_migration "01", "create_foo"
|
||||||
|
|
||||||
assert_raises ActiveRecord::PendingMigrationError do
|
assert_raises ActiveRecord::PendingMigrationError do
|
||||||
CheckPending.new(Proc.new { }).call({})
|
CheckPending.new(@app).call({})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Continues failing
|
||||||
|
assert_raises ActiveRecord::PendingMigrationError do
|
||||||
|
CheckPending.new(@app).call({})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_checks_if_supported
|
def test_checks_if_supported
|
||||||
ActiveRecord::SchemaMigration.create_table
|
run_migrations
|
||||||
migrator = Base.connection.migration_context
|
|
||||||
capture(:stdout) { migrator.migrate }
|
|
||||||
|
|
||||||
assert_nil CheckPending.new(Proc.new { }).call({})
|
check_pending = CheckPending.new(@app)
|
||||||
|
|
||||||
|
@app.expect :call, nil, [{}]
|
||||||
|
check_pending.call({})
|
||||||
|
@app.verify
|
||||||
|
|
||||||
|
# With cached result
|
||||||
|
@app.expect :call, nil, [{}]
|
||||||
|
check_pending.call({})
|
||||||
|
@app.verify
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_okay_with_no_migrations
|
||||||
|
check_pending = CheckPending.new(@app)
|
||||||
|
|
||||||
|
@app.expect :call, nil, [{}]
|
||||||
|
check_pending.call({})
|
||||||
|
@app.verify
|
||||||
|
|
||||||
|
# With cached result
|
||||||
|
@app.expect :call, nil, [{}]
|
||||||
|
check_pending.call({})
|
||||||
|
@app.verify
|
||||||
|
end
|
||||||
|
|
||||||
|
# Regression test for https://github.com/rails/rails/pull/29759
|
||||||
|
def test_understands_migrations_created_out_of_order
|
||||||
|
# With a prior file before even initialization
|
||||||
|
create_migration "05", "create_bar"
|
||||||
|
run_migrations
|
||||||
|
|
||||||
|
check_pending = CheckPending.new(@app)
|
||||||
|
|
||||||
|
@app.expect :call, nil, [{}]
|
||||||
|
check_pending.call({})
|
||||||
|
@app.verify
|
||||||
|
|
||||||
|
# It understands the new migration created at 01
|
||||||
|
create_migration "01", "create_foo"
|
||||||
|
assert_raises ActiveRecord::PendingMigrationError do
|
||||||
|
check_pending.call({})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue