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.
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