aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation/merger.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/relation/merger.rb')
-rw-r--r--activerecord/lib/active_record/relation/merger.rb189
1 files changed, 189 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
new file mode 100644
index 0000000000..4de7465128
--- /dev/null
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+
+module ActiveRecord
+ class Relation
+ class HashMerger # :nodoc:
+ attr_reader :relation, :hash
+
+ def initialize(relation, hash)
+ hash.assert_valid_keys(*Relation::VALUE_METHODS)
+
+ @relation = relation
+ @hash = hash
+ end
+
+ def merge #:nodoc:
+ Merger.new(relation, other).merge
+ end
+
+ # Applying values to a relation has some side effects. E.g.
+ # interpolation might take place for where values. So we should
+ # build a relation to merge in rather than directly merging
+ # the values.
+ def other
+ other = Relation.create(
+ relation.klass,
+ table: relation.table,
+ predicate_builder: relation.predicate_builder
+ )
+ hash.each { |k, v|
+ if k == :joins
+ if Hash === v
+ other.joins!(v)
+ else
+ other.joins!(*v)
+ end
+ elsif k == :select
+ other._select!(v)
+ else
+ other.send("#{k}!", v)
+ end
+ }
+ other
+ end
+ end
+
+ class Merger # :nodoc:
+ attr_reader :relation, :values, :other
+
+ def initialize(relation, other)
+ @relation = relation
+ @values = other.values
+ @other = other
+ end
+
+ NORMAL_VALUES = Relation::VALUE_METHODS -
+ Relation::CLAUSE_METHODS -
+ [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
+
+ def normal_values
+ NORMAL_VALUES
+ end
+
+ def merge
+ normal_values.each do |name|
+ value = values[name]
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ unless value.nil? || (value.blank? && false != value)
+ if name == :select
+ relation._select!(*value)
+ else
+ relation.send("#{name}!", *value)
+ end
+ end
+ end
+
+ merge_multi_values
+ merge_single_values
+ merge_clauses
+ merge_preloads
+ merge_joins
+ merge_outer_joins
+
+ relation
+ end
+
+ private
+
+ def merge_preloads
+ return if other.preload_values.empty? && other.includes_values.empty?
+
+ if other.klass == relation.klass
+ relation.preload!(*other.preload_values) unless other.preload_values.empty?
+ relation.includes!(other.includes_values) unless other.includes_values.empty?
+ else
+ reflection = relation.klass.reflect_on_all_associations.find do |r|
+ r.class_name == other.klass.name
+ end || return
+
+ unless other.preload_values.empty?
+ relation.preload! reflection.name => other.preload_values
+ end
+
+ unless other.includes_values.empty?
+ relation.includes! reflection.name => other.includes_values
+ end
+ end
+ end
+
+ def merge_joins
+ return if other.joins_values.blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*other.joins_values)
+ else
+ joins_dependency = other.joins_values.map do |join|
+ case join
+ when Hash, Symbol, Array
+ other.send(:construct_join_dependency, join)
+ else
+ join
+ end
+ end
+
+ relation.joins!(*joins_dependency)
+ end
+ end
+
+ def merge_outer_joins
+ return if other.left_outer_joins_values.blank?
+
+ if other.klass == relation.klass
+ relation.left_outer_joins!(*other.left_outer_joins_values)
+ else
+ joins_dependency = other.left_outer_joins_values.map do |join|
+ case join
+ when Hash, Symbol, Array
+ other.send(:construct_join_dependency, join)
+ else
+ join
+ end
+ end
+
+ relation.left_outer_joins!(*joins_dependency)
+ end
+ end
+
+ def merge_multi_values
+ if other.reordering_value
+ # override any order specified in the original relation
+ relation.reorder!(*other.order_values)
+ elsif other.order_values.any?
+ # merge in order_values from relation
+ relation.order!(*other.order_values)
+ end
+
+ extensions = other.extensions - relation.extensions
+ relation.extending!(*extensions) if extensions.any?
+ end
+
+ def merge_single_values
+ relation.lock_value ||= other.lock_value if other.lock_value
+
+ unless other.create_with_value.blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
+ end
+ end
+
+ def merge_clauses
+ relation.from_clause = other.from_clause if replace_from_clause?
+
+ where_clause = relation.where_clause.merge(other.where_clause)
+ relation.where_clause = where_clause unless where_clause.empty?
+
+ having_clause = relation.having_clause.merge(other.having_clause)
+ relation.having_clause = having_clause unless having_clause.empty?
+ end
+
+ def replace_from_clause?
+ relation.from_clause.empty? && !other.from_clause.empty? &&
+ relation.klass.base_class == other.klass.base_class
+ end
+ end
+ end
+end