squeel 1.0.11 → 1.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -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