squeel 1.0.9 → 1.0.11

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.
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