squeel 0.5.0

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