aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation/merger.rb
blob: c2f0a82fd3acdbecc775eb82a7b40c60e3a05cc4 (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
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/keys'

module ActiveRecord
  class Relation
    class HashMerger
      attr_reader :relation, :hash

      def initialize(relation, hash)
        hash.assert_valid_keys(*Relation::VALUE_METHODS)

        @relation = relation
        @hash     = hash
      end

      def merge
        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.new(relation.klass, relation.table)
        hash.each { |k, v| other.send("#{k}!", v) }
        other
      end
    end

    class Merger
      attr_reader :relation, :other

      def initialize(relation, other)
        if other.default_scoped? && other.klass != relation.klass
          other = other.with_default_scope
        end

        @relation = relation
        @other    = other
      end

      def values
        @other.values
      end

      def normal_values
        Relation::SINGLE_VALUE_METHODS +
          Relation::MULTI_VALUE_METHODS -
          [:where, :joins, :order, :bind, :reverse_order, :lock, :create_with, :reordering]
      end

      def merge
        normal_values.each do |name|
          value = values[name]
          relation.send("#{name}!", value) unless value.blank?
        end

        merge_multi_values
        merge_single_values
        merge_joins

        relation
      end

      private

      def merge_multi_values
        relation.where_values = merged_wheres
        relation.bind_values  = merged_binds

        if values[:reordering]
          # override any order specified in the original relation
          relation.reorder! values[:order]
        elsif values[:order]
          # merge in order_values from r
          relation.order! values[:order]
        end

        relation.extend(*values[:extending]) unless values[:extending].blank?
      end

      def merge_single_values
        relation.lock_value          = values[:lock] unless relation.lock_value
        relation.reverse_order_value = values[:reverse_order]

        unless values[:create_with].blank?
          relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
        end
      end

      def merge_joins
        return if values[:joins].blank?

        if other.klass == relation.klass
          relation.joins!(values[:joins])
        else
          joins_to_stash, other_joins = values[:joins].partition { |join|
            case join
            when Hash, Symbol, Array
              true
            else
              false
            end
          }

          join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, joins_to_stash, [])
          relation.joins!(join_dependency.join_associations + other_joins)
        end
      end

      def merged_binds
        if values[:bind]
          (relation.bind_values + values[:bind]).uniq(&:first)
        else
          relation.bind_values
        end
      end

      def merged_wheres
        if values[:where]
          merged_wheres = relation.where_values + values[:where]

          unless relation.where_values.empty?
            # Remove duplicates, last one wins.
            seen = Hash.new { |h,table| h[table] = {} }
            merged_wheres = merged_wheres.reverse.reject { |w|
              nuke = false
              if w.respond_to?(:operator) && w.operator == :==
                name              = w.left.name
                table             = w.left.relation.name
                nuke              = seen[table][name]
                seen[table][name] = true
              end
              nuke
            }.reverse
          end

          merged_wheres
        else
          relation.where_values
        end
      end
    end
  end
end