From b98fa60604c8b12b8de14288267240561b2534b9 Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Mon, 16 Oct 2023 07:42:55 +0200 Subject: [PATCH] Use active support lazy load hooks Extend `ActiveRecord::Base` functionality only after `ActiveRecord` has been fully loaded. Ref: thoughtbot/factory_bot_rails#426 Close #231 --- lib/chrono_model.rb | 65 +++++++-------------------- lib/chrono_model/chrono.rb | 17 +++++++ lib/chrono_model/railtie.rb | 88 ++++++++++++++++++++----------------- 3 files changed, 81 insertions(+), 89 deletions(-) create mode 100644 lib/chrono_model/chrono.rb diff --git a/lib/chrono_model.rb b/lib/chrono_model.rb index f90c2326..b10173a9 100644 --- a/lib/chrono_model.rb +++ b/lib/chrono_model.rb @@ -2,12 +2,14 @@ require 'active_record' +require 'chrono_model/chrono' require 'chrono_model/conversions' require 'chrono_model/patches' require 'chrono_model/adapter' require 'chrono_model/time_machine' require 'chrono_model/time_gate' require 'chrono_model/version' +require 'chrono_model/railtie' module ChronoModel class Error < ActiveRecord::ActiveRecordError # :nodoc: @@ -33,59 +35,24 @@ def self.history_models end end -if defined?(Rails::Railtie) - require 'chrono_model/railtie' -end +ActiveSupport.on_load :active_record do + extend ChronoModel::Chrono -ActiveRecord::Base.instance_eval do - # Checks whether this Active Recoed model is backed by a temporal table - # - def chrono? - return false unless connection.respond_to? :is_chrono? + # Hooks into Association#scope to pass the As-Of time automatically + # to methods that load associated ChronoModel records. + ActiveRecord::Associations::Association.prepend ChronoModel::Patches::Association - connection.is_chrono?(table_name) - end -end + # Hooks into Relation#build_arel to use `:joins` on your ChronoModels + # and join data from associated records As-Of time. + ActiveRecord::Relation.prepend ChronoModel::Patches::Relation -# Hooks into Association#scope to pass the As-Of time automatically -# to methods that load associated ChronoModel records. -# -ActiveRecord::Associations::Association.instance_eval do - prepend ChronoModel::Patches::Association -end + # Hooks in two points of the AR Preloader to preload As-Of time records of + # associated ChronoModels. is used by `.includes`, `.preload`, and `.eager_load`. + ActiveRecord::Associations::Preloader.prepend ChronoModel::Patches::Preloader -# Hooks into Relation#build_arel to use :joins on your ChronoModels -# and join data from associated records As-Of time. -# -ActiveRecord::Relation.instance_eval do - prepend ChronoModel::Patches::Relation -end + ActiveRecord::Associations::Preloader::Association.prepend ChronoModel::Patches::Preloader::Association -# Hooks in two points of the AR Preloader to preload As-Of time records of -# associated ChronoModels. is used by .includes, .preload and .eager_load. -# -ActiveRecord::Associations::Preloader.instance_eval do - prepend ChronoModel::Patches::Preloader -end + ActiveRecord::Associations::Preloader::ThroughAssociation.prepend ChronoModel::Patches::Preloader::ThroughAssociation -ActiveRecord::Associations::Preloader::Association.instance_eval do - prepend ChronoModel::Patches::Preloader::Association -end - -ActiveRecord::Associations::Preloader::ThroughAssociation.instance_eval do - prepend ChronoModel::Patches::Preloader::ThroughAssociation -end - -ActiveRecord::Batches::BatchEnumerator.instance_eval do - prepend ChronoModel::Patches::Batches::BatchEnumerator -end - -if defined?(Rails::DBConsole) && Rails.version < '7.1' - Rails::DBConsole.instance_eval do - if Rails.version < '6.1' - prepend ChronoModel::Patches::DBConsole::Config - else - prepend ChronoModel::Patches::DBConsole::DbConfig - end - end + ActiveRecord::Batches::BatchEnumerator.prepend ChronoModel::Patches::Batches::BatchEnumerator end diff --git a/lib/chrono_model/chrono.rb b/lib/chrono_model/chrono.rb new file mode 100644 index 00000000..83853032 --- /dev/null +++ b/lib/chrono_model/chrono.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ChronoModel + # A module to add to ActiveRecord::Base to check if they are backed by + # temporal tables. + module Chrono + # Checks whether this Active Record model is backed by a temporal table + # + # @return [Boolean] false if the connection does not respond to is_chrono? + # the result of connection.is_chrono?(table_name) otherwise + def chrono? + return false unless connection.respond_to? :is_chrono? + + connection.is_chrono?(table_name) + end + end +end diff --git a/lib/chrono_model/railtie.rb b/lib/chrono_model/railtie.rb index 951555b2..c760abb3 100644 --- a/lib/chrono_model/railtie.rb +++ b/lib/chrono_model/railtie.rb @@ -1,54 +1,62 @@ # frozen_string_literal: true -require 'active_record/tasks/chronomodel_database_tasks' - -module ChronoModel - class Railtie < ::Rails::Railtie - TASKS_CLASS = ActiveRecord::Tasks::ChronomodelDatabaseTasks +if defined?(Rails) + module ChronoModel + class Railtie < ::Rails::Railtie + rake_tasks do + load 'active_record/tasks/chronomodel_database_tasks.rb' + + def task_config + if Rails.version < '6.1' + ActiveRecord::Tasks::DatabaseTasks.current_config.with_indifferent_access + else + ActiveRecord::Base.connection_db_config + end + end - def task_config - if Rails.version < '6.1' - ActiveRecord::Tasks::DatabaseTasks.current_config.with_indifferent_access - else - ActiveRecord::Base.connection_db_config - end - end + # Register our database tasks under our adapter name + if Rails.version < '5.2' + ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, ActiveRecord::Tasks::ChronomodelDatabaseTasks) + else + ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, 'ActiveRecord::Tasks::ChronomodelDatabaseTasks') + end - # Register our database tasks under our adapter name - if Rails.version < '5.2' - ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS) - else - ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS.to_s) - end + if Rails.application.config.active_record.schema_format != :sql + raise 'In order to use ChronoModel, config.active_record.schema_format must be :sql!' + end - rake_tasks do - if Rails.application.config.active_record.schema_format != :sql - raise 'In order to use ChronoModel, config.active_record.schema_format must be :sql!' - end + if Rails.version < '6.1' + # Make schema:dump and schema:load invoke structure:dump and structure:load + Rake::Task['db:schema:dump'].clear.enhance(['environment']) do + Rake::Task['db:structure:dump'].invoke + end - if Rails.version < '6.1' - # Make schema:dump and schema:load invoke structure:dump and structure:load - Rake::Task['db:schema:dump'].clear.enhance(['environment']) do - Rake::Task['db:structure:dump'].invoke + Rake::Task['db:schema:load'].clear.enhance(['environment']) do + Rake::Task['db:structure:load'].invoke + end end - Rake::Task['db:schema:load'].clear.enhance(['environment']) do - Rake::Task['db:structure:load'].invoke + desc 'Dumps database into db/data.NOW.sql or file specified via DUMP=' + task 'db:data:dump' => :environment do + target = ENV['DUMP'] || Rails.root.join('db', "data.#{Time.now.to_f}.sql") + ActiveRecord::Tasks::ChronomodelDatabaseTasks.new(task_config).data_dump(target) end - end - - desc 'Dumps database into db/data.NOW.sql or file specified via DUMP=' - task 'db:data:dump' => :environment do - target = ENV['DUMP'] || Rails.root.join('db', "data.#{Time.now.to_f}.sql") - TASKS_CLASS.new(task_config).data_dump(target) - end - desc 'Loads database dump from file specified via DUMP=' - task 'db:data:load' => :environment do - source = ENV['DUMP'].presence or - raise ArgumentError, 'Invoke as rake db:data:load DUMP=/path/to/data.sql' - TASKS_CLASS.new(task_config).data_load(source) + desc 'Loads database dump from file specified via DUMP=' + task 'db:data:load' => :environment do + source = ENV['DUMP'].presence or + raise ArgumentError, 'Invoke as rake db:data:load DUMP=/path/to/data.sql' + ActiveRecord::Tasks::ChronomodelDatabaseTasks.new(task_config).data_load(source) + end end end end end + +if defined?(Rails::DBConsole) && Rails.version < '7.1' + if Rails.version < '6.1' + Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::Config + else + Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::DbConfig + end +end