require_relative "spec_helper" 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.insert(: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.insert(:name => 'abc', :value => 1) raise Interrupt, 'asdf' end end.must_raise(Interrupt) @db.transaction do @d.insert(:name => 'abc', :value => 1) raise Sequel::Rollback end.must_be_nil proc do @db.transaction(:rollback=>:reraise) do @d.insert(:name => 'abc', :value => 1) raise Sequel::Rollback end end.must_raise(Sequel::Rollback) @db.transaction(:rollback=>:always) do @d.insert(: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.insert(:name => 'abc', :value => 1) end end @d.count.must_equal 1 @d.delete @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction do raise Sequel::Rollback end end @d.count.must_equal 0 proc {@db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction do raise Interrupt, 'asdf' end end}.must_raise(Interrupt) @d.count.must_equal 0 end it "should support rollback_on_exit" do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit end @d.must_be_empty catch(:foo) do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit throw :foo end end @d.must_be_empty lambda do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit return true end end @d.must_be_empty @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit @db.rollback_on_exit(:cancel=>true) end @d.count.must_equal 1 @d.delete @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit(:cancel=>true) end @d.count.must_equal 1 @d.delete @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit @db.rollback_on_exit(:cancel=>true) @db.rollback_on_exit end @d.must_be_empty end if DB.supports_savepoints? it "should support rollback_on_exit inside savepoints" do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.rollback_on_exit end end @d.must_be_empty @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.rollback_on_exit @db.transaction(:savepoint=>true) do @d.insert(:name => 'ghi', :value => 3) end end end @d.must_be_empty @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.transaction(:savepoint=>true) do @db.rollback_on_exit @d.insert(:name => 'ghi', :value => 3) end end end @d.must_be_empty end it "should support rollback_on_exit with :savepoint option" do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.rollback_on_exit(:savepoint=>true) end end @d.select_order_map(:value).must_equal [1] @d.delete @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.rollback_on_exit(:savepoint=>true) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>true) @d.insert(:name => 'ghi', :value => 3) end end end @d.select_order_map(:value).must_equal [1] end it "should support rollback_on_exit with :savepoint=>Integer" do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.rollback_on_exit(:savepoint=>2) end end @d.must_be_empty @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.rollback_on_exit(:savepoint=>3) end end @d.must_be_empty @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>2) @d.insert(:name => 'ghi', :value => 3) end end end @d.select_order_map(:value).must_equal [1] end it "should support rollback_on_exit with :savepoint=>Integer and :cancel" do @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>true) @d.insert(:name => 'def', :value => 2) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>2, :cancel=>true) @d.insert(:name => 'ghi', :value => 3) end end end @d.select_order_map(:value).must_equal [1, 2, 3] @d.delete @db.transaction do @db.rollback_on_exit(:savepoint=>true) @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>true) @d.insert(:name => 'def', :value => 2) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>3, :cancel=>true) @d.insert(:name => 'ghi', :value => 3) end end end @d.select_order_map(:value).must_equal [1, 2, 3] @d.delete @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.rollback_on_exit(:savepoint=>true) @db.transaction(:savepoint=>true) do @d.insert(:name => 'def', :value => 2) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>4, :cancel=>true) @d.insert(:name => 'ghi', :value => 3) end end end @d.select_order_map(:value).must_equal [1, 2, 3] @d.delete @db.transaction do @d.insert(:name => 'abc', :value => 1) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>2) @d.insert(:name => 'def', :value => 2) @db.transaction(:savepoint=>true) do @db.rollback_on_exit(:savepoint=>2, :cancel=>true) @d.insert(:name => 'ghi', :value => 3) end end end @d.must_be_empty end it "should handle table_exists? failures inside transactions" do @db.transaction do @d.insert(:name => '1') @db.table_exists?(:asadf098asd9asd98sa).must_equal false @d.insert(: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.insert(: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.insert(:name => '1') @db.transaction(:savepoint=>true) do @d.insert(:name => '2') @db.table_exists?(:asadf098asd9asd98sa).must_equal false @d.insert(:name => '3') end @d.insert(: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.insert(:name => '1') @db.transaction(:savepoint=>true) do @d.insert(:name => '2') @db.transaction do @d.insert(:name => '3') raise Sequel::Rollback end end @d.insert(:name => '4') @db.transaction do @d.insert(:name => '6') @db.transaction(:savepoint=>true) do @d.insert(:name => '7') raise Sequel::Rollback end end @d.insert(: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.insert(:name => '1') @db.transaction do @d.insert(:name => '2') @db.transaction do @d.insert(:name => '3') raise Sequel::Rollback end end @d.insert(:name => '4') @db.transaction(:auto_savepoint=>true) do @d.insert(:name => '6') @db.transaction do @d.insert(:name => '7') raise Sequel::Rollback end end @d.insert(: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].insert(: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 @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.insert(: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.insert(: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.insert(: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.insert(: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 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 it "should be supported using the :retry_on option" 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 it "should limit number of retries via the :num_retries option" 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