require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb') describe "Database transactions" do before(:all) do @db = DB @db.create_table!(:items, :engine=>'InnoDB'){String :name; Integer :value} @d = @db[:items] end before do @d.delete end after(:all) do @db.drop_table?(:items) end it "should support transactions" do @db.transaction{@d << {:name => 'abc', :value => 1}} @d.count.must_equal 1 end it "should have #transaction yield the connection" do @db.transaction{|conn| conn.wont_equal nil} end it "should have #in_transaction? work correctly" do @db.in_transaction?.must_equal false c = nil @db.transaction{c = @db.in_transaction?} c.must_equal true end it "should correctly rollback transactions" do proc do @db.transaction do @d << {:name => 'abc', :value => 1} raise Interrupt, 'asdf' end end.must_raise(Interrupt) @db.transaction do @d << {:name => 'abc', :value => 1} raise Sequel::Rollback end.must_be_nil proc do @db.transaction(:rollback=>:reraise) do @d << {:name => 'abc', :value => 1} raise Sequel::Rollback end end.must_raise(Sequel::Rollback) @db.transaction(:rollback=>:always) do @d << {:name => 'abc', :value => 1} 2 end.must_equal 2 @d.count.must_equal 0 end it "should support nested transactions" do @db.transaction do @db.transaction do @d << {:name => 'abc', :value => 1} end end @d.count.must_equal 1 @d.delete @db.transaction do @d << {:name => 'abc', :value => 1} @db.transaction do raise Sequel::Rollback end end @d.count.must_equal 0 proc {@db.transaction do @d << {:name => 'abc', :value => 1} @db.transaction do raise Interrupt, 'asdf' end end}.must_raise(Interrupt) @d.count.must_equal 0 end if DB.supports_savepoints? it "should handle table_exists? failures inside transactions" do @db.transaction do @d << {:name => '1'} @db.table_exists?(:asadf098asd9asd98sa).must_equal false @d << {:name => '2'} end @d.select_order_map(:name).must_equal %w'1 2' end it "should handle :rollback=>:always inside transactions" do @db.transaction do @db.transaction(:rollback=>:always) do @d << {:name => 'abc', :value => 1} 2 end.must_equal 2 end @d.select_order_map(:value).must_equal [] end it "should handle table_exists? failures inside savepoints" do @db.transaction do @d << {:name => '1'} @db.transaction(:savepoint=>true) do @d << {:name => '2'} @db.table_exists?(:asadf098asd9asd98sa).must_equal false @d << {:name => '3'} end @d << {:name => '4'} end @d.select_order_map(:name).must_equal %w'1 2 3 4' end it "should support nested transactions through savepoints using the savepoint option" do @db.transaction do @d << {:name => '1'} @db.transaction(:savepoint=>true) do @d << {:name => '2'} @db.transaction do @d << {:name => '3'} raise Sequel::Rollback end end @d << {:name => '4'} @db.transaction do @d << {:name => '6'} @db.transaction(:savepoint=>true) do @d << {:name => '7'} raise Sequel::Rollback end end @d << {:name => '5'} end @d.order(:name).map(:name).must_equal %w{1 4 5 6} end it "should support nested transactions through savepoints using the auto_savepoint option" do @db.transaction(:auto_savepoint=>true) do @d << {:name => '1'} @db.transaction do @d << {:name => '2'} @db.transaction do @d << {:name => '3'} raise Sequel::Rollback end end @d << {:name => '4'} @db.transaction(:auto_savepoint=>true) do @d << {:name => '6'} @db.transaction do @d << {:name => '7'} raise Sequel::Rollback end end @d << {:name => '5'} end @d.order(:name).map(:name).must_equal %w{1 4 5 6} end end it "should handle returning inside of the block by committing" do def ret_commit @db.transaction do @db[:items] << {:name => 'abc'} return end end @d.count.must_equal 0 ret_commit @d.count.must_equal 1 ret_commit @d.count.must_equal 2 proc do @db.transaction do raise Interrupt, 'asdf' end end.must_raise(Interrupt) @d.count.must_equal 2 end if DB.supports_prepared_transactions? it "should allow saving and destroying of model objects" do c = Class.new(Sequel::Model(@d)) c.set_primary_key :name c.unrestrict_primary_key c.use_after_commit_rollback = false @db.transaction(:prepare=>'XYZ'){c.create(:name => '1'); c.create(:name => '2').destroy} @db.commit_prepared_transaction('XYZ') @d.select_order_map(:name).must_equal ['1'] end it "should commit prepared transactions using commit_prepared_transaction" do @db.transaction(:prepare=>'XYZ'){@d << {:name => '1'}} @db.commit_prepared_transaction('XYZ') @d.select_order_map(:name).must_equal ['1'] end it "should rollback prepared transactions using rollback_prepared_transaction" do @db.transaction(:prepare=>'XYZ'){@d << {:name => '1'}} @db.rollback_prepared_transaction('XYZ') @d.select_order_map(:name).must_equal [] end if DB.supports_savepoints_in_prepared_transactions? it "should support savepoints when using prepared transactions" do @db.transaction(:prepare=>'XYZ'){@db.transaction(:savepoint=>true){@d << {:name => '1'}}} @db.commit_prepared_transaction('XYZ') @d.select_order_map(:name).must_equal ['1'] end end end it "should support all transaction isolation levels" do [:uncommitted, :committed, :repeatable, :serializable].each_with_index do |l, i| @db.transaction(:isolation=>l){@d << {:name => 'abc', :value => 1}} @d.count.must_equal i + 1 end end it "should support after_commit outside transactions" do c = nil @db.after_commit{c = 1} c.must_equal 1 end it "should support after_rollback outside transactions" do c = nil @db.after_rollback{c = 1} c.must_be_nil end it "should support after_commit inside transactions" do c = nil @db.transaction{@db.after_commit{c = 1}; c.must_be_nil} c.must_equal 1 end it "should support after_rollback inside transactions" do c = nil @db.transaction{@db.after_rollback{c = 1}; c.must_be_nil} c.must_be_nil end it "should not call after_commit if the transaction rolls back" do c = nil @db.transaction{@db.after_commit{c = 1}; c.must_be_nil; raise Sequel::Rollback} c.must_be_nil end it "should call after_rollback if the transaction rolls back" do c = nil @db.transaction{@db.after_rollback{c = 1}; c.must_be_nil; raise Sequel::Rollback} c.must_equal 1 end it "should support multiple after_commit blocks inside transactions" do c = [] @db.transaction{@db.after_commit{c << 1}; @db.after_commit{c << 2}; c.must_equal []} c.must_equal [1, 2] end it "should support multiple after_rollback blocks inside transactions" do c = [] @db.transaction{@db.after_rollback{c << 1}; @db.after_rollback{c << 2}; c.must_equal []; raise Sequel::Rollback} c.must_equal [1, 2] end it "should support after_commit inside nested transactions" do c = nil @db.transaction{@db.transaction{@db.after_commit{c = 1}}; c.must_be_nil} c.must_equal 1 end it "should support after_rollback inside nested transactions" do c = nil @db.transaction{@db.transaction{@db.after_rollback{c = 1}}; c.must_be_nil; raise Sequel::Rollback} c.must_equal 1 end if DB.supports_savepoints? it "should support after_commit inside savepoints" do c = nil @db.transaction{@db.transaction(:savepoint=>true){@db.after_commit{c = 1}}; c.must_be_nil} c.must_equal 1 end it "should support after_rollback inside savepoints" do c = nil @db.transaction{@db.transaction(:savepoint=>true){@db.after_rollback{c = 1}}; c.must_be_nil; raise Sequel::Rollback} c.must_equal 1 end end if DB.supports_prepared_transactions? it "should raise an error if you attempt to use after_commit or after_rollback inside a prepared transaction" do proc{@db.transaction(:prepare=>'XYZ'){@db.after_commit{}}}.must_raise(Sequel::Error) proc{@db.transaction(:prepare=>'XYZ'){@db.after_rollback{}}}.must_raise(Sequel::Error) end if DB.supports_savepoints_in_prepared_transactions? it "should raise an error if you attempt to use after_commit or after rollback inside a savepoint in a prepared transaction" do proc{@db.transaction(:prepare=>'XYZ'){@db.transaction(:savepoint=>true){@db.after_commit{}}}}.must_raise(Sequel::Error) proc{@db.transaction(:prepare=>'XYZ'){@db.transaction(:savepoint=>true){@db.after_rollback{}}}}.must_raise(Sequel::Error) end end end end if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby') and RUBY_VERSION < '1.9' describe "Database transactions and Thread#kill" do before do @db = DB @db.create_table!(:items, :engine=>'InnoDB'){String :name; Integer :value} @d = @db[:items] end after do @db.drop_table?(:items) end it "should handle transactions inside threads" do q = Queue.new q1 = Queue.new t = Thread.new do @db.transaction do @d << {:name => 'abc', :value => 1} q1.push nil q.pop @d << {:name => 'def', :value => 2} end end q1.pop t.kill @d.count.must_equal 0 end if DB.supports_savepoints? it "should handle transactions with savepoints inside threads" do q = Queue.new q1 = Queue.new t = Thread.new do @db.transaction do @d << {:name => 'abc', :value => 1} @db.transaction(:savepoint=>true) do @d << {:name => 'def', :value => 2} q1.push nil q.pop @d << {:name => 'ghi', :value => 3} end @d << {:name => 'jkl', :value => 4} end end q1.pop t.kill @d.count.must_equal 0 end end end end describe "Database transaction retrying" do before(:all) do @db = DB @db.create_table!(:items, :engine=>'InnoDB'){String :a, :unique=>true, :null=>false} @d = @db[:items] end before do @d.delete end after(:all) do @db.drop_table?(:items) end cspecify "should be supported using the :retry_on option", [:db2] do @d.insert('b') @d.insert('c') s = 'a' @db.transaction(:retry_on=>Sequel::ConstraintViolation) do s = s.succ @d.insert(s) end @d.select_order_map(:a).must_equal %w'b c d' end cspecify "should limit number of retries via the :num_retries option", [:db2] do @d.insert('b') @d.insert('c') s = 'a' lambda do @db.transaction(:num_retries=>1, :retry_on=>Sequel::ConstraintViolation) do s = s.succ @d.insert(s) end end.must_raise(Sequel::UniqueConstraintViolation, Sequel::ConstraintViolation) @d.select_order_map(:a).must_equal %w'b c' end end