squeel 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/compat'
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/context'
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/preloader_extensions'
@@ -0,0 +1,40 @@
1
+ require 'squeel/adapters/active_record/relation_extensions'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+ module RelationExtensions
7
+
8
+ def build_arel
9
+ arel = table.from table
10
+
11
+ build_join_dependency(arel, @joins_values) unless @joins_values.empty?
12
+
13
+ collapse_wheres(arel, where_visit((@where_values - ['']).uniq))
14
+
15
+ arel.having(*having_visit(@having_values.uniq.reject{|h| h.blank?})) unless @having_values.empty?
16
+
17
+ arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
18
+ arel.skip(@offset_value) if @offset_value
19
+
20
+ arel.group(*group_visit(@group_values.uniq.reject{|g| g.blank?})) unless @group_values.empty?
21
+
22
+ order = order_visit(@order_values.uniq)
23
+ order = reverse_sql_order(attrs_to_orderings(order)) if @reverse_order_value
24
+ arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
25
+
26
+ build_select(arel, select_visit(@select_values.uniq))
27
+
28
+ arel.distinct(@uniq_value)
29
+ arel.from(from_visit(@from_value)) if @from_value
30
+ arel.lock(@lock_value) if @lock_value
31
+
32
+ arel
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveRecord::Relation.send :include, Squeel::Adapters::ActiveRecord::RelationExtensions
@@ -20,3 +20,5 @@ module Squeel
20
20
  end
21
21
  end
22
22
  end
23
+
24
+ ActiveRecord::Base.extend Squeel::Adapters::ActiveRecord::BaseExtensions
@@ -1 +1,22 @@
1
- require 'squeel/adapters/active_record/3.1/compat'
1
+ module Arel
2
+
3
+ module Nodes
4
+
5
+ class Grouping < Unary
6
+ include Arel::Predications
7
+ end unless Grouping.include?(Arel::Predications)
8
+
9
+ end
10
+
11
+ module Visitors
12
+
13
+ class DepthFirst < Visitor
14
+
15
+ unless method_defined?(:visit_Arel_Nodes_InfixOperation)
16
+ alias :visit_Arel_Nodes_InfixOperation :binary
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -1 +1,84 @@
1
- require 'squeel/adapters/active_record/3.1/context'
1
+ require 'squeel/context'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+ class Context < ::Squeel::Context
7
+
8
+ def initialize(object)
9
+ super
10
+ @base = object.join_base
11
+ @engine = @base.arel_engine
12
+ @arel_visitor = get_arel_visitor
13
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
14
+ end
15
+
16
+ def find(object, parent = @base)
17
+ if JoinPart === parent
18
+ case object
19
+ when String, Symbol, Nodes::Stub
20
+ assoc_name = object.to_s
21
+ @object.join_associations.detect { |j|
22
+ j.reflection.name.to_s == assoc_name && j.parent == parent
23
+ }
24
+ when Nodes::Join
25
+ @object.join_associations.detect { |j|
26
+ j.reflection.name == object.name && j.parent == parent &&
27
+ (object.polymorphic? ? j.reflection.klass == object._klass : true)
28
+ }
29
+ else
30
+ @object.join_associations.detect { |j|
31
+ j.reflection == object && j.parent == parent
32
+ }
33
+ end
34
+ end
35
+ end
36
+
37
+ def traverse(keypath, parent = @base, include_endpoint = false)
38
+ parent = @base if keypath.absolute?
39
+ keypath.path_without_endpoint.each do |key|
40
+ parent = find(key, parent) || key
41
+ end
42
+ parent = find(keypath.endpoint, parent) if include_endpoint
43
+
44
+ parent
45
+ end
46
+
47
+ private
48
+
49
+ def get_table(object)
50
+ if [Symbol, String, Nodes::Stub].include?(object.class)
51
+ Arel::Table.new(object.to_s, :engine => @engine)
52
+ elsif Nodes::Join === object
53
+ object._klass ? object._klass.arel_table : Arel::Table.new(object._name, :engine => @engine)
54
+ elsif object.respond_to?(:aliased_table_name)
55
+ Arel::Table.new(object.table_name, :as => object.aliased_table_name, :engine => @engine)
56
+ else
57
+ raise ArgumentError, "Unable to get table for #{object}"
58
+ end
59
+ end
60
+
61
+ def classify(object)
62
+ if Class === object
63
+ object
64
+ elsif object.respond_to? :active_record
65
+ object.active_record
66
+ else
67
+ raise ArgumentError, "#{object} can't be converted to a class"
68
+ end
69
+ end
70
+
71
+ def get_arel_visitor
72
+ @engine.connection.visitor
73
+ end
74
+
75
+ end
76
+
77
+ if defined?(::ActiveRecord::Associations::JoinDependency::JoinPart)
78
+ JoinPart = ::ActiveRecord::Associations::JoinDependency::JoinPart
79
+ elsif defined?(::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase)
80
+ JoinPart = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase
81
+ end
82
+ end
83
+ end
84
+ end
@@ -31,6 +31,17 @@ module Squeel
31
31
  end
32
32
 
33
33
  end
34
+
35
+ if defined?(::ActiveRecord::Associations::JoinDependency)
36
+ JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation
37
+ JoinDependency = ::ActiveRecord::Associations::JoinDependency
38
+ elsif defined?(::ActiveRecord::Associations::ClassMethods::JoinDependency)
39
+ JoinAssociation = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
40
+ JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
41
+ end
42
+
43
+ JoinDependency.send :include, Adapters::ActiveRecord::JoinDependencyExtensions
44
+
34
45
  end
35
46
  end
36
47
  end
@@ -1 +1,23 @@
1
- require 'squeel/adapters/active_record/3.1/preloader_extensions'
1
+ module Squeel
2
+ module Adapters
3
+ module ActiveRecord
4
+ module PreloaderExtensions
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ alias_method_chain :run, :squeel
9
+ end
10
+ end
11
+
12
+ def run_with_squeel
13
+ unless records.empty?
14
+ Visitors::PreloadVisitor.new.accept(associations).each { |association| preload(association) }
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ ActiveRecord::Associations::Preloader.send :include, Squeel::Adapters::ActiveRecord::PreloaderExtensions
@@ -1,35 +1,394 @@
1
- require 'squeel/adapters/active_record/3.1/relation_extensions'
1
+ require 'active_record'
2
2
 
3
3
  module Squeel
4
4
  module Adapters
5
5
  module ActiveRecord
6
6
  module RelationExtensions
7
7
 
8
- def build_arel
9
- arel = table.from table
8
+ attr_writer :join_dependency
9
+ private :join_dependency=
10
10
 
11
- build_join_dependency(arel, @joins_values) unless @joins_values.empty?
11
+ # Returns a JoinDependency for the current relation.
12
+ #
13
+ # We don't need to clear out @join_dependency by overriding #reset,
14
+ # because the default #reset already does this, despite never setting
15
+ # it anywhere that I can find. Serendipity, I say!
16
+ def join_dependency
17
+ @join_dependency ||= (build_join_dependency(table.from(table), @joins_values) && @join_dependency)
18
+ end
19
+
20
+ %w(where having group order select from).each do |visitor|
21
+ define_method "#{visitor}_visit" do |values|
22
+ Visitors.const_get("#{visitor.capitalize}Visitor").new(
23
+ Context.new(join_dependency)
24
+ ).accept(values)
25
+ end
26
+ end
27
+
28
+ # We need to be able to support merging two relations without having
29
+ # to get our hooks too deeply into ActiveRecord. That proves to be
30
+ # easier said than done. I hate Relation#merge. If Squeel has a
31
+ # nemesis, Relation#merge would be it.
32
+ #
33
+ # Whatever code you see here currently is my current best attempt at
34
+ # coexisting peacefully with said nemesis.
35
+ def merge(r, equalities_resolved = false)
36
+ if ::ActiveRecord::Relation === r && !equalities_resolved
37
+ if self.table_name != r.table_name
38
+ super(r.visited)
39
+ else
40
+ merge_resolving_duplicate_squeel_equalities(r)
41
+ end
42
+ else
43
+ super(r)
44
+ end
45
+ end
46
+
47
+ def visited
48
+ clone.visit!
49
+ end
50
+
51
+ def visit!
52
+ @where_values = where_visit((@where_values - ['']).uniq)
53
+ @having_values = having_visit(@having_values.uniq.reject{|h| h.blank?})
54
+ # FIXME: AR barfs on ARel attributes in group_values. Workaround?
55
+ # @group_values = group_visit(@group_values.uniq.reject{|g| g.blank?})
56
+ @order_values = order_visit(@order_values.uniq.reject{|o| o.blank?})
57
+ @select_values = select_visit(@select_values.uniq)
58
+
59
+ self
60
+ end
61
+
62
+ # reverse_sql_order will reverse the order of strings or Orderings,
63
+ # but not attributes
64
+ def attrs_to_orderings(order)
65
+ order.map do |o|
66
+ Arel::Attribute === o ? o.asc : o
67
+ end
68
+ end
69
+
70
+ # So, building a select for a count query in ActiveRecord is
71
+ # pretty heavily dependent on select_values containing strings.
72
+ # I'd initially expected that I could just hack together a fix
73
+ # to select_for_count and everything would fall in line, but
74
+ # unfortunately, pretty much everything from that point on
75
+ # in ActiveRecord::Calculations#perform_calculation expects
76
+ # the column to be a string, or at worst, a symbol.
77
+ #
78
+ # In the long term, I would like to refactor the code in
79
+ # Rails core, but for now, I'm going to settle for this hack
80
+ # that tries really hard to coerce things to a string.
81
+ def select_for_count
82
+ visited_values = select_visit(select_values.uniq)
83
+ if visited_values.size == 1
84
+ select = visited_values.first
85
+
86
+ str_select = case select
87
+ when String
88
+ select
89
+ when Symbol
90
+ select.to_s
91
+ else
92
+ select.to_sql if select.respond_to?(:to_sql)
93
+ end
94
+
95
+ str_select if str_select && str_select !~ /[,*]/
96
+ end
97
+ end
98
+
99
+ def build_join_dependency(manager, joins)
100
+ buckets = joins.group_by do |join|
101
+ case join
102
+ when String
103
+ 'string_join'
104
+ when Hash, Symbol, Array, Nodes::Stub, Nodes::Join, Nodes::KeyPath
105
+ 'association_join'
106
+ when JoinAssociation
107
+ 'stashed_join'
108
+ when Arel::Nodes::Join
109
+ 'join_node'
110
+ else
111
+ raise 'unknown class: %s' % join.class.name
112
+ end
113
+ end
114
+
115
+ association_joins = buckets['association_join'] || []
116
+ stashed_association_joins = buckets['stashed_join'] || []
117
+ join_nodes = (buckets['join_node'] || []).uniq
118
+ string_joins = (buckets['string_join'] || []).map { |x|
119
+ x.strip
120
+ }.uniq
121
+
122
+ join_list = join_nodes + custom_join_ast(manager, string_joins)
123
+
124
+ # All of that duplication just to do this...
125
+ self.join_dependency = JoinDependency.new(
126
+ @klass,
127
+ association_joins,
128
+ join_list
129
+ )
130
+
131
+ join_dependency.graft(*stashed_association_joins)
132
+
133
+ @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
134
+
135
+ join_dependency.join_associations.each do |association|
136
+ association.join_to(manager)
137
+ end
138
+
139
+ manager.join_sources.concat join_list
140
+
141
+ manager
142
+ end
143
+
144
+ def includes(*args)
145
+ if block_given? && args.empty?
146
+ super(DSL.eval &Proc.new)
147
+ else
148
+ super
149
+ end
150
+ end
151
+
152
+ def preload(*args)
153
+ if block_given? && args.empty?
154
+ super(DSL.eval &Proc.new)
155
+ else
156
+ super
157
+ end
158
+ end
159
+
160
+ def eager_load(*args)
161
+ if block_given? && args.empty?
162
+ super(DSL.eval &Proc.new)
163
+ else
164
+ super
165
+ end
166
+ end
167
+
168
+ def select(value = Proc.new)
169
+ if block_given? && Proc === value
170
+ if value.arity > 0
171
+ to_a.select {|*block_args| value.call(*block_args)}
172
+ else
173
+ relation = clone
174
+ relation.select_values += Array.wrap(DSL.eval &value)
175
+ relation
176
+ end
177
+ else
178
+ super
179
+ end
180
+ end
181
+
182
+ def group(*args)
183
+ if block_given? && args.empty?
184
+ super(DSL.eval &Proc.new)
185
+ else
186
+ super
187
+ end
188
+ end
189
+
190
+ def order(*args)
191
+ if block_given? && args.empty?
192
+ super(DSL.eval &Proc.new)
193
+ else
194
+ super
195
+ end
196
+ end
197
+
198
+ def reorder(*args)
199
+ if block_given? && args.empty?
200
+ super(DSL.eval &Proc.new)
201
+ else
202
+ super
203
+ end
204
+ end
205
+
206
+ def joins(*args)
207
+ if block_given? && args.empty?
208
+ super(DSL.eval &Proc.new)
209
+ else
210
+ super
211
+ end
212
+ end
213
+
214
+ def where(opts = Proc.new, *rest)
215
+ if block_given? && Proc === opts
216
+ super(DSL.eval &opts)
217
+ else
218
+ super
219
+ end
220
+ end
12
221
 
13
- collapse_wheres(arel, where_visit((@where_values - ['']).uniq))
222
+ def having(*args)
223
+ if block_given? && args.empty?
224
+ super(DSL.eval &Proc.new)
225
+ else
226
+ super
227
+ end
228
+ end
14
229
 
15
- arel.having(*having_visit(@having_values.uniq.reject{|h| h.blank?})) unless @having_values.empty?
230
+ def from(*args)
231
+ if block_given? && args.empty?
232
+ super(DSL.eval &Proc.new)
233
+ else
234
+ super
235
+ end
236
+ end
16
237
 
17
- arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
18
- arel.skip(@offset_value) if @offset_value
238
+ def build_where(opts, other = [])
239
+ case opts
240
+ when String, Array
241
+ super
242
+ else # Let's prevent PredicateBuilder from doing its thing
243
+ [opts, *other].map do |arg|
244
+ case arg
245
+ when Array # Just in case there's an array in there somewhere
246
+ @klass.send(:sanitize_sql, arg)
247
+ when Hash
248
+ @klass.send(:expand_hash_conditions_for_aggregates, arg)
249
+ else
250
+ arg
251
+ end
252
+ end
253
+ end
254
+ end
19
255
 
20
- arel.group(*group_visit(@group_values.uniq.reject{|g| g.blank?})) unless @group_values.empty?
256
+ def collapse_wheres(arel, wheres)
257
+ wheres = Array(wheres)
258
+ binaries = wheres.grep(Arel::Nodes::Binary)
21
259
 
22
- order = order_visit(@order_values)
23
- order = reverse_sql_order(attrs_to_orderings(order)) if @reverse_order_value
24
- arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
260
+ groups = binaries.group_by {|b| [b.class, b.left]}
25
261
 
26
- build_select(arel, select_visit(@select_values.uniq))
262
+ groups.each do |_, bins|
263
+ arel.where(Arel::Nodes::And.new(bins))
264
+ end
265
+
266
+ (wheres - binaries).each do |where|
267
+ where = Arel.sql(where) if String === where
268
+ arel.where(Arel::Nodes::Grouping.new(where))
269
+ end
270
+ end
271
+
272
+ def find_equality_predicates(nodes)
273
+ nodes.map { |node|
274
+ case node
275
+ when Arel::Nodes::Equality
276
+ if node.left.respond_to?(:relation) &&
277
+ node.left.relation.name == table_name
278
+ node
279
+ end
280
+ when Arel::Nodes::Grouping
281
+ find_equality_predicates([node.expr])
282
+ when Arel::Nodes::And
283
+ find_equality_predicates(node.children)
284
+ else
285
+ nil
286
+ end
287
+ }.compact.flatten
288
+ end
289
+
290
+ def flatten_nodes(nodes)
291
+ nodes.map { |node|
292
+ case node
293
+ when Array
294
+ flatten_nodes(node)
295
+ when Nodes::And
296
+ flatten_nodes(node.children)
297
+ when Nodes::Grouping
298
+ flatten_nodes(node.expr)
299
+ else
300
+ node
301
+ end
302
+ }.flatten
303
+ end
304
+
305
+ def merge_resolving_duplicate_squeel_equalities(r)
306
+ left = clone
307
+ right = r.clone
308
+ left.where_values = flatten_nodes(left.where_values)
309
+ right.where_values = flatten_nodes(right.where_values)
310
+ right_equalities = right.where_values.select do |obj|
311
+ Nodes::Predicate === obj && obj.method_name == :eq
312
+ end
313
+ right.where_values -= right_equalities
314
+ left.where_values = resolve_duplicate_squeel_equalities(
315
+ left.where_values + right_equalities
316
+ )
317
+ left.merge(right, true)
318
+ end
319
+
320
+ def resolve_duplicate_squeel_equalities(wheres)
321
+ seen = {}
322
+ wheres.reverse.reject { |n|
323
+ nuke = false
324
+ if Nodes::Predicate === n && n.method_name == :eq
325
+ nuke = seen[n.expr]
326
+ seen[n.expr] = true
327
+ end
328
+ nuke
329
+ }.reverse
330
+ end
331
+
332
+ # Simulate the logic that occurs in #to_a
333
+ #
334
+ # This will let us get a dump of the SQL that will be run against the
335
+ # DB for debug purposes without actually running the query.
336
+ def debug_sql
337
+ if eager_loading?
338
+ including = (@eager_load_values + @includes_values).uniq
339
+ join_dependency = JoinDependency.new(@klass, including, [])
340
+ construct_relation_for_association_find(join_dependency).to_sql
341
+ else
342
+ arel.to_sql
343
+ end
344
+ end
345
+
346
+ ### ZOMG ALIAS_METHOD_CHAIN IS BELOW. HIDE YOUR EYES!
347
+ # ...
348
+ # ...
349
+ # ...
350
+ # Since you're still looking, let me explain this horrible
351
+ # transgression you see before you.
352
+ #
353
+ # You see, Relation#where_values_hash is defined on the
354
+ # ActiveRecord::Relation class, itself.
355
+ #
356
+ # Since it's defined there, but I would very much like to modify its
357
+ # behavior, I have three choices:
358
+ #
359
+ # 1. Inherit from ActiveRecord::Relation in a Squeel::Relation
360
+ # class, and make an attempt to usurp all of the various calls
361
+ # to methods on ActiveRecord::Relation by doing some really
362
+ # evil stuff with constant reassignment, all for the sake of
363
+ # being able to use super().
364
+ #
365
+ # 2. Submit a patch to Rails core, breaking this method off into
366
+ # another module, all for my own selfish desire to use super()
367
+ # while mucking about in Rails internals.
368
+ #
369
+ # 3. Use alias_method_chain, and say 10 hail Hanssons as penance.
370
+ #
371
+ # I opted to go with #3. Except for the hail Hansson thing.
372
+ # Unless you're DHH, in which case, I totally said them.
373
+ #
374
+ # If you'd like to read more about alias_method_chain, see
375
+ # http://erniemiller.org/2011/02/03/when-to-use-alias_method_chain/
376
+
377
+ def self.included(base)
378
+ base.class_eval do
379
+ alias_method_chain :where_values_hash, :squeel
380
+ end
381
+ end
27
382
 
28
- arel.distinct(@uniq_value)
29
- arel.from(from_visit(@from_value)) if @from_value
30
- arel.lock(@lock_value) if @lock_value
383
+ # where_values_hash is used in scope_for_create. It's what allows
384
+ # new records to be created with any equality values that exist in
385
+ # your model's default scope. We hijack it in order to dig down into
386
+ # And and Grouping nodes, which are equivalent to seeing top-level
387
+ # Equality nodes in stock AR terms.
388
+ def where_values_hash_with_squeel
389
+ equalities = find_equality_predicates(where_visit(with_default_scope.where_values))
31
390
 
32
- arel
391
+ Hash[equalities.map { |where| [where.left.name, where.right] }]
33
392
  end
34
393
 
35
394
  end