sequel 3.23.0 → 3.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/CHANGELOG +64 -0
  2. data/doc/association_basics.rdoc +43 -5
  3. data/doc/model_hooks.rdoc +64 -27
  4. data/doc/prepared_statements.rdoc +8 -4
  5. data/doc/reflection.rdoc +8 -2
  6. data/doc/release_notes/3.23.0.txt +1 -1
  7. data/doc/release_notes/3.24.0.txt +420 -0
  8. data/lib/sequel/adapters/db2.rb +8 -1
  9. data/lib/sequel/adapters/firebird.rb +25 -9
  10. data/lib/sequel/adapters/informix.rb +4 -19
  11. data/lib/sequel/adapters/jdbc.rb +34 -17
  12. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  13. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  14. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  15. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  16. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  17. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  18. data/lib/sequel/adapters/postgres.rb +30 -1
  19. data/lib/sequel/adapters/shared/access.rb +10 -0
  20. data/lib/sequel/adapters/shared/informix.rb +45 -0
  21. data/lib/sequel/adapters/shared/mssql.rb +82 -8
  22. data/lib/sequel/adapters/shared/mysql.rb +25 -7
  23. data/lib/sequel/adapters/shared/postgres.rb +39 -6
  24. data/lib/sequel/adapters/shared/sqlite.rb +57 -5
  25. data/lib/sequel/adapters/sqlite.rb +8 -3
  26. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  27. data/lib/sequel/ast_transformer.rb +190 -0
  28. data/lib/sequel/core.rb +1 -1
  29. data/lib/sequel/database/misc.rb +6 -0
  30. data/lib/sequel/database/query.rb +33 -3
  31. data/lib/sequel/database/schema_methods.rb +6 -2
  32. data/lib/sequel/dataset/features.rb +6 -0
  33. data/lib/sequel/dataset/prepared_statements.rb +17 -2
  34. data/lib/sequel/dataset/query.rb +17 -0
  35. data/lib/sequel/dataset/sql.rb +2 -53
  36. data/lib/sequel/exceptions.rb +4 -0
  37. data/lib/sequel/extensions/to_dot.rb +95 -83
  38. data/lib/sequel/model.rb +5 -0
  39. data/lib/sequel/model/associations.rb +80 -14
  40. data/lib/sequel/model/base.rb +182 -55
  41. data/lib/sequel/model/exceptions.rb +3 -1
  42. data/lib/sequel/plugins/association_pks.rb +6 -4
  43. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  44. data/lib/sequel/plugins/many_through_many.rb +8 -3
  45. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  46. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  47. data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
  48. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  49. data/lib/sequel/sql.rb +8 -0
  50. data/lib/sequel/version.rb +1 -1
  51. data/spec/adapters/postgres_spec.rb +43 -18
  52. data/spec/core/connection_pool_spec.rb +56 -77
  53. data/spec/core/database_spec.rb +25 -0
  54. data/spec/core/dataset_spec.rb +127 -16
  55. data/spec/core/expression_filters_spec.rb +13 -0
  56. data/spec/core/schema_spec.rb +6 -1
  57. data/spec/extensions/association_pks_spec.rb +7 -0
  58. data/spec/extensions/defaults_setter_spec.rb +64 -0
  59. data/spec/extensions/many_through_many_spec.rb +60 -4
  60. data/spec/extensions/nested_attributes_spec.rb +1 -0
  61. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  62. data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
  63. data/spec/extensions/prepared_statements_spec.rb +72 -0
  64. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  65. data/spec/extensions/to_dot_spec.rb +3 -5
  66. data/spec/integration/associations_test.rb +155 -1
  67. data/spec/integration/dataset_test.rb +8 -1
  68. data/spec/integration/plugin_test.rb +119 -0
  69. data/spec/integration/prepared_statement_test.rb +72 -1
  70. data/spec/integration/schema_test.rb +66 -8
  71. data/spec/integration/transaction_test.rb +40 -0
  72. data/spec/model/associations_spec.rb +349 -8
  73. data/spec/model/base_spec.rb +59 -0
  74. data/spec/model/hooks_spec.rb +161 -0
  75. data/spec/model/record_spec.rb +24 -0
  76. 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
- 50.times{Thread.new{@cpool.hold{sleep 0.001}}}
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
- @cpool.hold{Thread.new{@cpool.hold{}}; sleep 0.03}
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
- m = (defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx') ? 30 : 1
172
+ q, q1 = Queue.new, Queue.new
170
173
 
171
- t1 = Thread.new {@pool.hold {|c| cc = c; c1 = c.dup; while c == 'herro';sleep 0.01;end}}
172
- sleep 0.02 * m
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; while c == 'hello';sleep 0.01;end}}
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
- sleep 0.05 * m
192
-
193
- # connection held by t2
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
- t2.should_not be_alive
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
- stop = nil
251
+ q, q1, q2 = Queue.new, Queue.new, Queue.new
243
252
 
244
- 5.times {|i| threads << Thread.new {@pool.hold {|c| cc[i] = c; while !stop;sleep 0.02;end}}; sleep 0.02}
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
- i = 0
259
+
252
260
  h = {}
261
+ i = 0
253
262
  threads.each{|t| h[t] = (i+=1)}
254
263
  @pool.allocated.should == h
255
-
256
- threads[0].raise "your'e dead"
257
- sleep 0.02
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
- stop = nil
275
+ q, q1 = Queue.new, Queue.new
276
276
 
277
- 5.times {|i| threads << Thread.new {@pool.hold {|c| cc[i] = c; while !stop;sleep 0.01;end}}; sleep 0.01}
278
- sleep 0.02
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
- stop = true
294
- sleep 0.1
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
- @pool = Sequel::ConnectionPool.get_pool(CONNECTION_POOL_DEFAULTS.merge(:max_connections=>5)) {@invoked_count += 1}
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
- @pool = Sequel::ConnectionPool.get_pool(CONNECTION_POOL_DEFAULTS.merge(:max_connections=>5, :servers=>{})) {@invoked_count += 1}
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
- stop = nil
560
- 5.times {|i| threads << Thread.new{pool.hold(:server1){|c| sleep 0.05}}}
561
- sleep 0.1
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
@@ -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
@@ -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
- d1 = @d1.select(:id1, :id2)
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
- @sql_used = sql
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
- d1.instance_variable_get(:@sql_used).should == "SELECT id1, id2 FROM test WHERE (region = 'Asia')"
551
- d1.instance_variable_set(:@sql_used, nil)
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
- d1.instance_variable_get(:@sql_used).should == "SELECT id1, id2 FROM test WHERE (region = 'Asia')"
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
- d1 = @d1.select(:id1, :id2)
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
- @sql_used = sql
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
- d1.instance_variable_get(:@sql_used).should == "SELECT id1, id2 FROM test WHERE (region = 'Asia')"
566
- d1.instance_variable_set(:@sql_used, nil)
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
- d1.instance_variable_get(:@sql_used).should == "SELECT id1, id2 FROM test WHERE (region = 'Asia')"
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
- @db.prepared_statements.keys.sort_by{|k| k.to_s}.should == [:dn, :fn, :in, :sn, :un]
3309
- [:sn, :fn, :dn, :un, :in].each_with_index{|x, i| @db.prepared_statements[x].should == pss[i]}
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