squeel 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +8 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +41 -0
  5. data/Rakefile +19 -0
  6. data/lib/core_ext/hash.rb +13 -0
  7. data/lib/core_ext/symbol.rb +36 -0
  8. data/lib/squeel.rb +26 -0
  9. data/lib/squeel/adapters/active_record.rb +6 -0
  10. data/lib/squeel/adapters/active_record/join_association.rb +90 -0
  11. data/lib/squeel/adapters/active_record/join_dependency.rb +68 -0
  12. data/lib/squeel/adapters/active_record/relation.rb +292 -0
  13. data/lib/squeel/configuration.rb +25 -0
  14. data/lib/squeel/constants.rb +23 -0
  15. data/lib/squeel/contexts/join_dependency_context.rb +74 -0
  16. data/lib/squeel/dsl.rb +31 -0
  17. data/lib/squeel/nodes.rb +10 -0
  18. data/lib/squeel/nodes/and.rb +8 -0
  19. data/lib/squeel/nodes/binary.rb +23 -0
  20. data/lib/squeel/nodes/function.rb +84 -0
  21. data/lib/squeel/nodes/join.rb +51 -0
  22. data/lib/squeel/nodes/key_path.rb +127 -0
  23. data/lib/squeel/nodes/nary.rb +35 -0
  24. data/lib/squeel/nodes/not.rb +8 -0
  25. data/lib/squeel/nodes/operation.rb +23 -0
  26. data/lib/squeel/nodes/operators.rb +27 -0
  27. data/lib/squeel/nodes/or.rb +8 -0
  28. data/lib/squeel/nodes/order.rb +35 -0
  29. data/lib/squeel/nodes/predicate.rb +49 -0
  30. data/lib/squeel/nodes/predicate_operators.rb +17 -0
  31. data/lib/squeel/nodes/stub.rb +113 -0
  32. data/lib/squeel/nodes/unary.rb +22 -0
  33. data/lib/squeel/predicate_methods.rb +22 -0
  34. data/lib/squeel/predicate_methods/function.rb +9 -0
  35. data/lib/squeel/predicate_methods/predicate.rb +11 -0
  36. data/lib/squeel/predicate_methods/stub.rb +9 -0
  37. data/lib/squeel/predicate_methods/symbol.rb +9 -0
  38. data/lib/squeel/version.rb +3 -0
  39. data/lib/squeel/visitors.rb +3 -0
  40. data/lib/squeel/visitors/base.rb +46 -0
  41. data/lib/squeel/visitors/order_visitor.rb +107 -0
  42. data/lib/squeel/visitors/predicate_visitor.rb +179 -0
  43. data/lib/squeel/visitors/select_visitor.rb +103 -0
  44. data/spec/blueprints/articles.rb +5 -0
  45. data/spec/blueprints/comments.rb +5 -0
  46. data/spec/blueprints/notes.rb +3 -0
  47. data/spec/blueprints/people.rb +4 -0
  48. data/spec/blueprints/tags.rb +3 -0
  49. data/spec/console.rb +22 -0
  50. data/spec/core_ext/symbol_spec.rb +68 -0
  51. data/spec/helpers/squeel_helper.rb +5 -0
  52. data/spec/spec_helper.rb +30 -0
  53. data/spec/squeel/adapters/active_record/join_association_spec.rb +18 -0
  54. data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +60 -0
  55. data/spec/squeel/adapters/active_record/relation_spec.rb +437 -0
  56. data/spec/squeel/contexts/join_dependency_context_spec.rb +43 -0
  57. data/spec/squeel/dsl_spec.rb +73 -0
  58. data/spec/squeel/nodes/function_spec.rb +149 -0
  59. data/spec/squeel/nodes/join_spec.rb +27 -0
  60. data/spec/squeel/nodes/key_path_spec.rb +92 -0
  61. data/spec/squeel/nodes/operation_spec.rb +149 -0
  62. data/spec/squeel/nodes/operators_spec.rb +87 -0
  63. data/spec/squeel/nodes/order_spec.rb +30 -0
  64. data/spec/squeel/nodes/predicate_operators_spec.rb +88 -0
  65. data/spec/squeel/nodes/predicate_spec.rb +92 -0
  66. data/spec/squeel/nodes/stub_spec.rb +178 -0
  67. data/spec/squeel/visitors/order_visitor_spec.rb +128 -0
  68. data/spec/squeel/visitors/predicate_visitor_spec.rb +267 -0
  69. data/spec/squeel/visitors/select_visitor_spec.rb +115 -0
  70. data/spec/support/schema.rb +101 -0
  71. data/squeel.gemspec +44 -0
  72. metadata +221 -0
@@ -0,0 +1,5 @@
1
+ module SqueelHelper
2
+ def dsl(&block)
3
+ Squeel::DSL.evaluate(&block)
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ require 'machinist/active_record'
2
+ require 'sham'
3
+ require 'faker'
4
+
5
+ Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
6
+ require f
7
+ end
8
+
9
+ Sham.define do
10
+ name { Faker::Name.name }
11
+ title { Faker::Lorem.sentence }
12
+ body { Faker::Lorem.paragraph }
13
+ salary {|index| 30000 + (index * 1000)}
14
+ tag_name { Faker::Lorem.words(3).join(' ') }
15
+ note { Faker::Lorem.words(7).join(' ') }
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.before(:suite) { Schema.create }
20
+ config.before(:all) { Sham.reset(:before_all) }
21
+ config.before(:each) { Sham.reset(:before_each) }
22
+
23
+ config.include SqueelHelper
24
+ end
25
+
26
+ require 'squeel'
27
+
28
+ Squeel.configure do |config|
29
+ config.load_core_extensions :hash, :symbol
30
+ end
@@ -0,0 +1,18 @@
1
+ module Squeel
2
+ module Adapters
3
+ module ActiveRecord
4
+ describe JoinAssociation do
5
+ before do
6
+ @jd = ::ActiveRecord::Associations::JoinDependency.new(Note, {}, [])
7
+ @notable = Note.reflect_on_association(:notable)
8
+ end
9
+
10
+ it 'accepts a 4th parameter to set a polymorphic class' do
11
+ join_association = JoinAssociation.new(@notable, @jd, @jd.join_base, Article)
12
+ join_association.reflection.klass.should eq Article
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,60 @@
1
+ module Squeel
2
+ module Adapters
3
+ module ActiveRecord
4
+ describe JoinDependency do
5
+ before do
6
+ @jd = ::ActiveRecord::Associations::JoinDependency.new(Person, {}, [])
7
+ end
8
+
9
+ it 'joins with symbols' do
10
+ @jd.send(:build, :articles => :comments)
11
+ @jd.join_associations.should have(2).associations
12
+ @jd.join_associations.each do |association|
13
+ association.join_type.should eq Arel::InnerJoin
14
+ end
15
+ end
16
+
17
+ it 'joins with stubs' do
18
+ @jd.send(:build, Nodes::Stub.new(:articles) => Nodes::Stub.new(:comments))
19
+ @jd.join_associations.should have(2).associations
20
+ @jd.join_associations.each do |association|
21
+ association.join_type.should eq Arel::InnerJoin
22
+ end
23
+ @jd.join_associations[0].table_name.should eq 'articles'
24
+ @jd.join_associations[1].table_name.should eq 'comments'
25
+ end
26
+
27
+ it 'joins with key paths' do
28
+ @jd.send(:build, dsl{children.children.parent})
29
+ @jd.join_associations.should have(3).associations
30
+ @jd.join_associations.each do |association|
31
+ association.join_type.should eq Arel::InnerJoin
32
+ end
33
+ @jd.join_associations[0].aliased_table_name.should eq 'children_people'
34
+ @jd.join_associations[1].aliased_table_name.should eq 'children_people_2'
35
+ @jd.join_associations[2].aliased_table_name.should eq 'parents_people'
36
+ end
37
+
38
+ it 'joins with key paths as keys' do
39
+ @jd.send(:build, dsl{{children.parent => parent}})
40
+ @jd.join_associations.should have(3).associations
41
+ @jd.join_associations.each do |association|
42
+ association.join_type.should eq Arel::InnerJoin
43
+ end
44
+ @jd.join_associations[0].aliased_table_name.should eq 'children_people'
45
+ @jd.join_associations[1].aliased_table_name.should eq 'parents_people'
46
+ @jd.join_associations[2].aliased_table_name.should eq 'parents_people_2'
47
+ end
48
+
49
+ it 'joins using outer joins' do
50
+ @jd.send(:build, :articles.outer => :comments.outer)
51
+ @jd.join_associations.should have(2).associations
52
+ @jd.join_associations.each do |association|
53
+ association.join_type.should eq Arel::OuterJoin
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,437 @@
1
+ module Squeel
2
+ module Adapters
3
+ module ActiveRecord
4
+ describe Relation do
5
+
6
+ describe '#predicate_visitor' do
7
+
8
+ it 'creates a predicate visitor with a JoinDependencyContext for the relation' do
9
+ relation = Person.joins({
10
+ :children => {
11
+ :children => {
12
+ :parent => :parent
13
+ }
14
+ }
15
+ })
16
+
17
+ visitor = relation.predicate_visitor
18
+
19
+ visitor.should be_a Visitors::PredicateVisitor
20
+ table = visitor.contextualize(relation.join_dependency.join_parts.last)
21
+ table.table_alias.should eq 'parents_people_2'
22
+ end
23
+
24
+ end
25
+
26
+ describe '#order_visitor' do
27
+
28
+ it 'creates an order visitor with a JoinDependencyContext for the relation' do
29
+ relation = Person.joins({
30
+ :children => {
31
+ :children => {
32
+ :parent => :parent
33
+ }
34
+ }
35
+ })
36
+
37
+ visitor = relation.order_visitor
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
60
+ table = visitor.contextualize(relation.join_dependency.join_parts.last)
61
+ table.table_alias.should eq 'parents_people_2'
62
+ end
63
+
64
+ end
65
+
66
+ describe '#build_arel' do
67
+
68
+ it 'joins associations' do
69
+ relation = Person.joins({
70
+ :children => {
71
+ :children => {
72
+ :parent => :parent
73
+ }
74
+ }
75
+ })
76
+
77
+ arel = relation.build_arel
78
+
79
+ relation.join_dependency.join_associations.should have(4).items
80
+ arel.to_sql.should match /INNER JOIN "people" "parents_people_2" ON "parents_people_2"."id" = "parents_people"."parent_id"/
81
+ end
82
+
83
+ it 'joins associations with custom join types' do
84
+ relation = Person.joins({
85
+ :children.outer => {
86
+ :children => {
87
+ :parent => :parent.outer
88
+ }
89
+ }
90
+ })
91
+
92
+ arel = relation.build_arel
93
+
94
+ relation.join_dependency.join_associations.should have(4).items
95
+ arel.to_sql.should match /LEFT OUTER JOIN "people" "children_people"/
96
+ arel.to_sql.should match /LEFT OUTER JOIN "people" "parents_people_2" ON "parents_people_2"."id" = "parents_people"."parent_id"/
97
+ end
98
+
99
+ it 'only joins an association once, even if two overlapping joins_values hashes are given' do
100
+ relation = Person.joins({
101
+ :children => {
102
+ :children => {
103
+ :parent => :parent
104
+ }
105
+ }
106
+ }).joins({
107
+ :children => {
108
+ :children => {
109
+ :children => :parent
110
+ }
111
+ }
112
+ })
113
+
114
+ arel = relation.build_arel
115
+ relation.join_dependency.join_associations.should have(6).items
116
+ arel.to_sql.should match /INNER JOIN "people" "parents_people_3" ON "parents_people_3"."id" = "children_people_3"."parent_id"/
117
+ end
118
+
119
+ it 'visits wheres with a PredicateVisitor, converting them to ARel nodes' do
120
+ relation = Person.where(:name.matches => '%bob%')
121
+ arel = relation.build_arel
122
+ arel.to_sql.should match /"people"."name" LIKE '%bob%'/
123
+ end
124
+
125
+ it 'maps wheres inside a hash to their appropriate association table' do
126
+ relation = Person.joins({
127
+ :children => {
128
+ :children => {
129
+ :parent => :parent
130
+ }
131
+ }
132
+ }).where({
133
+ :children => {
134
+ :children => {
135
+ :parent => {
136
+ :parent => { :name => 'bob' }
137
+ }
138
+ }
139
+ }
140
+ })
141
+
142
+ arel = relation.build_arel
143
+
144
+ arel.to_sql.should match /"parents_people_2"."name" = 'bob'/
145
+ end
146
+
147
+ it 'combines multiple conditions of the same type against the same column with AND' do
148
+ relation = Person.where(:name.matches => '%bob%')
149
+ relation = relation.where(:name.matches => '%joe%')
150
+ arel = relation.build_arel
151
+ arel.to_sql.should match /"people"."name" LIKE '%bob%' AND "people"."name" LIKE '%joe%'/
152
+ end
153
+
154
+ it 'handles ORs between predicates' do
155
+ relation = Person.joins{articles}.where{(name =~ 'Joe%') | (articles.title =~ 'Hello%')}
156
+ arel = relation.build_arel
157
+ arel.to_sql.should match /OR/
158
+ end
159
+
160
+ it 'maintains groupings as given' do
161
+ relation = Person.where(dsl{(name == 'Ernie') | ((name =~ 'Bob%') & (name =~ '%by'))})
162
+ arel = relation.build_arel
163
+ arel.to_sql.should match /"people"."name" = 'Ernie' OR \("people"."name" LIKE 'Bob%' AND "people"."name" LIKE '%by'\)/
164
+ end
165
+
166
+ it 'maps havings inside a hash to their appropriate association table' do
167
+ relation = Person.joins({
168
+ :children => {
169
+ :children => {
170
+ :parent => :parent
171
+ }
172
+ }
173
+ }).having({
174
+ :children => {
175
+ :children => {
176
+ :parent => {
177
+ :parent => {:name => 'joe'}
178
+ }
179
+ }
180
+ }
181
+ })
182
+
183
+ arel = relation.build_arel
184
+
185
+ arel.to_sql.should match /HAVING "parents_people_2"."name" = 'joe'/
186
+ end
187
+
188
+ it 'maps orders inside a hash to their appropriate association table' do
189
+ relation = Person.joins({
190
+ :children => {
191
+ :children => {
192
+ :parent => :parent
193
+ }
194
+ }
195
+ }).order({
196
+ :children => {
197
+ :children => {
198
+ :parent => {
199
+ :parent => :id.asc
200
+ }
201
+ }
202
+ }
203
+ })
204
+
205
+ arel = relation.build_arel
206
+
207
+ arel.to_sql.should match /ORDER BY "parents_people_2"."id" ASC/
208
+ end
209
+
210
+ end
211
+
212
+ describe '#select' do
213
+
214
+ it 'accepts options from a block' do
215
+ standard = Person.select(:id)
216
+ block = Person.select {id}
217
+ block.to_sql.should eq standard.to_sql
218
+ end
219
+
220
+ it 'falls back to Array#select behavior with a block that has an arity' do
221
+ people = Person.select{|p| p.name =~ /John/}
222
+ people.should have(1).person
223
+ people.first.name.should eq 'Miss Cameron Johnson'
224
+ end
225
+
226
+ it 'behaves as normal with standard parameters' do
227
+ people = Person.select(:id)
228
+ people.should have(332).people
229
+ expect { people.first.name }.to raise_error ActiveModel::MissingAttributeError
230
+ end
231
+
232
+ it 'allows a function in the select values via Symbol#func' do
233
+ relation = Person.select(:max.func(:id).as('max_id'))
234
+ relation.first.max_id.should eq 332
235
+ end
236
+
237
+ it 'allows a function in the select values via block' do
238
+ relation = Person.select{max(id).as(max_id)}
239
+ relation.first.max_id.should eq 332
240
+ end
241
+
242
+ it 'allows an operation in the select values via block' do
243
+ relation = Person.select{[id, (id + 1).as('id_plus_one')]}.where('id_plus_one = 2')
244
+ relation.first.id.should eq 1
245
+ end
246
+
247
+ it 'allows custom operators in the select values via block' do
248
+ relation = Person.select{name.op('||', '-diddly').as(flanderized_name)}
249
+ relation.first.flanderized_name.should eq 'Aric Smith-diddly'
250
+ end
251
+
252
+ end
253
+
254
+ describe '#where' do
255
+
256
+ it 'builds options with a block' do
257
+ standard = Person.where(:name => 'bob')
258
+ block = Person.where{{name => 'bob'}}
259
+ block.to_sql.should eq standard.to_sql
260
+ end
261
+
262
+ it 'builds compound conditions with a block' do
263
+ block = Person.where{(name == 'bob') & (salary == 100000)}
264
+ block.to_sql.should match /"people"."name" = 'bob'/
265
+ block.to_sql.should match /AND/
266
+ block.to_sql.should match /"people"."salary" = 100000/
267
+ end
268
+
269
+ it 'allows mixing hash and operator syntax inside a block' do
270
+ block = Person.joins(:comments).
271
+ where{(name == 'bob') & {comments => (body == 'First post!')}}
272
+ block.to_sql.should match /"people"."name" = 'bob'/
273
+ block.to_sql.should match /AND/
274
+ block.to_sql.should match /"comments"."body" = 'First post!'/
275
+ end
276
+
277
+ it 'allows a condition on a function via block' do
278
+ relation = Person.where{coalesce(nil,id) == 5}
279
+ relation.first.id.should eq 5
280
+ end
281
+
282
+ it 'allows a condition on an operation via block' do
283
+ relation = Person.where{(id + 1) == 2}
284
+ relation.first.id.should eq 1
285
+ end
286
+
287
+ end
288
+
289
+ describe '#joins' do
290
+
291
+ it 'builds options with a block' do
292
+ standard = Person.joins(:children => :children)
293
+ block = Person.joins{{children => children}}
294
+ block.to_sql.should eq standard.to_sql
295
+ end
296
+
297
+ it 'accepts multiple top-level associations with a block' do
298
+ standard = Person.joins(:children, :articles, :comments)
299
+ block = Person.joins{[children, articles, comments]}
300
+ block.to_sql.should eq standard.to_sql
301
+ end
302
+
303
+ it 'joins polymorphic belongs_to associations' do
304
+ relation = Note.joins{notable(Article)}
305
+ relation.to_sql.should match /"notes"."notable_type" = 'Article'/
306
+ end
307
+
308
+ it "only joins once, even if two join types are used" do
309
+ relation = Person.joins(:articles.inner, :articles.outer)
310
+ relation.to_sql.scan("JOIN").size.should eq 1
311
+ end
312
+
313
+ end
314
+
315
+ describe '#having' do
316
+
317
+ it 'builds options with a block' do
318
+ standard = Person.having(:name => 'bob')
319
+ block = Person.having{{name => 'bob'}}
320
+ block.to_sql.should eq standard.to_sql
321
+ end
322
+
323
+ it 'allows complex conditions on aggregate columns' do
324
+ relation = Person.group(:parent_id).having{salary == max(salary)}
325
+ relation.first.name.should eq 'Gladyce Kulas'
326
+ end
327
+
328
+ it 'allows a condition on a function via block' do
329
+ relation = Person.group(:id).having{coalesce(nil,id) == 5}
330
+ relation.first.id.should eq 5
331
+ end
332
+
333
+ it 'allows a condition on an operation via block' do
334
+ relation = Person.group(:id).having{(id + 1) == 2}
335
+ relation.first.id.should eq 1
336
+ end
337
+
338
+ end
339
+
340
+ describe '#order' do
341
+
342
+ it 'builds options with a block' do
343
+ standard = Person.order(:name)
344
+ block = Person.order{name}
345
+ block.to_sql.should eq standard.to_sql
346
+ end
347
+
348
+ end
349
+
350
+ describe '#build_where' do
351
+
352
+ it 'sanitizes SQL as usual with strings' do
353
+ wheres = Person.where('name like ?', '%bob%').where_values
354
+ wheres.should eq ["name like '%bob%'"]
355
+ end
356
+
357
+ it 'sanitizes SQL as usual with strings and hash substitution' do
358
+ wheres = Person.where('name like :name', :name => '%bob%').where_values
359
+ wheres.should eq ["name like '%bob%'"]
360
+ end
361
+
362
+ it 'sanitizes SQL as usual with arrays' do
363
+ wheres = Person.where(['name like ?', '%bob%']).where_values
364
+ wheres.should eq ["name like '%bob%'"]
365
+ end
366
+
367
+ it 'adds hash where values without converting to ARel predicates' do
368
+ wheres = Person.where({:name => 'bob'}).where_values
369
+ wheres.should eq [{:name => 'bob'}]
370
+ end
371
+
372
+ end
373
+
374
+ describe '#debug_sql' do
375
+
376
+ it 'returns the query that would be run against the database, even if eager loading' do
377
+ relation = Person.includes(:comments, :articles).
378
+ where(:comments => {:body => 'First post!'}).
379
+ where(:articles => {:title => 'Hello, world!'})
380
+ relation.debug_sql.should_not eq relation.to_sql
381
+ relation.debug_sql.should match /SELECT "people"."id" AS t0_r0/
382
+ end
383
+
384
+ end
385
+
386
+ describe '#where_values_hash' do
387
+
388
+ it 'creates new records with equality predicates from wheres' do
389
+ @person = Person.where(:name => 'bob', :parent_id => 3).new
390
+ @person.parent_id.should eq 3
391
+ @person.name.should eq 'bob'
392
+ end
393
+
394
+ it 'uses the last supplied equality predicate in where_values when creating new records' do
395
+ @person = Person.where(:name => 'bob', :parent_id => 3).where(:name => 'joe').new
396
+ @person.parent_id.should eq 3
397
+ @person.name.should eq 'joe'
398
+ end
399
+
400
+ end
401
+
402
+ describe '#merge' do
403
+
404
+ it 'merges relations with the same base' do
405
+ relation = Person.where{name == 'bob'}.merge(Person.where{salary == 100000})
406
+ sql = relation.to_sql
407
+ sql.should match /"people"."name" = 'bob'/
408
+ sql.should match /"people"."salary" = 100000/
409
+ end
410
+
411
+ it 'merges relations with a different base' do
412
+ relation = Person.where{name == 'bob'}.merge(Article.where{title == 'Hello world!'})
413
+ sql = relation.to_sql
414
+ sql.should match /INNER JOIN "articles" ON "articles"."person_id" = "people"."id"/
415
+ sql.should match /"people"."name" = 'bob'/
416
+ sql.should match /"articles"."title" = 'Hello world!'/
417
+ end
418
+
419
+ end
420
+
421
+ describe '#to_a' do
422
+
423
+ it 'eager-loads associations with dependent conditions' do
424
+ relation = Person.includes(:comments, :articles).
425
+ where{{comments => {body => 'First post!'}}}
426
+ relation.size.should be 1
427
+ person = relation.first
428
+ person.name.should eq 'Gladyce Kulas'
429
+ person.comments.loaded?.should be true
430
+ end
431
+
432
+ end
433
+
434
+ end
435
+ end
436
+ end
437
+ end