sequel 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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