squeel 0.5.0 → 0.5.5

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 (41) hide show
  1. data/README.rdoc +115 -39
  2. data/lib/squeel/adapters/active_record.rb +22 -5
  3. data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
  4. data/lib/squeel/adapters/active_record/3.0/compat.rb +143 -0
  5. data/lib/squeel/adapters/active_record/3.0/context.rb +67 -0
  6. data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
  7. data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
  8. data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
  9. data/lib/squeel/adapters/active_record/context.rb +67 -0
  10. data/lib/squeel/adapters/active_record/join_association.rb +10 -56
  11. data/lib/squeel/adapters/active_record/join_dependency.rb +22 -7
  12. data/lib/squeel/adapters/active_record/preloader.rb +21 -0
  13. data/lib/squeel/adapters/active_record/relation.rb +84 -38
  14. data/lib/squeel/context.rb +38 -0
  15. data/lib/squeel/dsl.rb +1 -1
  16. data/lib/squeel/nodes/join.rb +18 -0
  17. data/lib/squeel/nodes/key_path.rb +2 -2
  18. data/lib/squeel/nodes/stub.rb +5 -1
  19. data/lib/squeel/version.rb +1 -1
  20. data/lib/squeel/visitors.rb +2 -2
  21. data/lib/squeel/visitors/{order_visitor.rb → attribute_visitor.rb} +1 -2
  22. data/lib/squeel/visitors/predicate_visitor.rb +13 -11
  23. data/lib/squeel/visitors/symbol_visitor.rb +48 -0
  24. data/spec/helpers/squeel_helper.rb +17 -1
  25. data/spec/spec_helper.rb +31 -0
  26. data/spec/squeel/adapters/active_record/context_spec.rb +50 -0
  27. data/spec/squeel/adapters/active_record/join_association_spec.rb +1 -1
  28. data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +1 -1
  29. data/spec/squeel/adapters/active_record/relation_spec.rb +166 -25
  30. data/spec/squeel/dsl_spec.rb +6 -6
  31. data/spec/squeel/nodes/join_spec.rb +16 -3
  32. data/spec/squeel/nodes/stub_spec.rb +12 -0
  33. data/spec/squeel/visitors/{order_visitor_spec.rb → attribute_visitor_spec.rb} +4 -5
  34. data/spec/squeel/visitors/predicate_visitor_spec.rb +18 -6
  35. data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
  36. data/squeel.gemspec +2 -2
  37. metadata +21 -13
  38. data/lib/squeel/contexts/join_dependency_context.rb +0 -74
  39. data/lib/squeel/visitors/select_visitor.rb +0 -103
  40. data/spec/squeel/contexts/join_dependency_context_spec.rb +0 -43
  41. data/spec/squeel/visitors/select_visitor_spec.rb +0 -115
@@ -33,7 +33,11 @@ module Squeel
33
33
 
34
34
  def method_missing(method_id, *args)
35
35
  super if method_id == :to_ary
36
- KeyPath.new(self.symbol, method_id)
36
+ if (args.size == 1) && (Class === args[0])
37
+ KeyPath.new(self, Join.new(method_id, Arel::InnerJoin, args[0]))
38
+ else
39
+ KeyPath.new(self, method_id)
40
+ end
37
41
  end
38
42
 
39
43
  def ==(value)
@@ -1,3 +1,3 @@
1
1
  module Squeel
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.5"
3
3
  end
@@ -1,3 +1,3 @@
1
1
  require 'squeel/visitors/predicate_visitor'
2
- require 'squeel/visitors/order_visitor'
3
- require 'squeel/visitors/select_visitor'
2
+ require 'squeel/visitors/attribute_visitor'
3
+ require 'squeel/visitors/symbol_visitor'
@@ -1,9 +1,8 @@
1
1
  require 'squeel/visitors/base'
2
- require 'squeel/contexts/join_dependency_context'
3
2
 
4
3
  module Squeel
5
4
  module Visitors
6
- class OrderVisitor < Base
5
+ class AttributeVisitor < Base
7
6
 
8
7
  def visit_Hash(o, parent)
9
8
  o.map do |k, v|
@@ -1,5 +1,4 @@
1
1
  require 'squeel/visitors/base'
2
- require 'squeel/contexts/join_dependency_context'
3
2
 
4
3
  module Squeel
5
4
  module Visitors
@@ -24,11 +23,7 @@ module Squeel
24
23
  end
25
24
 
26
25
  def visit_Array(o, parent)
27
- if o.first.is_a? String
28
- sanitize_sql(o, parent)
29
- else
30
- o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
31
- end
26
+ o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
32
27
  end
33
28
 
34
29
  def visit_Squeel_Nodes_KeyPath(o, parent)
@@ -37,13 +32,16 @@ module Squeel
37
32
  accept(o.endpoint, parent)
38
33
  end
39
34
 
35
+ def visit_Squeel_Nodes_Stub(o, parent)
36
+ contextualize(parent)[o.symbol]
37
+ end
38
+
40
39
  def visit_Squeel_Nodes_Predicate(o, parent)
41
40
  value = o.value
42
- case value
43
- when Nodes::Function
44
- value = accept(value, parent)
45
- when Nodes::KeyPath
41
+ if Nodes::KeyPath === value
46
42
  value = can_accept?(value.endpoint) ? accept(value, parent) : contextualize(traverse(value, parent))[value.endpoint.to_sym]
43
+ else
44
+ value = accept(value, parent) if can_accept?(value)
47
45
  end
48
46
  if Nodes::Function === o.expr
49
47
  accept(o.expr, parent).send(o.method_name, value)
@@ -68,6 +66,10 @@ module Squeel
68
66
  Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
69
67
  end
70
68
 
69
+ def visit_ActiveRecord_Relation(o, parent)
70
+ o.arel
71
+ end
72
+
71
73
  def visit_Squeel_Nodes_Operation(o, parent)
72
74
  args = o.args.map do |arg|
73
75
  case arg
@@ -114,7 +116,7 @@ module Squeel
114
116
  when Hash, Nodes::Predicate, Nodes::Unary, Nodes::Binary, Nodes::Nary
115
117
  true
116
118
  when Nodes::KeyPath
117
- can_accept?(v.endpoint)
119
+ can_accept?(v.endpoint) && !(Nodes::Stub === v.endpoint)
118
120
  when Array
119
121
  (!v.empty? && v.all? {|val| can_accept?(val)})
120
122
  else
@@ -0,0 +1,48 @@
1
+ require 'squeel/visitors/base'
2
+
3
+ module Squeel
4
+ module Visitors
5
+ class SymbolVisitor < Base
6
+
7
+ def initialize
8
+ end
9
+
10
+ def accept(object, parent = nil)
11
+ visit(object, parent)
12
+ end
13
+
14
+ private
15
+
16
+ def visit_Array(o, parent)
17
+ o.map {|e| accept(e, parent)}.flatten
18
+ end
19
+
20
+ def visit_Hash(o, parent)
21
+ {}.tap do |hash|
22
+ o.each do |key, value|
23
+ hash[accept(key, parent)] = accept(value, parent)
24
+ end
25
+ end
26
+ end
27
+
28
+ def visit_Symbol(o, parent)
29
+ o
30
+ end
31
+
32
+ def visit_Squeel_Nodes_Stub(o, parent)
33
+ o.symbol
34
+ end
35
+
36
+ def visit_Squeel_Nodes_KeyPath(o, parent)
37
+ o.path_with_endpoint.reverse.map(&:to_sym).inject do |hash, key|
38
+ {key => hash}
39
+ end
40
+ end
41
+
42
+ def visit_Squeel_Nodes_Join(o, parent)
43
+ o.name
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,21 @@
1
1
  module SqueelHelper
2
2
  def dsl(&block)
3
- Squeel::DSL.evaluate(&block)
3
+ Squeel::DSL.eval(&block)
4
+ end
5
+
6
+ def queries_for
7
+ $queries_executed = []
8
+ yield
9
+ $queries_executed
10
+ ensure
11
+ %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
12
+ end
13
+
14
+ def new_join_dependency(*args)
15
+ if defined?(ActiveRecord::Associations::JoinDependency)
16
+ ActiveRecord::Associations::JoinDependency.new(*args)
17
+ else
18
+ ActiveRecord::Associations::ClassMethods::JoinDependency.new(*args)
19
+ end
4
20
  end
5
21
  end
@@ -2,6 +2,30 @@ require 'machinist/active_record'
2
2
  require 'sham'
3
3
  require 'faker'
4
4
 
5
+ module ActiveRecord
6
+ class SQLCounter
7
+ IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/,
8
+ /SELECT name\s+FROM sqlite_master\s+WHERE type = 'table' AND NOT name = 'sqlite_sequence'/]
9
+
10
+ # FIXME: this needs to be refactored so specific database can add their own
11
+ # ignored SQL. This ignored SQL is for Oracle.
12
+ IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
13
+
14
+ def initialize
15
+ $queries_executed = []
16
+ end
17
+
18
+ def call(name, start, finish, message_id, values)
19
+ sql = values[:sql]
20
+
21
+ unless 'CACHE' == values[:name]
22
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
23
+ end
24
+ end
25
+ end
26
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
27
+ end
28
+
5
29
  Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
6
30
  require f
7
31
  end
@@ -23,6 +47,13 @@ RSpec.configure do |config|
23
47
  config.include SqueelHelper
24
48
  end
25
49
 
50
+ RSpec::Matchers.define :be_like do |expected|
51
+ match do |actual|
52
+ actual.gsub(/^\s+|\s+$/, '').gsub(/\s+/, ' ').strip ==
53
+ expected.gsub(/^\s+|\s+$/, '').gsub(/\s+/, ' ').strip
54
+ end
55
+ end
56
+
26
57
  require 'squeel'
27
58
 
28
59
  Squeel.configure do |config|
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+ describe Context do
7
+ before do
8
+ @jd = new_join_dependency(Person, {
9
+ :children => {
10
+ :children => {
11
+ :parent => :parent
12
+ }
13
+ }
14
+ }, [])
15
+ @c = Context.new(@jd)
16
+ end
17
+
18
+ it 'finds associations' do
19
+ last_association = @jd.join_parts.last
20
+ next_to_last_association = @jd.join_parts[-2]
21
+
22
+ @c.find(:parent, next_to_last_association).should eq last_association
23
+ end
24
+
25
+ it 'contextualizes join parts with the proper alias' do
26
+ table = @c.contextualize @jd.join_parts.last
27
+ table.table_alias.should eq 'parents_people_2'
28
+ end
29
+
30
+ it 'contextualizes symbols as a generic table' do
31
+ table = @c.contextualize :table
32
+ table.name.should eq 'table'
33
+ table.table_alias.should be_nil
34
+ end
35
+
36
+ it 'contextualizes polymorphic Join nodes to the arel_table of their klass' do
37
+ table = @c.contextualize Nodes::Join.new(:notable, Arel::InnerJoin, Article)
38
+ table.name.should eq 'articles'
39
+ table.table_alias.should be_nil
40
+ end
41
+
42
+ it 'contextualizes non-polymorphic Join nodes to the table for their name' do
43
+ table = @c.contextualize Nodes::Join.new(:notes, Arel::InnerJoin)
44
+ table.name.should eq 'notes'
45
+ table.table_alias.should be_nil
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -3,7 +3,7 @@ module Squeel
3
3
  module ActiveRecord
4
4
  describe JoinAssociation do
5
5
  before do
6
- @jd = ::ActiveRecord::Associations::JoinDependency.new(Note, {}, [])
6
+ @jd = new_join_dependency(Note, {}, [])
7
7
  @notable = Note.reflect_on_association(:notable)
8
8
  end
9
9
 
@@ -3,7 +3,7 @@ module Squeel
3
3
  module ActiveRecord
4
4
  describe JoinDependency do
5
5
  before do
6
- @jd = ::ActiveRecord::Associations::JoinDependency.new(Person, {}, [])
6
+ @jd = new_join_dependency(Person, {}, [])
7
7
  end
8
8
 
9
9
  it 'joins with symbols' do
@@ -5,7 +5,7 @@ module Squeel
5
5
 
6
6
  describe '#predicate_visitor' do
7
7
 
8
- it 'creates a predicate visitor with a JoinDependencyContext for the relation' do
8
+ it 'creates a predicate visitor with a Context for the relation' do
9
9
  relation = Person.joins({
10
10
  :children => {
11
11
  :children => {
@@ -23,9 +23,9 @@ module Squeel
23
23
 
24
24
  end
25
25
 
26
- describe '#order_visitor' do
26
+ describe '#attribute_visitor' do
27
27
 
28
- it 'creates an order visitor with a JoinDependencyContext for the relation' do
28
+ it 'creates an attribute visitor with a Context for the relation' do
29
29
  relation = Person.joins({
30
30
  :children => {
31
31
  :children => {
@@ -34,29 +34,9 @@ module Squeel
34
34
  }
35
35
  })
36
36
 
37
- visitor = relation.order_visitor
37
+ visitor = relation.attribute_visitor
38
38
 
39
- visitor.should be_a Visitors::OrderVisitor
40
- table = visitor.contextualize(relation.join_dependency.join_parts.last)
41
- table.table_alias.should eq 'parents_people_2'
42
- end
43
-
44
- end
45
-
46
- describe '#select_visitor' do
47
-
48
- it 'creates a select visitor with a JoinDependencyContext for the relation' do
49
- relation = Person.joins({
50
- :children => {
51
- :children => {
52
- :parent => :parent
53
- }
54
- }
55
- })
56
-
57
- visitor = relation.select_visitor
58
-
59
- visitor.should be_a Visitors::SelectVisitor
39
+ visitor.should be_a Visitors::AttributeVisitor
60
40
  table = visitor.contextualize(relation.join_dependency.join_parts.last)
61
41
  table.table_alias.should eq 'parents_people_2'
62
42
  end
@@ -209,6 +189,116 @@ module Squeel
209
189
 
210
190
  end
211
191
 
192
+ describe '#includes' do
193
+
194
+ it 'builds options with a block' do
195
+ standard = Person.includes(:children => :children).where(:children => {:children => {:name => 'bob'}})
196
+ block = Person.includes{{children => children}}.where(:children => {:children => {:name => 'bob'}})
197
+ block.debug_sql.should eq standard.debug_sql
198
+ end
199
+
200
+ it 'eager loads multiple top-level associations with a block' do
201
+ standard = Person.includes(:children, :articles, :comments).where(:children => {:name => 'bob'})
202
+ block = Person.includes{[children, articles, comments]}.where(:children => {:name => 'bob'})
203
+ block.debug_sql.should eq standard.debug_sql
204
+ end
205
+
206
+ it 'eager loads polymorphic belongs_to associations' do
207
+ relation = Note.includes{notable(Article)}.where{{notable(Article) => {title => 'hey'}}}
208
+ relation.debug_sql.should match /"notes"."notable_type" = 'Article'/
209
+ end
210
+
211
+ it 'eager loads multiple polymorphic belongs_to associations' do
212
+ relation = Note.includes{[notable(Article), notable(Person)]}.
213
+ where{{notable(Article) => {title => 'hey'}}}.
214
+ where{{notable(Person) => {name => 'joe'}}}
215
+ relation.debug_sql.should match /"notes"."notable_type" = 'Article'/
216
+ relation.debug_sql.should match /"notes"."notable_type" = 'Person'/
217
+ end
218
+
219
+ it "only includes once, even if two join types are used" do
220
+ relation = Person.includes(:articles.inner, :articles.outer).where(:articles => {:title => 'hey'})
221
+ relation.debug_sql.scan("JOIN").size.should eq 1
222
+ end
223
+
224
+ it 'includes a keypath' do
225
+ relation = Note.includes{notable(Article).person.children}.where{notable(Article).person.children.name == 'Ernie'}
226
+ relation.debug_sql.should match /SELECT "notes".* FROM "notes" LEFT OUTER JOIN "articles" ON "articles"."id" = "notes"."notable_id" AND "notes"."notable_type" = 'Article' LEFT OUTER JOIN "people" ON "people"."id" = "articles"."person_id" LEFT OUTER JOIN "people" "children_people" ON "children_people"."parent_id" = "people"."id"/
227
+ end
228
+
229
+ end
230
+
231
+ describe '#preload' do
232
+
233
+ it 'builds options with a block' do
234
+ relation = Person.preload{children}
235
+ queries_for {relation.all}.should have(2).items
236
+ queries_for {relation.first.children}.should have(0).items
237
+ end
238
+
239
+ it 'builds options with a keypath' do
240
+ relation = Person.preload{articles.comments}
241
+ queries_for {relation.all}.should have(3).items
242
+ queries_for {relation.first.articles.first.comments}.should have(0).items
243
+ end
244
+
245
+ it 'builds options with a hash' do
246
+ relation = Person.preload{{
247
+ articles => {
248
+ comments => person
249
+ }
250
+ }}
251
+
252
+ queries_for {relation.all}.should have(4).items
253
+
254
+ queries_for {
255
+ relation.first.articles
256
+ relation.first.articles.first.comments
257
+ relation.first.articles.first.comments.first.person
258
+ }.should have(0).items
259
+ end
260
+
261
+ end
262
+
263
+ describe '#eager_load' do
264
+
265
+ it 'builds options with a block' do
266
+ standard = Person.eager_load(:children => :children)
267
+ block = Person.eager_load{{children => children}}
268
+ block.debug_sql.should eq standard.debug_sql
269
+ queries_for {block.all}.should have(1).item
270
+ queries_for {block.first.children}.should have(0).items
271
+ end
272
+
273
+ it 'eager loads multiple top-level associations with a block' do
274
+ standard = Person.eager_load(:children, :articles, :comments)
275
+ block = Person.eager_load{[children, articles, comments]}
276
+ block.debug_sql.should eq standard.debug_sql
277
+ end
278
+
279
+ it 'eager loads polymorphic belongs_to associations' do
280
+ relation = Note.eager_load{notable(Article)}
281
+ relation.debug_sql.should match /"notes"."notable_type" = 'Article'/
282
+ end
283
+
284
+ it 'eager loads multiple polymorphic belongs_to associations' do
285
+ relation = Note.eager_load{[notable(Article), notable(Person)]}
286
+ relation.debug_sql.should match /"notes"."notable_type" = 'Article'/
287
+ relation.debug_sql.should match /"notes"."notable_type" = 'Person'/
288
+ end
289
+
290
+ it "only eager_load once, even if two join types are used" do
291
+ relation = Person.eager_load(:articles.inner, :articles.outer)
292
+ relation.debug_sql.scan("JOIN").size.should eq 1
293
+ end
294
+
295
+ it 'eager_load a keypath' do
296
+ relation = Note.eager_load{notable(Article).person.children}
297
+ relation.debug_sql.should match /SELECT "notes".* FROM "notes" LEFT OUTER JOIN "articles" ON "articles"."id" = "notes"."notable_id" AND "notes"."notable_type" = 'Article' LEFT OUTER JOIN "people" ON "people"."id" = "articles"."person_id" LEFT OUTER JOIN "people" "children_people" ON "children_people"."parent_id" = "people"."id"/
298
+ end
299
+
300
+ end
301
+
212
302
  describe '#select' do
213
303
 
214
304
  it 'accepts options from a block' do
@@ -251,6 +341,16 @@ module Squeel
251
341
 
252
342
  end
253
343
 
344
+ describe '#group' do
345
+
346
+ it 'builds options with a block' do
347
+ standard = Person.group(:name)
348
+ block = Person.group{name}
349
+ block.to_sql.should eq standard.to_sql
350
+ end
351
+
352
+ end
353
+
254
354
  describe '#where' do
255
355
 
256
356
  it 'builds options with a block' do
@@ -284,6 +384,23 @@ module Squeel
284
384
  relation.first.id.should eq 1
285
385
  end
286
386
 
387
+ it 'maps conditions onto their proper table with multiple polymorphic joins' do
388
+ relation = Note.joins{[notable(Article).outer, notable(Person).outer]}
389
+ people_notes = relation.where{notable(Person).salary > 30000}
390
+ article_notes = relation.where{notable(Article).title =~ '%'}
391
+ people_and_article_notes = relation.where{(notable(Person).salary > 30000) | (notable(Article).title =~ '%')}
392
+ people_notes.should have(10).items
393
+ article_notes.should have(30).items
394
+ people_and_article_notes.should have(40).items
395
+ end
396
+
397
+ it 'allows a subquery on the value side of a predicate' do
398
+ old_and_busted = Person.where(:name => ['Aric Smith', 'Gladyce Kulas'])
399
+ new_hotness = Person.where{name.in(Person.select{name}.where{name.in(['Aric Smith', 'Gladyce Kulas'])})}
400
+ new_hotness.should have(2).items
401
+ old_and_busted.to_a.should eq new_hotness.to_a
402
+ end
403
+
287
404
  end
288
405
 
289
406
  describe '#joins' do
@@ -305,11 +422,22 @@ module Squeel
305
422
  relation.to_sql.should match /"notes"."notable_type" = 'Article'/
306
423
  end
307
424
 
425
+ it 'joins multiple polymorphic belongs_to associations' do
426
+ relation = Note.joins{[notable(Article), notable(Person)]}
427
+ relation.to_sql.should match /"notes"."notable_type" = 'Article'/
428
+ relation.to_sql.should match /"notes"."notable_type" = 'Person'/
429
+ end
430
+
308
431
  it "only joins once, even if two join types are used" do
309
432
  relation = Person.joins(:articles.inner, :articles.outer)
310
433
  relation.to_sql.scan("JOIN").size.should eq 1
311
434
  end
312
435
 
436
+ it 'joins a keypath' do
437
+ relation = Note.joins{notable(Article).person.children}
438
+ relation.to_sql.should match /SELECT "notes".* FROM "notes" INNER JOIN "articles" ON "articles"."id" = "notes"."notable_id" AND "notes"."notable_type" = 'Article' INNER JOIN "people" ON "people"."id" = "articles"."person_id" INNER JOIN "people" "children_people" ON "children_people"."parent_id" = "people"."id"/
439
+ end
440
+
313
441
  end
314
442
 
315
443
  describe '#having' do
@@ -347,6 +475,19 @@ module Squeel
347
475
 
348
476
  end
349
477
 
478
+ describe '#reorder' do
479
+ before do
480
+ @standard = Person.order(:name)
481
+ end
482
+
483
+ it 'builds options with a block' do
484
+ block = Person.reorder{id}
485
+ block.to_sql.should_not eq @standard.to_sql
486
+ block.to_sql.should match /ORDER BY "people"."id"/
487
+ end
488
+
489
+ end
490
+
350
491
  describe '#build_where' do
351
492
 
352
493
  it 'sanitizes SQL as usual with strings' do