sequel 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG +16 -0
  2. data/README +4 -1
  3. data/Rakefile +17 -19
  4. data/doc/prepared_statements.rdoc +104 -0
  5. data/doc/sharding.rdoc +113 -0
  6. data/lib/sequel_core/adapters/ado.rb +24 -17
  7. data/lib/sequel_core/adapters/db2.rb +30 -33
  8. data/lib/sequel_core/adapters/dbi.rb +15 -13
  9. data/lib/sequel_core/adapters/informix.rb +13 -14
  10. data/lib/sequel_core/adapters/jdbc.rb +243 -60
  11. data/lib/sequel_core/adapters/jdbc/mysql.rb +32 -24
  12. data/lib/sequel_core/adapters/jdbc/postgresql.rb +32 -2
  13. data/lib/sequel_core/adapters/jdbc/sqlite.rb +16 -20
  14. data/lib/sequel_core/adapters/mysql.rb +164 -76
  15. data/lib/sequel_core/adapters/odbc.rb +21 -34
  16. data/lib/sequel_core/adapters/openbase.rb +10 -7
  17. data/lib/sequel_core/adapters/oracle.rb +17 -23
  18. data/lib/sequel_core/adapters/postgres.rb +246 -35
  19. data/lib/sequel_core/adapters/shared/mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/shared/mysql.rb +34 -26
  21. data/lib/sequel_core/adapters/shared/postgres.rb +82 -38
  22. data/lib/sequel_core/adapters/shared/sqlite.rb +48 -16
  23. data/lib/sequel_core/adapters/sqlite.rb +141 -44
  24. data/lib/sequel_core/connection_pool.rb +85 -63
  25. data/lib/sequel_core/database.rb +46 -17
  26. data/lib/sequel_core/dataset.rb +21 -40
  27. data/lib/sequel_core/dataset/convenience.rb +3 -3
  28. data/lib/sequel_core/dataset/prepared_statements.rb +218 -0
  29. data/lib/sequel_core/exceptions.rb +0 -12
  30. data/lib/sequel_model/base.rb +1 -2
  31. data/lib/sequel_model/plugins.rb +1 -1
  32. data/spec/adapters/ado_spec.rb +32 -3
  33. data/spec/adapters/mysql_spec.rb +7 -8
  34. data/spec/integration/prepared_statement_test.rb +106 -0
  35. data/spec/sequel_core/connection_pool_spec.rb +105 -3
  36. data/spec/sequel_core/database_spec.rb +41 -3
  37. data/spec/sequel_core/dataset_spec.rb +117 -7
  38. data/spec/sequel_core/spec_helper.rb +2 -2
  39. data/spec/sequel_model/model_spec.rb +0 -6
  40. data/spec/sequel_model/spec_helper.rb +1 -1
  41. metadata +11 -6
  42. data/lib/sequel_core/adapters/adapter_skeleton.rb +0 -54
  43. data/lib/sequel_core/adapters/odbc_mssql.rb +0 -106
@@ -4,15 +4,6 @@ module Sequel
4
4
 
5
5
  # Raised when Sequel is unable to load a specified adapter.
6
6
  class AdapterNotFound < Error ; end
7
-
8
- # Raise when an invalid expression is encountered inside a block filter.
9
- class InvalidExpression < Error; end
10
-
11
- # Represents an Invalid filter.
12
- class InvalidFilter < Error ; end
13
-
14
- # Represents an invalid join type.
15
- class InvalidJoinType < Error ; end
16
7
 
17
8
  # Raised on an invalid operation.
18
9
  class InvalidOperation < Error; end
@@ -35,8 +26,5 @@ module Sequel
35
26
  # Rollback is a special error used to rollback a transactions.
36
27
  # A transaction block will catch this error and won't pass further up the stack.
37
28
  class Rollback < Error ; end
38
-
39
- # Should be raised inside a worker loop to tell it to stop working.
40
- class WorkerStop < RuntimeError ; end
41
29
  end
42
30
  end
@@ -62,7 +62,7 @@ module Sequel
62
62
  insert_multiple intersect interval invert_order join join_table last
63
63
  left_outer_join limit map multi_insert naked order order_by order_more
64
64
  paginate print query range reverse_order right_outer_join select
65
- select_all select_more set set_graph_aliases single_value size to_csv to_hash
65
+ select_all select_more server set set_graph_aliases single_value size to_csv to_hash
66
66
  transform union uniq unfiltered unordered update where'.map{|x| x.to_sym}
67
67
 
68
68
  # Instance variables that are inherited in subclasses
@@ -79,7 +79,6 @@ module Sequel
79
79
  # first before a dataset lookup is attempted unless a hash is supplied.
80
80
  def self.[](*args)
81
81
  args = args.first if (args.size == 1)
82
- raise(Error::InvalidFilter, "Did you mean to supply a hash?") if args === true || args === false
83
82
 
84
83
  if Hash === args
85
84
  dataset[args]
@@ -35,7 +35,7 @@ module Sequel
35
35
  if m.const_defined?("DatasetMethods")
36
36
  dataset.meta_def(:"#{plugin}_opts") {args.first}
37
37
  dataset.extend(m::DatasetMethods)
38
- def_dataset_method(*m::DatasetMethods.instance_methods)
38
+ def_dataset_method(*m::DatasetMethods.public_instance_methods)
39
39
  end
40
40
  end
41
41
  metaalias :is_a, :is
@@ -1,17 +1,46 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
2
 
3
3
  unless defined?(ADO_DB)
4
- ADO_DB = Sequel.connect(:adapter => 'ado', :driver => "{Microsoft Access Driver (*.mdb)}; DBQ=c:\\Nwind.mdb")
4
+ ADO_DB = Sequel.ado(:host => 'MY_SQL_SERVER', :database => 'MyDB', :user => 'my_pwd', :password => 'my_usr')
5
5
  end
6
6
 
7
7
  context "An ADO dataset" do
8
- setup do
8
+ before(:each) do
9
9
  ADO_DB.create_table!(:items) { text :name }
10
10
  end
11
-
11
+
12
12
  specify "should not raise exceptions when working with empty datasets" do
13
13
  lambda {
14
14
  ADO_DB[:items].all
15
15
  }.should_not raise_error
16
16
  end
17
17
  end
18
+
19
+ context "An MSSQL dataset" do
20
+ before(:each) do
21
+ ADO_DB.create_table!(:items) { text :name }
22
+ end
23
+
24
+ specify "should assign a default name to anonymous columns" do
25
+ col = ADO_DB.fetch('SELECT COUNT(*) FROM items').columns[0]
26
+ col.to_s.should == '(no column name)'
27
+ end
28
+
29
+ specify "should support counting" do
30
+ ADO_DB[:items] << {:name => 'my name' }
31
+ ADO_DB[:items].count.should == 1
32
+ end
33
+
34
+ specify "should support first" do
35
+ ADO_DB[:items] << {:name => 'x' }
36
+ ADO_DB[:items] << {:name => 'y' }
37
+ ADO_DB[:items].first[:name].should == 'x'
38
+ end
39
+
40
+ specify "should support limit" do
41
+ 3.times do
42
+ ADO_DB[:items] << {:name => 'my name' }
43
+ end
44
+ ADO_DB[:items].limit(2).all.size.should == 2
45
+ end
46
+ end
@@ -41,13 +41,6 @@ context "A MySQL database" do
41
41
  teardown do
42
42
  Sequel.convert_tinyint_to_bool = true
43
43
  end
44
-
45
- specify "should provide disconnect functionality" do
46
- @db.tables
47
- @db.pool.size.should == 1
48
- @db.disconnect
49
- @db.pool.size.should == 0
50
- end
51
44
 
52
45
  specify "should provide the server version" do
53
46
  @db.server_version.should >= 40000
@@ -64,6 +57,12 @@ context "A MySQL database" do
64
57
  {:id => 3, :name => 'ghi'}
65
58
  ]
66
59
  end
60
+
61
+ specify "should provide disconnect functionality" do
62
+ @db.pool.size.should == 1
63
+ @db.disconnect
64
+ @db.pool.size.should == 0
65
+ end
67
66
 
68
67
  specify "should convert Mysql::Errors to Sequel::Errors" do
69
68
  proc{@db << "SELECT 1 + blah;"}.should raise_error(Sequel::Error)
@@ -319,7 +318,7 @@ context "MySQL join expressions" do
319
318
  end
320
319
 
321
320
  specify "should raise error for :full_outer join requests." do
322
- lambda{@ds.join_table(:full_outer, :nodes)}.should raise_error(Sequel::Error::InvalidJoinType)
321
+ lambda{@ds.join_table(:full_outer, :nodes)}.should raise_error(Sequel::Error)
323
322
  end
324
323
  specify "should support natural left joins" do
325
324
  @ds.join_table(:natural_left, :nodes).sql.should == \
@@ -0,0 +1,106 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe "Prepared Statements and Bound Arguments" do
4
+ before do
5
+ INTEGRATION_DB.create_table!(:items) do
6
+ primary_key :id
7
+ integer :number
8
+ end
9
+ @c = Class.new(Sequel::Model(:items))
10
+ @ds = INTEGRATION_DB[:items]
11
+ @ds.insert(:number=>10)
12
+ @ds.meta_def(:ba) do |sym|
13
+ prepared_arg_placeholder == '$' ? :"#{sym}__int" : sym
14
+ end
15
+ clear_sqls
16
+ end
17
+ after do
18
+ INTEGRATION_DB.drop_table(:items)
19
+ end
20
+
21
+ specify "should support bound variables with select, all, and first" do
22
+ @ds.filter(:number=>@ds.ba(:$n)).call(:select, :n=>10).should == [{:id=>1, :number=>10}]
23
+ @ds.filter(:number=>@ds.ba(:$n)).call(:all, :n=>10).should == [{:id=>1, :number=>10}]
24
+ @ds.filter(:number=>@ds.ba(:$n)).call(:first, :n=>10).should == {:id=>1, :number=>10}
25
+ end
26
+
27
+ specify "should support bound variables with insert" do
28
+ @ds.call(:insert, {:n=>20, :i=>100}, :id=>@ds.ba(:$i), :number=>@ds.ba(:$n))
29
+ @ds.count.should == 2
30
+ @ds.order(:id).all.should == [{:id=>1, :number=>10}, {:id=>100, :number=>20}]
31
+ end
32
+
33
+ specify "should have insert return primary key value when using bound arguments" do
34
+ @ds.call(:insert, {:n=>20}, :number=>@ds.ba(:$n)).should == 2
35
+ @ds.filter(:id=>2).first[:number].should == 20
36
+ end
37
+
38
+ specify "should support bound variables with delete" do
39
+ @ds.filter(:number=>@ds.ba(:$n)).call(:delete, :n=>10).should == 1
40
+ @ds.count.should == 0
41
+ end
42
+
43
+ specify "should support bound variables with update" do
44
+ @ds.filter(:number=>@ds.ba(:$n)).call(:update, {:n=>10, :nn=>20}, :number=>:number+@ds.ba(:$nn)).should == 1
45
+ @ds.all.should == [{:id=>1, :number=>30}]
46
+ end
47
+
48
+ specify "should support prepared statements with select, first, and all" do
49
+ @ds.filter(:number=>@ds.ba(:$n)).prepare(:select, :select_n)
50
+ INTEGRATION_DB.call(:select_n, :n=>10).should == [{:id=>1, :number=>10}]
51
+ @ds.filter(:number=>@ds.ba(:$n)).prepare(:all, :select_n)
52
+ INTEGRATION_DB.call(:select_n, :n=>10).should == [{:id=>1, :number=>10}]
53
+ @ds.filter(:number=>@ds.ba(:$n)).prepare(:first, :select_n)
54
+ INTEGRATION_DB.call(:select_n, :n=>10).should == {:id=>1, :number=>10}
55
+ if INTEGRATION_DB.uri =~ /jdbc:sqlite:/
56
+ # Work around for open prepared statements on a table not allowing the
57
+ # dropping of a table when using SQLite over JDBC
58
+ INTEGRATION_DB.synchronize{|c| c.prepared_statements[:select_n][1].close}
59
+ end
60
+ end
61
+
62
+ specify "should support prepared statements with insert" do
63
+ @ds.prepare(:insert, :insert_n, :id=>@ds.ba(:$i), :number=>@ds.ba(:$n))
64
+ INTEGRATION_DB.call(:insert_n, :n=>20, :i=>100)
65
+ @ds.count.should == 2
66
+ @ds.order(:id).all.should == [{:id=>1, :number=>10}, {:id=>100, :number=>20}]
67
+ end
68
+
69
+ specify "should have insert return primary key value when using prepared statements" do
70
+ @ds.prepare(:insert, :insert_n, :number=>@ds.ba(:$n))
71
+ INTEGRATION_DB.call(:insert_n, :n=>20).should == 2
72
+ @ds.filter(:id=>2).first[:number].should == 20
73
+ end
74
+
75
+ specify "should support prepared statements with delete" do
76
+ @ds.filter(:number=>@ds.ba(:$n)).prepare(:delete, :delete_n)
77
+ INTEGRATION_DB.call(:delete_n, :n=>10).should == 1
78
+ @ds.count.should == 0
79
+ end
80
+
81
+ specify "should support prepared statements with update" do
82
+ @ds.filter(:number=>@ds.ba(:$n)).prepare(:update, :update_n, :number=>:number+@ds.ba(:$nn))
83
+ INTEGRATION_DB.call(:update_n, :n=>10, :nn=>20).should == 1
84
+ @ds.all.should == [{:id=>1, :number=>30}]
85
+ end
86
+
87
+ specify "model datasets should return model instances when using select, all, and first with bound variables" do
88
+ @c.filter(:number=>@ds.ba(:$n)).call(:select, :n=>10).should == [@c.load(:id=>1, :number=>10)]
89
+ @c.filter(:number=>@ds.ba(:$n)).call(:all, :n=>10).should == [@c.load(:id=>1, :number=>10)]
90
+ @c.filter(:number=>@ds.ba(:$n)).call(:first, :n=>10).should == @c.load(:id=>1, :number=>10)
91
+ end
92
+
93
+ specify "model datasets should return model instances when using select, all, and first with prepared statements" do
94
+ @c.filter(:number=>@ds.ba(:$n)).prepare(:select, :select_n)
95
+ INTEGRATION_DB.call(:select_n, :n=>10).should == [@c.load(:id=>1, :number=>10)]
96
+ @c.filter(:number=>@ds.ba(:$n)).prepare(:all, :select_n)
97
+ INTEGRATION_DB.call(:select_n, :n=>10).should == [@c.load(:id=>1, :number=>10)]
98
+ @c.filter(:number=>@ds.ba(:$n)).prepare(:first, :select_n)
99
+ INTEGRATION_DB.call(:select_n, :n=>10).should == @c.load(:id=>1, :number=>10)
100
+ if INTEGRATION_DB.uri =~ /jdbc:sqlite:/
101
+ # Work around for open prepared statements on a table not allowing the
102
+ # dropping of a table when using SQLite over JDBC
103
+ INTEGRATION_DB.synchronize{|c| c.prepared_statements[:select_n][1].close}
104
+ end
105
+ end
106
+ end
@@ -56,9 +56,9 @@ context "A connection pool handling connections" do
56
56
  end
57
57
 
58
58
  specify "#make_new should not make more than max_size connections" do
59
- @cpool.send(:make_new).should == :got_connection
60
- @cpool.send(:make_new).should == :got_connection
61
- @cpool.send(:make_new).should == nil
59
+ @cpool.send(:make_new, :default).should == :got_connection
60
+ @cpool.send(:make_new, :default).should == :got_connection
61
+ @cpool.send(:make_new, :default).should == nil
62
62
  @cpool.created_count.should == 2
63
63
  end
64
64
  end
@@ -341,6 +341,65 @@ context "ConnectionPool#disconnect" do
341
341
  end
342
342
  end
343
343
 
344
+ context "A connection pool with multiple servers" do
345
+ setup do
346
+ @invoked_counts = Hash.new(0)
347
+ @pool = Sequel::ConnectionPool.new(CONNECTION_POOL_DEFAULTS.merge(:servers=>{:read_only=>{}})){|server| "#{server}#{@invoked_counts[server] += 1}"}
348
+ end
349
+
350
+ specify "should use the :default server by default" do
351
+ @pool.size.should == 0
352
+ @pool.hold do |c|
353
+ c.should == "default1"
354
+ @pool.allocated.should == {Thread.current=>"default1"}
355
+ end
356
+ @pool.available_connections.should == ["default1"]
357
+ @pool.size.should == 1
358
+ @invoked_counts.should == {:default=>1}
359
+ end
360
+
361
+ specify "should use the requested server if server is given" do
362
+ @pool.size(:read_only).should == 0
363
+ @pool.hold(:read_only) do |c|
364
+ c.should == "read_only1"
365
+ @pool.allocated(:read_only).should == {Thread.current=>"read_only1"}
366
+ end
367
+ @pool.available_connections(:read_only).should == ["read_only1"]
368
+ @pool.size(:read_only).should == 1
369
+ @invoked_counts.should == {:read_only=>1}
370
+ end
371
+
372
+ specify "#hold should only yield connections for the server requested" do
373
+ @pool.hold(:read_only) do |c|
374
+ c.should == "read_only1"
375
+ @pool.allocated(:read_only).should == {Thread.current=>"read_only1"}
376
+ @pool.hold do |d|
377
+ d.should == "default1"
378
+ @pool.hold do |e|
379
+ e.should == d
380
+ @pool.hold(:read_only){|b| b.should == c}
381
+ end
382
+ @pool.allocated.should == {Thread.current=>"default1"}
383
+ end
384
+ end
385
+ @invoked_counts.should == {:read_only=>1, :default=>1}
386
+ end
387
+
388
+ specify "#disconnect should disconnect from all servers" do
389
+ @pool.hold(:read_only){}
390
+ @pool.hold{}
391
+ conns = []
392
+ @pool.size.should == 1
393
+ @pool.size(:read_only).should == 1
394
+ @pool.disconnect{|c| conns << c}
395
+ conns.sort.should == %w'default1 read_only1'
396
+ @pool.size.should == 0
397
+ @pool.size(:read_only).should == 0
398
+ @pool.hold(:read_only){|c| c.should == 'read_only2'}
399
+ @pool.hold{|c| c.should == 'default2'}
400
+ end
401
+ end
402
+
344
403
  context "SingleThreadedPool" do
345
404
  setup do
346
405
  @pool = Sequel::SingleThreadedPool.new(CONNECTION_POOL_DEFAULTS){1234}
@@ -361,3 +420,46 @@ context "SingleThreadedPool" do
361
420
  @pool.conn.should be_nil
362
421
  end
363
422
  end
423
+
424
+ context "A single threaded pool with multiple servers" do
425
+ setup do
426
+ @pool = Sequel::SingleThreadedPool.new(CONNECTION_POOL_DEFAULTS.merge(:servers=>{:read_only=>{}})){|server| server}
427
+ end
428
+
429
+ specify "should use the :default server by default" do
430
+ @pool.hold{|c| c.should == :default}
431
+ @pool.conn.should == :default
432
+ end
433
+
434
+ specify "should use the requested server if server is given" do
435
+ @pool.hold(:read_only){|c| c.should == :read_only}
436
+ @pool.conn(:read_only).should == :read_only
437
+ end
438
+
439
+ specify "#hold should only yield connections for the server requested" do
440
+ @pool.hold(:read_only) do |c|
441
+ c.should == :read_only
442
+ @pool.hold do |d|
443
+ d.should == :default
444
+ @pool.hold do |e|
445
+ e.should == d
446
+ @pool.hold(:read_only){|b| b.should == c}
447
+ end
448
+ end
449
+ end
450
+ @pool.conn.should == :default
451
+ @pool.conn(:read_only).should == :read_only
452
+ end
453
+
454
+ specify "#disconnect should disconnect from all servers" do
455
+ @pool.hold(:read_only){}
456
+ @pool.hold{}
457
+ conns = []
458
+ @pool.conn.should == :default
459
+ @pool.conn(:read_only).should == :read_only
460
+ @pool.disconnect{|c| conns << c}
461
+ conns.sort_by{|x| x.to_s}.should == [:default, :read_only]
462
+ @pool.conn.should == nil
463
+ @pool.conn(:read_only).should == nil
464
+ end
465
+ end
@@ -150,7 +150,7 @@ end
150
150
  context "Database#<<" do
151
151
  setup do
152
152
  @c = Class.new(Sequel::Database) do
153
- define_method(:execute) {|sql| sql}
153
+ define_method(:execute) {|sql, opts| sql}
154
154
  end
155
155
  @db = @c.new({})
156
156
  end
@@ -238,7 +238,7 @@ end
238
238
  class DummyDatabase < Sequel::Database
239
239
  attr_reader :sqls
240
240
 
241
- def execute(sql)
241
+ def execute(sql, opts={})
242
242
  @sqls ||= []
243
243
  @sqls << sql
244
244
  end
@@ -455,7 +455,7 @@ end
455
455
 
456
456
  class Dummy3Database < Sequel::Database
457
457
  attr_reader :sql, :transactions
458
- def execute(sql); @sql ||= []; @sql << sql; end
458
+ def execute(sql, opts={}); @sql ||= []; @sql << sql; end
459
459
 
460
460
  class DummyConnection
461
461
  def initialize(db); @db = db; end
@@ -962,3 +962,41 @@ context "Database#get" do
962
962
  @db.sqls.last.should == 'SELECT version()'
963
963
  end
964
964
  end
965
+
966
+ context "Database#call" do
967
+ specify "should call the prepared statement with the given name" do
968
+ db = MockDatabase.new
969
+ db[:items].prepare(:select, :select_all)
970
+ db.call(:select_all).should == [{:id => 1, :x => 1}]
971
+ db[:items].filter(:n=>:$n).prepare(:select, :select_n)
972
+ db.call(:select_n, :n=>1).should == [{:id => 1, :x => 1}]
973
+ db.sqls.should == ['SELECT * FROM items', 'SELECT * FROM items WHERE (n = 1)']
974
+ end
975
+ end
976
+
977
+ context "Database#server_opts" do
978
+ specify "should return the general opts if no :servers option is used" do
979
+ opts = {:host=>1, :database=>2}
980
+ MockDatabase.new(opts).send(:server_opts, :server1).should == {:host=>1, :database=>2}
981
+ end
982
+
983
+ specify "should return the general opts if entry for the server is present in the :servers option" do
984
+ opts = {:host=>1, :database=>2, :servers=>{}}
985
+ MockDatabase.new(opts).send(:server_opts, :server1).should == {:host=>1, :database=>2}
986
+ end
987
+
988
+ specify "should return the general opts merged with the specific opts if given as a hash" do
989
+ opts = {:host=>1, :database=>2, :servers=>{:server1=>{:host=>3}}}
990
+ MockDatabase.new(opts).send(:server_opts, :server1).should == {:host=>3, :database=>2}
991
+ end
992
+
993
+ specify "should return the sgeneral opts merged with the specific opts if given as a proc" do
994
+ opts = {:host=>1, :database=>2, :servers=>{:server1=>proc{|db| {:host=>4}}}}
995
+ MockDatabase.new(opts).send(:server_opts, :server1).should == {:host=>4, :database=>2}
996
+ end
997
+
998
+ specify "should raise an error if the specific opts is not a proc or hash" do
999
+ opts = {:host=>1, :database=>2, :servers=>{:server1=>2}}
1000
+ proc{MockDatabase.new(opts).send(:server_opts, :server1)}.should raise_error(Sequel::Error)
1001
+ end
1002
+ end
@@ -2244,7 +2244,7 @@ context "Dataset#multi_insert" do
2244
2244
  @dbc = Class.new do
2245
2245
  attr_reader :sqls
2246
2246
 
2247
- def execute(sql)
2247
+ def execute(sql, opts={})
2248
2248
  @sqls ||= []
2249
2249
  @sqls << sql
2250
2250
  end
@@ -2717,7 +2717,7 @@ context "Dataset#create_view" do
2717
2717
  @dbc = Class.new(Sequel::Database) do
2718
2718
  attr_reader :sqls
2719
2719
 
2720
- def execute(sql)
2720
+ def execute(sql, opts={})
2721
2721
  @sqls ||= []
2722
2722
  @sqls << sql
2723
2723
  end
@@ -2738,7 +2738,7 @@ context "Dataset#create_or_replace_view" do
2738
2738
  @dbc = Class.new(Sequel::Database) do
2739
2739
  attr_reader :sqls
2740
2740
 
2741
- def execute(sql)
2741
+ def execute(sql, opts={})
2742
2742
  @sqls ||= []
2743
2743
  @sqls << sql
2744
2744
  end
@@ -2950,7 +2950,7 @@ context "Dataset.dataset_classes" do
2950
2950
  end
2951
2951
  end
2952
2952
 
2953
- context "Dataset default #fetch_rows, #insert, #update, and #delete" do
2953
+ context "Dataset default #fetch_rows, #insert, #update, and #delete, #execute" do
2954
2954
  setup do
2955
2955
  @db = Sequel::Database.new
2956
2956
  @ds = @db[:items]
@@ -2961,17 +2961,127 @@ context "Dataset default #fetch_rows, #insert, #update, and #delete" do
2961
2961
  end
2962
2962
 
2963
2963
  specify "#delete should execute delete SQL" do
2964
- @db.should_receive(:execute).once.with('DELETE FROM items')
2964
+ @db.should_receive(:execute).once.with('DELETE FROM items', :server=>:default)
2965
2965
  @ds.delete
2966
2966
  end
2967
2967
 
2968
2968
  specify "#insert should execute insert SQL" do
2969
- @db.should_receive(:execute).once.with('INSERT INTO items DEFAULT VALUES')
2969
+ @db.should_receive(:execute).once.with('INSERT INTO items DEFAULT VALUES', :server=>:default)
2970
2970
  @ds.insert([])
2971
2971
  end
2972
2972
 
2973
2973
  specify "#update should execute update SQL" do
2974
- @db.should_receive(:execute).once.with('UPDATE items SET number = 1')
2974
+ @db.should_receive(:execute).once.with('UPDATE items SET number = 1', :server=>:default)
2975
2975
  @ds.update(:number=>1)
2976
2976
  end
2977
+
2978
+ specify "#execute should execute the SQL on the database" do
2979
+ @db.should_receive(:execute).once.with('SELECT 1', :server=>:read_only)
2980
+ @ds.send(:execute, 'SELECT 1')
2981
+ end
2982
+ end
2983
+
2984
+ context "Dataset prepared statements and bound variables " do
2985
+ setup do
2986
+ @db = Sequel::Database.new
2987
+ @db.meta_eval{attr_accessor :sqls}
2988
+ @db.sqls = []
2989
+ def @db.execute(sql, opts={})
2990
+ @sqls << sql
2991
+ end
2992
+ @ds = @db[:items]
2993
+ def @ds.fetch_rows(sql, &block)
2994
+ execute(sql)
2995
+ end
2996
+ end
2997
+
2998
+ specify "#call should take a type and bind hash and interpolate it" do
2999
+ @ds.filter(:num=>:$n).call(:select, :n=>1)
3000
+ @ds.filter(:num=>:$n).call(:first, :n=>1)
3001
+ @ds.filter(:num=>:$n).call(:delete, :n=>1)
3002
+ @ds.filter(:num=>:$n).call(:update, {:n=>1, :n2=>2}, :num=>:$n2)
3003
+ @ds.call(:insert, {:n=>1}, :num=>:$n)
3004
+ @db.sqls.should == ['SELECT * FROM items WHERE (num = 1)',
3005
+ 'SELECT * FROM items WHERE (num = 1) LIMIT 1',
3006
+ 'DELETE FROM items WHERE (num = 1)',
3007
+ 'UPDATE items SET num = 2 WHERE (num = 1)',
3008
+ 'INSERT INTO items (num) VALUES (1)']
3009
+ end
3010
+
3011
+ specify "#prepare should take a type and name and store it in the database for later use with call" do
3012
+ pss = []
3013
+ pss << @ds.filter(:num=>:$n).prepare(:select, :sn)
3014
+ pss << @ds.filter(:num=>:$n).prepare(:first, :fn)
3015
+ pss << @ds.filter(:num=>:$n).prepare(:delete, :dn)
3016
+ pss << @ds.filter(:num=>:$n).prepare(:update, :un, :num=>:$n2)
3017
+ pss << @ds.prepare(:insert, :in, :num=>:$n)
3018
+ @db.prepared_statements.keys.sort_by{|k| k.to_s}.should == [:dn, :fn, :in, :sn, :un]
3019
+ [:sn, :fn, :dn, :un, :in].each_with_index{|x, i| @db.prepared_statements[x].should == pss[i]}
3020
+ @db.call(:sn, :n=>1)
3021
+ @db.call(:fn, :n=>1)
3022
+ @db.call(:dn, :n=>1)
3023
+ @db.call(:un, :n=>1, :n2=>2)
3024
+ @db.call(:in, :n=>1)
3025
+ @db.sqls.should == ['SELECT * FROM items WHERE (num = 1)',
3026
+ 'SELECT * FROM items WHERE (num = 1) LIMIT 1',
3027
+ 'DELETE FROM items WHERE (num = 1)',
3028
+ 'UPDATE items SET num = 2 WHERE (num = 1)',
3029
+ 'INSERT INTO items (num) VALUES (1)']
3030
+ end
3031
+
3032
+ specify "#inspect should indicate it is a prepared statement with the prepared SQL" do
3033
+ @ds.filter(:num=>:$n).prepare(:select, :sn).inspect.should == \
3034
+ '<Sequel::Dataset/PreparedStatement "SELECT * FROM items WHERE (num = $n)">'
3035
+ end
2977
3036
  end
3037
+
3038
+ context Sequel::Dataset::UnnumberedArgumentMapper do
3039
+ setup do
3040
+ @db = Sequel::Database.new
3041
+ @db.meta_eval{attr_accessor :sqls}
3042
+ @db.sqls = []
3043
+ def @db.execute(sql, opts={})
3044
+ @sqls << [sql, *opts[:arguments]]
3045
+ end
3046
+ @ds = @db[:items].filter(:num=>:$n)
3047
+ def @ds.fetch_rows(sql, &block)
3048
+ execute(sql)
3049
+ end
3050
+ def @ds.execute(sql, opts={}, &block)
3051
+ @db.execute(sql, {:arguments=>bind_arguments}.merge(opts))
3052
+ end
3053
+ @ps = @ds.prepare(:select, :sn)
3054
+ @ps.extend(Sequel::Dataset::UnnumberedArgumentMapper)
3055
+ end
3056
+
3057
+ specify "#inspect should show the actual SQL submitted to the database" do
3058
+ @ps.inspect.should == '<Sequel::Dataset/PreparedStatement "SELECT * FROM items WHERE (num = ?)">'
3059
+ end
3060
+
3061
+ specify "should submitted the SQL to the database with placeholders and bind variables" do
3062
+ @ps.call(:n=>1)
3063
+ @db.sqls.should == [["SELECT * FROM items WHERE (num = ?)", 1]]
3064
+ end
3065
+ end
3066
+
3067
+ context "Sequel::Dataset#server" do
3068
+ specify "should set the server to use for the dataset" do
3069
+ @db = Sequel::Database.new
3070
+ @ds = @db[:items].server(:s)
3071
+ sqls = []
3072
+ @db.meta_def(:execute) do |sql, opts|
3073
+ sqls << [sql, opts[:server]]
3074
+ end
3075
+ def @ds.fetch_rows(sql, &block)
3076
+ execute(sql)
3077
+ end
3078
+ @ds.all
3079
+ @ds.server(:i).insert(:a=>1)
3080
+ @ds.server(:d).delete
3081
+ @ds.server(:u).update(:a=>:a+1)
3082
+ sqls.should == [['SELECT * FROM items', :s],
3083
+ ['INSERT INTO items (a) VALUES (1)', :i],
3084
+ ['DELETE FROM items', :d],
3085
+ ['UPDATE items SET a = (a + 1)', :u]]
3086
+ end
3087
+ end