sequel 3.23.0 → 3.24.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.
- data/CHANGELOG +64 -0
- data/doc/association_basics.rdoc +43 -5
- data/doc/model_hooks.rdoc +64 -27
- data/doc/prepared_statements.rdoc +8 -4
- data/doc/reflection.rdoc +8 -2
- data/doc/release_notes/3.23.0.txt +1 -1
- data/doc/release_notes/3.24.0.txt +420 -0
- data/lib/sequel/adapters/db2.rb +8 -1
- data/lib/sequel/adapters/firebird.rb +25 -9
- data/lib/sequel/adapters/informix.rb +4 -19
- data/lib/sequel/adapters/jdbc.rb +34 -17
- data/lib/sequel/adapters/jdbc/h2.rb +5 -0
- data/lib/sequel/adapters/jdbc/informix.rb +31 -0
- data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
- data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
- data/lib/sequel/adapters/postgres.rb +30 -1
- data/lib/sequel/adapters/shared/access.rb +10 -0
- data/lib/sequel/adapters/shared/informix.rb +45 -0
- data/lib/sequel/adapters/shared/mssql.rb +82 -8
- data/lib/sequel/adapters/shared/mysql.rb +25 -7
- data/lib/sequel/adapters/shared/postgres.rb +39 -6
- data/lib/sequel/adapters/shared/sqlite.rb +57 -5
- data/lib/sequel/adapters/sqlite.rb +8 -3
- data/lib/sequel/adapters/swift/mysql.rb +9 -0
- data/lib/sequel/ast_transformer.rb +190 -0
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database/misc.rb +6 -0
- data/lib/sequel/database/query.rb +33 -3
- data/lib/sequel/database/schema_methods.rb +6 -2
- data/lib/sequel/dataset/features.rb +6 -0
- data/lib/sequel/dataset/prepared_statements.rb +17 -2
- data/lib/sequel/dataset/query.rb +17 -0
- data/lib/sequel/dataset/sql.rb +2 -53
- data/lib/sequel/exceptions.rb +4 -0
- data/lib/sequel/extensions/to_dot.rb +95 -83
- data/lib/sequel/model.rb +5 -0
- data/lib/sequel/model/associations.rb +80 -14
- data/lib/sequel/model/base.rb +182 -55
- data/lib/sequel/model/exceptions.rb +3 -1
- data/lib/sequel/plugins/association_pks.rb +6 -4
- data/lib/sequel/plugins/defaults_setter.rb +58 -0
- data/lib/sequel/plugins/many_through_many.rb +8 -3
- data/lib/sequel/plugins/prepared_statements.rb +140 -0
- data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
- data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
- data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
- data/lib/sequel/sql.rb +8 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +43 -18
- data/spec/core/connection_pool_spec.rb +56 -77
- data/spec/core/database_spec.rb +25 -0
- data/spec/core/dataset_spec.rb +127 -16
- data/spec/core/expression_filters_spec.rb +13 -0
- data/spec/core/schema_spec.rb +6 -1
- data/spec/extensions/association_pks_spec.rb +7 -0
- data/spec/extensions/defaults_setter_spec.rb +64 -0
- data/spec/extensions/many_through_many_spec.rb +60 -4
- data/spec/extensions/nested_attributes_spec.rb +1 -0
- data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
- data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
- data/spec/extensions/prepared_statements_spec.rb +72 -0
- data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
- data/spec/extensions/to_dot_spec.rb +3 -5
- data/spec/integration/associations_test.rb +155 -1
- data/spec/integration/dataset_test.rb +8 -1
- data/spec/integration/plugin_test.rb +119 -0
- data/spec/integration/prepared_statement_test.rb +72 -1
- data/spec/integration/schema_test.rb +66 -8
- data/spec/integration/transaction_test.rb +40 -0
- data/spec/model/associations_spec.rb +349 -8
- data/spec/model/base_spec.rb +59 -0
- data/spec/model/hooks_spec.rb +161 -0
- data/spec/model/record_spec.rb +24 -0
- metadata +21 -4
@@ -84,7 +84,9 @@ describe "A connection pool handling connections" do
|
|
84
84
|
end
|
85
85
|
|
86
86
|
specify "#make_new should not make more than max_size connections" do
|
87
|
-
|
87
|
+
q = Queue.new
|
88
|
+
50.times{Thread.new{@cpool.hold{q.pop}}}
|
89
|
+
50.times{q.push nil}
|
88
90
|
@cpool.created_count.should <= @max_size
|
89
91
|
end
|
90
92
|
|
@@ -96,7 +98,8 @@ describe "A connection pool handling connections" do
|
|
96
98
|
|
97
99
|
specify "#hold should remove the connection if a DatabaseDisconnectError is raised" do
|
98
100
|
@cpool.created_count.should == 0
|
99
|
-
|
101
|
+
q, q1 = Queue.new, Queue.new
|
102
|
+
@cpool.hold{Thread.new{@cpool.hold{q1.pop; q.push nil}; q1.pop; q.push nil}; q1.push nil; q.pop; q1.push nil; q.pop}
|
100
103
|
@cpool.created_count.should == 2
|
101
104
|
proc{@cpool.hold{raise Sequel::DatabaseDisconnectError}}.should raise_error(Sequel::DatabaseDisconnectError)
|
102
105
|
@cpool.created_count.should == 1
|
@@ -166,15 +169,14 @@ describe "A connection pool with a max size of 1" do
|
|
166
169
|
|
167
170
|
specify "should let only one thread access the connection at any time" do
|
168
171
|
cc,c1, c2 = nil
|
169
|
-
|
172
|
+
q, q1 = Queue.new, Queue.new
|
170
173
|
|
171
|
-
t1 = Thread.new {@pool.hold {|c| cc = c; c1 = c.dup;
|
172
|
-
|
174
|
+
t1 = Thread.new {@pool.hold {|c| cc = c; c1 = c.dup; q1.push nil; q.pop}}
|
175
|
+
q1.pop
|
173
176
|
cc.should == 'herro'
|
174
177
|
c1.should == 'herro'
|
175
178
|
|
176
|
-
t2 = Thread.new {@pool.hold {|c| c2 = c.dup;
|
177
|
-
sleep 0.02 * m
|
179
|
+
t2 = Thread.new {@pool.hold {|c| c2 = c.dup; q1.push nil; q.pop;}}
|
178
180
|
|
179
181
|
# connection held by t1
|
180
182
|
t1.should be_alive
|
@@ -186,26 +188,22 @@ describe "A connection pool with a max size of 1" do
|
|
186
188
|
|
187
189
|
@pool.available_connections.should be_empty
|
188
190
|
@pool.allocated.should == {t1=>cc}
|
189
|
-
|
191
|
+
|
190
192
|
cc.gsub!('rr', 'll')
|
191
|
-
|
192
|
-
|
193
|
-
|
193
|
+
q.push nil
|
194
|
+
q1.pop
|
195
|
+
|
194
196
|
t1.should_not be_alive
|
195
197
|
t2.should be_alive
|
196
|
-
|
198
|
+
|
197
199
|
c2.should == 'hello'
|
198
200
|
|
199
201
|
@pool.available_connections.should be_empty
|
200
202
|
@pool.allocated.should == {t2=>cc}
|
201
203
|
|
202
|
-
cc.gsub!('ll', 'rr')
|
203
|
-
sleep 0.05 * m
|
204
|
-
|
205
204
|
#connection released
|
206
|
-
|
207
|
-
|
208
|
-
cc.should == 'herro'
|
205
|
+
q.push nil
|
206
|
+
t2.join
|
209
207
|
|
210
208
|
@invoked_count.should == 1
|
211
209
|
@pool.size.should == 1
|
@@ -236,34 +234,36 @@ describe "A connection pool with a max size of 1" do
|
|
236
234
|
end
|
237
235
|
|
238
236
|
shared_examples_for "A threaded connection pool" do
|
237
|
+
specify "should raise a PoolTimeout error if a connection couldn't be acquired before timeout" do
|
238
|
+
x = nil
|
239
|
+
q, q1 = Queue.new, Queue.new
|
240
|
+
pool = Sequel::ConnectionPool.get_pool(@cp_opts.merge(:max_connections=>1, :pool_timeout=>0)) {@invoked_count += 1}
|
241
|
+
t = Thread.new{pool.hold{|c| q1.push nil; q.pop}}
|
242
|
+
q1.pop
|
243
|
+
proc{pool.hold{|c|}}.should raise_error(Sequel::PoolTimeout)
|
244
|
+
q.push nil
|
245
|
+
t.join
|
246
|
+
end
|
247
|
+
|
239
248
|
specify "should let five threads simultaneously access separate connections" do
|
240
249
|
cc = {}
|
241
250
|
threads = []
|
242
|
-
|
251
|
+
q, q1, q2 = Queue.new, Queue.new, Queue.new
|
243
252
|
|
244
|
-
5.times
|
245
|
-
sleep 0.04
|
253
|
+
5.times{|i| threads << Thread.new{@pool.hold{|c| q.pop; cc[i] = c; q1.push nil; q2.pop}}; q.push nil; q1.pop}
|
246
254
|
threads.each {|t| t.should be_alive}
|
247
255
|
cc.size.should == 5
|
248
256
|
@invoked_count.should == 5
|
249
257
|
@pool.size.should == 5
|
250
258
|
@pool.available_connections.should be_empty
|
251
|
-
|
259
|
+
|
252
260
|
h = {}
|
261
|
+
i = 0
|
253
262
|
threads.each{|t| h[t] = (i+=1)}
|
254
263
|
@pool.allocated.should == h
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
threads[3].raise "your'e dead too"
|
259
|
-
|
260
|
-
sleep 0.02
|
261
|
-
|
262
|
-
@pool.available_connections.should == [1, 4]
|
263
|
-
@pool.allocated.should == {threads[1]=>2, threads[2]=>3, threads[4]=>5}
|
264
|
-
|
265
|
-
stop = true
|
266
|
-
sleep 0.04
|
264
|
+
@pool.available_connections.should == []
|
265
|
+
5.times{q2.push nil}
|
266
|
+
threads.each{|t| t.join}
|
267
267
|
|
268
268
|
@pool.available_connections.size.should == 5
|
269
269
|
@pool.allocated.should be_empty
|
@@ -272,16 +272,15 @@ shared_examples_for "A threaded connection pool" do
|
|
272
272
|
specify "should block threads until a connection becomes available" do
|
273
273
|
cc = {}
|
274
274
|
threads = []
|
275
|
-
|
275
|
+
q, q1 = Queue.new, Queue.new
|
276
276
|
|
277
|
-
5.times
|
278
|
-
|
277
|
+
5.times{|i| threads << Thread.new{@pool.hold{|c| cc[i] = c; q1.push nil; q.pop}}}
|
278
|
+
5.times{q1.pop}
|
279
279
|
threads.each {|t| t.should be_alive}
|
280
280
|
@pool.available_connections.should be_empty
|
281
281
|
|
282
|
-
3.times {|i| threads << Thread.new {@pool.hold {|c| cc[i + 5] = c}}}
|
282
|
+
3.times {|i| threads << Thread.new {@pool.hold {|c| cc[i + 5] = c; q1.push nil}}}
|
283
283
|
|
284
|
-
sleep 0.02
|
285
284
|
threads[5].should be_alive
|
286
285
|
threads[6].should be_alive
|
287
286
|
threads[7].should be_alive
|
@@ -290,8 +289,10 @@ shared_examples_for "A threaded connection pool" do
|
|
290
289
|
cc[6].should be_nil
|
291
290
|
cc[7].should be_nil
|
292
291
|
|
293
|
-
|
294
|
-
|
292
|
+
5.times{q.push nil}
|
293
|
+
5.times{|i| threads[i].join}
|
294
|
+
3.times{q1.pop}
|
295
|
+
3.times{|i| threads[i+5].join}
|
295
296
|
|
296
297
|
threads.each {|t| t.should_not be_alive}
|
297
298
|
|
@@ -305,7 +306,8 @@ end
|
|
305
306
|
describe "Threaded Unsharded Connection Pool" do
|
306
307
|
before do
|
307
308
|
@invoked_count = 0
|
308
|
-
@
|
309
|
+
@cp_opts = CONNECTION_POOL_DEFAULTS.merge(:max_connections=>5)
|
310
|
+
@pool = Sequel::ConnectionPool.get_pool(@cp_opts) {@invoked_count += 1}
|
309
311
|
end
|
310
312
|
|
311
313
|
it_should_behave_like "A threaded connection pool"
|
@@ -314,7 +316,8 @@ end
|
|
314
316
|
describe "Threaded Sharded Connection Pool" do
|
315
317
|
before do
|
316
318
|
@invoked_count = 0
|
317
|
-
@
|
319
|
+
@cp_opts = CONNECTION_POOL_DEFAULTS.merge(:max_connections=>5, :servers=>{})
|
320
|
+
@pool = Sequel::ConnectionPool.get_pool(@cp_opts) {@invoked_count += 1}
|
318
321
|
end
|
319
322
|
|
320
323
|
it_should_behave_like "A threaded connection pool"
|
@@ -324,19 +327,15 @@ describe "ConnectionPool#disconnect" do
|
|
324
327
|
before do
|
325
328
|
@count = 0
|
326
329
|
@pool = Sequel::ConnectionPool.get_pool(CONNECTION_POOL_DEFAULTS.merge(:max_connections=>5, :servers=>{})) {{:id => @count += 1}}
|
330
|
+
threads = []
|
331
|
+
q, q1 = Queue.new, Queue.new
|
332
|
+
5.times {|i| threads << Thread.new {@pool.hold {|c| q1.push nil; q.pop}}}
|
333
|
+
5.times{q1.pop}
|
334
|
+
5.times{q.push nil}
|
335
|
+
threads.each {|t| t.join}
|
327
336
|
end
|
328
337
|
|
329
338
|
specify "should invoke the given block for each available connection" do
|
330
|
-
threads = []
|
331
|
-
stop = nil
|
332
|
-
5.times {|i| threads << Thread.new {@pool.hold {|c| while !stop;sleep 0.01;end}}; sleep 0.01}
|
333
|
-
while @pool.size < 5
|
334
|
-
sleep 0.02
|
335
|
-
end
|
336
|
-
stop = true
|
337
|
-
sleep 0.1
|
338
|
-
threads.each {|t| t.join}
|
339
|
-
|
340
339
|
@pool.size.should == 5
|
341
340
|
@pool.available_connections.size.should == 5
|
342
341
|
@pool.available_connections.each {|c| c[:id].should_not be_nil}
|
@@ -346,34 +345,13 @@ describe "ConnectionPool#disconnect" do
|
|
346
345
|
end
|
347
346
|
|
348
347
|
specify "should remove all available connections" do
|
349
|
-
threads = []
|
350
|
-
stop = nil
|
351
|
-
5.times {|i| threads << Thread.new {@pool.hold {|c| while !stop;sleep 0.01;end}}; sleep 0.01}
|
352
|
-
while @pool.size < 5
|
353
|
-
sleep 0.02
|
354
|
-
end
|
355
|
-
stop = true
|
356
|
-
sleep 0.1
|
357
|
-
threads.each {|t| t.join}
|
358
|
-
|
359
348
|
@pool.size.should == 5
|
360
349
|
@pool.disconnect
|
361
350
|
@pool.size.should == 0
|
362
351
|
end
|
363
352
|
|
364
353
|
specify "should disconnect connections in use as soon as they are no longer in use" do
|
365
|
-
threads = []
|
366
|
-
stop = nil
|
367
|
-
5.times {|i| threads << Thread.new {@pool.hold {|c| while !stop;sleep 0.01;end}}; sleep 0.01}
|
368
|
-
while @pool.size < 5
|
369
|
-
sleep 0.02
|
370
|
-
end
|
371
|
-
stop = true
|
372
|
-
sleep 0.1
|
373
|
-
threads.each {|t| t.join}
|
374
|
-
|
375
354
|
@pool.size.should == 5
|
376
|
-
|
377
355
|
@pool.hold do |conn|
|
378
356
|
@pool.available_connections.size.should == 4
|
379
357
|
@pool.available_connections.each {|c| c.should_not be(conn)}
|
@@ -556,9 +534,10 @@ describe "A connection pool with multiple servers" do
|
|
556
534
|
specify "#remove_servers should disconnect available connections immediately" do
|
557
535
|
pool = Sequel::ConnectionPool.get_pool(:max_connections=>5, :servers=>{:server1=>{}}){|s| s}
|
558
536
|
threads = []
|
559
|
-
|
560
|
-
5.times {|i| threads << Thread.new{pool.hold(:server1){|c|
|
561
|
-
|
537
|
+
q, q1 = Queue.new, Queue.new
|
538
|
+
5.times {|i| threads << Thread.new {pool.hold(:server1){|c| q1.push nil; q.pop}}}
|
539
|
+
5.times{q1.pop}
|
540
|
+
5.times{q.push nil}
|
562
541
|
threads.each {|t| t.join}
|
563
542
|
|
564
543
|
pool.size(:server1).should == 5
|
data/spec/core/database_spec.rb
CHANGED
@@ -397,6 +397,12 @@ describe "Database#tables" do
|
|
397
397
|
end
|
398
398
|
end
|
399
399
|
|
400
|
+
describe "Database#views" do
|
401
|
+
specify "should raise Sequel::NotImplemented" do
|
402
|
+
proc {Sequel::Database.new.views}.should raise_error(Sequel::NotImplemented)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
400
406
|
describe "Database#indexes" do
|
401
407
|
specify "should raise Sequel::NotImplemented" do
|
402
408
|
proc {Sequel::Database.new.indexes(:table)}.should raise_error(Sequel::NotImplemented)
|
@@ -803,6 +809,25 @@ describe "Database#transaction" do
|
|
803
809
|
t.join
|
804
810
|
@db.transactions.should be_empty
|
805
811
|
end
|
812
|
+
|
813
|
+
if (!defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
|
814
|
+
specify "should handle Thread#kill for transactions inside threads" do
|
815
|
+
q = Queue.new
|
816
|
+
q1 = Queue.new
|
817
|
+
t = Thread.new do
|
818
|
+
@db.transaction do
|
819
|
+
@db.execute 'DROP TABLE test'
|
820
|
+
q1.push nil
|
821
|
+
q.pop
|
822
|
+
@db.execute 'DROP TABLE test2'
|
823
|
+
end
|
824
|
+
end
|
825
|
+
q1.pop
|
826
|
+
t.kill
|
827
|
+
t.join
|
828
|
+
@db.sql.should == ['BEGIN', 'DROP TABLE test', 'ROLLBACK']
|
829
|
+
end
|
830
|
+
end
|
806
831
|
end
|
807
832
|
|
808
833
|
describe "Database#transaction with savepoints" do
|
data/spec/core/dataset_spec.rb
CHANGED
@@ -538,34 +538,55 @@ describe "Dataset#where" do
|
|
538
538
|
|
539
539
|
specify "should handle IN/NOT IN queries with multiple columns and a dataset where the database doesn't support it" do
|
540
540
|
@dataset.meta_def(:supports_multiple_column_in?){false}
|
541
|
-
|
541
|
+
db = MockDatabase.new()
|
542
|
+
d1 = db[:test].select(:id1, :id2).filter(:region=>'Asia')
|
543
|
+
d1.instance_variable_set(:@columns, [:id1, :id2])
|
542
544
|
def d1.fetch_rows(sql)
|
543
|
-
|
545
|
+
db << sql
|
544
546
|
@columns = [:id1, :id2]
|
545
547
|
yield(:id1=>1, :id2=>2)
|
546
548
|
yield(:id1=>3, :id2=>4)
|
547
549
|
end
|
548
|
-
d1.instance_variable_get(:@sql_used).should == nil
|
549
550
|
@dataset.filter([:id1, :id2] => d1).sql.should == "SELECT * FROM test WHERE (((id1 = 1) AND (id2 = 2)) OR ((id1 = 3) AND (id2 = 4)))"
|
550
|
-
|
551
|
-
|
551
|
+
db.sqls.should == ["SELECT id1, id2 FROM test WHERE (region = 'Asia')"]
|
552
|
+
db.sqls.clear
|
552
553
|
@dataset.exclude([:id1, :id2] => d1).sql.should == "SELECT * FROM test WHERE (((id1 != 1) OR (id2 != 2)) AND ((id1 != 3) OR (id2 != 4)))"
|
553
|
-
|
554
|
+
db.sqls.should == ["SELECT id1, id2 FROM test WHERE (region = 'Asia')"]
|
554
555
|
end
|
555
556
|
|
556
557
|
specify "should handle IN/NOT IN queries with multiple columns and an empty dataset where the database doesn't support it" do
|
557
558
|
@dataset.meta_def(:supports_multiple_column_in?){false}
|
558
|
-
|
559
|
+
db = MockDatabase.new()
|
560
|
+
d1 = db[:test].select(:id1, :id2).filter(:region=>'Asia')
|
561
|
+
d1.instance_variable_set(:@columns, [:id1, :id2])
|
559
562
|
def d1.fetch_rows(sql)
|
560
|
-
|
563
|
+
db << sql
|
561
564
|
@columns = [:id1, :id2]
|
562
565
|
end
|
563
|
-
d1.instance_variable_get(:@sql_used).should == nil
|
564
566
|
@dataset.filter([:id1, :id2] => d1).sql.should == "SELECT * FROM test WHERE ((id1 != id1) AND (id2 != id2))"
|
565
|
-
|
566
|
-
|
567
|
+
db.sqls.should == ["SELECT id1, id2 FROM test WHERE (region = 'Asia')"]
|
568
|
+
db.sqls.clear
|
567
569
|
@dataset.exclude([:id1, :id2] => d1).sql.should == "SELECT * FROM test WHERE (1 = 1)"
|
568
|
-
|
570
|
+
db.sqls.should == ["SELECT id1, id2 FROM test WHERE (region = 'Asia')"]
|
571
|
+
end
|
572
|
+
|
573
|
+
specify "should handle IN/NOT IN queries for datasets with row_procs" do
|
574
|
+
@dataset.meta_def(:supports_multiple_column_in?){false}
|
575
|
+
db = MockDatabase.new()
|
576
|
+
d1 = db[:test].select(:id1, :id2).filter(:region=>'Asia')
|
577
|
+
d1.row_proc = proc{|h| Object.new}
|
578
|
+
d1.instance_variable_set(:@columns, [:id1, :id2])
|
579
|
+
def d1.fetch_rows(sql)
|
580
|
+
db << sql
|
581
|
+
@columns = [:id1, :id2]
|
582
|
+
yield(:id1=>1, :id2=>2)
|
583
|
+
yield(:id1=>3, :id2=>4)
|
584
|
+
end
|
585
|
+
@dataset.filter([:id1, :id2] => d1).sql.should == "SELECT * FROM test WHERE (((id1 = 1) AND (id2 = 2)) OR ((id1 = 3) AND (id2 = 4)))"
|
586
|
+
db.sqls.should == ["SELECT id1, id2 FROM test WHERE (region = 'Asia')"]
|
587
|
+
db.sqls.clear
|
588
|
+
@dataset.exclude([:id1, :id2] => d1).sql.should == "SELECT * FROM test WHERE (((id1 != 1) OR (id2 != 2)) AND ((id1 != 3) OR (id2 != 4)))"
|
589
|
+
db.sqls.should == ["SELECT id1, id2 FROM test WHERE (region = 'Asia')"]
|
569
590
|
end
|
570
591
|
|
571
592
|
specify "should accept a subquery for an EXISTS clause" do
|
@@ -3283,6 +3304,7 @@ describe "Dataset prepared statements and bound variables " do
|
|
3283
3304
|
ds
|
3284
3305
|
end
|
3285
3306
|
@ds = @db[:items]
|
3307
|
+
@ds.meta_def(:insert_sql){|*v| "#{super(*v)}#{' RETURNING *' if opts.has_key?(:returning)}" }
|
3286
3308
|
end
|
3287
3309
|
|
3288
3310
|
specify "#call should take a type and bind hash and interpolate it" do
|
@@ -3291,11 +3313,13 @@ describe "Dataset prepared statements and bound variables " do
|
|
3291
3313
|
@ds.filter(:num=>:$n).call(:delete, :n=>1)
|
3292
3314
|
@ds.filter(:num=>:$n).call(:update, {:n=>1, :n2=>2}, :num=>:$n2)
|
3293
3315
|
@ds.call(:insert, {:n=>1}, :num=>:$n)
|
3316
|
+
@ds.call(:insert_select, {:n=>1}, :num=>:$n)
|
3294
3317
|
@db.sqls.should == ['SELECT * FROM items WHERE (num = 1)',
|
3295
3318
|
'SELECT * FROM items WHERE (num = 1) LIMIT 1',
|
3296
3319
|
'DELETE FROM items WHERE (num = 1)',
|
3297
3320
|
'UPDATE items SET num = 2 WHERE (num = 1)',
|
3298
|
-
'INSERT INTO items (num) VALUES (1)'
|
3321
|
+
'INSERT INTO items (num) VALUES (1)',
|
3322
|
+
'INSERT INTO items (num) VALUES (1) RETURNING *']
|
3299
3323
|
end
|
3300
3324
|
|
3301
3325
|
specify "#prepare should take a type and name and store it in the database for later use with call" do
|
@@ -3305,18 +3329,21 @@ describe "Dataset prepared statements and bound variables " do
|
|
3305
3329
|
pss << @ds.filter(:num=>:$n).prepare(:delete, :dn)
|
3306
3330
|
pss << @ds.filter(:num=>:$n).prepare(:update, :un, :num=>:$n2)
|
3307
3331
|
pss << @ds.prepare(:insert, :in, :num=>:$n)
|
3308
|
-
@
|
3309
|
-
[:
|
3332
|
+
pss << @ds.prepare(:insert_select, :ins, :num=>:$n)
|
3333
|
+
@db.prepared_statements.keys.sort_by{|k| k.to_s}.should == [:dn, :fn, :in, :ins, :sn, :un]
|
3334
|
+
[:sn, :fn, :dn, :un, :in, :ins].each_with_index{|x, i| @db.prepared_statements[x].should == pss[i]}
|
3310
3335
|
@db.call(:sn, :n=>1)
|
3311
3336
|
@db.call(:fn, :n=>1)
|
3312
3337
|
@db.call(:dn, :n=>1)
|
3313
3338
|
@db.call(:un, :n=>1, :n2=>2)
|
3314
3339
|
@db.call(:in, :n=>1)
|
3340
|
+
@db.call(:ins, :n=>1)
|
3315
3341
|
@db.sqls.should == ['SELECT * FROM items WHERE (num = 1)',
|
3316
3342
|
'SELECT * FROM items WHERE (num = 1) LIMIT 1',
|
3317
3343
|
'DELETE FROM items WHERE (num = 1)',
|
3318
3344
|
'UPDATE items SET num = 2 WHERE (num = 1)',
|
3319
|
-
'INSERT INTO items (num) VALUES (1)'
|
3345
|
+
'INSERT INTO items (num) VALUES (1)',
|
3346
|
+
'INSERT INTO items (num) VALUES (1) RETURNING *']
|
3320
3347
|
end
|
3321
3348
|
|
3322
3349
|
specify "#inspect should indicate it is a prepared statement with the prepared SQL" do
|
@@ -3599,6 +3626,76 @@ describe "Sequel::Dataset#qualify_to_first_source" do
|
|
3599
3626
|
end
|
3600
3627
|
end
|
3601
3628
|
|
3629
|
+
describe "Sequel::Dataset#unbind" do
|
3630
|
+
before do
|
3631
|
+
@ds = MockDatabase.new[:t]
|
3632
|
+
@u = proc{|ds| ds, bv = ds.unbind; [ds.sql, bv]}
|
3633
|
+
end
|
3634
|
+
|
3635
|
+
specify "should unbind values assigned to equality and inequality statements" do
|
3636
|
+
@ds.filter(:foo=>1).unbind.first.sql.should == "SELECT * FROM t WHERE (foo = $foo)"
|
3637
|
+
@ds.exclude(:foo=>1).unbind.first.sql.should == "SELECT * FROM t WHERE (foo != $foo)"
|
3638
|
+
@ds.filter{foo > 1}.unbind.first.sql.should == "SELECT * FROM t WHERE (foo > $foo)"
|
3639
|
+
@ds.filter{foo >= 1}.unbind.first.sql.should == "SELECT * FROM t WHERE (foo >= $foo)"
|
3640
|
+
@ds.filter{foo < 1}.unbind.first.sql.should == "SELECT * FROM t WHERE (foo < $foo)"
|
3641
|
+
@ds.filter{foo <= 1}.unbind.first.sql.should == "SELECT * FROM t WHERE (foo <= $foo)"
|
3642
|
+
end
|
3643
|
+
|
3644
|
+
specify "should return variables that could be used bound to recreate the previous query" do
|
3645
|
+
@ds.filter(:foo=>1).unbind.last.should == {:foo=>1}
|
3646
|
+
@ds.exclude(:foo=>1).unbind.last.should == {:foo=>1}
|
3647
|
+
end
|
3648
|
+
|
3649
|
+
specify "should handle numerics, strings, dates, times, and datetimes" do
|
3650
|
+
@u[@ds.filter(:foo=>1)].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>1}]
|
3651
|
+
@u[@ds.filter(:foo=>1.0)].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>1.0}]
|
3652
|
+
@u[@ds.filter(:foo=>BigDecimal.new('1.0'))].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>BigDecimal.new('1.0')}]
|
3653
|
+
@u[@ds.filter(:foo=>'a')].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>'a'}]
|
3654
|
+
@u[@ds.filter(:foo=>Date.today)].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>Date.today}]
|
3655
|
+
t = Time.now
|
3656
|
+
@u[@ds.filter(:foo=>t)].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>t}]
|
3657
|
+
dt = DateTime.now
|
3658
|
+
@u[@ds.filter(:foo=>dt)].should == ["SELECT * FROM t WHERE (foo = $foo)", {:foo=>dt}]
|
3659
|
+
end
|
3660
|
+
|
3661
|
+
specify "should not unbind literal strings" do
|
3662
|
+
@u[@ds.filter(:foo=>'a'.lit)].should == ["SELECT * FROM t WHERE (foo = a)", {}]
|
3663
|
+
end
|
3664
|
+
|
3665
|
+
specify "should not unbind Identifiers, QualifiedIdentifiers, or Symbols used as booleans" do
|
3666
|
+
@u[@ds.filter(:foo).filter{bar}.filter{foo__bar}].should == ["SELECT * FROM t WHERE (foo AND bar AND foo.bar)", {}]
|
3667
|
+
end
|
3668
|
+
|
3669
|
+
specify "should not unbind for values it doesn't understand" do
|
3670
|
+
@u[@ds.filter(:foo=>Class.new{def sql_literal(ds) 'bar' end}.new)].should == ["SELECT * FROM t WHERE (foo = bar)", {}]
|
3671
|
+
end
|
3672
|
+
|
3673
|
+
specify "should handle QualifiedIdentifiers" do
|
3674
|
+
@u[@ds.filter{foo__bar > 1}].should == ["SELECT * FROM t WHERE (foo.bar > $foo.bar)", {:"foo.bar"=>1}]
|
3675
|
+
end
|
3676
|
+
|
3677
|
+
specify "should handle deep nesting" do
|
3678
|
+
@u[@ds.filter{foo > 1}.and{bar < 2}.or(:baz=>3).and({~{:x=>4}=>true}.case(false))].should == ["SELECT * FROM t WHERE ((((foo > $foo) AND (bar < $bar)) OR (baz = $baz)) AND (CASE WHEN (x != $x) THEN 't' ELSE 'f' END))", {:foo=>1, :bar=>2, :baz=>3, :x=>4}]
|
3679
|
+
end
|
3680
|
+
|
3681
|
+
specify "should handle JOIN ON" do
|
3682
|
+
@u[@ds.cross_join(:x).join(:a, [:u]).join(:b, [[:c, :d], [:e,1]])].should == ["SELECT * FROM t CROSS JOIN x INNER JOIN a USING (u) INNER JOIN b ON ((b.c = a.d) AND (b.e = $b.e))", {:"b.e"=>1}]
|
3683
|
+
end
|
3684
|
+
|
3685
|
+
specify "should raise an UnbindDuplicate exception if same variable is used with multiple different values" do
|
3686
|
+
proc{@ds.filter(:foo=>1).or(:foo=>2).unbind}.should raise_error(Sequel::UnbindDuplicate)
|
3687
|
+
end
|
3688
|
+
|
3689
|
+
specify "should handle case where the same variable has the same value in multiple places " do
|
3690
|
+
@u[@ds.filter(:foo=>1).or(:foo=>1)].should == ["SELECT * FROM t WHERE ((foo = $foo) OR (foo = $foo))", {:foo=>1}]
|
3691
|
+
end
|
3692
|
+
|
3693
|
+
specify "should raise Error for unhandled objects inside Identifiers and QualifiedIndentifiers" do
|
3694
|
+
proc{@ds.filter(Sequel::SQL::Identifier.new([]) > 1).unbind}.should raise_error(Sequel::Error)
|
3695
|
+
proc{@ds.filter{foo.qualify({}) > 1}.unbind}.should raise_error(Sequel::Error)
|
3696
|
+
end
|
3697
|
+
end
|
3698
|
+
|
3602
3699
|
describe "Sequel::Dataset #with and #with_recursive" do
|
3603
3700
|
before do
|
3604
3701
|
@db = MockDatabase.new
|
@@ -3977,3 +4074,17 @@ describe "Dataset#lock_style and for_update" do
|
|
3977
4074
|
@ds.lock_style("FOR SHARE").sql.should == "SELECT * FROM t FOR SHARE"
|
3978
4075
|
end
|
3979
4076
|
end
|
4077
|
+
|
4078
|
+
describe "Custom ASTTransformer" do
|
4079
|
+
specify "should transform given objects" do
|
4080
|
+
c = Class.new(Sequel::ASTTransformer) do
|
4081
|
+
def v(s)
|
4082
|
+
(s.is_a?(Symbol) || s.is_a?(String)) ? :"#{s}#{s}" : super
|
4083
|
+
end
|
4084
|
+
end.new
|
4085
|
+
ds = MockDatabase.new[:t].cross_join(:a___g).join(:b___h, [:c]).join(:d___i, :e=>:f)
|
4086
|
+
ds.sql.should == 'SELECT * FROM t CROSS JOIN a AS g INNER JOIN b AS h USING (c) INNER JOIN d AS i ON (i.e = h.f)'
|
4087
|
+
ds.clone(:from=>c.transform(ds.opts[:from]), :join=>c.transform(ds.opts[:join])).sql.should ==
|
4088
|
+
'SELECT * FROM tt CROSS JOIN aa AS gg INNER JOIN bb AS hh USING (cc) INNER JOIN dd AS ii ON (ii.ee = hh.ff)'
|
4089
|
+
end
|
4090
|
+
end
|