lib/chrono_model/patches/preloader.rb
# frozen_string_literal: true
module ChronoModel
module Patches
# Patches ActiveRecord::Associations::Preloader to add support for
# temporal associations. This is tying itself to Rails internals
# and it is ugly :-(.
#
module Preloader
attr_reader :chronomodel_options
# We overwrite the initializer in order to pass the +as_of_time+
# parameter above in the build_preloader
#
def initialize(**options)
@chronomodel_options = options.extract!(:as_of_time, :model)
options[:scope] = chronomodel_scope(options[:scope]) if options.key?(:scope)
if options.empty?
super()
else
super(**options)
end
end
# Patches the AR Preloader (lib/active_record/associations/preloader.rb)
# in order to carry around the +as_of_time+ of the original invocation.
#
# * The +records+ are the parent records where the association is defined
# * The +associations+ are the association names involved in preloading
# * The +given_preload_scope+ is the preloading scope, that is used only
# in the :through association and it holds the intermediate records
# _through_ which the final associated records are eventually fetched.
#
# As the +preload_scope+ is passed around to all the different
# incarnations of the preloader strategies, we are using it to pass
# around the +as_of_time+ of the original query invocation, so that
# preloaded records are preloaded honoring the +as_of_time+.
#
# The +preload_scope+ is present only in through associations, but the
# preloader interfaces expect it to be always defined, for consistency.
#
# For `:through` associations, the +given_preload_scope+ is already a
# +Relation+, that already has the +as_of_time+ getters and setters,
# so we use it directly.
#
def preload(records, associations, given_preload_scope = nil)
super(records, associations, chronomodel_scope(given_preload_scope))
end
private
def chronomodel_scope(given_preload_scope)
preload_scope = given_preload_scope
if chronomodel_options[:as_of_time]
preload_scope ||= ChronoModel::Patches::AsOfTimeRelation.new(chronomodel_options[:model])
preload_scope.as_of_time!(chronomodel_options[:as_of_time])
end
preload_scope
end
module Association
# Builds the preloader scope taking into account a potential
# +as_of_time+ passed down the call chain starting at the
# end user invocation.
#
def build_scope
scope = super
if preload_scope.try(:as_of_time)
scope = scope.as_of(preload_scope.as_of_time)
end
scope
end
end
module ThroughAssociation
# Builds the preloader scope taking into account a potential
# +as_of_time+ passed down the call chain starting at the
# end user invocation.
#
def through_scope
scope = super
return unless scope # Rails 5.2 may not return a scope
if preload_scope.try(:as_of_time)
scope = scope.as_of(preload_scope.as_of_time)
end
scope
end
end
end
end
end