squeel 1.0.9 → 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/CHANGELOG.md +10 -0
  2. data/lib/squeel.rb +5 -0
  3. data/lib/squeel/adapters/active_record/3.0/association_preload_extensions.rb +2 -2
  4. data/lib/squeel/adapters/active_record/3.0/relation_extensions.rb +98 -46
  5. data/lib/squeel/adapters/active_record/3.1/preloader_extensions.rb +2 -2
  6. data/lib/squeel/adapters/active_record/3.1/relation_extensions.rb +71 -85
  7. data/lib/squeel/adapters/active_record/base_extensions.rb +0 -19
  8. data/lib/squeel/adapters/active_record/relation_extensions.rb +6 -9
  9. data/lib/squeel/nodes/binary.rb +4 -0
  10. data/lib/squeel/nodes/function.rb +10 -0
  11. data/lib/squeel/nodes/nary.rb +1 -1
  12. data/lib/squeel/nodes/order.rb +11 -1
  13. data/lib/squeel/nodes/predicate.rb +2 -2
  14. data/lib/squeel/nodes/stub.rb +2 -0
  15. data/lib/squeel/nodes/unary.rb +4 -0
  16. data/lib/squeel/version.rb +1 -1
  17. data/lib/squeel/visitors.rb +10 -3
  18. data/lib/squeel/visitors/from_visitor.rb +6 -0
  19. data/lib/squeel/visitors/group_visitor.rb +6 -0
  20. data/lib/squeel/visitors/having_visitor.rb +9 -0
  21. data/lib/squeel/visitors/order_visitor.rb +20 -0
  22. data/lib/squeel/visitors/predicate_visitation.rb +126 -0
  23. data/lib/squeel/visitors/predicate_visitor.rb +3 -326
  24. data/lib/squeel/visitors/{symbol_visitor.rb → preload_visitor.rb} +4 -4
  25. data/lib/squeel/visitors/select_visitor.rb +7 -0
  26. data/lib/squeel/visitors/visitor.rb +244 -12
  27. data/lib/squeel/visitors/where_visitor.rb +8 -0
  28. data/spec/helpers/squeel_helper.rb +14 -1
  29. data/spec/squeel/adapters/active_record/relation_extensions_spec.rb +65 -49
  30. data/spec/squeel/nodes/as_spec.rb +20 -0
  31. data/spec/squeel/nodes/function_spec.rb +6 -0
  32. data/spec/squeel/nodes/grouping_spec.rb +6 -0
  33. data/spec/squeel/nodes/key_path_spec.rb +3 -3
  34. data/spec/squeel/nodes/operation_spec.rb +6 -0
  35. data/spec/squeel/nodes/order_spec.rb +6 -1
  36. data/spec/squeel/nodes/predicate_operators_spec.rb +1 -1
  37. data/spec/squeel/nodes/predicate_spec.rb +14 -1
  38. data/spec/squeel/nodes/sifter_spec.rb +2 -1
  39. data/spec/squeel/nodes/stub_spec.rb +4 -0
  40. data/spec/squeel/visitors/order_visitor_spec.rb +36 -0
  41. data/spec/squeel/visitors/{symbol_visitor_spec.rb → preload_visitor_spec.rb} +4 -3
  42. data/spec/squeel/visitors/select_visitor_spec.rb +26 -0
  43. data/spec/squeel/visitors/{attribute_visitor_spec.rb → visitor_spec.rb} +23 -37
  44. data/spec/support/models.rb +4 -0
  45. metadata +22 -10
  46. data/lib/squeel/visitors/attribute_visitor.rb +0 -214
@@ -1,10 +1,10 @@
1
- require 'squeel/visitors/visitor'
2
-
3
1
  module Squeel
4
2
  module Visitors
5
- class SymbolVisitor < Visitor
3
+ class PreloadVisitor < Visitor
6
4
 
7
- def initialize
5
+ def initialize(_ = nil)
6
+ # Unused. Just here to provide consistency in method signature
7
+ # among subclasses of Visitor
8
8
  end
9
9
 
10
10
  def accept(object, parent = nil)
@@ -0,0 +1,7 @@
1
+ module Squeel
2
+ module Visitors
3
+ class SelectVisitor < Visitor
4
+ include PredicateVisitation
5
+ end
6
+ end
7
+ end
@@ -61,14 +61,53 @@ module Squeel
61
61
  @hash_context_depth > 0
62
62
  end
63
63
 
64
+ # @return [Boolean] Whether the given value implies a context change
65
+ # @param v The value to consider
66
+ def implies_hash_context_shift?(v)
67
+ can_visit?(v)
68
+ end
69
+
70
+ # Change context (by setting the new parent to the result of a #find or
71
+ # #traverse on the key), then accept the given value.
72
+ #
73
+ # @param k The hash key
74
+ # @param v The hash value
75
+ # @param parent The current parent object in the context
76
+ # @return The visited value
77
+ def visit_with_hash_context_shift(k, v, parent)
78
+ @hash_context_depth += 1
79
+
80
+ parent = case k
81
+ when Nodes::KeyPath
82
+ traverse(k, parent, true)
83
+ else
84
+ find(k, parent)
85
+ end
86
+
87
+ can_visit?(v) ? visit(v, parent || k) : v
88
+ ensure
89
+ @hash_context_depth -= 1
90
+ end
91
+
92
+ # If there is no context change, the default behavior is to return the
93
+ # value unchanged. Subclasses will alter this behavior as needed.
94
+ #
95
+ # @param k The hash key
96
+ # @param v The hash value
97
+ # @param parent The current parent object in the context
98
+ # @return The same value we just received.
99
+ def visit_without_hash_context_shift(k, v, parent)
100
+ v
101
+ end
102
+
64
103
  # Important to avoid accidentally allowing the default ARel visitor's
65
104
  # last_column quoting behavior (where a value is quoted as though it
66
105
  # is of the type of the last visited column). This can wreak havoc with
67
106
  # Functions and Operations.
68
107
  #
69
108
  # @param object The object to check
70
- # @return [Boolean] Whether or not the ARel visitor will try to quote the object if
71
- # not passed as an SqlLiteral.
109
+ # @return [Boolean] Whether or not the ARel visitor will try to quote the
110
+ # object if not passed as an SqlLiteral.
72
111
  def quoted?(object)
73
112
  case object
74
113
  when Arel::Nodes::SqlLiteral, Bignum, Fixnum, Arel::SelectManager
@@ -117,16 +156,6 @@ module Squeel
117
156
  retry
118
157
  end
119
158
 
120
- # Visit an array, which involves accepting any values we know how to
121
- # accept, and skipping the rest.
122
- #
123
- # @param [Array] o The Array to visit
124
- # @param parent The current parent object in the context
125
- # @return [Array] The visited array
126
- def visit_Array(o, parent)
127
- o.map { |v| can_visit?(v) ? visit(v, parent) : v }.flatten
128
- end
129
-
130
159
  # Pass an object through the visitor unmodified. This is
131
160
  # in order to allow objects that don't require modification
132
161
  # to be handled by ARel directly.
@@ -139,6 +168,209 @@ module Squeel
139
168
  end
140
169
  alias :visit_Fixnum :visit_passthrough
141
170
  alias :visit_Bignum :visit_passthrough
171
+ alias :visit_String :visit_passthrough
172
+
173
+ # Visit an array, which involves accepting any values we know how to
174
+ # accept, and skipping the rest.
175
+ #
176
+ # @param [Array] o The Array to visit
177
+ # @param parent The current parent object in the context
178
+ # @return [Array] The visited array
179
+ def visit_Array(o, parent)
180
+ o.map { |v| can_visit?(v) ? visit(v, parent) : v }.flatten
181
+ end
182
+
183
+ # Visit a Hash. This entails iterating through each key and value and
184
+ # visiting each value in turn.
185
+ #
186
+ # @param [Hash] o The Hash to visit
187
+ # @param parent The current parent object in the context
188
+ # @return [Array] An array of values for use in an ordering, grouping, etc.
189
+ def visit_Hash(o, parent)
190
+ o.map do |k, v|
191
+ if implies_hash_context_shift?(v)
192
+ visit_with_hash_context_shift(k, v, parent)
193
+ else
194
+ visit_without_hash_context_shift(k, v, parent)
195
+ end
196
+ end.flatten
197
+ end
198
+
199
+ # Visit a symbol. This will return an attribute named after the symbol
200
+ # against the current parent's contextualized table.
201
+ #
202
+ # @param [Symbol] o The symbol to visit
203
+ # @param parent The symbol's parent within the context
204
+ # @return [Arel::Attribute] An attribute on the contextualized parent
205
+ # table
206
+ def visit_Symbol(o, parent)
207
+ contextualize(parent)[o]
208
+ end
209
+
210
+ # Visit a stub. This will return an attribute named after the stub against
211
+ # the current parent's contextualized table.
212
+ #
213
+ # @param [Nodes::Stub] o The stub to visit
214
+ # @param parent The stub's parent within the context
215
+ # @return [Arel::Attribute] An attribute on the contextualized parent
216
+ # table
217
+ def visit_Squeel_Nodes_Stub(o, parent)
218
+ contextualize(parent)[o.to_s]
219
+ end
220
+
221
+ # Visit a keypath. This will traverse the keypath's "path", setting a new
222
+ # parent as though the keypath's endpoint was in a deeply-nested hash,
223
+ # then visit the endpoint with the new parent.
224
+ #
225
+ # @param [Nodes::KeyPath] o The keypath to visit
226
+ # @param parent The keypath's parent within the context
227
+ # @return The visited endpoint, with the parent from the KeyPath's path.
228
+ def visit_Squeel_Nodes_KeyPath(o, parent)
229
+ parent = traverse(o, parent)
230
+
231
+ visit(o.endpoint, parent)
232
+ end
233
+
234
+ # Visit a Literal by converting it to an ARel SqlLiteral
235
+ #
236
+ # @param [Nodes::Literal] o The Literal to visit
237
+ # @param parent The parent object in the context (unused)
238
+ # @return [Arel::Nodes::SqlLiteral] An SqlLiteral
239
+ def visit_Squeel_Nodes_Literal(o, parent)
240
+ Arel.sql(o.expr)
241
+ end
242
+
243
+ # Visit a Squeel As node, resulting in am ARel As node.
244
+ #
245
+ # @param [Nodes::As] The As node to visit
246
+ # @param parent The parent object in the context
247
+ # @return [Arel::Nodes::As] The resulting as node.
248
+ def visit_Squeel_Nodes_As(o, parent)
249
+ left = visit(o.left, parent)
250
+ # Some nodes, like Arel::SelectManager, have their own #as methods,
251
+ # with behavior that we don't want to clobber.
252
+ if left.respond_to?(:as)
253
+ left.as(o.right)
254
+ else
255
+ Arel::Nodes::As.new(left, o.right)
256
+ end
257
+ end
258
+
259
+ # Visit a Squeel And node, returning an ARel Grouping containing an
260
+ # ARel And node.
261
+ #
262
+ # @param [Nodes::And] o The And node to visit
263
+ # @param parent The parent object in the context
264
+ # @return [Arel::Nodes::Grouping] A grouping node, containnig an ARel
265
+ # And node as its expression. All children will be visited before
266
+ # being passed to the And.
267
+ def visit_Squeel_Nodes_And(o, parent)
268
+ Arel::Nodes::Grouping.new(Arel::Nodes::And.new(visit(o.children, parent)))
269
+ end
270
+
271
+ # Visit a Squeel Or node, returning an ARel Or node.
272
+ #
273
+ # @param [Nodes::Or] o The Or node to visit
274
+ # @param parent The parent object in the context
275
+ # @return [Arel::Nodes::Or] An ARel Or node, with left and right sides visited
276
+ def visit_Squeel_Nodes_Or(o, parent)
277
+ Arel::Nodes::Grouping.new(Arel::Nodes::Or.new(visit(o.left, parent), (visit(o.right, parent))))
278
+ end
279
+
280
+ # Visit a Squeel Not node, returning an ARel Not node.
281
+ #
282
+ # @param [Nodes::Not] o The Not node to visit
283
+ # @param parent The parent object in the context
284
+ # @return [Arel::Nodes::Not] An ARel Not node, with expression visited
285
+ def visit_Squeel_Nodes_Not(o, parent)
286
+ Arel::Nodes::Not.new(visit(o.expr, parent))
287
+ end
288
+
289
+ # Visit a Squeel Grouping node, returning an ARel Grouping node.
290
+ #
291
+ # @param [Nodes::Grouping] o The Grouping node to visit
292
+ # @param parent The parent object in the context
293
+ # @return [Arel::Nodes::Grouping] An ARel Grouping node, with expression visited
294
+ def visit_Squeel_Nodes_Grouping(o, parent)
295
+ Arel::Nodes::Grouping.new(visit(o.expr, parent))
296
+ end
297
+ #
298
+ # Visit a Squeel function, returning an ARel NamedFunction node.
299
+ #
300
+ # @param [Nodes::Function] o The function node to visit
301
+ # @param parent The parent object in the context
302
+ # @return [Arel::Nodes::NamedFunction] A named function node. Function
303
+ # arguments are visited, if necessary, before being passed to the NamedFunction.
304
+ def visit_Squeel_Nodes_Function(o, parent)
305
+ args = o.args.map do |arg|
306
+ case arg
307
+ when Nodes::Function, Nodes::As, Nodes::Literal, Nodes::Grouping, Nodes::KeyPath, Nodes::KeyPath
308
+ visit(arg, parent)
309
+ when ActiveRecord::Relation
310
+ arg.arel.ast
311
+ when Symbol, Nodes::Stub
312
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_s])
313
+ else
314
+ quote arg
315
+ end
316
+ end
317
+
318
+ Arel::Nodes::NamedFunction.new(o.name, args)
319
+ end
320
+
321
+ # Visit a Squeel operation node, convering it to an ARel InfixOperation
322
+ # (or subclass, as appropriate)
323
+ #
324
+ # @param [Nodes::Operation] o The Operation node to visit
325
+ # @param parent The parent object in the context
326
+ # @return [Arel::Nodes::InfixOperation] The InfixOperation (or Addition,
327
+ # Multiplication, etc) node, with both operands visited, if needed.
328
+ def visit_Squeel_Nodes_Operation(o, parent)
329
+ args = o.args.map do |arg|
330
+ case arg
331
+ when Nodes::Function, Nodes::As, Nodes::Literal, Nodes::Grouping, Nodes::KeyPath
332
+ visit(arg, parent)
333
+ when Symbol, Nodes::Stub
334
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_s])
335
+ else
336
+ quote arg
337
+ end
338
+ end
339
+
340
+ op = case o.operator
341
+ when :+
342
+ Arel::Nodes::Addition.new(args[0], args[1])
343
+ when :-
344
+ Arel::Nodes::Subtraction.new(args[0], args[1])
345
+ when :*
346
+ Arel::Nodes::Multiplication.new(args[0], args[1])
347
+ when :/
348
+ Arel::Nodes::Division.new(args[0], args[1])
349
+ else
350
+ Arel::Nodes::InfixOperation.new(o.operator, args[0], args[1])
351
+ end
352
+
353
+ op
354
+ end
355
+
356
+ # Visit an ActiveRecord Relation, returning an Arel::SelectManager
357
+ # @param [ActiveRecord::Relation] o The Relation to visit
358
+ # @param parent The parent object in the context
359
+ # @return [Arel::SelectManager] The ARel select manager that represents
360
+ # the relation's query
361
+ def visit_ActiveRecord_Relation(o, parent)
362
+ o.arel
363
+ end
364
+
365
+ # Visit ActiveRecord::Base objects. These should be converted to their
366
+ # id before being used in a comparison.
367
+ #
368
+ # @param [ActiveRecord::Base] o The AR::Base object to visit
369
+ # @param parent The current parent object in the context
370
+ # @return [Fixnum] The id of the object
371
+ def visit_ActiveRecord_Base(o, parent)
372
+ o.id
373
+ end
142
374
 
143
375
  end
144
376
  end
@@ -0,0 +1,8 @@
1
+ require 'squeel/visitors/predicate_visitor'
2
+
3
+ module Squeel
4
+ module Visitors
5
+ class WhereVisitor < PredicateVisitor
6
+ end
7
+ end
8
+ end
@@ -22,4 +22,17 @@ module SqueelHelper
22
22
  ActiveRecord::Associations::ClassMethods::JoinDependency.new(*args)
23
23
  end
24
24
  end
25
- end
25
+
26
+ def activerecord_version_at_least(version_string)
27
+ required_version_parts = version_string.split('.', 3).map(&:to_i)
28
+ (0..2).each do |index|
29
+ required_version_parts[index] ||= 0
30
+ end
31
+ actual_version_parts = [
32
+ ActiveRecord::VERSION::MAJOR,
33
+ ActiveRecord::VERSION::MINOR,
34
+ ActiveRecord::VERSION::TINY
35
+ ]
36
+ (actual_version_parts <=> required_version_parts) >= 0
37
+ end
38
+ end
@@ -5,46 +5,6 @@ module Squeel
5
5
  module ActiveRecord
6
6
  describe RelationExtensions do
7
7
 
8
- describe '#predicate_visitor' do
9
-
10
- it 'creates a predicate visitor with a Context for the relation' do
11
- relation = Person.joins({
12
- :children => {
13
- :children => {
14
- :parent => :parent
15
- }
16
- }
17
- })
18
-
19
- visitor = relation.predicate_visitor
20
-
21
- visitor.should be_a Visitors::PredicateVisitor
22
- table = visitor.contextualize(relation.join_dependency._join_parts.last)
23
- table.table_alias.should eq 'parents_people_2'
24
- end
25
-
26
- end
27
-
28
- describe '#attribute_visitor' do
29
-
30
- it 'creates an attribute visitor with a Context for the relation' do
31
- relation = Person.joins({
32
- :children => {
33
- :children => {
34
- :parent => :parent
35
- }
36
- }
37
- })
38
-
39
- visitor = relation.attribute_visitor
40
-
41
- visitor.should be_a Visitors::AttributeVisitor
42
- table = visitor.contextualize(relation.join_dependency._join_parts.last)
43
- table.table_alias.should eq 'parents_people_2'
44
- end
45
-
46
- end
47
-
48
8
  describe 'finding by attribute' do
49
9
 
50
10
  it 'returns nil when passed an empty string' do
@@ -452,11 +412,13 @@ module Squeel
452
412
  end
453
413
 
454
414
  it 'works with non-strings in group' do
455
- pending "Requires some big hacks to execute_grouped_calculation"
456
- counts = Person.group{name.op('||', '-diddly')}.count
457
- counts.should eq Person.group{name.op('||', '-diddly')}.count
415
+ if activerecord_version_at_least '3.2.7'
416
+ counts = Person.group{name.op('||', '-diddly')}.count
417
+ counts.should eq Person.group("name || '-diddly'").count
418
+ else
419
+ pending 'Unsupported in ActiveRecord < 3.2.7'
420
+ end
458
421
  end
459
-
460
422
  end
461
423
 
462
424
  describe '#group' do
@@ -628,6 +590,16 @@ module Squeel
628
590
 
629
591
  end
630
592
 
593
+ describe '#from' do
594
+ it 'creates froms with a block' do
595
+ expected = /SELECT "sub"."name" AS aliased_name FROM \(SELECT "people"."name" FROM "people"\s*\) sub/
596
+ block = Person.from{Person.select{name}.as('sub')}.
597
+ select{sub.name.as('aliased_name')}
598
+ sql = block.to_sql
599
+ sql.should match expected
600
+ end
601
+ end
602
+
631
603
  describe '#build_where' do
632
604
 
633
605
  it 'sanitizes SQL as usual with strings' do
@@ -672,6 +644,24 @@ module Squeel
672
644
  @person.name.should eq 'bob'
673
645
  end
674
646
 
647
+ it 'creates new records with equality predicates from has_many associations' do
648
+ if activerecord_version_at_least '3.1.0'
649
+ person = Person.first
650
+ article = person.articles_with_condition.new
651
+ article.person.should eq person
652
+ article.title.should eq 'Condition'
653
+ else
654
+ pending 'Unsupported on ActiveRecord < 3.1'
655
+ end
656
+ end
657
+
658
+ it 'creates new records with equality predicates from has_many :through associations' do
659
+ pending "When ActiveRecord supports this, we'll want to, too"
660
+ person = Person.first
661
+ comment = person.article_comments_with_first_post.new
662
+ comment.body.should eq 'first post'
663
+ end
664
+
675
665
  it "maintains activerecord default scope functionality" do
676
666
  PersonNamedBill.new.name.should eq 'Bill'
677
667
  end
@@ -740,12 +730,24 @@ module Squeel
740
730
  sql.should match /Bert/
741
731
  end
742
732
 
743
- it 'uses the given equality condition in the case of a conflicting where from a default scope in AR >= 3.1' do
744
- relation = PersonNamedBill.where{name == 'Ernie'}
733
+ it 'uses the given equality condition in the case of a conflicting where from a default scope' do
734
+ if activerecord_version_at_least '3.1'
735
+ relation = PersonNamedBill.where{name == 'Ernie'}
736
+ sql = relation.to_sql
737
+ sql.should_not match /Bill/
738
+ sql.should match /Ernie/
739
+ else
740
+ pending 'Unsupported in ActiveRecord < 3.1'
741
+ end
742
+ end
743
+
744
+ it 'allows scopes to join/query a table through two different associations and uses the correct alias' do
745
+ relation = Person.with_article_title('hi').
746
+ with_article_condition_title('yo')
745
747
  sql = relation.to_sql
746
- sql.should_not match /Bill/
747
- sql.should match /Ernie/
748
- end unless ::ActiveRecord::VERSION::MINOR == 0
748
+ sql.should match /"articles"."title" = 'hi'/
749
+ sql.should match /"articles_with_conditions_people"."title" = 'yo'/
750
+ end
749
751
 
750
752
  it "doesn't ruin everything when a scope returns nil" do
751
753
  relation = Person.nil_scope
@@ -770,6 +772,20 @@ module Squeel
770
772
  sql.scan(/"people"."id"/).should have(1).item
771
773
  end
772
774
 
775
+ it 'merges scopes that contain functions' do
776
+ relation = PersonNamedBill.scoped.with_salary_equal_to(100)
777
+ sql = relation.to_sql
778
+ sql.should match /abs\("people"."salary"\) = 100/
779
+ end
780
+
781
+ it 'uses last equality when merging two scopes with identical function equalities' do
782
+ relation = PersonNamedBill.scoped.with_salary_equal_to(100).
783
+ with_salary_equal_to(200)
784
+ sql = relation.to_sql
785
+ sql.should_not match /abs\("people"."salary"\) = 100/
786
+ sql.should match /abs\("people"."salary"\) = 200/
787
+ end
788
+
773
789
  end
774
790
 
775
791
  describe '#to_a' do