sequel 0.1.7 → 0.1.8
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.
- data/CHANGELOG +50 -28
- data/README +1 -1
- data/Rakefile +13 -3
- data/lib/sequel/database.rb +2 -2
- data/lib/sequel/dataset.rb +180 -674
- data/lib/sequel/dataset/dataset_convenience.rb +132 -0
- data/lib/sequel/dataset/dataset_sql.rb +564 -0
- data/lib/sequel/dbi.rb +5 -4
- data/lib/sequel/model.rb +2 -2
- data/lib/sequel/mysql.rb +5 -48
- data/lib/sequel/odbc.rb +7 -12
- data/lib/sequel/postgres.rb +22 -73
- data/lib/sequel/sqlite.rb +54 -15
- data/spec/adapters/sqlite_spec.rb +104 -0
- data/spec/connection_pool_spec.rb +270 -0
- data/spec/core_ext_spec.rb +127 -0
- data/spec/database_spec.rb +366 -0
- data/spec/dataset_spec.rb +1449 -0
- data/spec/expressions_spec.rb +151 -0
- metadata +12 -2
@@ -0,0 +1,1449 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../lib/sequel')
|
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_merge for chainability." do
|
22
|
+
d1 = @dataset.clone_merge(: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_merge(: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
|
+
|
43
|
+
specify "should raise NotImplementedError for the dataset interface methods" do
|
44
|
+
proc {@dataset.fetch_rows('abc')}.should raise_error(NotImplementedError)
|
45
|
+
proc {@dataset.insert(1, 2, 3)}.should raise_error(NotImplementedError)
|
46
|
+
proc {@dataset.update(:name => 'abc')}.should raise_error(NotImplementedError)
|
47
|
+
proc {@dataset.delete}.should raise_error(NotImplementedError)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "Dataset#clone_merge" do
|
52
|
+
setup do
|
53
|
+
@dataset = Sequel::Dataset.new(nil).from(:items)
|
54
|
+
end
|
55
|
+
|
56
|
+
specify "should return a clone self" do
|
57
|
+
clone = @dataset.clone_merge({})
|
58
|
+
clone.class.should == @dataset.class
|
59
|
+
clone.db.should == @dataset.db
|
60
|
+
clone.opts.should == @dataset.opts
|
61
|
+
end
|
62
|
+
|
63
|
+
specify "should merge the specified options" do
|
64
|
+
clone = @dataset.clone_merge(1 => 2)
|
65
|
+
clone.opts.should == {1 => 2, :from => [:items]}
|
66
|
+
end
|
67
|
+
|
68
|
+
specify "should overwrite existing options" do
|
69
|
+
clone = @dataset.clone_merge(:from => [:other])
|
70
|
+
clone.opts.should == {:from => [:other]}
|
71
|
+
end
|
72
|
+
|
73
|
+
specify "should create a clone with a deep copy of options" do
|
74
|
+
clone = @dataset.clone_merge(:from => [:other])
|
75
|
+
@dataset.opts[:from].should == [:items]
|
76
|
+
clone.opts[:from].should == [:other]
|
77
|
+
end
|
78
|
+
|
79
|
+
specify "should return an object with the same modules included" do
|
80
|
+
m = Module.new do
|
81
|
+
def __xyz__; "xyz"; end
|
82
|
+
end
|
83
|
+
@dataset.extend(m)
|
84
|
+
@dataset.clone_merge({}).should respond_to(:__xyz__)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "A simple dataset" do
|
89
|
+
setup do
|
90
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
91
|
+
end
|
92
|
+
|
93
|
+
specify "should format a select statement" do
|
94
|
+
@dataset.select_sql.should == 'SELECT * FROM test'
|
95
|
+
end
|
96
|
+
|
97
|
+
specify "should format a delete statement" do
|
98
|
+
@dataset.delete_sql.should == 'DELETE FROM test'
|
99
|
+
end
|
100
|
+
|
101
|
+
specify "should format an insert statement" do
|
102
|
+
@dataset.insert_sql.should == 'INSERT INTO test DEFAULT VALUES'
|
103
|
+
@dataset.insert_sql(:name => 'wxyz', :price => 342).
|
104
|
+
should match(/INSERT INTO test \(name, price\) VALUES \('wxyz', 342\)|INSERT INTO test \(price, name\) VALUES \(342, 'wxyz'\)/)
|
105
|
+
@dataset.insert_sql('a', 2, 6.5).should ==
|
106
|
+
"INSERT INTO test VALUES ('a', 2, 6.5)"
|
107
|
+
end
|
108
|
+
|
109
|
+
specify "should format an update statement" do
|
110
|
+
@dataset.update_sql(:name => 'abc').should ==
|
111
|
+
"UPDATE test SET name = 'abc'"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "A dataset with multiple tables in its FROM clause" do
|
116
|
+
setup do
|
117
|
+
@dataset = Sequel::Dataset.new(nil).from(:t1, :t2)
|
118
|
+
end
|
119
|
+
|
120
|
+
specify "should raise on #update_sql" do
|
121
|
+
proc {@dataset.update_sql(:a=>1)}.should raise_error
|
122
|
+
end
|
123
|
+
|
124
|
+
specify "should raise on #delete_sql" do
|
125
|
+
proc {@dataset.delete_sql}.should raise_error
|
126
|
+
end
|
127
|
+
|
128
|
+
specify "should generate a select query FROM all specified tables" do
|
129
|
+
@dataset.select_sql.should == "SELECT * FROM t1, t2"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "Dataset#where" do
|
134
|
+
setup do
|
135
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
136
|
+
@d1 = @dataset.where(:region => 'Asia')
|
137
|
+
@d2 = @dataset.where('(region = ?)', 'Asia')
|
138
|
+
@d3 = @dataset.where("(a = 1)")
|
139
|
+
end
|
140
|
+
|
141
|
+
specify "should work with hashes" do
|
142
|
+
@dataset.where(:name => 'xyz', :price => 342).select_sql.
|
143
|
+
should match(/WHERE \(name = 'xyz'\) AND \(price = 342\)|WHERE \(price = 342\) AND \(name = 'xyz'\)/)
|
144
|
+
end
|
145
|
+
|
146
|
+
specify "should work with arrays (ala ActiveRecord)" do
|
147
|
+
@dataset.where('price < ? AND id in (?)', 100, [1, 2, 3]).select_sql.should ==
|
148
|
+
"SELECT * FROM test WHERE price < 100 AND id in (1, 2, 3)"
|
149
|
+
end
|
150
|
+
|
151
|
+
specify "should work with strings (custom SQL expressions)" do
|
152
|
+
@dataset.where('(a = 1 AND b = 2)').select_sql.should ==
|
153
|
+
"SELECT * FROM test WHERE (a = 1 AND b = 2)"
|
154
|
+
end
|
155
|
+
|
156
|
+
specify "should affect select, delete and update statements" do
|
157
|
+
@d1.select_sql.should == "SELECT * FROM test WHERE (region = 'Asia')"
|
158
|
+
@d1.delete_sql.should == "DELETE FROM test WHERE (region = 'Asia')"
|
159
|
+
@d1.update_sql(:GDP => 0).should == "UPDATE test SET GDP = 0 WHERE (region = 'Asia')"
|
160
|
+
|
161
|
+
@d2.select_sql.should == "SELECT * FROM test WHERE (region = 'Asia')"
|
162
|
+
@d2.delete_sql.should == "DELETE FROM test WHERE (region = 'Asia')"
|
163
|
+
@d2.update_sql(:GDP => 0).should == "UPDATE test SET GDP = 0 WHERE (region = 'Asia')"
|
164
|
+
|
165
|
+
@d3.select_sql.should == "SELECT * FROM test WHERE (a = 1)"
|
166
|
+
@d3.delete_sql.should == "DELETE FROM test WHERE (a = 1)"
|
167
|
+
@d3.update_sql(:GDP => 0).should == "UPDATE test SET GDP = 0 WHERE (a = 1)"
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
specify "should be composable using AND operator (for scoping)" do
|
172
|
+
# hashes are merged, no problem
|
173
|
+
@d1.where(:size => 'big').select_sql.should ==
|
174
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND (size = 'big')"
|
175
|
+
|
176
|
+
# hash and string
|
177
|
+
@d1.where('population > 1000').select_sql.should ==
|
178
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND (population > 1000)"
|
179
|
+
@d1.where('(a > 1) OR (b < 2)').select_sql.should ==
|
180
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND ((a > 1) OR (b < 2))"
|
181
|
+
|
182
|
+
# hash and array
|
183
|
+
@d1.where('(GDP > ?)', 1000).select_sql.should ==
|
184
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND (GDP > 1000)"
|
185
|
+
|
186
|
+
# array and array
|
187
|
+
@d2.where('(GDP > ?)', 1000).select_sql.should ==
|
188
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND (GDP > 1000)"
|
189
|
+
|
190
|
+
# array and hash
|
191
|
+
@d2.where(:name => ['Japan', 'China']).select_sql.should ==
|
192
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND (name IN ('Japan', 'China'))"
|
193
|
+
|
194
|
+
# array and string
|
195
|
+
@d2.where('GDP > ?').select_sql.should ==
|
196
|
+
"SELECT * FROM test WHERE (region = 'Asia') AND (GDP > ?)"
|
197
|
+
|
198
|
+
# string and string
|
199
|
+
@d3.where('b = 2').select_sql.should ==
|
200
|
+
"SELECT * FROM test WHERE (a = 1) AND (b = 2)"
|
201
|
+
|
202
|
+
# string and hash
|
203
|
+
@d3.where(:c => 3).select_sql.should ==
|
204
|
+
"SELECT * FROM test WHERE (a = 1) AND (c = 3)"
|
205
|
+
|
206
|
+
# string and array
|
207
|
+
@d3.where('(d = ?)', 4).select_sql.should ==
|
208
|
+
"SELECT * FROM test WHERE (a = 1) AND (d = 4)"
|
209
|
+
|
210
|
+
# string and proc expr
|
211
|
+
@d3.where {e < 5}.select_sql.should ==
|
212
|
+
"SELECT * FROM test WHERE (a = 1) AND (e < 5)"
|
213
|
+
end
|
214
|
+
|
215
|
+
specify "should raise if the dataset is grouped" do
|
216
|
+
proc {@dataset.group(:t).where(:a => 1)}.should raise_error
|
217
|
+
end
|
218
|
+
|
219
|
+
specify "should accept ranges" do
|
220
|
+
@dataset.filter(:id => 4..7).sql.should ==
|
221
|
+
'SELECT * FROM test WHERE (id >= 4 AND id <= 7)'
|
222
|
+
@dataset.filter(:id => 4...7).sql.should ==
|
223
|
+
'SELECT * FROM test WHERE (id >= 4 AND id < 7)'
|
224
|
+
|
225
|
+
@dataset.filter {id == (4..7)}.sql.should ==
|
226
|
+
'SELECT * FROM test WHERE (id >= 4 AND id <= 7)'
|
227
|
+
|
228
|
+
@dataset.filter {id.in 4..7}.sql.should ==
|
229
|
+
'SELECT * FROM test WHERE (id >= 4 AND id <= 7)'
|
230
|
+
end
|
231
|
+
|
232
|
+
specify "should accept nil" do
|
233
|
+
@dataset.filter(:owner_id => nil).sql.should ==
|
234
|
+
'SELECT * FROM test WHERE (owner_id IS NULL)'
|
235
|
+
|
236
|
+
@dataset.filter{owner_id.nil?}.sql.should ==
|
237
|
+
'SELECT * FROM test WHERE (owner_id IS NULL)'
|
238
|
+
end
|
239
|
+
|
240
|
+
specify "should accept a subquery" do
|
241
|
+
# select all countries that have GDP greater than the average for Asia
|
242
|
+
@dataset.filter('gdp > ?', @d1.select(:gdp.AVG)).sql.should ==
|
243
|
+
"SELECT * FROM test WHERE gdp > (SELECT avg(gdp) FROM test WHERE (region = 'Asia'))"
|
244
|
+
|
245
|
+
@dataset.filter(:id => @d1.select(:id)).sql.should ==
|
246
|
+
"SELECT * FROM test WHERE (id IN (SELECT id FROM test WHERE (region = 'Asia')))"
|
247
|
+
end
|
248
|
+
|
249
|
+
specify "should accept a subquery for an EXISTS clause" do
|
250
|
+
a = @dataset.filter {price < 100}
|
251
|
+
@dataset.filter(a.exists).sql.should ==
|
252
|
+
'SELECT * FROM test WHERE EXISTS (SELECT 1 FROM test WHERE (price < 100))'
|
253
|
+
end
|
254
|
+
|
255
|
+
specify "should accept proc expressions (nice!)" do
|
256
|
+
d = @d1.select(:gdp.AVG)
|
257
|
+
@dataset.filter {gdp > d}.sql.should ==
|
258
|
+
"SELECT * FROM test WHERE (gdp > (SELECT avg(gdp) FROM test WHERE (region = 'Asia')))"
|
259
|
+
|
260
|
+
@dataset.filter {id.in 4..7}.sql.should ==
|
261
|
+
'SELECT * FROM test WHERE (id >= 4 AND id <= 7)'
|
262
|
+
|
263
|
+
@dataset.filter {c == 3}.sql.should ==
|
264
|
+
'SELECT * FROM test WHERE (c = 3)'
|
265
|
+
|
266
|
+
@dataset.filter {id == :items__id}.sql.should ==
|
267
|
+
'SELECT * FROM test WHERE (id = items.id)'
|
268
|
+
|
269
|
+
@dataset.filter {a < 1}.sql.should ==
|
270
|
+
'SELECT * FROM test WHERE (a < 1)'
|
271
|
+
|
272
|
+
@dataset.filter {a <=> 1}.sql.should ==
|
273
|
+
'SELECT * FROM test WHERE NOT (a = 1)'
|
274
|
+
|
275
|
+
@dataset.filter {a >= 1 && b <= 2}.sql.should ==
|
276
|
+
'SELECT * FROM test WHERE (a >= 1) AND (b <= 2)'
|
277
|
+
|
278
|
+
@dataset.filter {c =~ 'ABC%'}.sql.should ==
|
279
|
+
"SELECT * FROM test WHERE (c LIKE 'ABC%')"
|
280
|
+
|
281
|
+
@dataset.filter {test.ccc =~ 'ABC%'}.sql.should ==
|
282
|
+
"SELECT * FROM test WHERE (test.ccc LIKE 'ABC%')"
|
283
|
+
end
|
284
|
+
|
285
|
+
specify "should raise SequelError for invalid proc expressions" do
|
286
|
+
proc {@dataset.filter {Object.czxczxcz}}.should raise_error(SequelError)
|
287
|
+
proc {@dataset.filter {a.bcvxv}}.should raise_error(SequelError)
|
288
|
+
proc {@dataset.filter {x}}.should raise_error(SequelError)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
context "Dataset#or" do
|
293
|
+
setup do
|
294
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
295
|
+
@d1 = @dataset.where(:x => 1)
|
296
|
+
end
|
297
|
+
|
298
|
+
specify "should raise if no filter exists" do
|
299
|
+
proc {@dataset.or(:a => 1)}.should raise_error(SequelError)
|
300
|
+
end
|
301
|
+
|
302
|
+
specify "should add an alternative expression to the where clause" do
|
303
|
+
@d1.or(:y => 2).sql.should ==
|
304
|
+
'SELECT * FROM test WHERE (x = 1) OR (y = 2)'
|
305
|
+
end
|
306
|
+
|
307
|
+
specify "should accept all forms of filters" do
|
308
|
+
# probably not exhaustive, but good enough
|
309
|
+
@d1.or('(y > ?)', 2).sql.should ==
|
310
|
+
'SELECT * FROM test WHERE (x = 1) OR (y > 2)'
|
311
|
+
|
312
|
+
(@d1.or {yy > 3}).sql.should ==
|
313
|
+
'SELECT * FROM test WHERE (x = 1) OR (yy > 3)'
|
314
|
+
end
|
315
|
+
|
316
|
+
specify "should correctly add parens to give predictable results" do
|
317
|
+
@d1.filter(:y => 2).or(:z => 3).sql.should ==
|
318
|
+
'SELECT * FROM test WHERE ((x = 1) AND (y = 2)) OR (z = 3)'
|
319
|
+
|
320
|
+
@d1.or(:y => 2).filter(:z => 3).sql.should ==
|
321
|
+
'SELECT * FROM test WHERE ((x = 1) OR (y = 2)) AND (z = 3)'
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
context "Dataset#and" do
|
326
|
+
setup do
|
327
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
328
|
+
@d1 = @dataset.where(:x => 1)
|
329
|
+
end
|
330
|
+
|
331
|
+
specify "should raise if no filter exists" do
|
332
|
+
proc {@dataset.and(:a => 1)}.should raise_error(SequelError)
|
333
|
+
end
|
334
|
+
|
335
|
+
specify "should add an alternative expression to the where clause" do
|
336
|
+
@d1.and(:y => 2).sql.should ==
|
337
|
+
'SELECT * FROM test WHERE (x = 1) AND (y = 2)'
|
338
|
+
end
|
339
|
+
|
340
|
+
specify "should accept all forms of filters" do
|
341
|
+
# probably not exhaustive, but good enough
|
342
|
+
@d1.and('(y > ?)', 2).sql.should ==
|
343
|
+
'SELECT * FROM test WHERE (x = 1) AND (y > 2)'
|
344
|
+
|
345
|
+
(@d1.and {yy > 3}).sql.should ==
|
346
|
+
'SELECT * FROM test WHERE (x = 1) AND (yy > 3)'
|
347
|
+
end
|
348
|
+
|
349
|
+
specify "should correctly add parens to give predictable results" do
|
350
|
+
@d1.or(:y => 2).and(:z => 3).sql.should ==
|
351
|
+
'SELECT * FROM test WHERE ((x = 1) OR (y = 2)) AND (z = 3)'
|
352
|
+
|
353
|
+
@d1.and(:y => 2).or(:z => 3).sql.should ==
|
354
|
+
'SELECT * FROM test WHERE ((x = 1) AND (y = 2)) OR (z = 3)'
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
context "Dataset#exclude" do
|
359
|
+
setup do
|
360
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
361
|
+
end
|
362
|
+
|
363
|
+
specify "should correctly include the NOT operator when one condition is given" do
|
364
|
+
@dataset.exclude(:region=>'Asia').select_sql.should ==
|
365
|
+
"SELECT * FROM test WHERE NOT (region = 'Asia')"
|
366
|
+
end
|
367
|
+
|
368
|
+
specify "should take multiple conditions as a hash and express the logic correctly in SQL" do
|
369
|
+
@dataset.exclude(:region => 'Asia', :name => 'Japan').select_sql.
|
370
|
+
should match(Regexp.union(/WHERE NOT \(\(region = 'Asia'\) AND \(name = 'Japan'\)\)/,
|
371
|
+
/WHERE NOT \(\(name = 'Japan'\) AND \(region = 'Asia'\)\)/))
|
372
|
+
end
|
373
|
+
|
374
|
+
specify "should parenthesize a single string condition correctly" do
|
375
|
+
@dataset.exclude("region = 'Asia' AND name = 'Japan'").select_sql.should ==
|
376
|
+
"SELECT * FROM test WHERE NOT (region = 'Asia' AND name = 'Japan')"
|
377
|
+
end
|
378
|
+
|
379
|
+
specify "should parenthesize an array condition correctly" do
|
380
|
+
@dataset.exclude('region = ? AND name = ?', 'Asia', 'Japan').select_sql.should ==
|
381
|
+
"SELECT * FROM test WHERE NOT (region = 'Asia' AND name = 'Japan')"
|
382
|
+
end
|
383
|
+
|
384
|
+
specify "should corrently parenthesize when it is used twice" do
|
385
|
+
@dataset.exclude(:region => 'Asia').exclude(:name => 'Japan').select_sql.should ==
|
386
|
+
"SELECT * FROM test WHERE NOT (region = 'Asia') AND NOT (name = 'Japan')"
|
387
|
+
end
|
388
|
+
|
389
|
+
specify "should support proc expressions" do
|
390
|
+
@dataset.exclude {id == (6...12)}.sql.should ==
|
391
|
+
'SELECT * FROM test WHERE NOT ((id >= 6 AND id < 12))'
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
context "Dataset#having" do
|
396
|
+
setup do
|
397
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
398
|
+
@grouped = @dataset.group(:region).select(:region, :population.SUM, :gdp.AVG)
|
399
|
+
@d1 = @grouped.having('sum(population) > 10')
|
400
|
+
@d2 = @grouped.having(:region => 'Asia')
|
401
|
+
@fields = "region, sum(population), avg(gdp)"
|
402
|
+
end
|
403
|
+
|
404
|
+
specify "should raise if the dataset is not grouped" do
|
405
|
+
proc {@dataset.having('avg(gdp) > 10')}.should raise_error
|
406
|
+
end
|
407
|
+
|
408
|
+
specify "should affect select statements" do
|
409
|
+
@d1.select_sql.should ==
|
410
|
+
"SELECT #{@fields} FROM test GROUP BY region HAVING sum(population) > 10"
|
411
|
+
end
|
412
|
+
|
413
|
+
specify "should support proc expressions" do
|
414
|
+
@grouped.having {SUM(:population) > 10}.sql.should ==
|
415
|
+
"SELECT #{@fields} FROM test GROUP BY region HAVING (sum(population) > 10)"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
context "a grouped dataset" do
|
420
|
+
setup do
|
421
|
+
@dataset = Sequel::Dataset.new(nil).from(:test).group(:type_id)
|
422
|
+
end
|
423
|
+
|
424
|
+
specify "should raise when trying to generate an update statement" do
|
425
|
+
proc {@dataset.update_sql(:id => 0)}.should raise_error
|
426
|
+
end
|
427
|
+
|
428
|
+
specify "should raise when trying to generate a delete statement" do
|
429
|
+
proc {@dataset.delete_sql}.should raise_error
|
430
|
+
end
|
431
|
+
|
432
|
+
specify "should specify the grouping in generated select statement" do
|
433
|
+
@dataset.select_sql.should ==
|
434
|
+
"SELECT * FROM test GROUP BY type_id"
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
context "Dataset#literal" do
|
440
|
+
setup do
|
441
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
442
|
+
end
|
443
|
+
|
444
|
+
specify "should escape strings properly" do
|
445
|
+
@dataset.literal('abc').should == "'abc'"
|
446
|
+
@dataset.literal('a"x"bc').should == "'a\"x\"bc'"
|
447
|
+
@dataset.literal("a'bc").should == "'a''bc'"
|
448
|
+
@dataset.literal("a''bc").should == "'a''''bc'"
|
449
|
+
end
|
450
|
+
|
451
|
+
specify "should literalize numbers properly" do
|
452
|
+
@dataset.literal(1).should == "1"
|
453
|
+
@dataset.literal(1.5).should == "1.5"
|
454
|
+
end
|
455
|
+
|
456
|
+
specify "should literalize nil as NULL" do
|
457
|
+
@dataset.literal(nil).should == "NULL"
|
458
|
+
end
|
459
|
+
|
460
|
+
specify "should literalize an array properly" do
|
461
|
+
@dataset.literal([]).should == "NULL"
|
462
|
+
@dataset.literal([1, 'abc', 3]).should == "1, 'abc', 3"
|
463
|
+
@dataset.literal([1, "a'b''c", 3]).should == "1, 'a''b''''c', 3"
|
464
|
+
end
|
465
|
+
|
466
|
+
specify "should literalize symbols as column references" do
|
467
|
+
@dataset.literal(:name).should == "name"
|
468
|
+
@dataset.literal(:items__name).should == "items.name"
|
469
|
+
end
|
470
|
+
|
471
|
+
specify "should raise an error for unsupported types" do
|
472
|
+
proc {@dataset.literal({})}.should raise_error
|
473
|
+
end
|
474
|
+
|
475
|
+
specify "should literalize datasets as subqueries" do
|
476
|
+
d = @dataset.from(:test)
|
477
|
+
d.literal(d).should == "(#{d.sql})"
|
478
|
+
end
|
479
|
+
|
480
|
+
specify "should literalize Time properly" do
|
481
|
+
t = Time.now
|
482
|
+
s = t.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'")
|
483
|
+
@dataset.literal(t).should == s
|
484
|
+
end
|
485
|
+
|
486
|
+
specify "should literalize Date properly" do
|
487
|
+
d = Date.today
|
488
|
+
s = d.strftime("DATE '%Y-%m-%d'")
|
489
|
+
@dataset.literal(d).should == s
|
490
|
+
end
|
491
|
+
|
492
|
+
specify "should not literalize expression strings" do
|
493
|
+
@dataset.literal('col1 + 2'.expr).should == 'col1 + 2'
|
494
|
+
|
495
|
+
@dataset.update_sql(:a => 'a + 2'.expr).should ==
|
496
|
+
'UPDATE test SET a = a + 2'
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
context "Dataset#from" do
|
501
|
+
setup do
|
502
|
+
@dataset = Sequel::Dataset.new(nil)
|
503
|
+
end
|
504
|
+
|
505
|
+
specify "should accept a Dataset" do
|
506
|
+
proc {@dataset.from(@dataset)}.should_not raise_error
|
507
|
+
end
|
508
|
+
|
509
|
+
specify "should format a Dataset as a subquery if it has had options set" do
|
510
|
+
@dataset.from(@dataset.from(:a).where(:a=>1)).select_sql.should ==
|
511
|
+
"SELECT * FROM (SELECT * FROM a WHERE (a = 1))"
|
512
|
+
end
|
513
|
+
|
514
|
+
specify "should use the relevant table name if given a simple dataset" do
|
515
|
+
@dataset.from(@dataset.from(:a)).select_sql.should ==
|
516
|
+
"SELECT * FROM a"
|
517
|
+
end
|
518
|
+
|
519
|
+
specify "should raise if no source is given" do
|
520
|
+
proc {@dataset.from(@dataset.from).select_sql}.should raise_error(SequelError)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
context "Dataset#select" do
|
525
|
+
setup do
|
526
|
+
@d = Sequel::Dataset.new(nil).from(:test)
|
527
|
+
end
|
528
|
+
|
529
|
+
specify "should accept variable arity" do
|
530
|
+
@d.select(:name).sql.should == 'SELECT name FROM test'
|
531
|
+
@d.select(:a, :b, :test__c).sql.should == 'SELECT a, b, test.c FROM test'
|
532
|
+
end
|
533
|
+
|
534
|
+
specify "should accept mixed types (strings and symbols)" do
|
535
|
+
@d.select('aaa').sql.should == 'SELECT aaa FROM test'
|
536
|
+
@d.select(:a, 'b').sql.should == 'SELECT a, b FROM test'
|
537
|
+
@d.select(:test__cc, 'test.d AS e').sql.should ==
|
538
|
+
'SELECT test.cc, test.d AS e FROM test'
|
539
|
+
@d.select('test.d AS e', :test__cc).sql.should ==
|
540
|
+
'SELECT test.d AS e, test.cc FROM test'
|
541
|
+
|
542
|
+
# symbol helpers
|
543
|
+
@d.select(:test.ALL).sql.should ==
|
544
|
+
'SELECT test.* FROM test'
|
545
|
+
@d.select(:test__name.AS(:n)).sql.should ==
|
546
|
+
'SELECT test.name AS n FROM test'
|
547
|
+
@d.select(:test__name___n).sql.should ==
|
548
|
+
'SELECT test.name AS n FROM test'
|
549
|
+
end
|
550
|
+
|
551
|
+
specify "should use the wildcard if no arguments are given" do
|
552
|
+
@d.select.sql.should == 'SELECT * FROM test'
|
553
|
+
end
|
554
|
+
|
555
|
+
specify "should overrun the previous select option" do
|
556
|
+
@d.select(:a, :b, :c).select.sql.should == 'SELECT * FROM test'
|
557
|
+
@d.select(:price).select(:name).sql.should == 'SELECT name FROM test'
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
context "Dataset#order" do
|
562
|
+
setup do
|
563
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
564
|
+
end
|
565
|
+
|
566
|
+
specify "should include an ORDER BY clause in the select statement" do
|
567
|
+
@dataset.order(:name).sql.should ==
|
568
|
+
'SELECT * FROM test ORDER BY name'
|
569
|
+
end
|
570
|
+
|
571
|
+
specify "should accept multiple arguments" do
|
572
|
+
@dataset.order(:name, :price.DESC).sql.should ==
|
573
|
+
'SELECT * FROM test ORDER BY name, price DESC'
|
574
|
+
end
|
575
|
+
|
576
|
+
specify "should overrun a previous ordering" do
|
577
|
+
@dataset.order(:name).order(:stamp).sql.should ==
|
578
|
+
'SELECT * FROM test ORDER BY stamp'
|
579
|
+
end
|
580
|
+
|
581
|
+
specify "should accept a string" do
|
582
|
+
@dataset.order('dada ASC').sql.should ==
|
583
|
+
'SELECT * FROM test ORDER BY dada ASC'
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
context "Dataset#reverse_order" do
|
588
|
+
setup do
|
589
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
590
|
+
end
|
591
|
+
|
592
|
+
specify "should use DESC as default order" do
|
593
|
+
@dataset.reverse_order(:name).sql.should ==
|
594
|
+
'SELECT * FROM test ORDER BY name DESC'
|
595
|
+
end
|
596
|
+
|
597
|
+
specify "should invert the order given" do
|
598
|
+
@dataset.reverse_order(:name.DESC).sql.should ==
|
599
|
+
'SELECT * FROM test ORDER BY name'
|
600
|
+
end
|
601
|
+
|
602
|
+
specify "should accept multiple arguments" do
|
603
|
+
@dataset.reverse_order(:name, :price.DESC).sql.should ==
|
604
|
+
'SELECT * FROM test ORDER BY name DESC, price'
|
605
|
+
end
|
606
|
+
|
607
|
+
specify "should reverse a previous ordering if no arguments are given" do
|
608
|
+
@dataset.order(:name).reverse_order.sql.should ==
|
609
|
+
'SELECT * FROM test ORDER BY name DESC'
|
610
|
+
@dataset.order('clumsy DESC, fool').reverse_order.sql.should ==
|
611
|
+
'SELECT * FROM test ORDER BY clumsy, fool DESC'
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
context "Dataset#limit" do
|
616
|
+
setup do
|
617
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
618
|
+
end
|
619
|
+
|
620
|
+
specify "should include a LIMIT clause in the select statement" do
|
621
|
+
@dataset.limit(10).sql.should ==
|
622
|
+
'SELECT * FROM test LIMIT 10'
|
623
|
+
end
|
624
|
+
|
625
|
+
specify "should accept ranges" do
|
626
|
+
@dataset.limit(3..7).sql.should ==
|
627
|
+
'SELECT * FROM test LIMIT 5 OFFSET 3'
|
628
|
+
|
629
|
+
@dataset.limit(3...7).sql.should ==
|
630
|
+
'SELECT * FROM test LIMIT 4 OFFSET 3'
|
631
|
+
end
|
632
|
+
|
633
|
+
specify "should include an offset if a second argument is given" do
|
634
|
+
@dataset.limit(6, 10).sql.should ==
|
635
|
+
'SELECT * FROM test LIMIT 6 OFFSET 10'
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
context "Dataset#naked" do
|
640
|
+
setup do
|
641
|
+
@d1 = Sequel::Dataset.new(nil, {1 => 2, 3 => 4})
|
642
|
+
@d2 = Sequel::Dataset.new(nil, {1 => 2, 3 => 4}).set_model(Object)
|
643
|
+
end
|
644
|
+
|
645
|
+
specify "should return a clone with :naked option set" do
|
646
|
+
naked = @d1.naked
|
647
|
+
naked.opts[:naked].should be_true
|
648
|
+
end
|
649
|
+
|
650
|
+
specify "should remove any existing reference to a model class" do
|
651
|
+
naked = @d2.naked
|
652
|
+
naked.opts[:models].should be_nil
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
context "Dataset#qualified_field_name" do
|
657
|
+
setup do
|
658
|
+
@dataset = Sequel::Dataset.new(nil).from(:test)
|
659
|
+
end
|
660
|
+
|
661
|
+
specify "should return the same if already qualified" do
|
662
|
+
@dataset.qualified_field_name('test.a', :items).should == 'test.a'
|
663
|
+
@dataset.qualified_field_name(:ccc__b, :items).should == 'ccc.b'
|
664
|
+
end
|
665
|
+
|
666
|
+
specify "should qualify the field with the supplied table name" do
|
667
|
+
@dataset.qualified_field_name('a', :items).should == 'items.a'
|
668
|
+
@dataset.qualified_field_name(:b1, :items).should == 'items.b1'
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
class DummyDataset < Sequel::Dataset
|
673
|
+
VALUES = [
|
674
|
+
{:a => 1, :b => 2},
|
675
|
+
{:a => 3, :b => 4},
|
676
|
+
{:a => 5, :b => 6}
|
677
|
+
]
|
678
|
+
def fetch_rows(sql, &block)
|
679
|
+
VALUES.each(&block)
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
context "Dataset#map" do
|
684
|
+
setup do
|
685
|
+
@d = DummyDataset.new(nil).from(:items)
|
686
|
+
end
|
687
|
+
|
688
|
+
specify "should provide the usual functionality if no argument is given" do
|
689
|
+
@d.map {|n| n[:a] + n[:b]}.should == [3, 7, 11]
|
690
|
+
end
|
691
|
+
|
692
|
+
specify "should map using #[fieldname] if fieldname is given" do
|
693
|
+
@d.map(:a).should == [1, 3, 5]
|
694
|
+
end
|
695
|
+
|
696
|
+
specify "should return the complete dataset values if nothing is given" do
|
697
|
+
@d.map.should == DummyDataset::VALUES
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
context "Dataset#to_hash" do
|
702
|
+
setup do
|
703
|
+
@d = DummyDataset.new(nil).from(:items)
|
704
|
+
end
|
705
|
+
|
706
|
+
specify "should provide a hash with the first field as key and the second as value" do
|
707
|
+
@d.to_hash(:a, :b).should == {1 => 2, 3 => 4, 5 => 6}
|
708
|
+
@d.to_hash(:b, :a).should == {2 => 1, 4 => 3, 6 => 5}
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
context "Dataset#uniq" do
|
713
|
+
setup do
|
714
|
+
@dataset = Sequel::Dataset.new(nil).from(:test).select(:name)
|
715
|
+
end
|
716
|
+
|
717
|
+
specify "should include DISTINCT clause in statement" do
|
718
|
+
@dataset.uniq.sql.should == 'SELECT DISTINCT name FROM test'
|
719
|
+
end
|
720
|
+
|
721
|
+
specify "should be aliased by Dataset#distinct" do
|
722
|
+
@dataset.distinct.sql.should == 'SELECT DISTINCT name FROM test'
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
context "Dataset#count" do
|
727
|
+
setup do
|
728
|
+
@c = Class.new(Sequel::Dataset) do
|
729
|
+
def self.sql
|
730
|
+
@@sql
|
731
|
+
end
|
732
|
+
|
733
|
+
def fetch_rows(sql)
|
734
|
+
@@sql = sql
|
735
|
+
yield({1 => 1})
|
736
|
+
end
|
737
|
+
end
|
738
|
+
@dataset = @c.new(nil).from(:test)
|
739
|
+
end
|
740
|
+
|
741
|
+
specify "should format SQL propertly" do
|
742
|
+
@dataset.count.should == 1
|
743
|
+
@c.sql.should == 'SELECT COUNT(*) FROM test'
|
744
|
+
end
|
745
|
+
|
746
|
+
specify "should be aliased by #size" do
|
747
|
+
@dataset.size.should == 1
|
748
|
+
end
|
749
|
+
|
750
|
+
specify "should include the where clause if it's there" do
|
751
|
+
@dataset.filter {abc < 30}.count.should == 1
|
752
|
+
@c.sql.should == 'SELECT COUNT(*) FROM test WHERE (abc < 30)'
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
context "Dataset#join_table" do
|
757
|
+
setup do
|
758
|
+
@d = Sequel::Dataset.new(nil).from(:items)
|
759
|
+
end
|
760
|
+
|
761
|
+
specify "should format the JOIN clause properly" do
|
762
|
+
@d.join_table(:left_outer, :categories, :category_id => :id).sql.should ==
|
763
|
+
'SELECT * FROM items LEFT OUTER JOIN categories ON (categories.category_id = items.id)'
|
764
|
+
end
|
765
|
+
|
766
|
+
specify "should include WHERE clause if applicable" do
|
767
|
+
@d.filter {price < 100}.join_table(:right_outer, :categories, :category_id => :id).sql.should ==
|
768
|
+
'SELECT * FROM items RIGHT OUTER JOIN categories ON (categories.category_id = items.id) WHERE (price < 100)'
|
769
|
+
end
|
770
|
+
|
771
|
+
specify "should include ORDER BY clause if applicable" do
|
772
|
+
@d.order(:stamp).join_table(:full_outer, :categories, :category_id => :id).sql.should ==
|
773
|
+
'SELECT * FROM items FULL OUTER JOIN categories ON (categories.category_id = items.id) ORDER BY stamp'
|
774
|
+
end
|
775
|
+
|
776
|
+
specify "should support multiple joins" do
|
777
|
+
@d.join_table(:inner, :b, :items_id).join_table(:left_outer, :c, :b_id => :b__id).sql.should ==
|
778
|
+
'SELECT * FROM items INNER JOIN b ON (b.items_id = items.id) LEFT OUTER JOIN c ON (c.b_id = b.id)'
|
779
|
+
end
|
780
|
+
|
781
|
+
specify "should use id as implicit relation primary key if ommited" do
|
782
|
+
@d.join_table(:left_outer, :categories, :category_id).sql.should ==
|
783
|
+
@d.join_table(:left_outer, :categories, :category_id => :id).sql
|
784
|
+
|
785
|
+
# when doing multiple joins, id should be qualified using the last joined table
|
786
|
+
@d.join_table(:right_outer, :b, :items_id).join_table(:full_outer, :c, :b_id).sql.should ==
|
787
|
+
'SELECT * FROM items RIGHT OUTER JOIN b ON (b.items_id = items.id) FULL OUTER JOIN c ON (c.b_id = b.id)'
|
788
|
+
end
|
789
|
+
|
790
|
+
specify "should support left outer joins" do
|
791
|
+
@d.join_table(:left_outer, :categories, :category_id).sql.should ==
|
792
|
+
'SELECT * FROM items LEFT OUTER JOIN categories ON (categories.category_id = items.id)'
|
793
|
+
|
794
|
+
@d.left_outer_join(:categories, :category_id).sql.should ==
|
795
|
+
'SELECT * FROM items LEFT OUTER JOIN categories ON (categories.category_id = items.id)'
|
796
|
+
end
|
797
|
+
|
798
|
+
specify "should support right outer joins" do
|
799
|
+
@d.join_table(:right_outer, :categories, :category_id).sql.should ==
|
800
|
+
'SELECT * FROM items RIGHT OUTER JOIN categories ON (categories.category_id = items.id)'
|
801
|
+
|
802
|
+
@d.right_outer_join(:categories, :category_id).sql.should ==
|
803
|
+
'SELECT * FROM items RIGHT OUTER JOIN categories ON (categories.category_id = items.id)'
|
804
|
+
end
|
805
|
+
|
806
|
+
specify "should support full outer joins" do
|
807
|
+
@d.join_table(:full_outer, :categories, :category_id).sql.should ==
|
808
|
+
'SELECT * FROM items FULL OUTER JOIN categories ON (categories.category_id = items.id)'
|
809
|
+
|
810
|
+
@d.full_outer_join(:categories, :category_id).sql.should ==
|
811
|
+
'SELECT * FROM items FULL OUTER JOIN categories ON (categories.category_id = items.id)'
|
812
|
+
end
|
813
|
+
|
814
|
+
specify "should support inner joins" do
|
815
|
+
@d.join_table(:inner, :categories, :category_id).sql.should ==
|
816
|
+
'SELECT * FROM items INNER JOIN categories ON (categories.category_id = items.id)'
|
817
|
+
|
818
|
+
@d.inner_join(:categories, :category_id).sql.should ==
|
819
|
+
'SELECT * FROM items INNER JOIN categories ON (categories.category_id = items.id)'
|
820
|
+
end
|
821
|
+
|
822
|
+
specify "should default to an inner join" do
|
823
|
+
@d.join_table(nil, :categories, :category_id).sql.should ==
|
824
|
+
'SELECT * FROM items INNER JOIN categories ON (categories.category_id = items.id)'
|
825
|
+
|
826
|
+
@d.join(:categories, :category_id).sql.should ==
|
827
|
+
'SELECT * FROM items INNER JOIN categories ON (categories.category_id = items.id)'
|
828
|
+
end
|
829
|
+
|
830
|
+
specify "should raise if an invalid join type is specified" do
|
831
|
+
proc {@d.join_table(:invalid, :a, :b)}.should raise_error(SequelError)
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
context "Dataset#[]=" do
|
836
|
+
setup do
|
837
|
+
c = Class.new(Sequel::Dataset) do
|
838
|
+
def last_sql
|
839
|
+
@@last_sql
|
840
|
+
end
|
841
|
+
|
842
|
+
def update(*args)
|
843
|
+
@@last_sql = update_sql(*args)
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
@d = c.new(nil).from(:items)
|
848
|
+
end
|
849
|
+
|
850
|
+
specify "should perform an update on the specified filter" do
|
851
|
+
@d[:a => 1] = {:x => 3}
|
852
|
+
@d.last_sql.should == 'UPDATE items SET x = 3 WHERE (a = 1)'
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
context "Dataset#insert_multiple" do
|
857
|
+
setup do
|
858
|
+
c = Class.new(Sequel::Dataset) do
|
859
|
+
attr_reader :inserts
|
860
|
+
def insert(arg)
|
861
|
+
@inserts ||= []
|
862
|
+
@inserts << arg
|
863
|
+
end
|
864
|
+
end
|
865
|
+
|
866
|
+
@d = c.new(nil)
|
867
|
+
end
|
868
|
+
|
869
|
+
specify "should insert all items in the supplied array" do
|
870
|
+
@d.insert_multiple [:aa, 5, 3, {1 => 2}]
|
871
|
+
@d.inserts.should == [:aa, 5, 3, {1 => 2}]
|
872
|
+
end
|
873
|
+
|
874
|
+
specify "should pass array items through the supplied block if given" do
|
875
|
+
a = ["inevitable", "hello", "the ticking clock"]
|
876
|
+
@d.insert_multiple(a) {|i| i.gsub('l', 'r')}
|
877
|
+
@d.inserts.should == ["inevitabre", "herro", "the ticking crock"]
|
878
|
+
end
|
879
|
+
end
|
880
|
+
|
881
|
+
context "Dataset aggregate methods" do
|
882
|
+
setup do
|
883
|
+
c = Class.new(Sequel::Dataset) do
|
884
|
+
def fetch_rows(sql)
|
885
|
+
yield({1 => sql})
|
886
|
+
end
|
887
|
+
end
|
888
|
+
@d = c.new(nil).from(:test)
|
889
|
+
end
|
890
|
+
|
891
|
+
specify "should include min" do
|
892
|
+
@d.min(:a).should == 'SELECT min(a) FROM test'
|
893
|
+
end
|
894
|
+
|
895
|
+
specify "should include max" do
|
896
|
+
@d.max(:b).should == 'SELECT max(b) FROM test'
|
897
|
+
end
|
898
|
+
|
899
|
+
specify "should include sum" do
|
900
|
+
@d.sum(:c).should == 'SELECT sum(c) FROM test'
|
901
|
+
end
|
902
|
+
|
903
|
+
specify "should include avg" do
|
904
|
+
@d.avg(:d).should == 'SELECT avg(d) FROM test'
|
905
|
+
end
|
906
|
+
|
907
|
+
specify "should accept qualified fields" do
|
908
|
+
@d.avg(:test__bc).should == 'SELECT avg(test.bc) FROM test'
|
909
|
+
end
|
910
|
+
end
|
911
|
+
|
912
|
+
context "Dataset#first" do
|
913
|
+
setup do
|
914
|
+
@c = Class.new(Sequel::Dataset) do
|
915
|
+
@@last_dataset = nil
|
916
|
+
@@last_opts = nil
|
917
|
+
|
918
|
+
def self.last_dataset
|
919
|
+
@@last_dataset
|
920
|
+
end
|
921
|
+
|
922
|
+
def self.last_opts
|
923
|
+
@@last_opts
|
924
|
+
end
|
925
|
+
|
926
|
+
def single_record(opts = nil)
|
927
|
+
@@last_opts = @opts.merge(opts || {})
|
928
|
+
{:a => 1, :b => 2}
|
929
|
+
end
|
930
|
+
|
931
|
+
def all
|
932
|
+
@@last_dataset = self
|
933
|
+
[{:a => 1, :b => 2}] * @opts[:limit]
|
934
|
+
end
|
935
|
+
end
|
936
|
+
@d = @c.new(nil).from(:test)
|
937
|
+
end
|
938
|
+
|
939
|
+
specify "should return the first matching record if a hash is specified" do
|
940
|
+
@d.first(:z => 26).should == {:a => 1, :b => 2}
|
941
|
+
@c.last_opts[:where].should == ('(z = 26)')
|
942
|
+
|
943
|
+
@d.first('z = ?', 15)
|
944
|
+
@c.last_opts[:where].should == ('z = 15')
|
945
|
+
end
|
946
|
+
|
947
|
+
specify "should return a single record if no argument is given" do
|
948
|
+
@d.first.should == {:a => 1, :b => 2}
|
949
|
+
end
|
950
|
+
|
951
|
+
specify "should set the limit according to the given number" do
|
952
|
+
@d.first
|
953
|
+
@c.last_opts[:limit].should == 1
|
954
|
+
|
955
|
+
i = rand(10) + 10
|
956
|
+
@d.first(i)
|
957
|
+
@c.last_dataset.opts[:limit].should == i
|
958
|
+
end
|
959
|
+
|
960
|
+
specify "should return an array with the records if argument is greater than 1" do
|
961
|
+
i = rand(10) + 10
|
962
|
+
r = @d.first(i)
|
963
|
+
r.should be_a_kind_of(Array)
|
964
|
+
r.size.should == i
|
965
|
+
r.each {|row| row.should == {:a => 1, :b => 2}}
|
966
|
+
end
|
967
|
+
end
|
968
|
+
|
969
|
+
context "Dataset#last" do
|
970
|
+
setup do
|
971
|
+
@c = Class.new(Sequel::Dataset) do
|
972
|
+
@@last_dataset = nil
|
973
|
+
|
974
|
+
def self.last_dataset
|
975
|
+
@@last_dataset
|
976
|
+
end
|
977
|
+
|
978
|
+
def single_record(opts = nil)
|
979
|
+
@@last_dataset = clone_merge(opts) if opts
|
980
|
+
{:a => 1, :b => 2}
|
981
|
+
end
|
982
|
+
|
983
|
+
def all
|
984
|
+
@@last_dataset = self
|
985
|
+
[{:a => 1, :b => 2}] * @opts[:limit]
|
986
|
+
end
|
987
|
+
end
|
988
|
+
@d = @c.new(nil).from(:test)
|
989
|
+
end
|
990
|
+
|
991
|
+
specify "should raise if no order is given" do
|
992
|
+
proc {@d.last}.should raise_error(SequelError)
|
993
|
+
proc {@d.last(2)}.should raise_error(SequelError)
|
994
|
+
proc {@d.order(:a).last}.should_not raise_error
|
995
|
+
proc {@d.order(:a).last(2)}.should_not raise_error
|
996
|
+
end
|
997
|
+
|
998
|
+
specify "should invert the order" do
|
999
|
+
@d.order(:a).last
|
1000
|
+
@c.last_dataset.opts[:order].should == ['a DESC']
|
1001
|
+
|
1002
|
+
@d.order(:b.DESC).last
|
1003
|
+
@c.last_dataset.opts[:order].should == ['b']
|
1004
|
+
|
1005
|
+
@d.order(:c, :d).last
|
1006
|
+
@c.last_dataset.opts[:order].should == ['c DESC', 'd DESC']
|
1007
|
+
|
1008
|
+
@d.order(:e.DESC, :f).last
|
1009
|
+
@c.last_dataset.opts[:order].should == ['e', 'f DESC']
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
specify "should return the first matching record if a hash is specified" do
|
1013
|
+
@d.order(:a).last(:z => 26).should == {:a => 1, :b => 2}
|
1014
|
+
@c.last_dataset.opts[:where].should == ('(z = 26)')
|
1015
|
+
|
1016
|
+
@d.order(:a).last('z = ?', 15)
|
1017
|
+
@c.last_dataset.opts[:where].should == ('z = 15')
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
specify "should return a single record if no argument is given" do
|
1021
|
+
@d.order(:a).last.should == {:a => 1, :b => 2}
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
specify "should set the limit according to the given number" do
|
1025
|
+
i = rand(10) + 10
|
1026
|
+
r = @d.order(:a).last(i)
|
1027
|
+
@c.last_dataset.opts[:limit].should == i
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
specify "should return an array with the records if argument is greater than 1" do
|
1031
|
+
i = rand(10) + 10
|
1032
|
+
r = @d.order(:a).last(i)
|
1033
|
+
r.should be_a_kind_of(Array)
|
1034
|
+
r.size.should == i
|
1035
|
+
r.each {|row| row.should == {:a => 1, :b => 2}}
|
1036
|
+
end
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
context "Dataset set operations" do
|
1040
|
+
setup do
|
1041
|
+
@a = Sequel::Dataset.new(nil).from(:a).filter(:z => 1)
|
1042
|
+
@b = Sequel::Dataset.new(nil).from(:b).filter(:z => 2)
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
specify "should support UNION and UNION ALL" do
|
1046
|
+
@a.union(@b).sql.should == \
|
1047
|
+
"SELECT * FROM a WHERE (z = 1) UNION SELECT * FROM b WHERE (z = 2)"
|
1048
|
+
@b.union(@a, true).sql.should == \
|
1049
|
+
"SELECT * FROM b WHERE (z = 2) UNION ALL SELECT * FROM a WHERE (z = 1)"
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
specify "should support INTERSECT and INTERSECT ALL" do
|
1053
|
+
@a.intersect(@b).sql.should == \
|
1054
|
+
"SELECT * FROM a WHERE (z = 1) INTERSECT SELECT * FROM b WHERE (z = 2)"
|
1055
|
+
@b.intersect(@a, true).sql.should == \
|
1056
|
+
"SELECT * FROM b WHERE (z = 2) INTERSECT ALL SELECT * FROM a WHERE (z = 1)"
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
specify "should support EXCEPT and EXCEPT ALL" do
|
1060
|
+
@a.except(@b).sql.should == \
|
1061
|
+
"SELECT * FROM a WHERE (z = 1) EXCEPT SELECT * FROM b WHERE (z = 2)"
|
1062
|
+
@b.except(@a, true).sql.should == \
|
1063
|
+
"SELECT * FROM b WHERE (z = 2) EXCEPT ALL SELECT * FROM a WHERE (z = 1)"
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
context "Dataset#[]" do
|
1068
|
+
setup do
|
1069
|
+
@c = Class.new(Sequel::Dataset) do
|
1070
|
+
@@last_dataset = nil
|
1071
|
+
|
1072
|
+
def self.last_dataset
|
1073
|
+
@@last_dataset
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
def single_record(opts = nil)
|
1077
|
+
@@last_dataset = opts ? clone_merge(opts) : self
|
1078
|
+
{1 => 2, 3 => 4}
|
1079
|
+
end
|
1080
|
+
end
|
1081
|
+
@d = @c.new(nil).from(:test)
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
specify "should return a single record filtered according to the given conditions" do
|
1085
|
+
@d[:name => 'didi'].should == {1 => 2, 3 => 4}
|
1086
|
+
@c.last_dataset.opts[:where].should == "(name = 'didi')"
|
1087
|
+
|
1088
|
+
@d[:id => 5..45].should == {1 => 2, 3 => 4}
|
1089
|
+
@c.last_dataset.opts[:where].should == "(id >= 5 AND id <= 45)"
|
1090
|
+
end
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
context "Dataset#single_record" do
|
1094
|
+
setup do
|
1095
|
+
@c = Class.new(Sequel::Dataset) do
|
1096
|
+
def fetch_rows(sql)
|
1097
|
+
yield sql
|
1098
|
+
end
|
1099
|
+
end
|
1100
|
+
@cc = Class.new(@c) do
|
1101
|
+
def fetch_rows(sql); end
|
1102
|
+
end
|
1103
|
+
@d = @c.new(nil).from(:test)
|
1104
|
+
@e = @cc.new(nil).from(:test)
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
specify "should call each and return the first record" do
|
1108
|
+
@d.single_record.should == 'SELECT * FROM test'
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
specify "should pass opts to each" do
|
1112
|
+
@d.single_record(:limit => 3).should == 'SELECT * FROM test LIMIT 3'
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
specify "should return nil if no record is present" do
|
1116
|
+
@e.single_record.should be_nil
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
context "Dataset#single_value" do
|
1121
|
+
setup do
|
1122
|
+
@c = Class.new(Sequel::Dataset) do
|
1123
|
+
def fetch_rows(sql)
|
1124
|
+
yield({1 => sql})
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
@d = @c.new(nil).from(:test)
|
1128
|
+
end
|
1129
|
+
|
1130
|
+
specify "should call each and return the first value of the first record" do
|
1131
|
+
@d.single_value.should == 'SELECT * FROM test'
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
specify "should pass opts to each" do
|
1135
|
+
@d.single_value(:limit => 3).should == 'SELECT * FROM test LIMIT 3'
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
context "Dataset#set_model" do
|
1140
|
+
setup do
|
1141
|
+
@c = Class.new(Sequel::Dataset) do
|
1142
|
+
def fetch_rows(sql, &block)
|
1143
|
+
(1..10).each(&block)
|
1144
|
+
end
|
1145
|
+
end
|
1146
|
+
@dataset = @c.new(nil).from(:items)
|
1147
|
+
@m = Class.new do
|
1148
|
+
attr_accessor :c
|
1149
|
+
def initialize(c); @c = c; end
|
1150
|
+
def ==(o); @c == o.c; end
|
1151
|
+
end
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
specify "should clear the models hash and restore the stock #each if nil is specified" do
|
1155
|
+
@dataset.set_model(@m)
|
1156
|
+
@dataset.set_model(nil)
|
1157
|
+
@dataset.first.should == 1
|
1158
|
+
@dataset.model_classes.should be_nil
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
specify "should clear the models hash and restore the stock #each if nothing is specified" do
|
1162
|
+
@dataset.set_model(@m)
|
1163
|
+
@dataset.set_model
|
1164
|
+
@dataset.first.should == 1
|
1165
|
+
@dataset.model_classes.should be_nil
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
specify "should alter #each to provide model instances" do
|
1169
|
+
@dataset.first.should == 1
|
1170
|
+
@dataset.set_model(@m)
|
1171
|
+
@dataset.first.should == @m.new(1)
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
specify "should extend the dataset with a #destroy method" do
|
1175
|
+
@dataset.should_not respond_to(:destroy)
|
1176
|
+
@dataset.set_model(@m)
|
1177
|
+
@dataset.should respond_to(:destroy)
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
specify "should set opts[:naked] to nil" do
|
1181
|
+
@dataset.opts[:naked] = true
|
1182
|
+
@dataset.set_model(@m)
|
1183
|
+
@dataset.opts[:naked].should be_nil
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
specify "should provide support for polymorphic model instantiation" do
|
1187
|
+
@m1 = Class.new(@m)
|
1188
|
+
@m2 = Class.new(@m)
|
1189
|
+
@dataset.set_model(0, 0 => @m1, 1 => @m2)
|
1190
|
+
all = @dataset.all
|
1191
|
+
all[0].class.should == @m2
|
1192
|
+
all[1].class.should == @m1
|
1193
|
+
all[2].class.should == @m2
|
1194
|
+
all[3].class.should == @m1
|
1195
|
+
#...
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
context "Dataset#model_classes" do
|
1200
|
+
setup do
|
1201
|
+
@c = Class.new(Sequel::Dataset) do
|
1202
|
+
# # We don't need that for now
|
1203
|
+
# def fetch_rows(sql, &block)
|
1204
|
+
# (1..10).each(&block)
|
1205
|
+
# end
|
1206
|
+
end
|
1207
|
+
@dataset = @c.new(nil).from(:items)
|
1208
|
+
@m = Class.new do
|
1209
|
+
attr_accessor :c
|
1210
|
+
def initialize(c); @c = c; end
|
1211
|
+
def ==(o); @c == o.c; end
|
1212
|
+
end
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
specify "should return nil for a naked dataset" do
|
1216
|
+
@dataset.model_classes.should == nil
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
specify "should return a {nil => model_class} hash for a model dataset" do
|
1220
|
+
@dataset.set_model(@m)
|
1221
|
+
@dataset.model_classes.should == {nil => @m}
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
specify "should return the polymorphic hash for a polymorphic model dataset" do
|
1225
|
+
@m1 = Class.new(@m)
|
1226
|
+
@m2 = Class.new(@m)
|
1227
|
+
@dataset.set_model(0, 0 => @m1, 1 => @m2)
|
1228
|
+
@dataset.model_classes.should == {0 => @m1, 1 => @m2}
|
1229
|
+
end
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
context "Dataset#polymorphic_key" do
|
1233
|
+
setup do
|
1234
|
+
@c = Class.new(Sequel::Dataset) do
|
1235
|
+
# # We don't need this for now
|
1236
|
+
# def fetch_rows(sql, &block)
|
1237
|
+
# (1..10).each(&block)
|
1238
|
+
# end
|
1239
|
+
end
|
1240
|
+
@dataset = @c.new(nil).from(:items)
|
1241
|
+
@m = Class.new do
|
1242
|
+
attr_accessor :c
|
1243
|
+
def initialize(c); @c = c; end
|
1244
|
+
def ==(o); @c == o.c; end
|
1245
|
+
end
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
specify "should return nil for a naked dataset" do
|
1249
|
+
@dataset.polymorphic_key.should be_nil
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
specify "should return the polymorphic key" do
|
1253
|
+
@dataset.set_model(:id, nil => @m)
|
1254
|
+
@dataset.polymorphic_key.should == :id
|
1255
|
+
end
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
context "A model dataset" do
|
1259
|
+
setup do
|
1260
|
+
@c = Class.new(Sequel::Dataset) do
|
1261
|
+
def fetch_rows(sql, &block)
|
1262
|
+
(1..10).each(&block)
|
1263
|
+
end
|
1264
|
+
end
|
1265
|
+
@dataset = @c.new(nil).from(:items)
|
1266
|
+
@m = Class.new do
|
1267
|
+
attr_accessor :c
|
1268
|
+
def initialize(c); @c = c; end
|
1269
|
+
def ==(o); @c == o.c; end
|
1270
|
+
end
|
1271
|
+
@dataset.set_model(@m)
|
1272
|
+
end
|
1273
|
+
|
1274
|
+
specify "should supply naked records if the naked option is specified" do
|
1275
|
+
@dataset.each {|r| r.class.should == @m}
|
1276
|
+
@dataset.each(:naked => true) {|r| r.class.should == Fixnum}
|
1277
|
+
end
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
context "A polymorphic model dataset" do
|
1281
|
+
setup do
|
1282
|
+
@c = Class.new(Sequel::Dataset) do
|
1283
|
+
def fetch_rows(sql, &block)
|
1284
|
+
(1..10).each(&block)
|
1285
|
+
end
|
1286
|
+
end
|
1287
|
+
@dataset = @c.new(nil).from(:items)
|
1288
|
+
@m = Class.new do
|
1289
|
+
attr_accessor :c
|
1290
|
+
def initialize(c); @c = c; end
|
1291
|
+
def ==(o); @c == o.c; end
|
1292
|
+
end
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
specify "should use a nil key in the polymorphic hash to specify the default model class" do
|
1296
|
+
@m2 = Class.new(@m)
|
1297
|
+
@dataset.set_model(0, nil => @m, 1 => @m2)
|
1298
|
+
all = @dataset.all
|
1299
|
+
all[0].class.should == @m2
|
1300
|
+
all[1].class.should == @m
|
1301
|
+
all[2].class.should == @m2
|
1302
|
+
all[3].class.should == @m
|
1303
|
+
#...
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
specify "should raise SequelError if no suitable class is found in the polymorphic hash" do
|
1307
|
+
@m2 = Class.new(@m)
|
1308
|
+
@dataset.set_model(0, 1 => @m2)
|
1309
|
+
proc {@dataset.all}.should raise_error(SequelError)
|
1310
|
+
end
|
1311
|
+
|
1312
|
+
specify "should supply naked records if the naked option is specified" do
|
1313
|
+
@dataset.set_model(0, nil => @m)
|
1314
|
+
@dataset.each(:naked => true) {|r| r.class.should == Fixnum}
|
1315
|
+
end
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
context "Dataset#destroy" do
|
1319
|
+
setup do
|
1320
|
+
db = Object.new
|
1321
|
+
m = Module.new do
|
1322
|
+
def transaction; yield; end
|
1323
|
+
end
|
1324
|
+
db.extend(m)
|
1325
|
+
|
1326
|
+
DESTROYED = []
|
1327
|
+
|
1328
|
+
@m = Class.new do
|
1329
|
+
def initialize(c)
|
1330
|
+
@c = c
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
attr_accessor :c
|
1334
|
+
|
1335
|
+
def ==(o)
|
1336
|
+
@c == o.c
|
1337
|
+
end
|
1338
|
+
|
1339
|
+
def destroy
|
1340
|
+
DESTROYED << self
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
MODELS = [@m.new(12), @m.new(13)]
|
1344
|
+
|
1345
|
+
c = Class.new(Sequel::Dataset) do
|
1346
|
+
def fetch_rows(sql, &block)
|
1347
|
+
(12..13).each(&block)
|
1348
|
+
end
|
1349
|
+
end
|
1350
|
+
|
1351
|
+
@d = c.new(db).from(:test)
|
1352
|
+
@d.set_model(@m)
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
specify "should destroy raise for every model in the dataset" do
|
1356
|
+
count = @d.destroy
|
1357
|
+
count.should == 2
|
1358
|
+
DESTROYED.should == MODELS
|
1359
|
+
end
|
1360
|
+
end
|
1361
|
+
|
1362
|
+
context "Dataset#<<" do
|
1363
|
+
setup do
|
1364
|
+
@d = Sequel::Dataset.new(nil)
|
1365
|
+
@d.meta_def(:insert) do
|
1366
|
+
1234567890
|
1367
|
+
end
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
specify "should call #insert" do
|
1371
|
+
(@d << {:name => 1}).should == 1234567890
|
1372
|
+
end
|
1373
|
+
end
|
1374
|
+
|
1375
|
+
context "A paginated dataset" do
|
1376
|
+
setup do
|
1377
|
+
@d = Sequel::Dataset.new(nil)
|
1378
|
+
@d.meta_def(:count) {153}
|
1379
|
+
|
1380
|
+
@paginated = @d.paginate(1, 20)
|
1381
|
+
end
|
1382
|
+
|
1383
|
+
specify "should set the limit and offset options correctly" do
|
1384
|
+
@paginated.opts[:limit].should == 20
|
1385
|
+
@paginated.opts[:offset].should == 0
|
1386
|
+
end
|
1387
|
+
|
1388
|
+
specify "should set the page count correctly" do
|
1389
|
+
@paginated.page_count.should == 8
|
1390
|
+
@d.paginate(1, 50).page_count.should == 4
|
1391
|
+
end
|
1392
|
+
|
1393
|
+
specify "should set the current page number correctly" do
|
1394
|
+
@paginated.current_page.should == 1
|
1395
|
+
@d.paginate(3, 50).current_page.should == 3
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
specify "should return the next page number or nil if we're on the last" do
|
1399
|
+
@paginated.next_page.should == 2
|
1400
|
+
@d.paginate(4, 50).next_page.should be_nil
|
1401
|
+
end
|
1402
|
+
|
1403
|
+
specify "should return the previous page number or nil if we're on the last" do
|
1404
|
+
@paginated.prev_page.should be_nil
|
1405
|
+
@d.paginate(4, 50).prev_page.should == 3
|
1406
|
+
end
|
1407
|
+
end
|
1408
|
+
|
1409
|
+
context "Dataset#columns" do
|
1410
|
+
setup do
|
1411
|
+
@dataset = DummyDataset.new(nil).from(:items)
|
1412
|
+
@dataset.meta_def(:columns=) {|c| @columns = c}
|
1413
|
+
@dataset.meta_def(:first) {@columns = select_sql(nil)}
|
1414
|
+
end
|
1415
|
+
|
1416
|
+
specify "should return the value of @columns" do
|
1417
|
+
@dataset.columns = [:a, :b, :c]
|
1418
|
+
@dataset.columns.should == [:a, :b, :c]
|
1419
|
+
end
|
1420
|
+
|
1421
|
+
specify "should call first if @columns is nil" do
|
1422
|
+
@dataset.columns = nil
|
1423
|
+
@dataset.columns.should == 'SELECT * FROM items'
|
1424
|
+
@dataset.opts[:from] = [:nana]
|
1425
|
+
@dataset.columns.should == 'SELECT * FROM items'
|
1426
|
+
end
|
1427
|
+
end
|
1428
|
+
|
1429
|
+
require 'stringio'
|
1430
|
+
|
1431
|
+
context "Dataset#print" do
|
1432
|
+
setup do
|
1433
|
+
@output = StringIO.new
|
1434
|
+
@orig_stdout = $stdout
|
1435
|
+
$stdout = @output
|
1436
|
+
@dataset = DummyDataset.new(nil).from(:items)
|
1437
|
+
end
|
1438
|
+
|
1439
|
+
teardown do
|
1440
|
+
$stdout = @orig_stdout
|
1441
|
+
end
|
1442
|
+
|
1443
|
+
specify "should print out a table with the values" do
|
1444
|
+
@dataset.print(:a, :b)
|
1445
|
+
@output.rewind
|
1446
|
+
@output.read.should == \
|
1447
|
+
"+-+-+\n|a|b|\n+-+-+\n|1|2|\n|3|4|\n|5|6|\n+-+-+\n"
|
1448
|
+
end
|
1449
|
+
end
|