aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation/merger.rb
blob: 87a97803d5f93e6c38f663417e69d1328eb0d288 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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, relation.table, 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, :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

        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, rest = other.joins_values.partition do |join|
            case join
            when Hash, Symbol, Array
              true
            else
              false
            end
          end

          join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
                                                                           joins_dependency,
                                                                           [])
          relation.joins! rest

          @relation = relation.joins join_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
          # merge in order_values from relation
          relation.order! other.order_values
        end

        relation.extend(*other.extending_values) unless other.extending_values.blank?
      end

      def merge_single_values
        if relation.from_clause.empty?
          relation.from_clause = other.from_clause
        end
        relation.lock_value ||= other.lock_value

        unless other.create_with_value.blank?
          relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
        end
      end

      CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name|
        ["#{name}_clause", "#{name}_clause="]
      end

      def merge_clauses
        CLAUSE_METHOD_NAMES.each do |(reader, writer)|
          clause = relation.send(reader)
          other_clause = other.send(reader)
          relation.send(writer, clause.merge(other_clause))
        end
      end
    end
  end
end