From 6673d8ef3b857ede36fc74923bcf16d6964bdfc9 Mon Sep 17 00:00:00 2001 From: Adrianna Chang Date: Fri, 10 Jun 2022 13:22:21 -0400 Subject: [PATCH] Delegate migration methods to strategy object The default strategy will continue to forward messages to the connection adapter, but applications can configure a custom strategy class to use instead. --- activerecord/CHANGELOG.md | 8 +++++++ activerecord/lib/active_record.rb | 6 +++++ activerecord/lib/active_record/migration.rb | 11 +++++++-- .../migration/default_strategy.rb | 23 +++++++++++++++++++ .../migration/execution_strategy.rb | 19 +++++++++++++++ guides/source/configuring.md | 16 +++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 activerecord/lib/active_record/migration/default_strategy.rb create mode 100644 activerecord/lib/active_record/migration/execution_strategy.rb diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 5fcbfaf7be..925a1a3742 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,11 @@ +* Introduce strategy pattern for executing migrations. + + By default, migrations will use a strategy object that delegates the method + to the connection adapter. Consumers can implement custom strategy objects + to change how their migrations run. + + *Adrianna Chang* + * Add adapter option disallowing foreign keys This adds a new option to be added to `database.yml` which enables skipping diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 69fcaf887c..68048a7646 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -302,6 +302,12 @@ module ActiveRecord singleton_class.attr_accessor :timestamped_migrations self.timestamped_migrations = true + ## + # :singleton-method: + # Specify strategy to use for executing migrations. + singleton_class.attr_accessor :migration_strategy + self.migration_strategy = Migration::DefaultStrategy + ## # :singleton-method: # Specify whether schema dump should happen at the end of the diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 09f3c345b1..89dd5cf7a6 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -548,6 +548,8 @@ module ActiveRecord autoload :CommandRecorder, "active_record/migration/command_recorder" autoload :Compatibility, "active_record/migration/compatibility" autoload :JoinTable, "active_record/migration/join_table" + autoload :ExecutionStrategy, "active_record/migration/execution_strategy" + autoload :DefaultStrategy, "active_record/migration/default_strategy" # This must be defined before the inherited hook, below class Current < Migration # :nodoc: @@ -694,6 +696,10 @@ module ActiveRecord @connection = nil end + def execution_strategy + @execution_strategy ||= ActiveRecord.migration_strategy.new(self) + end + self.verbose = true # instantiate the delegate object after initialize is defined self.delegate = new @@ -881,6 +887,7 @@ module ActiveRecord end ensure @connection = nil + @execution_strategy = nil end def write(text = "") @@ -935,8 +942,8 @@ module ActiveRecord end end end - return super unless connection.respond_to?(method) - connection.send(method, *arguments, &block) + return super unless execution_strategy.respond_to?(method) + execution_strategy.send(method, *arguments, &block) end end ruby2_keywords(:method_missing) diff --git a/activerecord/lib/active_record/migration/default_strategy.rb b/activerecord/lib/active_record/migration/default_strategy.rb new file mode 100644 index 0000000000..a29922d0df --- /dev/null +++ b/activerecord/lib/active_record/migration/default_strategy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ActiveRecord + class Migration + # The default strategy for executing migrations. Delegates method calls + # to the connection adapter. + class DefaultStrategy < ExecutionStrategy # :nodoc: + private + def method_missing(method, *arguments, &block) + connection.send(method, *arguments, &block) + end + ruby2_keywords(:method_missing) + + def respond_to_missing?(method, *) + connection.respond_to?(method) || super + end + + def connection + migration.connection + end + end + end +end diff --git a/activerecord/lib/active_record/migration/execution_strategy.rb b/activerecord/lib/active_record/migration/execution_strategy.rb new file mode 100644 index 0000000000..299e89029a --- /dev/null +++ b/activerecord/lib/active_record/migration/execution_strategy.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ActiveRecord + class Migration + # ExecutionStrategy is used by the migration to respond to any method calls + # that the migration class does not implement directly. This is the base strategy. + # All strategies should inherit from this class. + # + # The ExecutionStrategy receives the current +migration+ when initialized. + class ExecutionStrategy # :nodoc: + def initialize(migration) + @migration = migration + end + + private + attr_reader :migration + end + end +end diff --git a/guides/source/configuring.md b/guides/source/configuring.md index f295829f97..f69726e1b4 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -767,6 +767,22 @@ Specifies if an error should be raised if the order of a query is ignored during Controls whether migrations are numbered with serial integers or with timestamps. The default is `true`, to use timestamps, which are preferred if there are multiple developers working on the same application. +#### `config.active_record.migration_strategy` + +Controls the strategy class used to perform schema statement methods in a migration. The default class +delegates to the connection adapter. Custom strategies should inherit from `ActiveRecord::Migration::ExecutionStrategy`, +or may inherit from `DefaultStrategy`, which will preserve the default behaviour for methods that aren't implemented: + +```ruby +class CustomMigrationStrategy < ActiveRecord::Migration::DefaultStrategy + def drop_table(*) + raise "Dropping tables is not supported!" + end +end + +config.active_record.migration_strategy = CustomMigrationStrategy +``` + #### `config.active_record.lock_optimistically` Controls whether Active Record will use optimistic locking and is `true` by default.