sequel 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/CHANGELOG +1551 -4
  2. data/README +306 -19
  3. data/Rakefile +84 -56
  4. data/bin/sequel +106 -0
  5. data/doc/cheat_sheet.rdoc +225 -0
  6. data/doc/dataset_filtering.rdoc +182 -0
  7. data/lib/sequel_core.rb +136 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
  9. data/lib/sequel_core/adapters/ado.rb +80 -0
  10. data/lib/sequel_core/adapters/db2.rb +148 -0
  11. data/lib/sequel_core/adapters/dbi.rb +117 -0
  12. data/lib/sequel_core/adapters/informix.rb +78 -0
  13. data/lib/sequel_core/adapters/jdbc.rb +186 -0
  14. data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
  15. data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
  16. data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
  17. data/lib/sequel_core/adapters/mysql.rb +231 -0
  18. data/lib/sequel_core/adapters/odbc.rb +155 -0
  19. data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/openbase.rb +64 -0
  21. data/lib/sequel_core/adapters/oracle.rb +170 -0
  22. data/lib/sequel_core/adapters/postgres.rb +199 -0
  23. data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
  24. data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
  25. data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
  26. data/lib/sequel_core/adapters/sqlite.rb +138 -0
  27. data/lib/sequel_core/connection_pool.rb +194 -0
  28. data/lib/sequel_core/core_ext.rb +203 -0
  29. data/lib/sequel_core/core_sql.rb +184 -0
  30. data/lib/sequel_core/database.rb +471 -0
  31. data/lib/sequel_core/database/schema.rb +156 -0
  32. data/lib/sequel_core/dataset.rb +457 -0
  33. data/lib/sequel_core/dataset/callback.rb +13 -0
  34. data/lib/sequel_core/dataset/convenience.rb +245 -0
  35. data/lib/sequel_core/dataset/pagination.rb +96 -0
  36. data/lib/sequel_core/dataset/query.rb +41 -0
  37. data/lib/sequel_core/dataset/schema.rb +15 -0
  38. data/lib/sequel_core/dataset/sql.rb +889 -0
  39. data/lib/sequel_core/deprecated.rb +26 -0
  40. data/lib/sequel_core/exceptions.rb +42 -0
  41. data/lib/sequel_core/migration.rb +187 -0
  42. data/lib/sequel_core/object_graph.rb +216 -0
  43. data/lib/sequel_core/pretty_table.rb +71 -0
  44. data/lib/sequel_core/schema.rb +2 -0
  45. data/lib/sequel_core/schema/generator.rb +239 -0
  46. data/lib/sequel_core/schema/sql.rb +325 -0
  47. data/lib/sequel_core/sql.rb +812 -0
  48. data/lib/sequel_model.rb +5 -1
  49. data/lib/sequel_model/association_reflection.rb +3 -8
  50. data/lib/sequel_model/base.rb +15 -10
  51. data/lib/sequel_model/inflector.rb +3 -5
  52. data/lib/sequel_model/plugins.rb +1 -1
  53. data/lib/sequel_model/record.rb +11 -3
  54. data/lib/sequel_model/schema.rb +4 -4
  55. data/lib/sequel_model/validations.rb +6 -1
  56. data/spec/adapters/ado_spec.rb +17 -0
  57. data/spec/adapters/informix_spec.rb +96 -0
  58. data/spec/adapters/mysql_spec.rb +764 -0
  59. data/spec/adapters/oracle_spec.rb +222 -0
  60. data/spec/adapters/postgres_spec.rb +441 -0
  61. data/spec/adapters/spec_helper.rb +7 -0
  62. data/spec/adapters/sqlite_spec.rb +400 -0
  63. data/spec/integration/dataset_test.rb +51 -0
  64. data/spec/integration/eager_loader_test.rb +702 -0
  65. data/spec/integration/schema_test.rb +102 -0
  66. data/spec/integration/spec_helper.rb +44 -0
  67. data/spec/integration/type_test.rb +43 -0
  68. data/spec/rcov.opts +2 -0
  69. data/spec/sequel_core/connection_pool_spec.rb +363 -0
  70. data/spec/sequel_core/core_ext_spec.rb +156 -0
  71. data/spec/sequel_core/core_sql_spec.rb +427 -0
  72. data/spec/sequel_core/database_spec.rb +964 -0
  73. data/spec/sequel_core/dataset_spec.rb +2977 -0
  74. data/spec/sequel_core/expression_filters_spec.rb +346 -0
  75. data/spec/sequel_core/migration_spec.rb +261 -0
  76. data/spec/sequel_core/object_graph_spec.rb +234 -0
  77. data/spec/sequel_core/pretty_table_spec.rb +58 -0
  78. data/spec/sequel_core/schema_generator_spec.rb +122 -0
  79. data/spec/sequel_core/schema_spec.rb +497 -0
  80. data/spec/sequel_core/spec_helper.rb +51 -0
  81. data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
  82. data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
  83. data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
  84. data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
  85. data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
  86. data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
  87. data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
  88. data/spec/sequel_model/inflector_spec.rb +119 -0
  89. data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
  90. data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
  91. data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
  92. data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
  93. data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
  94. data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
  95. data/spec/spec_config.rb +9 -0
  96. data/spec/spec_config.rb.example +10 -0
  97. metadata +110 -37
  98. data/spec/inflector_spec.rb +0 -34
@@ -0,0 +1,2977 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ context "Dataset" do
4
+ setup do
5
+ @dataset = Sequel::Dataset.new("db")
6
+ end
7
+
8
+ specify "should accept database and opts in initialize" do
9
+ db = "db"
10
+ opts = {:from => :test}
11
+ d = Sequel::Dataset.new(db, opts)
12
+ d.db.should be(db)
13
+ d.opts.should be(opts)
14
+
15
+ d = Sequel::Dataset.new(db)
16
+ d.db.should be(db)
17
+ d.opts.should be_a_kind_of(Hash)
18
+ d.opts.should == {}
19
+ end
20
+
21
+ specify "should provide clone for chainability" do
22
+ d1 = @dataset.clone(:from => [:test])
23
+ d1.class.should == @dataset.class
24
+ d1.should_not == @dataset
25
+ d1.db.should be(@dataset.db)
26
+ d1.opts[:from].should == [:test]
27
+ @dataset.opts[:from].should be_nil
28
+
29
+ d2 = d1.clone(:order => [:name])
30
+ d2.class.should == @dataset.class
31
+ d2.should_not == d1
32
+ d2.should_not == @dataset
33
+ d2.db.should be(@dataset.db)
34
+ d2.opts[:from].should == [:test]
35
+ d2.opts[:order].should == [:name]
36
+ d1.opts[:order].should be_nil
37
+ end
38
+
39
+ specify "should include Enumerable" do
40
+ Sequel::Dataset.included_modules.should include(Enumerable)
41
+ end
42
+ end
43
+
44
+ context "Dataset#clone" do
45
+ setup do
46
+ @dataset = Sequel::Dataset.new(nil).from(:items)
47
+ end
48
+
49
+ specify "should create an exact copy of the dataset" do
50
+ @c = Class.new
51
+ @dataset.set_model(@c)
52
+ @clone = @dataset.clone
53
+
54
+ @clone.should_not === @dataset
55
+ @clone.class.should == @dataset.class
56
+ @clone.model_classes.should == @dataset.model_classes
57
+ end
58
+
59
+ specify "should deep-copy the dataset opts" do
60
+ @clone = @dataset.clone
61
+
62
+ @clone.opts.should_not equal(@dataset.opts)
63
+ @dataset.filter!(:a => 'b')
64
+ @clone.opts[:filter].should be_nil
65
+ end
66
+
67
+ specify "should return a clone self" do
68
+ clone = @dataset.clone({})
69
+ clone.class.should == @dataset.class
70
+ clone.db.should == @dataset.db
71
+ clone.opts.should == @dataset.opts
72
+ end
73
+
74
+ specify "should merge the specified options" do
75
+ clone = @dataset.clone(1 => 2)
76
+ clone.opts.should == {1 => 2, :from => [:items]}
77
+ end
78
+
79
+ specify "should overwrite existing options" do
80
+ clone = @dataset.clone(:from => [:other])
81
+ clone.opts.should == {:from => [:other]}
82
+ end
83
+
84
+ specify "should create a clone with a deep copy of options" do
85
+ clone = @dataset.clone(:from => [:other])
86
+ @dataset.opts[:from].should == [:items]
87
+ clone.opts[:from].should == [:other]
88
+ end
89
+
90
+ specify "should return an object with the same modules included" do
91
+ m = Module.new do
92
+ def __xyz__; "xyz"; end
93
+ end
94
+ @dataset.extend(m)
95
+ @dataset.clone({}).should respond_to(:__xyz__)
96
+ end
97
+ end
98
+
99
+ context "A simple dataset" do
100
+ setup do
101
+ @dataset = Sequel::Dataset.new(nil).from(:test)
102
+ end
103
+
104
+ specify "should format a select statement" do
105
+ @dataset.select_sql.should == 'SELECT * FROM test'
106
+ end
107
+
108
+ specify "should format a delete statement" do
109
+ @dataset.delete_sql.should == 'DELETE FROM test'
110
+ end
111
+
112
+ specify "should format an insert statement with default values" do
113
+ @dataset.insert_sql.should == 'INSERT INTO test DEFAULT VALUES'
114
+ end
115
+
116
+ specify "should format an insert statement with hash" do
117
+ @dataset.insert_sql(:name => 'wxyz', :price => 342).
118
+ should match(/INSERT INTO test \(name, price\) VALUES \('wxyz', 342\)|INSERT INTO test \(price, name\) VALUES \(342, 'wxyz'\)/)
119
+
120
+ @dataset.insert_sql({}).should == "INSERT INTO test DEFAULT VALUES"
121
+ end
122
+
123
+ specify "should format an insert statement with string keys" do
124
+ @dataset.insert_sql('name' => 'wxyz', 'price' => 342).
125
+ should match(/INSERT INTO test \(name, price\) VALUES \('wxyz', 342\)|INSERT INTO test \(price, name\) VALUES \(342, 'wxyz'\)/)
126
+ end
127
+
128
+ specify "should format an insert statement with an object that respond_to? :values" do
129
+ dbb = Sequel::Database.new
130
+
131
+ v = Object.new
132
+ def v.values; {:a => 1}; end
133
+
134
+ @dataset.insert_sql(v).should == "INSERT INTO test (a) VALUES (1)"
135
+
136
+ def v.values; {}; end
137
+ @dataset.insert_sql(v).should == "INSERT INTO test DEFAULT VALUES"
138
+ end
139
+
140
+ specify "should format an insert statement with an arbitrary value" do
141
+ @dataset.insert_sql(123).should == "INSERT INTO test VALUES (123)"
142
+ end
143
+
144
+ specify "should format an insert statement with sub-query" do
145
+ @sub = Sequel::Dataset.new(nil).from(:something).filter(:x => 2)
146
+ @dataset.insert_sql(@sub).should == \
147
+ "INSERT INTO test (SELECT * FROM something WHERE (x = 2))"
148
+ end
149
+
150
+ specify "should format an insert statement with array" do
151
+ @dataset.insert_sql('a', 2, 6.5).should ==
152
+ "INSERT INTO test VALUES ('a', 2, 6.5)"
153
+ end
154
+
155
+ specify "should format an update statement" do
156
+ @dataset.update_sql(:name => 'abc').should ==
157
+ "UPDATE test SET name = 'abc'"
158
+ end
159
+
160
+ specify "should be able to return rows for arbitrary SQL" do
161
+ @dataset.select_sql(:sql => 'xxx yyy zzz').should ==
162
+ "xxx yyy zzz"
163
+ end
164
+ end
165
+
166
+ context "A dataset with multiple tables in its FROM clause" do
167
+ setup do
168
+ @dataset = Sequel::Dataset.new(nil).from(:t1, :t2)
169
+ end
170
+
171
+ specify "should raise on #update_sql" do
172
+ proc {@dataset.update_sql(:a=>1)}.should raise_error(Sequel::Error::InvalidOperation)
173
+ end
174
+
175
+ specify "should raise on #delete_sql" do
176
+ proc {@dataset.delete_sql}.should raise_error(Sequel::Error::InvalidOperation)
177
+ end
178
+
179
+ specify "should generate a select query FROM all specified tables" do
180
+ @dataset.select_sql.should == "SELECT * FROM t1, t2"
181
+ end
182
+ end
183
+
184
+ context "Dataset#where" do
185
+ setup do
186
+ @dataset = Sequel::Dataset.new(nil).from(:test)
187
+ @d1 = @dataset.where(:region => 'Asia')
188
+ @d2 = @dataset.where('region = ?', 'Asia')
189
+ @d3 = @dataset.where("a = 1")
190
+ end
191
+
192
+ specify "should work with hashes" do
193
+ @dataset.where(:name => 'xyz', :price => 342).select_sql.
194
+ should match(/WHERE \(\(name = 'xyz'\) AND \(price = 342\)\)|WHERE \(\(price = 342\) AND \(name = 'xyz'\)\)/)
195
+ end
196
+
197
+ specify "should work with arrays (ala ActiveRecord)" do
198
+ @dataset.where('price < ? AND id in ?', 100, [1, 2, 3]).select_sql.should ==
199
+ "SELECT * FROM test WHERE (price < 100 AND id in (1, 2, 3))"
200
+ end
201
+
202
+ specify "should work with strings (custom SQL expressions)" do
203
+ @dataset.where('(a = 1 AND b = 2)').select_sql.should ==
204
+ "SELECT * FROM test WHERE ((a = 1 AND b = 2))"
205
+ end
206
+
207
+ specify "should affect select, delete and update statements" do
208
+ @d1.select_sql.should == "SELECT * FROM test WHERE (region = 'Asia')"
209
+ @d1.delete_sql.should == "DELETE FROM test WHERE (region = 'Asia')"
210
+ @d1.update_sql(:GDP => 0).should == "UPDATE test SET GDP = 0 WHERE (region = 'Asia')"
211
+
212
+ @d2.select_sql.should == "SELECT * FROM test WHERE (region = 'Asia')"
213
+ @d2.delete_sql.should == "DELETE FROM test WHERE (region = 'Asia')"
214
+ @d2.update_sql(:GDP => 0).should == "UPDATE test SET GDP = 0 WHERE (region = 'Asia')"
215
+
216
+ @d3.select_sql.should == "SELECT * FROM test WHERE (a = 1)"
217
+ @d3.delete_sql.should == "DELETE FROM test WHERE (a = 1)"
218
+ @d3.update_sql(:GDP => 0).should == "UPDATE test SET GDP = 0 WHERE (a = 1)"
219
+
220
+ end
221
+
222
+ specify "should be composable using AND operator (for scoping)" do
223
+ # hashes are merged, no problem
224
+ @d1.where(:size => 'big').select_sql.should ==
225
+ "SELECT * FROM test WHERE ((region = 'Asia') AND (size = 'big'))"
226
+
227
+ # hash and string
228
+ @d1.where('population > 1000').select_sql.should ==
229
+ "SELECT * FROM test WHERE ((region = 'Asia') AND (population > 1000))"
230
+ @d1.where('(a > 1) OR (b < 2)').select_sql.should ==
231
+ "SELECT * FROM test WHERE ((region = 'Asia') AND ((a > 1) OR (b < 2)))"
232
+
233
+ # hash and array
234
+ @d1.where('GDP > ?', 1000).select_sql.should ==
235
+ "SELECT * FROM test WHERE ((region = 'Asia') AND (GDP > 1000))"
236
+
237
+ # array and array
238
+ @d2.where('GDP > ?', 1000).select_sql.should ==
239
+ "SELECT * FROM test WHERE ((region = 'Asia') AND (GDP > 1000))"
240
+
241
+ # array and hash
242
+ @d2.where(:name => ['Japan', 'China']).select_sql.should ==
243
+ "SELECT * FROM test WHERE ((region = 'Asia') AND (name IN ('Japan', 'China')))"
244
+
245
+ # array and string
246
+ @d2.where('GDP > ?').select_sql.should ==
247
+ "SELECT * FROM test WHERE ((region = 'Asia') AND (GDP > ?))"
248
+
249
+ # string and string
250
+ @d3.where('b = 2').select_sql.should ==
251
+ "SELECT * FROM test WHERE ((a = 1) AND (b = 2))"
252
+
253
+ # string and hash
254
+ @d3.where(:c => 3).select_sql.should ==
255
+ "SELECT * FROM test WHERE ((a = 1) AND (c = 3))"
256
+
257
+ # string and array
258
+ @d3.where('d = ?', 4).select_sql.should ==
259
+ "SELECT * FROM test WHERE ((a = 1) AND (d = 4))"
260
+ end
261
+
262
+ specify "should be composable using AND operator (for scoping) with block" do
263
+ @d3.where{:e < 5}.select_sql.should ==
264
+ "SELECT * FROM test WHERE ((a = 1) AND (e < 5))"
265
+ end
266
+
267
+ specify "should raise if the dataset is grouped" do
268
+ proc {@dataset.group(:t).where(:a => 1)}.should_not raise_error
269
+ @dataset.group(:t).where(:a => 1).sql.should ==
270
+ "SELECT * FROM test WHERE (a = 1) GROUP BY t"
271
+ end
272
+
273
+ specify "should accept ranges" do
274
+ @dataset.filter(:id => 4..7).sql.should ==
275
+ 'SELECT * FROM test WHERE ((id >= 4) AND (id <= 7))'
276
+ @dataset.filter(:id => 4...7).sql.should ==
277
+ 'SELECT * FROM test WHERE ((id >= 4) AND (id < 7))'
278
+
279
+ @dataset.filter(:table__id => 4..7).sql.should ==
280
+ 'SELECT * FROM test WHERE ((table.id >= 4) AND (table.id <= 7))'
281
+ @dataset.filter(:table__id => 4...7).sql.should ==
282
+ 'SELECT * FROM test WHERE ((table.id >= 4) AND (table.id < 7))'
283
+ end
284
+
285
+ specify "should accept nil" do
286
+ @dataset.filter(:owner_id => nil).sql.should ==
287
+ 'SELECT * FROM test WHERE (owner_id IS NULL)'
288
+ end
289
+
290
+ specify "should accept a subquery" do
291
+ @dataset.filter('gdp > ?', @d1.select(:avg[:gdp])).sql.should ==
292
+ "SELECT * FROM test WHERE (gdp > (SELECT avg(gdp) FROM test WHERE (region = 'Asia')))"
293
+
294
+ @dataset.filter(:id => @d1.select(:id)).sql.should ==
295
+ "SELECT * FROM test WHERE (id IN (SELECT id FROM test WHERE (region = 'Asia')))"
296
+ end
297
+
298
+ specify "should accept a subquery for an EXISTS clause" do
299
+ a = @dataset.filter(:price < 100)
300
+ @dataset.filter(a.exists).sql.should ==
301
+ 'SELECT * FROM test WHERE (EXISTS (SELECT * FROM test WHERE (price < 100)))'
302
+ end
303
+
304
+ specify "should accept proc expressions" do
305
+ d = @d1.select(:avg[:gdp])
306
+ @dataset.filter {:gdp > d}.sql.should ==
307
+ "SELECT * FROM test WHERE (gdp > (SELECT avg(gdp) FROM test WHERE (region = 'Asia')))"
308
+
309
+ @dataset.filter {:a < 1}.sql.should ==
310
+ 'SELECT * FROM test WHERE (a < 1)'
311
+
312
+ @dataset.filter {(:a >= 1) & (:b <= 2)}.sql.should ==
313
+ 'SELECT * FROM test WHERE ((a >= 1) AND (b <= 2))'
314
+
315
+ @dataset.filter {:c.like 'ABC%'}.sql.should ==
316
+ "SELECT * FROM test WHERE (c LIKE 'ABC%')"
317
+
318
+ @dataset.filter {:c.like 'ABC%'}.sql.should ==
319
+ "SELECT * FROM test WHERE (c LIKE 'ABC%')"
320
+
321
+ @dataset.filter {:c.like 'ABC%', '%XYZ'}.sql.should ==
322
+ "SELECT * FROM test WHERE ((c LIKE 'ABC%') OR (c LIKE '%XYZ'))"
323
+ end
324
+
325
+ specify "should work for grouped datasets" do
326
+ @dataset.group(:a).filter(:b => 1).sql.should ==
327
+ 'SELECT * FROM test WHERE (b = 1) GROUP BY a'
328
+ end
329
+
330
+ specify "should accept true and false as arguments" do
331
+ @dataset.filter(true).sql.should ==
332
+ "SELECT * FROM test WHERE 't'"
333
+ @dataset.filter(false).sql.should ==
334
+ "SELECT * FROM test WHERE 'f'"
335
+ end
336
+
337
+ specify "should allow the use of blocks and arguments simultaneously" do
338
+ @dataset.filter(:zz < 3){:yy > 3}.sql.should ==
339
+ 'SELECT * FROM test WHERE ((zz < 3) AND (yy > 3))'
340
+ end
341
+
342
+ specify "should yield a VirtualRow to the block" do
343
+ x = nil
344
+ @dataset.filter{|r| x = r; false}
345
+ x.should be_a_kind_of(Sequel::SQL::VirtualRow)
346
+ @dataset.filter{|r| ((r.name < 'b') & {r.table__id => 1}) | r.is_active(r.blah, r.xx, r.x__y_z)}.sql.should ==
347
+ "SELECT * FROM test WHERE (((name < 'b') AND (table.id = 1)) OR is_active(blah, xx, x.y_z))"
348
+ end
349
+
350
+ specify "should raise an error if an invalid argument is used" do
351
+ proc{@dataset.filter(1)}.should raise_error(Sequel::Error)
352
+ end
353
+
354
+ specify "should raise an error if a NumericExpression or StringExpression is used" do
355
+ proc{@dataset.filter(:x + 1)}.should raise_error(Sequel::Error)
356
+ proc{@dataset.filter(:x.sql_string)}.should raise_error(Sequel::Error)
357
+ end
358
+ end
359
+
360
+ context "Dataset#or" do
361
+ setup do
362
+ @dataset = Sequel::Dataset.new(nil).from(:test)
363
+ @d1 = @dataset.where(:x => 1)
364
+ end
365
+
366
+ specify "should raise if no filter exists" do
367
+ proc {@dataset.or(:a => 1)}.should raise_error(Sequel::Error)
368
+ end
369
+
370
+ specify "should add an alternative expression to the where clause" do
371
+ @d1.or(:y => 2).sql.should ==
372
+ 'SELECT * FROM test WHERE ((x = 1) OR (y = 2))'
373
+ end
374
+
375
+ specify "should accept all forms of filters" do
376
+ @d1.or('y > ?', 2).sql.should ==
377
+ 'SELECT * FROM test WHERE ((x = 1) OR (y > 2))'
378
+ @d1.or(:yy > 3).sql.should ==
379
+ 'SELECT * FROM test WHERE ((x = 1) OR (yy > 3))'
380
+ end
381
+
382
+ specify "should accept blocks passed to filter" do
383
+ @d1.or{:yy > 3}.sql.should ==
384
+ 'SELECT * FROM test WHERE ((x = 1) OR (yy > 3))'
385
+ end
386
+
387
+ specify "should correctly add parens to give predictable results" do
388
+ @d1.filter(:y => 2).or(:z => 3).sql.should ==
389
+ 'SELECT * FROM test WHERE (((x = 1) AND (y = 2)) OR (z = 3))'
390
+
391
+ @d1.or(:y => 2).filter(:z => 3).sql.should ==
392
+ 'SELECT * FROM test WHERE (((x = 1) OR (y = 2)) AND (z = 3))'
393
+ end
394
+
395
+ specify "should allow the use of blocks and arguments simultaneously" do
396
+ @d1.or(:zz < 3){:yy > 3}.sql.should ==
397
+ 'SELECT * FROM test WHERE ((x = 1) OR ((zz < 3) AND (yy > 3)))'
398
+ end
399
+ end
400
+
401
+ context "Dataset#and" do
402
+ setup do
403
+ @dataset = Sequel::Dataset.new(nil).from(:test)
404
+ @d1 = @dataset.where(:x => 1)
405
+ end
406
+
407
+ specify "should raise if no filter exists" do
408
+ proc {@dataset.and(:a => 1)}.should raise_error(Sequel::Error)
409
+ proc {@dataset.where(:a => 1).group(:t).and(:b => 2)}.should_not raise_error(Sequel::Error)
410
+ @dataset.where(:a => 1).group(:t).and(:b => 2).sql ==
411
+ "SELECT * FROM test WHERE (a = 1) AND (b = 2) GROUP BY t"
412
+ end
413
+
414
+ specify "should add an alternative expression to the where clause" do
415
+ @d1.and(:y => 2).sql.should ==
416
+ 'SELECT * FROM test WHERE ((x = 1) AND (y = 2))'
417
+ end
418
+
419
+ specify "should accept all forms of filters" do
420
+ # probably not exhaustive, but good enough
421
+ @d1.and('y > ?', 2).sql.should ==
422
+ 'SELECT * FROM test WHERE ((x = 1) AND (y > 2))'
423
+ @d1.and(:yy > 3).sql.should ==
424
+ 'SELECT * FROM test WHERE ((x = 1) AND (yy > 3))'
425
+ end
426
+
427
+ specify "should accept blocks passed to filter" do
428
+ @d1.and {:yy > 3}.sql.should ==
429
+ 'SELECT * FROM test WHERE ((x = 1) AND (yy > 3))'
430
+ end
431
+
432
+ specify "should correctly add parens to give predictable results" do
433
+ @d1.or(:y => 2).and(:z => 3).sql.should ==
434
+ 'SELECT * FROM test WHERE (((x = 1) OR (y = 2)) AND (z = 3))'
435
+
436
+ @d1.and(:y => 2).or(:z => 3).sql.should ==
437
+ 'SELECT * FROM test WHERE (((x = 1) AND (y = 2)) OR (z = 3))'
438
+ end
439
+ end
440
+
441
+ context "Dataset#exclude" do
442
+ setup do
443
+ @dataset = Sequel::Dataset.new(nil).from(:test)
444
+ end
445
+
446
+ specify "should correctly include the NOT operator when one condition is given" do
447
+ @dataset.exclude(:region=>'Asia').select_sql.should ==
448
+ "SELECT * FROM test WHERE (region != 'Asia')"
449
+ end
450
+
451
+ specify "should take multiple conditions as a hash and express the logic correctly in SQL" do
452
+ @dataset.exclude(:region => 'Asia', :name => 'Japan').select_sql.
453
+ should match(Regexp.union(/WHERE \(\(region != 'Asia'\) AND \(name != 'Japan'\)\)/,
454
+ /WHERE \(\(name != 'Japan'\) AND \(region != 'Asia'\)\)/))
455
+ end
456
+
457
+ specify "should parenthesize a single string condition correctly" do
458
+ @dataset.exclude("region = 'Asia' AND name = 'Japan'").select_sql.should ==
459
+ "SELECT * FROM test WHERE NOT (region = 'Asia' AND name = 'Japan')"
460
+ end
461
+
462
+ specify "should parenthesize an array condition correctly" do
463
+ @dataset.exclude('region = ? AND name = ?', 'Asia', 'Japan').select_sql.should ==
464
+ "SELECT * FROM test WHERE NOT (region = 'Asia' AND name = 'Japan')"
465
+ end
466
+
467
+ specify "should correctly parenthesize when it is used twice" do
468
+ @dataset.exclude(:region => 'Asia').exclude(:name => 'Japan').select_sql.should ==
469
+ "SELECT * FROM test WHERE ((region != 'Asia') AND (name != 'Japan'))"
470
+ end
471
+
472
+ specify "should support proc expressions" do
473
+ @dataset.exclude{:id < 6}.sql.should ==
474
+ 'SELECT * FROM test WHERE (id >= 6)'
475
+ end
476
+
477
+ specify "should allow the use of blocks and arguments simultaneously" do
478
+ @dataset.exclude(:id => (7..11)){:id < 6}.sql.should ==
479
+ 'SELECT * FROM test WHERE (((id < 7) OR (id > 11)) OR (id >= 6))'
480
+ end
481
+ end
482
+
483
+ context "Dataset#invert" do
484
+ setup do
485
+ @d = Sequel::Dataset.new(nil).from(:test)
486
+ end
487
+
488
+ specify "should raise error if the dataset is not filtered" do
489
+ proc{@d.invert}.should raise_error(Sequel::Error)
490
+ end
491
+
492
+ specify "should invert current filter if dataset is filtered" do
493
+ @d.filter(:x).invert.sql.should == 'SELECT * FROM test WHERE NOT x'
494
+ end
495
+
496
+ specify "should invert both having and where if both are preset" do
497
+ @d.filter(:x).group(:x).having(:x).invert.sql.should == 'SELECT * FROM test WHERE NOT x GROUP BY x HAVING NOT x'
498
+ end
499
+ end
500
+
501
+ context "Dataset#having" do
502
+ setup do
503
+ @dataset = Sequel::Dataset.new(nil).from(:test)
504
+ @grouped = @dataset.group(:region).select(:region, :sum[:population], :avg[:gdp])
505
+ @d1 = @grouped.having('sum(population) > 10')
506
+ @d2 = @grouped.having(:region => 'Asia')
507
+ @columns = "region, sum(population), avg(gdp)"
508
+ end
509
+
510
+ specify "should raise if the dataset is not grouped" do
511
+ proc {@dataset.having('avg(gdp) > 10')}.should raise_error(Sequel::Error::InvalidOperation)
512
+ end
513
+
514
+ specify "should affect select statements" do
515
+ @d1.select_sql.should ==
516
+ "SELECT #{@columns} FROM test GROUP BY region HAVING (sum(population) > 10)"
517
+ end
518
+
519
+ specify "should support proc expressions" do
520
+ @grouped.having {:sum[:population] > 10}.sql.should ==
521
+ "SELECT #{@columns} FROM test GROUP BY region HAVING (sum(population) > 10)"
522
+ end
523
+
524
+ specify "should work with and on the having clause" do
525
+ @grouped.having( :a > 1 ).and( :b < 2 ).sql.should ==
526
+ "SELECT #{@columns} FROM test GROUP BY region HAVING ((a > 1) AND (b < 2))"
527
+ end
528
+ end
529
+
530
+ context "a grouped dataset" do
531
+ setup do
532
+ @dataset = Sequel::Dataset.new(nil).from(:test).group(:type_id)
533
+ end
534
+
535
+ specify "should raise when trying to generate an update statement" do
536
+ proc {@dataset.update_sql(:id => 0)}.should raise_error
537
+ end
538
+
539
+ specify "should raise when trying to generate a delete statement" do
540
+ proc {@dataset.delete_sql}.should raise_error
541
+ end
542
+
543
+ specify "should specify the grouping in generated select statement" do
544
+ @dataset.select_sql.should ==
545
+ "SELECT * FROM test GROUP BY type_id"
546
+ end
547
+
548
+ specify "should format the right statement for counting (as a subquery)" do
549
+ db = MockDatabase.new
550
+ db[:test].select(:name).group(:name).count
551
+ db.sqls.should == ["SELECT COUNT(*) FROM (SELECT name FROM test GROUP BY name) t1 LIMIT 1"]
552
+ end
553
+ end
554
+
555
+ context "Dataset#group_by" do
556
+ setup do
557
+ @dataset = Sequel::Dataset.new(nil).from(:test).group_by(:type_id)
558
+ end
559
+
560
+ specify "should raise when trying to generate an update statement" do
561
+ proc {@dataset.update_sql(:id => 0)}.should raise_error
562
+ end
563
+
564
+ specify "should raise when trying to generate a delete statement" do
565
+ proc {@dataset.delete_sql}.should raise_error
566
+ end
567
+
568
+ specify "should specify the grouping in generated select statement" do
569
+ @dataset.select_sql.should ==
570
+ "SELECT * FROM test GROUP BY type_id"
571
+ @dataset.group_by(:a, :b).select_sql.should ==
572
+ "SELECT * FROM test GROUP BY a, b"
573
+ end
574
+
575
+ specify "should specify the grouping in generated select statement" do
576
+ @dataset.group_by(:type_id=>nil).select_sql.should ==
577
+ "SELECT * FROM test GROUP BY (type_id IS NULL)"
578
+ end
579
+
580
+ specify "should be aliased as #group" do
581
+ @dataset.group(:type_id=>nil).select_sql.should ==
582
+ "SELECT * FROM test GROUP BY (type_id IS NULL)"
583
+ end
584
+ end
585
+
586
+ context "Dataset#as" do
587
+ specify "should set up an alias" do
588
+ dataset = Sequel::Dataset.new(nil).from(:test)
589
+ dataset.select(dataset.limit(1).select(:name).as(:n)).sql.should == \
590
+ 'SELECT (SELECT name FROM test LIMIT 1) AS n FROM test'
591
+ end
592
+ end
593
+
594
+ context "Dataset#literal" do
595
+ setup do
596
+ @dataset = Sequel::Dataset.new(nil).from(:test)
597
+ end
598
+
599
+ specify "should escape strings properly" do
600
+ @dataset.literal('abc').should == "'abc'"
601
+ @dataset.literal('a"x"bc').should == "'a\"x\"bc'"
602
+ @dataset.literal("a'bc").should == "'a''bc'"
603
+ @dataset.literal("a''bc").should == "'a''''bc'"
604
+ @dataset.literal("a\\bc").should == "'a\\\\bc'"
605
+ @dataset.literal("a\\\\bc").should == "'a\\\\\\\\bc'"
606
+ @dataset.literal("a\\'bc").should == "'a\\\\''bc'"
607
+ end
608
+
609
+ specify "should literalize numbers properly" do
610
+ @dataset.literal(1).should == "1"
611
+ @dataset.literal(1.5).should == "1.5"
612
+ end
613
+
614
+ specify "should literalize nil as NULL" do
615
+ @dataset.literal(nil).should == "NULL"
616
+ end
617
+
618
+ specify "should literalize an array properly" do
619
+ @dataset.literal([]).should == "(NULL)"
620
+ @dataset.literal([1, 'abc', 3]).should == "(1, 'abc', 3)"
621
+ @dataset.literal([1, "a'b''c", 3]).should == "(1, 'a''b''''c', 3)"
622
+ end
623
+
624
+ specify "should literalize symbols as column references" do
625
+ @dataset.literal(:name).should == "name"
626
+ @dataset.literal(:items__name).should == "items.name"
627
+ end
628
+
629
+ specify "should raise an error for unsupported types" do
630
+ proc {@dataset.literal({})}.should raise_error
631
+ end
632
+
633
+ specify "should literalize datasets as subqueries" do
634
+ d = @dataset.from(:test)
635
+ d.literal(d).should == "(#{d.sql})"
636
+ end
637
+
638
+ specify "should literalize Time properly" do
639
+ t = Time.now
640
+ s = t.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'")
641
+ @dataset.literal(t).should == s
642
+ end
643
+
644
+ specify "should literalize Date properly" do
645
+ d = Date.today
646
+ s = d.strftime("DATE '%Y-%m-%d'")
647
+ @dataset.literal(d).should == s
648
+ end
649
+
650
+ specify "should not literalize expression strings" do
651
+ @dataset.literal('col1 + 2'.expr).should == 'col1 + 2'
652
+
653
+ @dataset.update_sql(:a => 'a + 2'.expr).should ==
654
+ 'UPDATE test SET a = a + 2'
655
+ end
656
+
657
+ specify "should literalize BigDecimal instances correctly" do
658
+ @dataset.literal(BigDecimal.new("80")).should == "80.0"
659
+ end
660
+
661
+ specify "should raise an Error if the object can't be literalized" do
662
+ proc{@dataset.literal(Object.new)}.should raise_error(Sequel::Error)
663
+ end
664
+ end
665
+
666
+ context "Dataset#from" do
667
+ setup do
668
+ @dataset = Sequel::Dataset.new(nil)
669
+ end
670
+
671
+ specify "should accept a Dataset" do
672
+ proc {@dataset.from(@dataset)}.should_not raise_error
673
+ end
674
+
675
+ specify "should format a Dataset as a subquery if it has had options set" do
676
+ @dataset.from(@dataset.from(:a).where(:a=>1)).select_sql.should ==
677
+ "SELECT * FROM (SELECT * FROM a WHERE (a = 1)) t1"
678
+ end
679
+
680
+ specify "should automatically alias sub-queries" do
681
+ @dataset.from(@dataset.from(:a).group(:b)).select_sql.should ==
682
+ "SELECT * FROM (SELECT * FROM a GROUP BY b) t1"
683
+
684
+ d1 = @dataset.from(:a).group(:b)
685
+ d2 = @dataset.from(:c).group(:d)
686
+
687
+ @dataset.from(d1, d2).sql.should ==
688
+ "SELECT * FROM (SELECT * FROM a GROUP BY b) t1, (SELECT * FROM c GROUP BY d) t2"
689
+ end
690
+
691
+ specify "should accept a hash for aliasing" do
692
+ @dataset.from(:a => :b).sql.should ==
693
+ "SELECT * FROM a b"
694
+
695
+ @dataset.from(:a => 'b').sql.should ==
696
+ "SELECT * FROM a b"
697
+
698
+ @dataset.from(:a => :c[:d]).sql.should ==
699
+ "SELECT * FROM a c(d)"
700
+
701
+ @dataset.from(@dataset.from(:a).group(:b) => :c).sql.should ==
702
+ "SELECT * FROM (SELECT * FROM a GROUP BY b) c"
703
+ end
704
+
705
+ specify "should always use a subquery if given a dataset" do
706
+ @dataset.from(@dataset.from(:a)).select_sql.should ==
707
+ "SELECT * FROM (SELECT * FROM a) t1"
708
+ end
709
+
710
+ specify "should raise if no source is given" do
711
+ proc {@dataset.from(@dataset.from).select_sql}.should raise_error(Sequel::Error)
712
+ end
713
+
714
+ specify "should accept sql functions" do
715
+ @dataset.from(:abc[:def]).select_sql.should ==
716
+ "SELECT * FROM abc(def)"
717
+
718
+ @dataset.from(:a[:i]).select_sql.should ==
719
+ "SELECT * FROM a(i)"
720
+ end
721
+ end
722
+
723
+ context "Dataset#select" do
724
+ setup do
725
+ @d = Sequel::Dataset.new(nil).from(:test)
726
+ end
727
+
728
+ specify "should accept variable arity" do
729
+ @d.select(:name).sql.should == 'SELECT name FROM test'
730
+ @d.select(:a, :b, :test__c).sql.should == 'SELECT a, b, test.c FROM test'
731
+ end
732
+
733
+ specify "should accept symbols and literal strings" do
734
+ @d.select('aaa'.lit).sql.should == 'SELECT aaa FROM test'
735
+ @d.select(:a, 'b'.lit).sql.should == 'SELECT a, b FROM test'
736
+ @d.select(:test__cc, 'test.d AS e'.lit).sql.should ==
737
+ 'SELECT test.cc, test.d AS e FROM test'
738
+ @d.select('test.d AS e'.lit, :test__cc).sql.should ==
739
+ 'SELECT test.d AS e, test.cc FROM test'
740
+
741
+ # symbol helpers
742
+ @d.select(:test.*).sql.should ==
743
+ 'SELECT test.* FROM test'
744
+ @d.select(:test__name.as(:n)).sql.should ==
745
+ 'SELECT test.name AS n FROM test'
746
+ @d.select(:test__name___n).sql.should ==
747
+ 'SELECT test.name AS n FROM test'
748
+ end
749
+
750
+ specify "should use the wildcard if no arguments are given" do
751
+ @d.select.sql.should == 'SELECT * FROM test'
752
+ end
753
+
754
+ specify "should accept a hash for AS values" do
755
+ @d.select(:name => 'n', :__ggh => 'age').sql.should =~
756
+ /SELECT ((name AS n, __ggh AS age)|(__ggh AS age, name AS n)) FROM test/
757
+ end
758
+
759
+ specify "should overrun the previous select option" do
760
+ @d.select!(:a, :b, :c).select.sql.should == 'SELECT * FROM test'
761
+ @d.select!(:price).select(:name).sql.should == 'SELECT name FROM test'
762
+ end
763
+
764
+ specify "should accept arbitrary objects and literalize them correctly" do
765
+ @d.select(1, :a, 't').sql.should == "SELECT 1, a, 't' FROM test"
766
+
767
+ @d.select(nil, :sum[:t], :x___y).sql.should == "SELECT NULL, sum(t), x AS y FROM test"
768
+
769
+ @d.select(nil, 1, :x => :y).sql.should == "SELECT NULL, 1, x AS y FROM test"
770
+ end
771
+ end
772
+
773
+ context "Dataset#select_all" do
774
+ setup do
775
+ @d = Sequel::Dataset.new(nil).from(:test)
776
+ end
777
+
778
+ specify "should select the wildcard" do
779
+ @d.select_all.sql.should == 'SELECT * FROM test'
780
+ end
781
+
782
+ specify "should overrun the previous select option" do
783
+ @d.select!(:a, :b, :c).select_all.sql.should == 'SELECT * FROM test'
784
+ end
785
+ end
786
+
787
+ context "Dataset#select_more" do
788
+ setup do
789
+ @d = Sequel::Dataset.new(nil).from(:test)
790
+ end
791
+
792
+ specify "should act like #select for datasets with no selection" do
793
+ @d.select_more(:a, :b).sql.should == 'SELECT a, b FROM test'
794
+ @d.select_all.select_more(:a, :b).sql.should == 'SELECT a, b FROM test'
795
+ @d.select(:blah).select_all.select_more(:a, :b).sql.should == 'SELECT a, b FROM test'
796
+ end
797
+
798
+ specify "should add to the currently selected columns" do
799
+ @d.select(:a).select_more(:b).sql.should == 'SELECT a, b FROM test'
800
+ @d.select(:a.*).select_more(:b.*).sql.should == 'SELECT a.*, b.* FROM test'
801
+ end
802
+ end
803
+
804
+ context "Dataset#order" do
805
+ setup do
806
+ @dataset = Sequel::Dataset.new(nil).from(:test)
807
+ end
808
+
809
+ specify "should include an ORDER BY clause in the select statement" do
810
+ @dataset.order(:name).sql.should ==
811
+ 'SELECT * FROM test ORDER BY name'
812
+ end
813
+
814
+ specify "should accept multiple arguments" do
815
+ @dataset.order(:name, :price.desc).sql.should ==
816
+ 'SELECT * FROM test ORDER BY name, price DESC'
817
+ end
818
+
819
+ specify "should overrun a previous ordering" do
820
+ @dataset.order(:name).order(:stamp).sql.should ==
821
+ 'SELECT * FROM test ORDER BY stamp'
822
+ end
823
+
824
+ specify "should accept a literal string" do
825
+ @dataset.order('dada ASC'.lit).sql.should ==
826
+ 'SELECT * FROM test ORDER BY dada ASC'
827
+ end
828
+
829
+ specify "should accept a hash as an expression" do
830
+ @dataset.order(:name=>nil).sql.should ==
831
+ 'SELECT * FROM test ORDER BY (name IS NULL)'
832
+ end
833
+
834
+ specify "should accept a nil to remove ordering" do
835
+ @dataset.order(:bah).order(nil).sql.should ==
836
+ 'SELECT * FROM test'
837
+ end
838
+ end
839
+
840
+ context "Dataset#unfiltered" do
841
+ setup do
842
+ @dataset = Sequel::Dataset.new(nil).from(:test)
843
+ end
844
+
845
+ specify "should remove filtering from the dataset" do
846
+ @dataset.filter(:score=>1).unfiltered.sql.should ==
847
+ 'SELECT * FROM test'
848
+ end
849
+ end
850
+
851
+ context "Dataset#unordered" do
852
+ setup do
853
+ @dataset = Sequel::Dataset.new(nil).from(:test)
854
+ end
855
+
856
+ specify "should remove ordering from the dataset" do
857
+ @dataset.order(:name).unordered.sql.should ==
858
+ 'SELECT * FROM test'
859
+ end
860
+ end
861
+
862
+ context "Dataset#order_by" do
863
+ setup do
864
+ @dataset = Sequel::Dataset.new(nil).from(:test)
865
+ end
866
+
867
+ specify "should include an ORDER BY clause in the select statement" do
868
+ @dataset.order_by(:name).sql.should ==
869
+ 'SELECT * FROM test ORDER BY name'
870
+ end
871
+
872
+ specify "should accept multiple arguments" do
873
+ @dataset.order_by(:name, :price.desc).sql.should ==
874
+ 'SELECT * FROM test ORDER BY name, price DESC'
875
+ end
876
+
877
+ specify "should overrun a previous ordering" do
878
+ @dataset.order_by(:name).order(:stamp).sql.should ==
879
+ 'SELECT * FROM test ORDER BY stamp'
880
+ end
881
+
882
+ specify "should accept a string" do
883
+ @dataset.order_by('dada ASC'.lit).sql.should ==
884
+ 'SELECT * FROM test ORDER BY dada ASC'
885
+ end
886
+
887
+ specify "should accept a nil to remove ordering" do
888
+ @dataset.order_by(:bah).order_by(nil).sql.should ==
889
+ 'SELECT * FROM test'
890
+ end
891
+ end
892
+
893
+ context "Dataset#order_more" do
894
+ setup do
895
+ @dataset = Sequel::Dataset.new(nil).from(:test)
896
+ end
897
+
898
+ specify "should include an ORDER BY clause in the select statement" do
899
+ @dataset.order_more(:name).sql.should ==
900
+ 'SELECT * FROM test ORDER BY name'
901
+ end
902
+
903
+ specify "should add to a previous ordering" do
904
+ @dataset.order(:name).order_more(:stamp.desc).sql.should ==
905
+ 'SELECT * FROM test ORDER BY name, stamp DESC'
906
+ end
907
+ end
908
+
909
+ context "Dataset#reverse_order" do
910
+ setup do
911
+ @dataset = Sequel::Dataset.new(nil).from(:test)
912
+ end
913
+
914
+ specify "should use DESC as default order" do
915
+ @dataset.reverse_order(:name).sql.should ==
916
+ 'SELECT * FROM test ORDER BY name DESC'
917
+ end
918
+
919
+ specify "should invert the order given" do
920
+ @dataset.reverse_order(:name.desc).sql.should ==
921
+ 'SELECT * FROM test ORDER BY name ASC'
922
+ end
923
+
924
+ specify "should invert the order for ASC expressions" do
925
+ @dataset.reverse_order(:name.asc).sql.should ==
926
+ 'SELECT * FROM test ORDER BY name DESC'
927
+ end
928
+
929
+ specify "should accept multiple arguments" do
930
+ @dataset.reverse_order(:name, :price.desc).sql.should ==
931
+ 'SELECT * FROM test ORDER BY name DESC, price ASC'
932
+ end
933
+
934
+ specify "should reverse a previous ordering if no arguments are given" do
935
+ @dataset.order(:name).reverse_order.sql.should ==
936
+ 'SELECT * FROM test ORDER BY name DESC'
937
+ @dataset.order(:clumsy.desc, :fool).reverse_order.sql.should ==
938
+ 'SELECT * FROM test ORDER BY clumsy ASC, fool DESC'
939
+ end
940
+
941
+ specify "should return an unordered dataset for a dataset with no order" do
942
+ @dataset.unordered.reverse_order.sql.should ==
943
+ 'SELECT * FROM test'
944
+ end
945
+
946
+ specify "should have #reverse alias" do
947
+ @dataset.order(:name).reverse.sql.should ==
948
+ 'SELECT * FROM test ORDER BY name DESC'
949
+ end
950
+ end
951
+
952
+ context "Dataset#limit" do
953
+ setup do
954
+ @dataset = Sequel::Dataset.new(nil).from(:test)
955
+ end
956
+
957
+ specify "should include a LIMIT clause in the select statement" do
958
+ @dataset.limit(10).sql.should ==
959
+ 'SELECT * FROM test LIMIT 10'
960
+ end
961
+
962
+ specify "should accept ranges" do
963
+ @dataset.limit(3..7).sql.should ==
964
+ 'SELECT * FROM test LIMIT 5 OFFSET 3'
965
+
966
+ @dataset.limit(3...7).sql.should ==
967
+ 'SELECT * FROM test LIMIT 4 OFFSET 3'
968
+ end
969
+
970
+ specify "should include an offset if a second argument is given" do
971
+ @dataset.limit(6, 10).sql.should ==
972
+ 'SELECT * FROM test LIMIT 6 OFFSET 10'
973
+ end
974
+
975
+ specify "should work with fixed sql datasets" do
976
+ @dataset.opts[:sql] = 'select * from cccc'
977
+ @dataset.limit(6, 10).sql.should ==
978
+ 'SELECT * FROM (select * from cccc) t1 LIMIT 6 OFFSET 10'
979
+ end
980
+
981
+ specify "should raise an error if an invalid limit or offset is used" do
982
+ proc{@dataset.limit(-1)}.should raise_error(Sequel::Error)
983
+ proc{@dataset.limit(0)}.should raise_error(Sequel::Error)
984
+ proc{@dataset.limit(1)}.should_not raise_error(Sequel::Error)
985
+ proc{@dataset.limit(1, -1)}.should raise_error(Sequel::Error)
986
+ proc{@dataset.limit(1, 0)}.should_not raise_error(Sequel::Error)
987
+ proc{@dataset.limit(1, 1)}.should_not raise_error(Sequel::Error)
988
+ end
989
+ end
990
+
991
+ context "Dataset#naked" do
992
+ setup do
993
+ @d1 = Sequel::Dataset.new(nil, {1 => 2, 3 => 4})
994
+ @d2 = Sequel::Dataset.new(nil, {1 => 2, 3 => 4}).set_model(Object)
995
+ end
996
+
997
+ specify "should return a clone with :naked option set" do
998
+ naked = @d1.naked
999
+ naked.opts[:naked].should be_true
1000
+ end
1001
+
1002
+ specify "should remove any existing reference to a model class" do
1003
+ naked = @d2.naked
1004
+ naked.opts[:models].should be_nil
1005
+ end
1006
+ end
1007
+
1008
+ context "Dataset#qualified_column_name" do
1009
+ setup do
1010
+ @dataset = Sequel::Dataset.new(nil).from(:test)
1011
+ end
1012
+
1013
+ specify "should return the literal value if not given a symbol" do
1014
+ @dataset.literal(@dataset.send(:qualified_column_name, 'ccc__b', :items)).should == "'ccc__b'"
1015
+ @dataset.literal(@dataset.send(:qualified_column_name, 3, :items)).should == '3'
1016
+ @dataset.literal(@dataset.send(:qualified_column_name, 'a'.lit, :items)).should == 'a'
1017
+ end
1018
+
1019
+ specify "should qualify the column with the supplied table name if given an unqualified symbol" do
1020
+ @dataset.literal(@dataset.send(:qualified_column_name, :b1, :items)).should == 'items.b1'
1021
+ end
1022
+
1023
+ specify "should not changed the qualifed column's table if given a qualified symbol" do
1024
+ @dataset.literal(@dataset.send(:qualified_column_name, :ccc__b, :items)).should == 'ccc.b'
1025
+ end
1026
+ end
1027
+
1028
+ class DummyDataset < Sequel::Dataset
1029
+ VALUES = [
1030
+ {:a => 1, :b => 2},
1031
+ {:a => 3, :b => 4},
1032
+ {:a => 5, :b => 6}
1033
+ ]
1034
+ def fetch_rows(sql, &block)
1035
+ VALUES.each(&block)
1036
+ end
1037
+ end
1038
+
1039
+ context "Dataset#map" do
1040
+ setup do
1041
+ @d = DummyDataset.new(nil).from(:items)
1042
+ end
1043
+
1044
+ specify "should provide the usual functionality if no argument is given" do
1045
+ @d.map {|n| n[:a] + n[:b]}.should == [3, 7, 11]
1046
+ end
1047
+
1048
+ specify "should map using #[column name] if column name is given" do
1049
+ @d.map(:a).should == [1, 3, 5]
1050
+ end
1051
+
1052
+ specify "should return the complete dataset values if nothing is given" do
1053
+ @d.map.to_a.should == DummyDataset::VALUES
1054
+ end
1055
+ end
1056
+
1057
+ context "Dataset#to_hash" do
1058
+ setup do
1059
+ @d = DummyDataset.new(nil).from(:items)
1060
+ end
1061
+
1062
+ specify "should provide a hash with the first column as key and the second as value" do
1063
+ @d.to_hash(:a, :b).should == {1 => 2, 3 => 4, 5 => 6}
1064
+ @d.to_hash(:b, :a).should == {2 => 1, 4 => 3, 6 => 5}
1065
+ end
1066
+
1067
+ specify "should provide a hash with the first column as key and the entire hash as value if the value column is blank or nil" do
1068
+ @d.to_hash(:a).should == {1 => {:a => 1, :b => 2}, 3 => {:a => 3, :b => 4}, 5 => {:a => 5, :b => 6}}
1069
+ @d.to_hash(:b).should == {2 => {:a => 1, :b => 2}, 4 => {:a => 3, :b => 4}, 6 => {:a => 5, :b => 6}}
1070
+ end
1071
+ end
1072
+
1073
+ context "Dataset#uniq" do
1074
+ setup do
1075
+ @db = MockDatabase.new
1076
+ @dataset = @db[:test].select(:name)
1077
+ end
1078
+
1079
+ specify "should include DISTINCT clause in statement" do
1080
+ @dataset.uniq.sql.should == 'SELECT DISTINCT name FROM test'
1081
+ end
1082
+
1083
+ specify "should be aliased by Dataset#distinct" do
1084
+ @dataset.distinct.sql.should == 'SELECT DISTINCT name FROM test'
1085
+ end
1086
+
1087
+ specify "should accept an expression list" do
1088
+ @dataset.uniq(:a, :b).sql.should == 'SELECT DISTINCT ON (a, b) name FROM test'
1089
+
1090
+ @dataset.uniq(:stamp.cast_as(:integer), :node_id=>nil).sql.should == 'SELECT DISTINCT ON (cast(stamp AS integer), (node_id IS NULL)) name FROM test'
1091
+ end
1092
+
1093
+ specify "should do a subselect for count" do
1094
+ @dataset.uniq.count
1095
+ @db.sqls.should == ['SELECT COUNT(*) FROM (SELECT DISTINCT name FROM test) t1 LIMIT 1']
1096
+ end
1097
+ end
1098
+
1099
+ context "Dataset#count" do
1100
+ setup do
1101
+ @c = Class.new(Sequel::Dataset) do
1102
+ def self.sql
1103
+ @@sql
1104
+ end
1105
+
1106
+ def fetch_rows(sql)
1107
+ @@sql = sql
1108
+ yield({1 => 1})
1109
+ end
1110
+ end
1111
+ @dataset = @c.new(nil).from(:test)
1112
+ end
1113
+
1114
+ specify "should format SQL properly" do
1115
+ @dataset.count.should == 1
1116
+ @c.sql.should == 'SELECT COUNT(*) FROM test LIMIT 1'
1117
+ end
1118
+
1119
+ specify "should be aliased by #size" do
1120
+ @dataset.size.should == 1
1121
+ end
1122
+
1123
+ specify "should include the where clause if it's there" do
1124
+ @dataset.filter(:abc < 30).count.should == 1
1125
+ @c.sql.should == 'SELECT COUNT(*) FROM test WHERE (abc < 30) LIMIT 1'
1126
+ end
1127
+
1128
+ specify "should count properly for datasets with fixed sql" do
1129
+ @dataset.opts[:sql] = "select abc from xyz"
1130
+ @dataset.count.should == 1
1131
+ @c.sql.should == "SELECT COUNT(*) FROM (select abc from xyz) t1 LIMIT 1"
1132
+ end
1133
+
1134
+ specify "should return limit if count is greater than it" do
1135
+ @dataset.limit(5).count.should == 1
1136
+ @c.sql.should == "SELECT COUNT(*) FROM (SELECT * FROM test LIMIT 5) t1 LIMIT 1"
1137
+ end
1138
+ end
1139
+
1140
+
1141
+ context "Dataset#group_and_count" do
1142
+ setup do
1143
+ @c = Class.new(Sequel::Dataset) do
1144
+ def self.sql
1145
+ @@sql
1146
+ end
1147
+
1148
+ def fetch_rows(sql)
1149
+ @@sql = sql
1150
+ yield({1 => 1})
1151
+ end
1152
+ end
1153
+ @ds = @c.new(nil).from(:test)
1154
+ end
1155
+
1156
+ specify "should format SQL properly" do
1157
+ @ds.group_and_count(:name).sql.should ==
1158
+ "SELECT name, count(*) AS count FROM test GROUP BY name ORDER BY count"
1159
+ end
1160
+
1161
+ specify "should accept multiple columns for grouping" do
1162
+ @ds.group_and_count(:a, :b).sql.should ==
1163
+ "SELECT a, b, count(*) AS count FROM test GROUP BY a, b ORDER BY count"
1164
+ end
1165
+
1166
+ specify "should work within query block" do
1167
+ @ds.query{group_and_count(:a, :b)}.sql.should ==
1168
+ "SELECT a, b, count(*) AS count FROM test GROUP BY a, b ORDER BY count"
1169
+ end
1170
+ end
1171
+
1172
+ context "Dataset#empty?" do
1173
+ specify "should return true if records exist in the dataset" do
1174
+ @c = Class.new(Sequel::Dataset) do
1175
+ def self.sql
1176
+ @@sql
1177
+ end
1178
+
1179
+ def fetch_rows(sql)
1180
+ @@sql = sql
1181
+ yield({1 => 1}) unless sql =~ /WHERE 'f'/
1182
+ end
1183
+ end
1184
+ @c.new(nil).from(:test).should_not be_empty
1185
+ @c.sql.should == 'SELECT 1 FROM test LIMIT 1'
1186
+ @c.new(nil).from(:test).filter(false).should be_empty
1187
+ @c.sql.should == "SELECT 1 FROM test WHERE 'f' LIMIT 1"
1188
+ end
1189
+ end
1190
+
1191
+ context "Dataset#join_table" do
1192
+ setup do
1193
+ @d = MockDataset.new(nil).from(:items)
1194
+ @d.quote_identifiers = true
1195
+ end
1196
+
1197
+ specify "should format the JOIN clause properly" do
1198
+ @d.join_table(:left_outer, :categories, :category_id => :id).sql.should ==
1199
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1200
+ end
1201
+
1202
+ specify "should handle multiple conditions on the same join table column" do
1203
+ @d.join_table(:left_outer, :categories, [[:category_id, :id], [:category_id, 0..100]]).sql.should ==
1204
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON (("categories"."category_id" = "items"."id") AND (("categories"."category_id" >= 0) AND ("categories"."category_id" <= 100)))'
1205
+ end
1206
+
1207
+ specify "should include WHERE clause if applicable" do
1208
+ @d.filter(:price < 100).join_table(:right_outer, :categories, :category_id => :id).sql.should ==
1209
+ 'SELECT * FROM "items" RIGHT OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id") WHERE ("price" < 100)'
1210
+ end
1211
+
1212
+ specify "should include ORDER BY clause if applicable" do
1213
+ @d.order(:stamp).join_table(:full_outer, :categories, :category_id => :id).sql.should ==
1214
+ 'SELECT * FROM "items" FULL OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id") ORDER BY "stamp"'
1215
+ end
1216
+
1217
+ specify "should support multiple joins" do
1218
+ @d.join_table(:inner, :b, :items_id=>:id).join_table(:left_outer, :c, :b_id => :b__id).sql.should ==
1219
+ 'SELECT * FROM "items" INNER JOIN "b" ON ("b"."items_id" = "items"."id") LEFT OUTER JOIN "c" ON ("c"."b_id" = "b"."id")'
1220
+ end
1221
+
1222
+ specify "should support left outer joins" do
1223
+ @d.join_table(:left_outer, :categories, :category_id=>:id).sql.should ==
1224
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1225
+
1226
+ @d.left_outer_join(:categories, :category_id=>:id).sql.should ==
1227
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1228
+ end
1229
+
1230
+ specify "should support right outer joins" do
1231
+ @d.join_table(:right_outer, :categories, :category_id=>:id).sql.should ==
1232
+ 'SELECT * FROM "items" RIGHT OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1233
+
1234
+ @d.right_outer_join(:categories, :category_id=>:id).sql.should ==
1235
+ 'SELECT * FROM "items" RIGHT OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1236
+ end
1237
+
1238
+ specify "should support full outer joins" do
1239
+ @d.join_table(:full_outer, :categories, :category_id=>:id).sql.should ==
1240
+ 'SELECT * FROM "items" FULL OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1241
+
1242
+ @d.full_outer_join(:categories, :category_id=>:id).sql.should ==
1243
+ 'SELECT * FROM "items" FULL OUTER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1244
+ end
1245
+
1246
+ specify "should support inner joins" do
1247
+ @d.join_table(:inner, :categories, :category_id=>:id).sql.should ==
1248
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1249
+
1250
+ @d.inner_join(:categories, :category_id=>:id).sql.should ==
1251
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1252
+ end
1253
+
1254
+ specify "should default to a plain join if nil is used for the type" do
1255
+ @d.join_table(nil, :categories, :category_id=>:id).sql.should ==
1256
+ 'SELECT * FROM "items" JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1257
+ end
1258
+
1259
+ specify "should use an inner join for Dataset#join" do
1260
+ @d.join(:categories, :category_id=>:id).sql.should ==
1261
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."category_id" = "items"."id")'
1262
+ end
1263
+
1264
+ specify "should support aliased tables" do
1265
+ @d.from('stats').join('players', {:id => :player_id}, 'p').sql.should ==
1266
+ 'SELECT * FROM "stats" INNER JOIN "players" AS "p" ON ("p"."id" = "stats"."player_id")'
1267
+
1268
+ ds = MockDataset.new(nil).from(:foo => :f)
1269
+ ds.quote_identifiers = true
1270
+ ds.join_table(:inner, :bar, :id => :bar_id).sql.should ==
1271
+ 'SELECT * FROM "foo" "f" INNER JOIN "bar" ON ("bar"."id" = "f"."bar_id")'
1272
+ end
1273
+
1274
+ specify "should allow for arbitrary conditions in the JOIN clause" do
1275
+ @d.join_table(:left_outer, :categories, :status => 0).sql.should ==
1276
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."status" = 0)'
1277
+ @d.join_table(:left_outer, :categories, :categorizable_type => "Post").sql.should ==
1278
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."categorizable_type" = \'Post\')'
1279
+ @d.join_table(:left_outer, :categories, :timestamp => "CURRENT_TIMESTAMP".lit).sql.should ==
1280
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."timestamp" = CURRENT_TIMESTAMP)'
1281
+ @d.join_table(:left_outer, :categories, :status => [1, 2, 3]).sql.should ==
1282
+ 'SELECT * FROM "items" LEFT OUTER JOIN "categories" ON ("categories"."status" IN (1, 2, 3))'
1283
+ end
1284
+
1285
+ specify "should raise error for a table without a source" do
1286
+ proc {Sequel::Dataset.new(nil).join('players', :id => :player_id)}. \
1287
+ should raise_error(Sequel::Error)
1288
+ end
1289
+
1290
+ specify "should support joining datasets" do
1291
+ ds = Sequel::Dataset.new(nil).from(:categories)
1292
+
1293
+ @d.join_table(:left_outer, ds, :item_id => :id).sql.should ==
1294
+ 'SELECT * FROM "items" LEFT OUTER JOIN (SELECT * FROM categories) AS "t1" ON ("t1"."item_id" = "items"."id")'
1295
+
1296
+ ds.filter!(:active => true)
1297
+
1298
+ @d.join_table(:left_outer, ds, :item_id => :id).sql.should ==
1299
+ 'SELECT * FROM "items" LEFT OUTER JOIN (SELECT * FROM categories WHERE (active = \'t\')) AS "t1" ON ("t1"."item_id" = "items"."id")'
1300
+ end
1301
+
1302
+ specify "should support joining datasets and aliasing the join" do
1303
+ ds = Sequel::Dataset.new(nil).from(:categories)
1304
+
1305
+ @d.join_table(:left_outer, ds, {:ds__item_id => :id}, :ds).sql.should ==
1306
+ 'SELECT * FROM "items" LEFT OUTER JOIN (SELECT * FROM categories) AS "ds" ON ("ds"."item_id" = "items"."id")'
1307
+ end
1308
+
1309
+ specify "should support joining multiple datasets" do
1310
+ ds = Sequel::Dataset.new(nil).from(:categories)
1311
+ ds2 = Sequel::Dataset.new(nil).from(:nodes).select(:name)
1312
+ ds3 = Sequel::Dataset.new(nil).from(:attributes).filter("name = 'blah'")
1313
+
1314
+ @d.join_table(:left_outer, ds, :item_id => :id).join_table(:inner, ds2, :node_id=>:id).join_table(:right_outer, ds3, :attribute_id=>:id).sql.should ==
1315
+ 'SELECT * FROM "items" LEFT OUTER JOIN (SELECT * FROM categories) AS "t1" ON ("t1"."item_id" = "items"."id") ' \
1316
+ 'INNER JOIN (SELECT name FROM nodes) AS "t2" ON ("t2"."node_id" = "t1"."id") ' \
1317
+ 'RIGHT OUTER JOIN (SELECT * FROM attributes WHERE (name = \'blah\')) AS "t3" ON ("t3"."attribute_id" = "t2"."id")'
1318
+ end
1319
+
1320
+ specify "should support joining objects that respond to :table_name" do
1321
+ ds = Object.new
1322
+ def ds.table_name; :categories end
1323
+
1324
+ @d.join(ds, :item_id => :id).sql.should ==
1325
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."item_id" = "items"."id")'
1326
+ end
1327
+
1328
+ specify "should support using a SQL String as the join condition" do
1329
+ @d.join(:categories, %{c.item_id = items.id}, :c).sql.should ==
1330
+ 'SELECT * FROM "items" INNER JOIN "categories" AS "c" ON (c.item_id = items.id)'
1331
+ end
1332
+
1333
+ specify "should support using a boolean column as the join condition" do
1334
+ @d.join(:categories, :active).sql.should ==
1335
+ 'SELECT * FROM "items" INNER JOIN "categories" ON "active"'
1336
+ end
1337
+
1338
+ specify "should support using an expression as the join condition" do
1339
+ @d.join(:categories, :number > 10).sql.should ==
1340
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("number" > 10)'
1341
+ end
1342
+
1343
+ specify "should support natural and cross joins using nil" do
1344
+ @d.join_table(:natural, :categories).sql.should ==
1345
+ 'SELECT * FROM "items" NATURAL JOIN "categories"'
1346
+ @d.join_table(:cross, :categories, nil).sql.should ==
1347
+ 'SELECT * FROM "items" CROSS JOIN "categories"'
1348
+ @d.join_table(:natural, :categories, nil, :c).sql.should ==
1349
+ 'SELECT * FROM "items" NATURAL JOIN "categories" AS "c"'
1350
+ end
1351
+
1352
+ specify "should support joins with a USING clause if an array of symbols is used" do
1353
+ @d.join(:categories, [:id]).sql.should ==
1354
+ 'SELECT * FROM "items" INNER JOIN "categories" USING ("id")'
1355
+ @d.join(:categories, [:id1, :id2]).sql.should ==
1356
+ 'SELECT * FROM "items" INNER JOIN "categories" USING ("id1", "id2")'
1357
+ end
1358
+
1359
+ specify "should raise an error if using an array of symbols with a block" do
1360
+ proc{@d.join(:categories, [:id]){|j,lj,js|}}.should raise_error(Sequel::Error)
1361
+ end
1362
+
1363
+ specify "should support using a block that receieves the join table/alias, last join table/alias, and array of previous joins" do
1364
+ @d.join(:categories) do |join_alias, last_join_alias, joins|
1365
+ join_alias.should == :categories
1366
+ last_join_alias.should == :items
1367
+ joins.should == []
1368
+ end
1369
+
1370
+ @d.from(:items=>:i).join(:categories, nil, :c) do |join_alias, last_join_alias, joins|
1371
+ join_alias.should == :c
1372
+ last_join_alias.should == :i
1373
+ joins.should == []
1374
+ end
1375
+
1376
+ @d.from(:items___i).join(:categories, nil, :c) do |join_alias, last_join_alias, joins|
1377
+ join_alias.should == :c
1378
+ last_join_alias.should == :i
1379
+ joins.should == []
1380
+ end
1381
+
1382
+ @d.join(:blah).join(:categories, nil, :c) do |join_alias, last_join_alias, joins|
1383
+ join_alias.should == :c
1384
+ last_join_alias.should == :blah
1385
+ joins.should be_a_kind_of(Array)
1386
+ joins.length.should == 1
1387
+ joins.first.should be_a_kind_of(Sequel::SQL::JoinClause)
1388
+ joins.first.join_type.should == :inner
1389
+ end
1390
+
1391
+ @d.join_table(:natural, :blah, nil, :b).join(:categories, nil, :c) do |join_alias, last_join_alias, joins|
1392
+ join_alias.should == :c
1393
+ last_join_alias.should == :b
1394
+ joins.should be_a_kind_of(Array)
1395
+ joins.length.should == 1
1396
+ joins.first.should be_a_kind_of(Sequel::SQL::JoinClause)
1397
+ joins.first.join_type.should == :natural
1398
+ end
1399
+
1400
+ @d.join(:blah).join(:categories).join(:blah2) do |join_alias, last_join_alias, joins|
1401
+ join_alias.should == :blah2
1402
+ last_join_alias.should == :categories
1403
+ joins.should be_a_kind_of(Array)
1404
+ joins.length.should == 2
1405
+ joins.first.should be_a_kind_of(Sequel::SQL::JoinClause)
1406
+ joins.first.table.should == :blah
1407
+ joins.last.should be_a_kind_of(Sequel::SQL::JoinClause)
1408
+ joins.last.table.should == :categories
1409
+ end
1410
+ end
1411
+
1412
+ specify "should use the block result as the only condition if no condition is given" do
1413
+ @d.join(:categories){|j,lj,js| {:b.qualify(j)=>:c.qualify(lj)}}.sql.should ==
1414
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."b" = "items"."c")'
1415
+ @d.join(:categories){|j,lj,js| :b.qualify(j) > :c.qualify(lj)}.sql.should ==
1416
+ 'SELECT * FROM "items" INNER JOIN "categories" ON ("categories"."b" > "items"."c")'
1417
+ end
1418
+
1419
+ specify "should combine the block conditions and argument conditions if both given" do
1420
+ @d.join(:categories, :a=>:d){|j,lj,js| {:b.qualify(j)=>:c.qualify(lj)}}.sql.should ==
1421
+ 'SELECT * FROM "items" INNER JOIN "categories" ON (("categories"."a" = "items"."d") AND ("categories"."b" = "items"."c"))'
1422
+ @d.join(:categories, :a=>:d){|j,lj,js| :b.qualify(j) > :c.qualify(lj)}.sql.should ==
1423
+ 'SELECT * FROM "items" INNER JOIN "categories" ON (("categories"."a" = "items"."d") AND ("categories"."b" > "items"."c"))'
1424
+ end
1425
+ end
1426
+
1427
+ context "Dataset#[]=" do
1428
+ setup do
1429
+ c = Class.new(Sequel::Dataset) do
1430
+ def last_sql
1431
+ @@last_sql
1432
+ end
1433
+
1434
+ def update(*args)
1435
+ @@last_sql = update_sql(*args)
1436
+ end
1437
+ end
1438
+
1439
+ @d = c.new(nil).from(:items)
1440
+ end
1441
+
1442
+ specify "should perform an update on the specified filter" do
1443
+ @d[:a => 1] = {:x => 3}
1444
+ @d.last_sql.should == 'UPDATE items SET x = 3 WHERE (a = 1)'
1445
+ end
1446
+ end
1447
+
1448
+ context "Dataset#set" do
1449
+ setup do
1450
+ c = Class.new(Sequel::Dataset) do
1451
+ def last_sql
1452
+ @@last_sql
1453
+ end
1454
+
1455
+ def update(*args, &block)
1456
+ @@last_sql = update_sql(*args, &block)
1457
+ end
1458
+ end
1459
+
1460
+ @d = c.new(nil).from(:items)
1461
+ end
1462
+
1463
+ specify "should act as alias to #update" do
1464
+ @d.set({:x => 3})
1465
+ @d.last_sql.should == 'UPDATE items SET x = 3'
1466
+ end
1467
+ end
1468
+
1469
+
1470
+ context "Dataset#insert_multiple" do
1471
+ setup do
1472
+ c = Class.new(Sequel::Dataset) do
1473
+ attr_reader :inserts
1474
+ def insert(arg)
1475
+ @inserts ||= []
1476
+ @inserts << arg
1477
+ end
1478
+ end
1479
+
1480
+ @d = c.new(nil)
1481
+ end
1482
+
1483
+ specify "should insert all items in the supplied array" do
1484
+ @d.insert_multiple [:aa, 5, 3, {1 => 2}]
1485
+ @d.inserts.should == [:aa, 5, 3, {1 => 2}]
1486
+ end
1487
+
1488
+ specify "should pass array items through the supplied block if given" do
1489
+ a = ["inevitable", "hello", "the ticking clock"]
1490
+ @d.insert_multiple(a) {|i| i.gsub('l', 'r')}
1491
+ @d.inserts.should == ["inevitabre", "herro", "the ticking crock"]
1492
+ end
1493
+ end
1494
+
1495
+ context "Dataset aggregate methods" do
1496
+ setup do
1497
+ c = Class.new(Sequel::Dataset) do
1498
+ def fetch_rows(sql)
1499
+ yield({1 => sql})
1500
+ end
1501
+ end
1502
+ @d = c.new(nil).from(:test)
1503
+ end
1504
+
1505
+ specify "should include min" do
1506
+ @d.min(:a).should == 'SELECT min(a) FROM test LIMIT 1'
1507
+ end
1508
+
1509
+ specify "should include max" do
1510
+ @d.max(:b).should == 'SELECT max(b) FROM test LIMIT 1'
1511
+ end
1512
+
1513
+ specify "should include sum" do
1514
+ @d.sum(:c).should == 'SELECT sum(c) FROM test LIMIT 1'
1515
+ end
1516
+
1517
+ specify "should include avg" do
1518
+ @d.avg(:d).should == 'SELECT avg(d) FROM test LIMIT 1'
1519
+ end
1520
+
1521
+ specify "should accept qualified columns" do
1522
+ @d.avg(:test__bc).should == 'SELECT avg(test.bc) FROM test LIMIT 1'
1523
+ end
1524
+ end
1525
+
1526
+ context "Dataset#range" do
1527
+ setup do
1528
+ c = Class.new(Sequel::Dataset) do
1529
+ @@sql = nil
1530
+
1531
+ def last_sql; @@sql; end
1532
+
1533
+ def fetch_rows(sql)
1534
+ @@sql = sql
1535
+ yield(:v1 => 1, :v2 => 10)
1536
+ end
1537
+ end
1538
+ @d = c.new(nil).from(:test)
1539
+ end
1540
+
1541
+ specify "should generate a correct SQL statement" do
1542
+ @d.range(:stamp)
1543
+ @d.last_sql.should == "SELECT min(stamp) AS v1, max(stamp) AS v2 FROM test LIMIT 1"
1544
+
1545
+ @d.filter(:price > 100).range(:stamp)
1546
+ @d.last_sql.should == "SELECT min(stamp) AS v1, max(stamp) AS v2 FROM test WHERE (price > 100) LIMIT 1"
1547
+ end
1548
+
1549
+ specify "should return a range object" do
1550
+ @d.range(:tryme).should == (1..10)
1551
+ end
1552
+ end
1553
+
1554
+ context "Dataset#interval" do
1555
+ setup do
1556
+ c = Class.new(Sequel::Dataset) do
1557
+ @@sql = nil
1558
+
1559
+ def last_sql; @@sql; end
1560
+
1561
+ def fetch_rows(sql)
1562
+ @@sql = sql
1563
+ yield(:v => 1234)
1564
+ end
1565
+ end
1566
+ @d = c.new(nil).from(:test)
1567
+ end
1568
+
1569
+ specify "should generate a correct SQL statement" do
1570
+ @d.interval(:stamp)
1571
+ @d.last_sql.should == "SELECT (max(stamp) - min(stamp)) FROM test LIMIT 1"
1572
+
1573
+ @d.filter(:price > 100).interval(:stamp)
1574
+ @d.last_sql.should == "SELECT (max(stamp) - min(stamp)) FROM test WHERE (price > 100) LIMIT 1"
1575
+ end
1576
+
1577
+ specify "should return an integer" do
1578
+ @d.interval(:tryme).should == 1234
1579
+ end
1580
+ end
1581
+
1582
+ context "Dataset #first and #last" do
1583
+ setup do
1584
+ @c = Class.new(Sequel::Dataset) do
1585
+ def each(opts = nil, &block)
1586
+ s = select_sql(opts)
1587
+ x = [:a,1,:b,2,s]
1588
+ i = /LIMIT (\d+)/.match(s)[1].to_i.times{yield x}
1589
+ end
1590
+ end
1591
+ @d = @c.new(nil).from(:test)
1592
+ end
1593
+
1594
+ specify "should return a single record if no argument is given" do
1595
+ @d.order(:a).first.should == [:a,1,:b,2, 'SELECT * FROM test ORDER BY a LIMIT 1']
1596
+ @d.order(:a).last.should == [:a,1,:b,2, 'SELECT * FROM test ORDER BY a DESC LIMIT 1']
1597
+ end
1598
+
1599
+ specify "should return the first/last matching record if argument is not an Integer" do
1600
+ @d.order(:a).first(:z => 26).should == [:a,1,:b,2, 'SELECT * FROM test WHERE (z = 26) ORDER BY a LIMIT 1']
1601
+ @d.order(:a).first('z = ?', 15).should == [:a,1,:b,2, 'SELECT * FROM test WHERE (z = 15) ORDER BY a LIMIT 1']
1602
+ @d.order(:a).last(:z => 26).should == [:a,1,:b,2, 'SELECT * FROM test WHERE (z = 26) ORDER BY a DESC LIMIT 1']
1603
+ @d.order(:a).last('z = ?', 15).should == [:a,1,:b,2, 'SELECT * FROM test WHERE (z = 15) ORDER BY a DESC LIMIT 1']
1604
+ end
1605
+
1606
+ specify "should set the limit and return an array of records if the given number is > 1" do
1607
+ i = rand(10) + 10
1608
+ r = @d.order(:a).first(i).should == [[:a,1,:b,2, "SELECT * FROM test ORDER BY a LIMIT #{i}"]] * i
1609
+ i = rand(10) + 10
1610
+ r = @d.order(:a).last(i).should == [[:a,1,:b,2, "SELECT * FROM test ORDER BY a DESC LIMIT #{i}"]] * i
1611
+ end
1612
+
1613
+ specify "should return the first matching record if a block is given without an argument" do
1614
+ @d.first{:z > 26}.should == [:a,1,:b,2, 'SELECT * FROM test WHERE (z > 26) LIMIT 1']
1615
+ @d.order(:name).last{:z > 26}.should == [:a,1,:b,2, 'SELECT * FROM test WHERE (z > 26) ORDER BY name DESC LIMIT 1']
1616
+ end
1617
+
1618
+ specify "should combine block and standard argument filters if argument is not an Integer" do
1619
+ @d.first(:y=>25){:z > 26}.should == [:a,1,:b,2, 'SELECT * FROM test WHERE ((z > 26) AND (y = 25)) LIMIT 1']
1620
+ @d.order(:name).last('y = ?', 16){:z > 26}.should == [:a,1,:b,2, 'SELECT * FROM test WHERE ((z > 26) AND (y = 16)) ORDER BY name DESC LIMIT 1']
1621
+ end
1622
+
1623
+ specify "should filter and return an array of records if an Integer argument is provided and a block is given" do
1624
+ i = rand(10) + 10
1625
+ r = @d.order(:a).first(i){:z > 26}.should == [[:a,1,:b,2, "SELECT * FROM test WHERE (z > 26) ORDER BY a LIMIT #{i}"]] * i
1626
+ i = rand(10) + 10
1627
+ r = @d.order(:a).last(i){:z > 26}.should == [[:a,1,:b,2, "SELECT * FROM test WHERE (z > 26) ORDER BY a DESC LIMIT #{i}"]] * i
1628
+ end
1629
+
1630
+ specify "#last should raise if no order is given" do
1631
+ proc {@d.last}.should raise_error(Sequel::Error)
1632
+ proc {@d.last(2)}.should raise_error(Sequel::Error)
1633
+ proc {@d.order(:a).last}.should_not raise_error
1634
+ proc {@d.order(:a).last(2)}.should_not raise_error
1635
+ end
1636
+
1637
+ specify "#last should invert the order" do
1638
+ @d.order(:a).last.pop.should == 'SELECT * FROM test ORDER BY a DESC LIMIT 1'
1639
+ @d.order(:b.desc).last.pop.should == 'SELECT * FROM test ORDER BY b ASC LIMIT 1'
1640
+ @d.order(:c, :d).last.pop.should == 'SELECT * FROM test ORDER BY c DESC, d DESC LIMIT 1'
1641
+ @d.order(:e.desc, :f).last.pop.should == 'SELECT * FROM test ORDER BY e ASC, f DESC LIMIT 1'
1642
+ end
1643
+ end
1644
+
1645
+ context "Dataset set operations" do
1646
+ setup do
1647
+ @a = Sequel::Dataset.new(nil).from(:a).filter(:z => 1)
1648
+ @b = Sequel::Dataset.new(nil).from(:b).filter(:z => 2)
1649
+ end
1650
+
1651
+ specify "should support UNION and UNION ALL" do
1652
+ @a.union(@b).sql.should == \
1653
+ "SELECT * FROM a WHERE (z = 1) UNION SELECT * FROM b WHERE (z = 2)"
1654
+ @b.union(@a, true).sql.should == \
1655
+ "SELECT * FROM b WHERE (z = 2) UNION ALL SELECT * FROM a WHERE (z = 1)"
1656
+ end
1657
+
1658
+ specify "should support INTERSECT and INTERSECT ALL" do
1659
+ @a.intersect(@b).sql.should == \
1660
+ "SELECT * FROM a WHERE (z = 1) INTERSECT SELECT * FROM b WHERE (z = 2)"
1661
+ @b.intersect(@a, true).sql.should == \
1662
+ "SELECT * FROM b WHERE (z = 2) INTERSECT ALL SELECT * FROM a WHERE (z = 1)"
1663
+ end
1664
+
1665
+ specify "should support EXCEPT and EXCEPT ALL" do
1666
+ @a.except(@b).sql.should == \
1667
+ "SELECT * FROM a WHERE (z = 1) EXCEPT SELECT * FROM b WHERE (z = 2)"
1668
+ @b.except(@a, true).sql.should == \
1669
+ "SELECT * FROM b WHERE (z = 2) EXCEPT ALL SELECT * FROM a WHERE (z = 1)"
1670
+ end
1671
+ end
1672
+
1673
+ context "Dataset#[]" do
1674
+ setup do
1675
+ @c = Class.new(Sequel::Dataset) do
1676
+ @@last_dataset = nil
1677
+
1678
+ def self.last_dataset
1679
+ @@last_dataset
1680
+ end
1681
+
1682
+ def single_record(opts = nil)
1683
+ @@last_dataset = opts ? clone(opts) : self
1684
+ {1 => 2, 3 => 4}
1685
+ end
1686
+ end
1687
+ @d = @c.new(nil).from(:test)
1688
+ end
1689
+
1690
+ specify "should return a single record filtered according to the given conditions" do
1691
+ @d[:name => 'didi'].should == {1 => 2, 3 => 4}
1692
+ @c.last_dataset.literal(@c.last_dataset.opts[:where]).should == "(name = 'didi')"
1693
+
1694
+ @d[:id => 5..45].should == {1 => 2, 3 => 4}
1695
+ @c.last_dataset.literal(@c.last_dataset.opts[:where]).should == "((id >= 5) AND (id <= 45))"
1696
+ end
1697
+ end
1698
+
1699
+ context "Dataset#single_record" do
1700
+ setup do
1701
+ @c = Class.new(Sequel::Dataset) do
1702
+ def fetch_rows(sql)
1703
+ yield sql
1704
+ end
1705
+ end
1706
+ @cc = Class.new(@c) do
1707
+ def fetch_rows(sql); end
1708
+ end
1709
+
1710
+ @d = @c.new(nil).from(:test)
1711
+ @e = @cc.new(nil).from(:test)
1712
+ end
1713
+
1714
+ specify "should call each with a limit of 1 and return the record" do
1715
+ @d.single_record.should == 'SELECT * FROM test LIMIT 1'
1716
+ end
1717
+
1718
+ specify "should pass opts to each" do
1719
+ @d.single_record(:order => [:name]).should == 'SELECT * FROM test ORDER BY name LIMIT 1'
1720
+ end
1721
+
1722
+ specify "should override the limit if passed as an option" do
1723
+ @d.single_record(:limit => 3).should == 'SELECT * FROM test LIMIT 1'
1724
+ end
1725
+
1726
+ specify "should return nil if no record is present" do
1727
+ @e.single_record.should be_nil
1728
+ end
1729
+ end
1730
+
1731
+ context "Dataset#single_value" do
1732
+ setup do
1733
+ @c = Class.new(Sequel::Dataset) do
1734
+ def fetch_rows(sql)
1735
+ yield({1 => sql})
1736
+ end
1737
+ end
1738
+ @cc = Class.new(@c) do
1739
+ def fetch_rows(sql); end
1740
+ end
1741
+
1742
+ @d = @c.new(nil).from(:test)
1743
+ @e = @cc.new(nil).from(:test)
1744
+ end
1745
+
1746
+ specify "should call each and return the first value of the first record" do
1747
+ @d.single_value.should == 'SELECT * FROM test LIMIT 1'
1748
+ end
1749
+
1750
+ specify "should pass opts to each" do
1751
+ @d.single_value(:from => [:blah]).should == 'SELECT * FROM blah LIMIT 1'
1752
+ end
1753
+
1754
+ specify "should return nil if no records" do
1755
+ @e.single_value.should be_nil
1756
+ end
1757
+ end
1758
+
1759
+ context "Dataset#get" do
1760
+ setup do
1761
+ @c = Class.new(Sequel::Dataset) do
1762
+ attr_reader :last_sql
1763
+
1764
+ def fetch_rows(sql)
1765
+ @last_sql = sql
1766
+ yield(:name => sql)
1767
+ end
1768
+ end
1769
+
1770
+ @d = @c.new(nil).from(:test)
1771
+ end
1772
+
1773
+ specify "should select the specified column and fetch its value" do
1774
+ @d.get(:name).should == "SELECT name FROM test LIMIT 1"
1775
+ @d.get(:abc).should == "SELECT abc FROM test LIMIT 1" # the first available value is returned always
1776
+ end
1777
+
1778
+ specify "should work with filters" do
1779
+ @d.filter(:id => 1).get(:name).should == "SELECT name FROM test WHERE (id = 1) LIMIT 1"
1780
+ end
1781
+
1782
+ specify "should work with aliased fields" do
1783
+ @d.get(:x__b.as(:name)).should == "SELECT x.b AS name FROM test LIMIT 1"
1784
+ end
1785
+ end
1786
+
1787
+ context "Dataset#set_row_proc" do
1788
+ setup do
1789
+ @c = Class.new(Sequel::Dataset) do
1790
+ def fetch_rows(sql, &block)
1791
+ # yield a hash with kind as the 1 bit of a number
1792
+ (1..10).each {|i| block.call({:kind => i[0]})}
1793
+ end
1794
+ end
1795
+ @dataset = @c.new(nil).from(:items)
1796
+ end
1797
+
1798
+ specify "should cause dataset to pass all rows through the filter" do
1799
+ @dataset.row_proc = proc{|h| h[:der] = h[:kind] + 2; h}
1800
+
1801
+ rows = @dataset.all
1802
+ rows.size.should == 10
1803
+
1804
+ rows.each {|r| r[:der].should == (r[:kind] + 2)}
1805
+ end
1806
+
1807
+ specify "should be copied over when dataset is cloned" do
1808
+ @dataset.row_proc = proc{|h| h[:der] = h[:kind] + 2; h}
1809
+
1810
+ @dataset.filter(:a => 1).first.should == {:kind => 1, :der => 3}
1811
+ end
1812
+ end
1813
+
1814
+ context "Dataset#set_model" do
1815
+ setup do
1816
+ @c = Class.new(Sequel::Dataset) do
1817
+ def fetch_rows(sql, &block)
1818
+ # yield a hash with kind as the 1 bit of a number
1819
+ (1..10).each {|i| block.call({:kind => i[0]})}
1820
+ end
1821
+ end
1822
+ @dataset = @c.new(nil).from(:items)
1823
+ @m = Class.new do
1824
+ attr_accessor :c, :args
1825
+ def initialize(c, *args); @c = c; @args = args; end
1826
+ def ==(o); (@c == o.c) && (@args = o.args); end
1827
+ end
1828
+ end
1829
+
1830
+ specify "should clear the models hash and restore the stock #each if nil is specified" do
1831
+ @dataset.set_model(@m)
1832
+ @dataset.set_model(nil)
1833
+ @dataset.first.should == {:kind => 1}
1834
+ @dataset.model_classes.should be_nil
1835
+ end
1836
+
1837
+ specify "should clear the models hash and restore the stock #each if nothing is specified" do
1838
+ @dataset.set_model(@m)
1839
+ @dataset.set_model(nil)
1840
+ @dataset.first.should == {:kind => 1}
1841
+ @dataset.model_classes.should be_nil
1842
+ end
1843
+
1844
+ specify "should alter #each to provide model instances" do
1845
+ @dataset.first.should == {:kind => 1}
1846
+ @dataset.set_model(@m)
1847
+ @dataset.first.should == @m.new({:kind => 1})
1848
+ end
1849
+
1850
+ specify "should set opts[:naked] to nil" do
1851
+ @dataset.opts[:naked] = true
1852
+ @dataset.set_model(@m)
1853
+ @dataset.opts[:naked].should be_nil
1854
+ end
1855
+
1856
+ specify "should send additional arguments to the models' initialize method" do
1857
+ @dataset.set_model(@m, 7, 6, 5)
1858
+ @dataset.first.should == @m.new({:kind => 1}, 7, 6, 5)
1859
+ end
1860
+
1861
+ specify "should provide support for polymorphic model instantiation" do
1862
+ @m1 = Class.new(@m)
1863
+ @m2 = Class.new(@m)
1864
+ @dataset.set_model(:kind, 0 => @m1, 1 => @m2)
1865
+ @dataset.opts[:polymorphic_key].should == :kind
1866
+ all = @dataset.all
1867
+ all[0].class.should == @m2
1868
+ all[1].class.should == @m1
1869
+ all[2].class.should == @m2
1870
+ all[3].class.should == @m1
1871
+ #...
1872
+
1873
+ # denude model
1874
+ @dataset.set_model(nil)
1875
+ @dataset.first.should == {:kind => 1}
1876
+ end
1877
+
1878
+ specify "should send additional arguments for polymorphic models as well" do
1879
+ @m1 = Class.new(@m)
1880
+ @m2 = Class.new(@m)
1881
+ @dataset.set_model(:kind, {0 => @m1, 1 => @m2}, :hey => :wow)
1882
+ all = @dataset.all
1883
+ all[0].class.should == @m2; all[0].args.should == [{:hey => :wow}]
1884
+ all[1].class.should == @m1; all[1].args.should == [{:hey => :wow}]
1885
+ all[2].class.should == @m2; all[2].args.should == [{:hey => :wow}]
1886
+ all[3].class.should == @m1; all[3].args.should == [{:hey => :wow}]
1887
+ end
1888
+
1889
+ specify "should raise for invalid parameters" do
1890
+ proc {@dataset.set_model('kind')}.should raise_error(ArgumentError)
1891
+ proc {@dataset.set_model(0)}.should raise_error(ArgumentError)
1892
+ proc {@dataset.set_model(:kind)}.should raise_error(ArgumentError) # no hash given
1893
+ end
1894
+ end
1895
+
1896
+ context "Dataset#model_classes" do
1897
+ setup do
1898
+ @c = Class.new(Sequel::Dataset) do
1899
+ # # We don't need that for now
1900
+ # def fetch_rows(sql, &block)
1901
+ # (1..10).each(&block)
1902
+ # end
1903
+ end
1904
+ @dataset = @c.new(nil).from(:items)
1905
+ @m = Class.new do
1906
+ attr_accessor :c
1907
+ def initialize(c); @c = c; end
1908
+ def ==(o); @c == o.c; end
1909
+ end
1910
+ end
1911
+
1912
+ specify "should return nil for a naked dataset" do
1913
+ @dataset.model_classes.should == nil
1914
+ end
1915
+
1916
+ specify "should return a {nil => model_class} hash for a model dataset" do
1917
+ @dataset.set_model(@m)
1918
+ @dataset.model_classes.should == {nil => @m}
1919
+ end
1920
+
1921
+ specify "should return the polymorphic hash for a polymorphic model dataset" do
1922
+ @m1 = Class.new(@m)
1923
+ @m2 = Class.new(@m)
1924
+ @dataset.set_model(:key, 0 => @m1, 1 => @m2)
1925
+ @dataset.model_classes.should == {0 => @m1, 1 => @m2}
1926
+ end
1927
+ end
1928
+
1929
+ context "Dataset#polymorphic_key" do
1930
+ setup do
1931
+ @c = Class.new(Sequel::Dataset) do
1932
+ # # We don't need this for now
1933
+ # def fetch_rows(sql, &block)
1934
+ # (1..10).each(&block)
1935
+ # end
1936
+ end
1937
+ @dataset = @c.new(nil).from(:items)
1938
+ @m = Class.new do
1939
+ attr_accessor :c
1940
+ def initialize(c); @c = c; end
1941
+ def ==(o); @c == o.c; end
1942
+ end
1943
+ end
1944
+
1945
+ specify "should return nil for a naked dataset" do
1946
+ @dataset.polymorphic_key.should be_nil
1947
+ end
1948
+
1949
+ specify "should return the polymorphic key" do
1950
+ @dataset.set_model(:id, nil => @m)
1951
+ @dataset.polymorphic_key.should == :id
1952
+ end
1953
+ end
1954
+
1955
+ context "A model dataset" do
1956
+ setup do
1957
+ @c = Class.new(Sequel::Dataset) do
1958
+ def fetch_rows(sql, &block)
1959
+ (1..10).each(&block)
1960
+ end
1961
+ end
1962
+ @dataset = @c.new(nil).from(:items)
1963
+ @m = Class.new do
1964
+ attr_accessor :c
1965
+ def initialize(c); @c = c; end
1966
+ def ==(o); @c == o.c; end
1967
+ end
1968
+ @dataset.set_model(@m)
1969
+ end
1970
+
1971
+ specify "should supply naked records if the naked option is specified" do
1972
+ @dataset.each {|r| r.class.should == @m}
1973
+ @dataset.each(:naked => true) {|r| r.class.should == Fixnum}
1974
+ end
1975
+ end
1976
+
1977
+ context "A polymorphic model dataset" do
1978
+ setup do
1979
+ @c = Class.new(Sequel::Dataset) do
1980
+ def fetch_rows(sql, &block)
1981
+ (1..10).each {|i| block.call(:bit => i[0])}
1982
+ end
1983
+ end
1984
+ @dataset = @c.new(nil).from(:items)
1985
+ @m = Class.new do
1986
+ attr_accessor :c
1987
+ def initialize(c); @c = c; end
1988
+ def ==(o); @c == o.c; end
1989
+ end
1990
+ end
1991
+
1992
+ specify "should use a nil key in the polymorphic hash to specify the default model class" do
1993
+ @m2 = Class.new(@m)
1994
+ @dataset.set_model(:bit, nil => @m, 1 => @m2)
1995
+ all = @dataset.all
1996
+ all[0].class.should == @m2
1997
+ all[1].class.should == @m
1998
+ all[2].class.should == @m2
1999
+ all[3].class.should == @m
2000
+ #...
2001
+ end
2002
+
2003
+ specify "should raise Sequel::Error if no suitable class is found in the polymorphic hash" do
2004
+ @m2 = Class.new(@m)
2005
+ @dataset.set_model(:bit, 1 => @m2)
2006
+ proc {@dataset.all}.should raise_error(Sequel::Error)
2007
+ end
2008
+
2009
+ specify "should supply naked records if the naked option is specified" do
2010
+ @dataset.set_model(:bit, nil => @m)
2011
+ @dataset.each(:naked => true) {|r| r.class.should == Hash}
2012
+ end
2013
+ end
2014
+
2015
+ context "A dataset with associated model class(es)" do
2016
+ setup do
2017
+ @c = Class.new(Sequel::Dataset) do
2018
+ def fetch_rows(sql, &block)
2019
+ block.call({:x => 1, :y => 2})
2020
+ end
2021
+ end
2022
+ @dataset = @c.new(nil).from(:items)
2023
+ @m1 = Class.new do
2024
+ attr_accessor :v
2025
+ def initialize(v); @v = v; end
2026
+ end
2027
+ @m2 = Class.new do
2028
+ attr_accessor :v, :vv
2029
+ def initialize(v = nil); @v = v; end
2030
+ def self.load(v); o = new(nil); o.vv = v; o; end
2031
+ end
2032
+ @m3 = Class.new(@m2)
2033
+ end
2034
+
2035
+ specify "should instantiate an instance by passing the record hash as argument" do
2036
+ @dataset.set_model(@m1)
2037
+ o = @dataset.first
2038
+ o.class.should == @m1
2039
+ o.v.should == {:x => 1, :y => 2}
2040
+ end
2041
+
2042
+ specify "should use the .load constructor if available" do
2043
+ @dataset.set_model(@m2)
2044
+ o = @dataset.first
2045
+ o.class.should == @m2
2046
+ o.v.should == nil
2047
+ o.vv.should == {:x => 1, :y => 2}
2048
+ end
2049
+
2050
+ specify "should use the .load constructor also for polymorphic datasets" do
2051
+ @dataset.set_model(:y, 1 => @m2, 2 => @m3)
2052
+ o = @dataset.first
2053
+ o.class.should == @m3
2054
+ o.v.should == nil
2055
+ o.vv.should == {:x => 1, :y => 2}
2056
+ end
2057
+ end
2058
+
2059
+ context "Dataset#<<" do
2060
+ setup do
2061
+ @d = Sequel::Dataset.new(nil)
2062
+ @d.meta_def(:insert) do |*args|
2063
+ 1234567890
2064
+ end
2065
+ end
2066
+
2067
+ specify "should call #insert" do
2068
+ (@d << {:name => 1}).should == 1234567890
2069
+ end
2070
+ end
2071
+
2072
+ context "A paginated dataset" do
2073
+ setup do
2074
+ @d = Sequel::Dataset.new(nil)
2075
+ @d.meta_def(:count) {153}
2076
+
2077
+ @paginated = @d.paginate(1, 20)
2078
+ end
2079
+
2080
+ specify "should raise an error if the dataset already has a limit" do
2081
+ proc{@d.limit(10).paginate(1,10)}.should raise_error(Sequel::Error)
2082
+ proc{@paginated.paginate(2,20)}.should raise_error(Sequel::Error)
2083
+ end
2084
+
2085
+ specify "should set the limit and offset options correctly" do
2086
+ @paginated.opts[:limit].should == 20
2087
+ @paginated.opts[:offset].should == 0
2088
+ end
2089
+
2090
+ specify "should set the page count correctly" do
2091
+ @paginated.page_count.should == 8
2092
+ @d.paginate(1, 50).page_count.should == 4
2093
+ end
2094
+
2095
+ specify "should set the current page number correctly" do
2096
+ @paginated.current_page.should == 1
2097
+ @d.paginate(3, 50).current_page.should == 3
2098
+ end
2099
+
2100
+ specify "should return the next page number or nil if we're on the last" do
2101
+ @paginated.next_page.should == 2
2102
+ @d.paginate(4, 50).next_page.should be_nil
2103
+ end
2104
+
2105
+ specify "should return the previous page number or nil if we're on the last" do
2106
+ @paginated.prev_page.should be_nil
2107
+ @d.paginate(4, 50).prev_page.should == 3
2108
+ end
2109
+
2110
+ specify "should return the page range" do
2111
+ @paginated.page_range.should == (1..8)
2112
+ @d.paginate(4, 50).page_range.should == (1..4)
2113
+ end
2114
+
2115
+ specify "should return the record range for the current page" do
2116
+ @paginated.current_page_record_range.should == (1..20)
2117
+ @d.paginate(4, 50).current_page_record_range.should == (151..153)
2118
+ @d.paginate(5, 50).current_page_record_range.should == (0..0)
2119
+ end
2120
+
2121
+ specify "should return the record count for the current page" do
2122
+ @paginated.current_page_record_count.should == 20
2123
+ @d.paginate(3, 50).current_page_record_count.should == 50
2124
+ @d.paginate(4, 50).current_page_record_count.should == 3
2125
+ @d.paginate(5, 50).current_page_record_count.should == 0
2126
+ end
2127
+
2128
+ specify "should know if current page is last page" do
2129
+ @paginated.last_page?.should be_false
2130
+ @d.paginate(2, 20).last_page?.should be_false
2131
+ @d.paginate(5, 30).last_page?.should be_false
2132
+ @d.paginate(6, 30).last_page?.should be_true
2133
+ end
2134
+
2135
+ specify "should know if current page is first page" do
2136
+ @paginated.first_page?.should be_true
2137
+ @d.paginate(1, 20).first_page?.should be_true
2138
+ @d.paginate(2, 20).first_page?.should be_false
2139
+ end
2140
+
2141
+ specify "should work with fixed sql" do
2142
+ ds = @d.clone(:sql => 'select * from blah')
2143
+ ds.meta_def(:count) {150}
2144
+ ds.paginate(2, 50).sql.should == 'SELECT * FROM (select * from blah) t1 LIMIT 50 OFFSET 50'
2145
+ end
2146
+ end
2147
+
2148
+ context "Dataset#each_page" do
2149
+ setup do
2150
+ @d = Sequel::Dataset.new(nil).from(:items)
2151
+ @d.meta_def(:count) {153}
2152
+ end
2153
+
2154
+ specify "should raise an error if the dataset already has a limit" do
2155
+ proc{@d.limit(10).each_page(10){}}.should raise_error(Sequel::Error)
2156
+ end
2157
+
2158
+ specify "should iterate over each page in the resultset as a paginated dataset" do
2159
+ a = []
2160
+ @d.each_page(50) {|p| a << p}
2161
+ a.map {|p| p.sql}.should == [
2162
+ 'SELECT * FROM items LIMIT 50 OFFSET 0',
2163
+ 'SELECT * FROM items LIMIT 50 OFFSET 50',
2164
+ 'SELECT * FROM items LIMIT 50 OFFSET 100',
2165
+ 'SELECT * FROM items LIMIT 50 OFFSET 150',
2166
+ ]
2167
+ end
2168
+ end
2169
+
2170
+ context "Dataset#columns" do
2171
+ setup do
2172
+ @dataset = DummyDataset.new(nil).from(:items)
2173
+ @dataset.meta_def(:columns=) {|c| @columns = c}
2174
+ i = 'a'
2175
+ @dataset.meta_def(:each) {|o| @columns = select_sql(o||@opts) + i; i = i.next}
2176
+ end
2177
+
2178
+ specify "should return the value of @columns if @columns is not nil" do
2179
+ @dataset.columns = [:a, :b, :c]
2180
+ @dataset.columns.should == [:a, :b, :c]
2181
+ end
2182
+
2183
+ specify "should attempt to get a single record and return @columns if @columns is nil" do
2184
+ @dataset.columns = nil
2185
+ @dataset.columns.should == 'SELECT * FROM items LIMIT 1a'
2186
+ @dataset.opts[:from] = [:nana]
2187
+ @dataset.columns.should == 'SELECT * FROM items LIMIT 1a'
2188
+ end
2189
+
2190
+ specify "should ignore any filters, orders, or DISTINCT clauses" do
2191
+ @dataset.filter!(:b=>100).order!(:b).distinct!(:b)
2192
+ @dataset.columns = nil
2193
+ @dataset.columns.should == 'SELECT * FROM items LIMIT 1a'
2194
+ end
2195
+ end
2196
+
2197
+ context "Dataset#columns!" do
2198
+ setup do
2199
+ @dataset = DummyDataset.new(nil).from(:items)
2200
+ i = 'a'
2201
+ @dataset.meta_def(:each) {|o| @columns = select_sql(o||@opts) + i; i = i.next}
2202
+ end
2203
+
2204
+ specify "should always attempt to get a record and return @columns" do
2205
+ @dataset.columns!.should == 'SELECT * FROM items LIMIT 1a'
2206
+ @dataset.columns!.should == 'SELECT * FROM items LIMIT 1b'
2207
+ @dataset.opts[:from] = [:nana]
2208
+ @dataset.columns!.should == 'SELECT * FROM nana LIMIT 1c'
2209
+ end
2210
+ end
2211
+
2212
+ require 'stringio'
2213
+
2214
+ context "Dataset#print" do
2215
+ setup do
2216
+ @output = StringIO.new
2217
+ @orig_stdout = $stdout
2218
+ $stdout = @output
2219
+ @dataset = DummyDataset.new(nil).from(:items)
2220
+ end
2221
+
2222
+ teardown do
2223
+ $stdout = @orig_stdout
2224
+ end
2225
+
2226
+ specify "should print out a table with the values" do
2227
+ @dataset.print(:a, :b)
2228
+ @output.rewind
2229
+ @output.read.should == \
2230
+ "+-+-+\n|a|b|\n+-+-+\n|1|2|\n|3|4|\n|5|6|\n+-+-+\n"
2231
+ end
2232
+
2233
+ specify "should default to the dataset's columns" do
2234
+ @dataset.meta_def(:columns) {[:a, :b]}
2235
+ @dataset.print
2236
+ @output.rewind
2237
+ @output.read.should == \
2238
+ "+-+-+\n|a|b|\n+-+-+\n|1|2|\n|3|4|\n|5|6|\n+-+-+\n"
2239
+ end
2240
+ end
2241
+
2242
+ context "Dataset#multi_insert" do
2243
+ setup do
2244
+ @dbc = Class.new do
2245
+ attr_reader :sqls
2246
+
2247
+ def execute(sql)
2248
+ @sqls ||= []
2249
+ @sqls << sql
2250
+ end
2251
+ alias execute_dui execute
2252
+
2253
+ def transaction
2254
+ @sqls ||= []
2255
+ @sqls << 'BEGIN'
2256
+ yield
2257
+ @sqls << 'COMMIT'
2258
+ end
2259
+ end
2260
+ @db = @dbc.new
2261
+
2262
+ @ds = Sequel::Dataset.new(@db).from(:items)
2263
+
2264
+ @list = [{:name => 'abc'}, {:name => 'def'}, {:name => 'ghi'}]
2265
+ end
2266
+
2267
+ specify "should join all inserts into a single SQL string" do
2268
+ @ds.multi_insert(@list)
2269
+ @db.sqls.should == [
2270
+ 'BEGIN',
2271
+ "INSERT INTO items (name) VALUES ('abc')",
2272
+ "INSERT INTO items (name) VALUES ('def')",
2273
+ "INSERT INTO items (name) VALUES ('ghi')",
2274
+ 'COMMIT'
2275
+ ]
2276
+ end
2277
+
2278
+ specify "should accept the :commit_every option for committing every x records" do
2279
+ @ds.multi_insert(@list, :commit_every => 2)
2280
+ @db.sqls.should == [
2281
+ 'BEGIN',
2282
+ "INSERT INTO items (name) VALUES ('abc')",
2283
+ "INSERT INTO items (name) VALUES ('def')",
2284
+ 'COMMIT',
2285
+ 'BEGIN',
2286
+ "INSERT INTO items (name) VALUES ('ghi')",
2287
+ 'COMMIT'
2288
+ ]
2289
+ end
2290
+
2291
+ specify "should accept the :slice option for committing every x records" do
2292
+ @ds.multi_insert(@list, :slice => 2)
2293
+ @db.sqls.should == [
2294
+ 'BEGIN',
2295
+ "INSERT INTO items (name) VALUES ('abc')",
2296
+ "INSERT INTO items (name) VALUES ('def')",
2297
+ 'COMMIT',
2298
+ 'BEGIN',
2299
+ "INSERT INTO items (name) VALUES ('ghi')",
2300
+ 'COMMIT'
2301
+ ]
2302
+ end
2303
+
2304
+ specify "should accept a columns array and a values array" do
2305
+ @ds.multi_insert([:x, :y], [[1, 2], [3, 4]])
2306
+ @db.sqls.should == [
2307
+ 'BEGIN',
2308
+ "INSERT INTO items (x, y) VALUES (1, 2)",
2309
+ "INSERT INTO items (x, y) VALUES (3, 4)",
2310
+ 'COMMIT'
2311
+ ]
2312
+ end
2313
+
2314
+ specify "should accept a columns array and a dataset" do
2315
+ @ds2 = Sequel::Dataset.new(@db).from(:cats).filter(:purr => true).select(:a, :b)
2316
+
2317
+ @ds.multi_insert([:x, :y], @ds2)
2318
+ @db.sqls.should == [
2319
+ 'BEGIN',
2320
+ "INSERT INTO items (x, y) VALUES (SELECT a, b FROM cats WHERE (purr = 't'))",
2321
+ 'COMMIT'
2322
+ ]
2323
+ end
2324
+
2325
+ specify "should accept a columns array and a values array with slice option" do
2326
+ @ds.multi_insert([:x, :y], [[1, 2], [3, 4], [5, 6]], :slice => 2)
2327
+ @db.sqls.should == [
2328
+ 'BEGIN',
2329
+ "INSERT INTO items (x, y) VALUES (1, 2)",
2330
+ "INSERT INTO items (x, y) VALUES (3, 4)",
2331
+ 'COMMIT',
2332
+ 'BEGIN',
2333
+ "INSERT INTO items (x, y) VALUES (5, 6)",
2334
+ 'COMMIT'
2335
+ ]
2336
+ end
2337
+
2338
+ specify "should be aliased by #import" do
2339
+ @ds.import([:x, :y], [[1, 2], [3, 4], [5, 6]], :slice => 2)
2340
+ @db.sqls.should == [
2341
+ 'BEGIN',
2342
+ "INSERT INTO items (x, y) VALUES (1, 2)",
2343
+ "INSERT INTO items (x, y) VALUES (3, 4)",
2344
+ 'COMMIT',
2345
+ 'BEGIN',
2346
+ "INSERT INTO items (x, y) VALUES (5, 6)",
2347
+ 'COMMIT'
2348
+ ]
2349
+ end
2350
+
2351
+ specify "should not do anything if no columns or values are given" do
2352
+ @ds.multi_insert
2353
+ @db.sqls.should be_nil
2354
+
2355
+ @ds.multi_insert([])
2356
+ @db.sqls.should be_nil
2357
+
2358
+ @ds.multi_insert([], [])
2359
+ @db.sqls.should be_nil
2360
+
2361
+ @ds.multi_insert([{}, {}])
2362
+ @db.sqls.should be_nil
2363
+
2364
+ @ds.multi_insert([:a, :b], [])
2365
+ @db.sqls.should be_nil
2366
+
2367
+ @ds.multi_insert([:x, :y], [[1, 2], [3, 4], [5, 6]], :slice => 2)
2368
+ @db.sqls.should == [
2369
+ 'BEGIN',
2370
+ "INSERT INTO items (x, y) VALUES (1, 2)",
2371
+ "INSERT INTO items (x, y) VALUES (3, 4)",
2372
+ 'COMMIT',
2373
+ 'BEGIN',
2374
+ "INSERT INTO items (x, y) VALUES (5, 6)",
2375
+ 'COMMIT'
2376
+ ]
2377
+ end
2378
+
2379
+ end
2380
+
2381
+ context "Dataset#query" do
2382
+ setup do
2383
+ @d = Sequel::Dataset.new(nil)
2384
+ end
2385
+
2386
+ specify "should support #from" do
2387
+ q = @d.query {from :xxx}
2388
+ q.class.should == @d.class
2389
+ q.sql.should == "SELECT * FROM xxx"
2390
+ end
2391
+
2392
+ specify "should support #select" do
2393
+ q = @d.query do
2394
+ select :a, :b___mongo
2395
+ from :yyy
2396
+ end
2397
+ q.class.should == @d.class
2398
+ q.sql.should == "SELECT a, b AS mongo FROM yyy"
2399
+ end
2400
+
2401
+ specify "should support #where" do
2402
+ q = @d.query do
2403
+ from :zzz
2404
+ where(:x + 2 > :y + 3)
2405
+ end
2406
+ q.class.should == @d.class
2407
+ q.sql.should == "SELECT * FROM zzz WHERE ((x + 2) > (y + 3))"
2408
+
2409
+ q = @d.from(:zzz).query do
2410
+ where((:x > 1) & (:y > 2))
2411
+ end
2412
+ q.class.should == @d.class
2413
+ q.sql.should == "SELECT * FROM zzz WHERE ((x > 1) AND (y > 2))"
2414
+
2415
+ q = @d.from(:zzz).query do
2416
+ where :x => 33
2417
+ end
2418
+ q.class.should == @d.class
2419
+ q.sql.should == "SELECT * FROM zzz WHERE (x = 33)"
2420
+ end
2421
+
2422
+ specify "should support #group_by and #having" do
2423
+ q = @d.query do
2424
+ from :abc
2425
+ group_by :id
2426
+ having(:x >= 2)
2427
+ end
2428
+ q.class.should == @d.class
2429
+ q.sql.should == "SELECT * FROM abc GROUP BY id HAVING (x >= 2)"
2430
+ end
2431
+
2432
+ specify "should support #order, #order_by" do
2433
+ q = @d.query do
2434
+ from :xyz
2435
+ order_by :stamp
2436
+ end
2437
+ q.class.should == @d.class
2438
+ q.sql.should == "SELECT * FROM xyz ORDER BY stamp"
2439
+ end
2440
+
2441
+ specify "should raise on non-chainable method calls" do
2442
+ proc {@d.query {first_source}}.should raise_error(Sequel::Error)
2443
+ end
2444
+
2445
+ specify "should raise on each, insert, update, delete" do
2446
+ proc {@d.query {each}}.should raise_error(Sequel::Error)
2447
+ proc {@d.query {insert(:x => 1)}}.should raise_error(Sequel::Error)
2448
+ proc {@d.query {update(:x => 1)}}.should raise_error(Sequel::Error)
2449
+ proc {@d.query {delete}}.should raise_error(Sequel::Error)
2450
+ end
2451
+ end
2452
+
2453
+ context "Dataset" do
2454
+ setup do
2455
+ @d = Sequel::Dataset.new(nil).from(:x)
2456
+ end
2457
+
2458
+ specify "should support self-changing select!" do
2459
+ @d.select!(:y)
2460
+ @d.sql.should == "SELECT y FROM x"
2461
+ end
2462
+
2463
+ specify "should support self-changing from!" do
2464
+ @d.from!(:y)
2465
+ @d.sql.should == "SELECT * FROM y"
2466
+ end
2467
+
2468
+ specify "should support self-changing order!" do
2469
+ @d.order!(:y)
2470
+ @d.sql.should == "SELECT * FROM x ORDER BY y"
2471
+ end
2472
+
2473
+ specify "should support self-changing filter!" do
2474
+ @d.filter!(:y => 1)
2475
+ @d.sql.should == "SELECT * FROM x WHERE (y = 1)"
2476
+ end
2477
+
2478
+ specify "should support self-changing filter! with block" do
2479
+ @d.filter!{:y < 2}
2480
+ @d.sql.should == "SELECT * FROM x WHERE (y < 2)"
2481
+ end
2482
+
2483
+ specify "should raise for ! methods that don't return a dataset" do
2484
+ proc {@d.opts!}.should raise_error(NameError)
2485
+ end
2486
+
2487
+ specify "should raise for missing methods" do
2488
+ proc {@d.xuyz}.should raise_error(NameError)
2489
+ proc {@d.xyz!}.should raise_error(NameError)
2490
+ proc {@d.xyz?}.should raise_error(NameError)
2491
+ end
2492
+
2493
+ specify "should support chaining of bang methods" do
2494
+ @d.order!(:y)
2495
+ @d.filter!(:y => 1)
2496
+ @d.sql.should == "SELECT * FROM x WHERE (y = 1) ORDER BY y"
2497
+ end
2498
+ end
2499
+
2500
+ context "Dataset#transform" do
2501
+ setup do
2502
+ @c = Class.new(Sequel::Dataset) do
2503
+ attr_accessor :raw
2504
+ attr_accessor :sql
2505
+
2506
+ def fetch_rows(sql, &block)
2507
+ block[@raw]
2508
+ end
2509
+
2510
+ def insert(v)
2511
+ @sql = insert_sql(v)
2512
+ end
2513
+
2514
+ def update(v)
2515
+ @sql = update_sql(v)
2516
+ end
2517
+ end
2518
+
2519
+ @ds = @c.new(nil).from(:items)
2520
+ @ds.transform(:x => [
2521
+ proc {|v| Marshal.load(v)},
2522
+ proc {|v| Marshal.dump(v)}
2523
+ ])
2524
+ end
2525
+
2526
+ specify "should change the dataset to transform values loaded from the database" do
2527
+ @ds.raw = {:x => Marshal.dump([1, 2, 3]), :y => 'hello'}
2528
+ @ds.first.should == {:x => [1, 2, 3], :y => 'hello'}
2529
+ @ds.raw = {:x => Marshal.dump([1, 2, 3]), :y => 'hello'}
2530
+ @ds.all.should == [{:x => [1, 2, 3], :y => 'hello'}]
2531
+ end
2532
+
2533
+ specify "should change the dataset to transform values saved to the database" do
2534
+ @ds.insert(:x => :toast)
2535
+ @ds.sql.should == "INSERT INTO items (x) VALUES ('#{Marshal.dump(:toast)}')"
2536
+
2537
+ @ds.insert(:y => 'butter')
2538
+ @ds.sql.should == "INSERT INTO items (y) VALUES ('butter')"
2539
+
2540
+ @ds.update(:x => ['dream'])
2541
+ @ds.sql.should == "UPDATE items SET x = '#{Marshal.dump(['dream'])}'"
2542
+ end
2543
+
2544
+ specify "should be transferred to cloned datasets" do
2545
+ @ds2 = @ds.filter(:a => 1)
2546
+
2547
+ @ds2.raw = {:x => Marshal.dump([1, 2, 3]), :y => 'hello'}
2548
+ @ds2.first.should == {:x => [1, 2, 3], :y => 'hello'}
2549
+
2550
+ @ds2.insert(:x => :toast)
2551
+ @ds2.sql.should == "INSERT INTO items (x) VALUES ('#{Marshal.dump(:toast)}')"
2552
+ end
2553
+
2554
+ specify "should work correctly together with set_row_proc" do
2555
+ @ds.row_proc = proc{|r| r[:z] = r[:x] * 2; r}
2556
+ @ds.raw = {:x => Marshal.dump("wow"), :y => 'hello'}
2557
+ @ds.first.should == {:x => "wow", :y => 'hello', :z => "wowwow"}
2558
+
2559
+ f = nil
2560
+ @ds.raw = {:x => Marshal.dump("wow"), :y => 'hello'}
2561
+ @ds.each(:naked => true) {|r| f = r}
2562
+ f.should == {:x => "wow", :y => 'hello'}
2563
+ end
2564
+
2565
+ specify "should leave the supplied values intact" do
2566
+ h = {:x => :toast}
2567
+ @ds.insert(h)
2568
+ h.should == {:x => :toast}
2569
+ end
2570
+ end
2571
+
2572
+ context "Dataset#transform" do
2573
+ setup do
2574
+ @c = Class.new(Sequel::Dataset) do
2575
+ attr_accessor :raw
2576
+ attr_accessor :sql
2577
+
2578
+ def fetch_rows(sql, &block)
2579
+ block[@raw]
2580
+ end
2581
+
2582
+ def insert(v)
2583
+ @sql = insert_sql(v)
2584
+ end
2585
+
2586
+ def update(v)
2587
+ @sql = update_sql(v)
2588
+ end
2589
+ end
2590
+
2591
+ @ds = @c.new(nil).from(:items)
2592
+ end
2593
+
2594
+ specify "should raise Sequel::Error for invalid transformations" do
2595
+ proc {@ds.transform(:x => 'mau')}.should raise_error(Sequel::Error::InvalidTransform)
2596
+ proc {@ds.transform(:x => :mau)}.should raise_error(Sequel::Error::InvalidTransform)
2597
+ proc {@ds.transform(:x => [])}.should raise_error(Sequel::Error::InvalidTransform)
2598
+ proc {@ds.transform(:x => ['mau'])}.should raise_error(Sequel::Error::InvalidTransform)
2599
+ proc {@ds.transform(:x => [proc {|v|}, proc {|v|}])}.should_not raise_error(Sequel::Error::InvalidTransform)
2600
+ end
2601
+
2602
+ specify "should support stock YAML transformation" do
2603
+ @ds.transform(:x => :yaml)
2604
+
2605
+ @ds.raw = {:x => [1, 2, 3].to_yaml, :y => 'hello'}
2606
+ @ds.first.should == {:x => [1, 2, 3], :y => 'hello'}
2607
+
2608
+ @ds.insert(:x => :toast)
2609
+ @ds.sql.should == "INSERT INTO items (x) VALUES ('#{:toast.to_yaml}')"
2610
+ @ds.insert(:y => 'butter')
2611
+ @ds.sql.should == "INSERT INTO items (y) VALUES ('butter')"
2612
+ @ds.update(:x => ['dream'])
2613
+ @ds.sql.should == "UPDATE items SET x = '#{['dream'].to_yaml}'"
2614
+
2615
+ @ds2 = @ds.filter(:a => 1)
2616
+ @ds2.raw = {:x => [1, 2, 3].to_yaml, :y => 'hello'}
2617
+ @ds2.first.should == {:x => [1, 2, 3], :y => 'hello'}
2618
+ @ds2.insert(:x => :toast)
2619
+ @ds2.sql.should == "INSERT INTO items (x) VALUES ('#{:toast.to_yaml}')"
2620
+
2621
+ @ds.row_proc = proc{|r| r[:z] = r[:x] * 2; r}
2622
+ @ds.raw = {:x => "wow".to_yaml, :y => 'hello'}
2623
+ @ds.first.should == {:x => "wow", :y => 'hello', :z => "wowwow"}
2624
+ f = nil
2625
+ @ds.raw = {:x => "wow".to_yaml, :y => 'hello'}
2626
+ @ds.each(:naked => true) {|r| f = r}
2627
+ f.should == {:x => "wow", :y => 'hello'}
2628
+ end
2629
+
2630
+ specify "should support stock Marshal transformation with Base64 encoding" do
2631
+ @ds.transform(:x => :marshal)
2632
+
2633
+ @ds.raw = {:x => [Marshal.dump([1, 2, 3])].pack('m'), :y => 'hello'}
2634
+ @ds.first.should == {:x => [1, 2, 3], :y => 'hello'}
2635
+
2636
+ @ds.insert(:x => :toast)
2637
+ @ds.sql.should == "INSERT INTO items (x) VALUES ('#{[Marshal.dump(:toast)].pack('m')}')"
2638
+ @ds.insert(:y => 'butter')
2639
+ @ds.sql.should == "INSERT INTO items (y) VALUES ('butter')"
2640
+ @ds.update(:x => ['dream'])
2641
+ @ds.sql.should == "UPDATE items SET x = '#{[Marshal.dump(['dream'])].pack('m')}'"
2642
+
2643
+ @ds2 = @ds.filter(:a => 1)
2644
+ @ds2.raw = {:x => [Marshal.dump([1, 2, 3])].pack('m'), :y => 'hello'}
2645
+ @ds2.first.should == {:x => [1, 2, 3], :y => 'hello'}
2646
+ @ds2.insert(:x => :toast)
2647
+ @ds2.sql.should == "INSERT INTO items (x) VALUES ('#{[Marshal.dump(:toast)].pack('m')}')"
2648
+
2649
+ @ds.row_proc = proc{|r| r[:z] = r[:x] * 2; r}
2650
+ @ds.raw = {:x => [Marshal.dump("wow")].pack('m'), :y => 'hello'}
2651
+ @ds.first.should == {:x => "wow", :y => 'hello', :z => "wowwow"}
2652
+ f = nil
2653
+ @ds.raw = {:x => [Marshal.dump("wow")].pack('m'), :y => 'hello'}
2654
+ @ds.each(:naked => true) {|r| f = r}
2655
+ f.should == {:x => "wow", :y => 'hello'}
2656
+ end
2657
+
2658
+ specify "should support loading of Marshalled values without Base64 encoding" do
2659
+ @ds.transform(:x => :marshal)
2660
+
2661
+ @ds.raw = {:x => Marshal.dump([1,2,3]), :y => nil}
2662
+ @ds.first.should == {:x => [1,2,3], :y => nil}
2663
+ end
2664
+
2665
+ specify "should return self" do
2666
+ @ds.transform(:x => :marshal).should be(@ds)
2667
+ end
2668
+ end
2669
+
2670
+ context "A dataset with a transform" do
2671
+ setup do
2672
+ @ds = Sequel::Dataset.new(nil).from(:items)
2673
+ @ds.transform(:x => :marshal)
2674
+ end
2675
+
2676
+ specify "should automatically transform hash filters" do
2677
+ @ds.filter(:y => 2).sql.should == 'SELECT * FROM items WHERE (y = 2)'
2678
+
2679
+ @ds.filter(:x => 2).sql.should == "SELECT * FROM items WHERE (x = '#{[Marshal.dump(2)].pack('m')}')"
2680
+ end
2681
+ end
2682
+
2683
+ context "Dataset#to_csv" do
2684
+ setup do
2685
+ @c = Class.new(Sequel::Dataset) do
2686
+ attr_accessor :data
2687
+ attr_accessor :columns
2688
+
2689
+ def fetch_rows(sql, &block)
2690
+ @data.each(&block)
2691
+ end
2692
+
2693
+ # naked should return self here because to_csv wants a naked result set.
2694
+ def naked
2695
+ self
2696
+ end
2697
+ end
2698
+
2699
+ @ds = @c.new(nil).from(:items)
2700
+ @ds.columns = [:a, :b, :c]
2701
+ @ds.data = [ {:a=>1, :b=>2, :c=>3}, {:a=>4, :b=>5, :c=>6}, {:a=>7, :b=>8, :c=>9} ]
2702
+ end
2703
+
2704
+ specify "should format a CSV representation of the records" do
2705
+ @ds.to_csv.should ==
2706
+ "a, b, c\r\n1, 2, 3\r\n4, 5, 6\r\n7, 8, 9\r\n"
2707
+ end
2708
+
2709
+ specify "should exclude column titles if so specified" do
2710
+ @ds.to_csv(false).should ==
2711
+ "1, 2, 3\r\n4, 5, 6\r\n7, 8, 9\r\n"
2712
+ end
2713
+ end
2714
+
2715
+ context "Dataset#create_view" do
2716
+ setup do
2717
+ @dbc = Class.new(Sequel::Database) do
2718
+ attr_reader :sqls
2719
+
2720
+ def execute(sql)
2721
+ @sqls ||= []
2722
+ @sqls << sql
2723
+ end
2724
+ end
2725
+ @db = @dbc.new
2726
+
2727
+ @ds = @db[:items].order(:abc).filter(:category => 'ruby')
2728
+ end
2729
+
2730
+ specify "should create a view with the dataset's sql" do
2731
+ @ds.create_view(:xyz)
2732
+ @db.sqls.should == ["CREATE VIEW xyz AS #{@ds.sql}"]
2733
+ end
2734
+ end
2735
+
2736
+ context "Dataset#create_or_replace_view" do
2737
+ setup do
2738
+ @dbc = Class.new(Sequel::Database) do
2739
+ attr_reader :sqls
2740
+
2741
+ def execute(sql)
2742
+ @sqls ||= []
2743
+ @sqls << sql
2744
+ end
2745
+ end
2746
+ @db = @dbc.new
2747
+
2748
+ @ds = @db[:items].order(:abc).filter(:category => 'ruby')
2749
+ end
2750
+
2751
+ specify "should create a view with the dataset's sql" do
2752
+ @ds.create_or_replace_view(:xyz)
2753
+ @db.sqls.should == ["CREATE OR REPLACE VIEW xyz AS #{@ds.sql}"]
2754
+ end
2755
+ end
2756
+
2757
+ context "Dataset#update_sql" do
2758
+ setup do
2759
+ @ds = Sequel::Dataset.new(nil).from(:items)
2760
+ end
2761
+
2762
+ specify "should accept strings" do
2763
+ @ds.update_sql("a = b").should == "UPDATE items SET a = b"
2764
+ end
2765
+
2766
+ specify "should accept hash with string keys" do
2767
+ @ds.update_sql('c' => 'd').should == "UPDATE items SET c = 'd'"
2768
+ end
2769
+
2770
+ specify "should accept array subscript references" do
2771
+ @ds.update_sql((:day|1) => 'd').should == "UPDATE items SET day[1] = 'd'"
2772
+ end
2773
+ end
2774
+
2775
+ context "Dataset#insert_sql" do
2776
+ setup do
2777
+ @ds = Sequel::Dataset.new(nil).from(:items)
2778
+ end
2779
+
2780
+ specify "should accept hash with symbol keys" do
2781
+ @ds.insert_sql(:c => 'd').should == "INSERT INTO items (c) VALUES ('d')"
2782
+ end
2783
+
2784
+ specify "should accept hash with string keys" do
2785
+ @ds.insert_sql('c' => 'd').should == "INSERT INTO items (c) VALUES ('d')"
2786
+ end
2787
+
2788
+ specify "should accept array subscript references" do
2789
+ @ds.insert_sql((:day|1) => 'd').should == "INSERT INTO items (day[1]) VALUES ('d')"
2790
+ end
2791
+ end
2792
+
2793
+ class DummyMummyDataset < Sequel::Dataset
2794
+ def first
2795
+ raise if @opts[:from] == [:a]
2796
+ true
2797
+ end
2798
+ end
2799
+
2800
+ class DummyMummyDatabase < Sequel::Database
2801
+ attr_reader :sqls
2802
+
2803
+ def execute(sql)
2804
+ @sqls ||= []
2805
+ @sqls << sql
2806
+ end
2807
+
2808
+ def transaction; yield; end
2809
+
2810
+ def dataset
2811
+ DummyMummyDataset.new(self)
2812
+ end
2813
+ end
2814
+
2815
+ context "Dataset#table_exists?" do
2816
+ setup do
2817
+ @db = DummyMummyDatabase.new
2818
+ @db.stub!(:tables).and_return([:a, :b])
2819
+ @db2 = DummyMummyDatabase.new
2820
+ end
2821
+
2822
+ specify "should use Database#tables if available" do
2823
+ @db[:a].table_exists?.should be_true
2824
+ @db[:b].table_exists?.should be_true
2825
+ @db[:c].table_exists?.should be_false
2826
+ end
2827
+
2828
+ specify "should otherwise try to select the first record from the table's dataset" do
2829
+ @db2[:a].table_exists?.should be_false
2830
+ @db2[:b].table_exists?.should be_true
2831
+ end
2832
+
2833
+ specify "should raise Sequel::Error if dataset references more than one table" do
2834
+ proc {@db.from(:a, :b).table_exists?}.should raise_error(Sequel::Error)
2835
+ end
2836
+
2837
+ specify "should raise Sequel::Error if dataset is from a subquery" do
2838
+ proc {@db.from(@db[:a]).table_exists?}.should raise_error(Sequel::Error)
2839
+ end
2840
+
2841
+ specify "should raise Sequel::Error if dataset has fixed sql" do
2842
+ proc {@db['select * from blah'].table_exists?}.should raise_error(Sequel::Error)
2843
+ end
2844
+ end
2845
+
2846
+ context "Dataset#inspect" do
2847
+ setup do
2848
+ @ds = Sequel::Dataset.new(nil).from(:blah)
2849
+ end
2850
+
2851
+ specify "should include the class name and the corresponding SQL statement" do
2852
+ @ds.inspect.should == '#<%s: %s>' % [@ds.class.to_s, @ds.sql.inspect]
2853
+ end
2854
+ end
2855
+
2856
+ context "Dataset#all" do
2857
+ setup do
2858
+ @c = Class.new(Sequel::Dataset) do
2859
+ def fetch_rows(sql, &block)
2860
+ block.call({:x => 1, :y => 2})
2861
+ block.call({:x => 3, :y => 4})
2862
+ block.call(sql)
2863
+ end
2864
+ end
2865
+ @dataset = @c.new(nil).from(:items)
2866
+ end
2867
+
2868
+ specify "should return an array with all records" do
2869
+ @dataset.all.should == [
2870
+ {:x => 1, :y => 2},
2871
+ {:x => 3, :y => 4},
2872
+ "SELECT * FROM items"
2873
+ ]
2874
+ end
2875
+
2876
+ specify "should accept options and pass them to #each" do
2877
+ @dataset.all(:limit => 33).should == [
2878
+ {:x => 1, :y => 2},
2879
+ {:x => 3, :y => 4},
2880
+ "SELECT * FROM items LIMIT 33"
2881
+ ]
2882
+ end
2883
+
2884
+ specify "should iterate over the array if a block is given" do
2885
+ a = []
2886
+
2887
+ @dataset.all do |r|
2888
+ a << (r.is_a?(Hash) ? r[:x] : r)
2889
+ end
2890
+
2891
+ a.should == [1, 3, "SELECT * FROM items"]
2892
+ end
2893
+ end
2894
+
2895
+ context "Dataset#grep" do
2896
+ setup do
2897
+ @ds = Sequel::Dataset.new(nil).from(:posts)
2898
+ end
2899
+
2900
+ specify "should format a SQL filter correctly" do
2901
+ @ds.grep(:title, 'ruby').sql.should ==
2902
+ "SELECT * FROM posts WHERE ((title LIKE 'ruby'))"
2903
+ end
2904
+
2905
+ specify "should support multiple columns" do
2906
+ @ds.grep([:title, :body], 'ruby').sql.should ==
2907
+ "SELECT * FROM posts WHERE ((title LIKE 'ruby') OR (body LIKE 'ruby'))"
2908
+ end
2909
+
2910
+ specify "should support multiple search terms" do
2911
+ @ds.grep(:title, ['abc', 'def']).sql.should ==
2912
+ "SELECT * FROM posts WHERE (((title LIKE 'abc') OR (title LIKE 'def')))"
2913
+ end
2914
+
2915
+ specify "should support multiple columns and search terms" do
2916
+ @ds.grep([:title, :body], ['abc', 'def']).sql.should ==
2917
+ "SELECT * FROM posts WHERE (((title LIKE 'abc') OR (title LIKE 'def')) OR ((body LIKE 'abc') OR (body LIKE 'def')))"
2918
+ end
2919
+
2920
+ specify "should support regexps though the database may not support it" do
2921
+ @ds.grep(:title, /ruby/).sql.should ==
2922
+ "SELECT * FROM posts WHERE ((title ~ 'ruby'))"
2923
+
2924
+ @ds.grep(:title, [/^ruby/, 'ruby']).sql.should ==
2925
+ "SELECT * FROM posts WHERE (((title ~ '^ruby') OR (title LIKE 'ruby')))"
2926
+ end
2927
+ end
2928
+
2929
+ context "Sequel.use_parse_tree" do
2930
+ specify "be false" do
2931
+ Sequel.use_parse_tree.should == false
2932
+ end
2933
+ end
2934
+
2935
+ context "Sequel.use_parse_tree=" do
2936
+ specify "raise an error if true" do
2937
+ proc{Sequel.use_parse_tree = true}.should raise_error(Sequel::Error)
2938
+ end
2939
+
2940
+ specify "do nothing if false" do
2941
+ proc{Sequel.use_parse_tree = false}.should_not raise_error
2942
+ end
2943
+ end
2944
+
2945
+ context "Dataset.dataset_classes" do
2946
+ specify "should be an array of dataset subclasses" do
2947
+ ds_class = Class.new(Sequel::Dataset)
2948
+ Sequel::Dataset.dataset_classes.should be_a_kind_of(Array)
2949
+ Sequel::Dataset.dataset_classes.should include(ds_class)
2950
+ end
2951
+ end
2952
+
2953
+ context "Dataset default #fetch_rows, #insert, #update, and #delete" do
2954
+ setup do
2955
+ @db = Sequel::Database.new
2956
+ @ds = @db[:items]
2957
+ end
2958
+
2959
+ specify "#fetch_rows should raise a NotImplementedError" do
2960
+ proc{@ds.fetch_rows(''){}}.should raise_error(NotImplementedError)
2961
+ end
2962
+
2963
+ specify "#delete should execute delete SQL" do
2964
+ @db.should_receive(:execute).once.with('DELETE FROM items')
2965
+ @ds.delete
2966
+ end
2967
+
2968
+ specify "#insert should execute insert SQL" do
2969
+ @db.should_receive(:execute).once.with('INSERT INTO items DEFAULT VALUES')
2970
+ @ds.insert([])
2971
+ end
2972
+
2973
+ specify "#update should execute update SQL" do
2974
+ @db.should_receive(:execute).once.with('UPDATE items SET number = 1')
2975
+ @ds.update(:number=>1)
2976
+ end
2977
+ end