Transactions

* dsl, state checks
* define transaction sql commands in connection
This commit is contained in:
Brian J. Cardiff 2016-11-15 23:46:11 -03:00
parent 1049d95562
commit 751f8b26ac
9 changed files with 293 additions and 1 deletions

View file

@ -42,11 +42,34 @@ class DummyDriver < DB::Driver
@connected = false
end
def create_transaction
DummyTransaction.new(self)
end
protected def do_close
super
end
end
class DummyTransaction < DB::TopLevelTransaction
getter committed = false
getter rolledback = false
def initialize(connection)
super(connection)
end
def commit
super
@committed = true
end
def rollback
super
@rolledback = true
end
end
class DummyStatement < DB::Statement
property params

158
spec/transaction_spec.cr Normal file
View file

@ -0,0 +1,158 @@
require "./spec_helper"
private class FooException < Exception
end
describe DB::Transaction do
it "begin/commit transaction from connection" do
with_dummy_connection do |cnn|
tx = cnn.begin_transaction
tx.commit
end
end
it "begin/rollback transaction from connection" do
with_dummy_connection do |cnn|
tx = cnn.begin_transaction
tx.rollback
end
end
it "raise if begin over existing transaction" do
with_dummy_connection do |cnn|
cnn.begin_transaction
expect_raises(DB::Error, "There is an existing transaction in this connection") do
cnn.begin_transaction
end
end
end
it "allow sequential transactions" do
with_dummy_connection do |cnn|
tx = cnn.begin_transaction
tx.rollback
tx = cnn.begin_transaction
tx.commit
end
end
it "transaction with block from connection should be committed" do
t = uninitialized DummyDriver::DummyTransaction
with_witness do |w|
with_dummy_connection do |cnn|
cnn.transaction do |tx|
if tx.is_a?(DummyDriver::DummyTransaction)
t = tx
w.check
end
end
end
end
t.committed.should be_true
t.rolledback.should be_false
end
it "transaction with block from connection should be rolledback if raise DB::Rollback" do
t = uninitialized DummyDriver::DummyTransaction
with_witness do |w|
with_dummy_connection do |cnn|
cnn.transaction do |tx|
if tx.is_a?(DummyDriver::DummyTransaction)
t = tx
w.check
end
raise DB::Rollback.new
end
end
end
t.rolledback.should be_true
t.committed.should be_false
end
it "transaction with block from connection should be rolledback if raise" do
t = uninitialized DummyDriver::DummyTransaction
with_witness do |w|
with_dummy_connection do |cnn|
expect_raises(FooException) do
cnn.transaction do |tx|
if tx.is_a?(DummyDriver::DummyTransaction)
t = tx
w.check
end
raise FooException.new
end
end
end
end
t.rolledback.should be_true
t.committed.should be_false
end
it "transaction can be committed within block" do
with_dummy_connection do |cnn|
cnn.transaction do |tx|
tx.commit
end
end
end
it "transaction can be rolledback within block" do
with_dummy_connection do |cnn|
cnn.transaction do |tx|
tx.rollback
end
end
end
it "transaction can't be committed twice" do
with_dummy_connection do |cnn|
cnn.transaction do |tx|
tx.commit
expect_raises(DB::Error, "Transaction already closed") do
tx.commit
end
end
end
end
it "transaction can't be rolledback twice" do
with_dummy_connection do |cnn|
cnn.transaction do |tx|
tx.rollback
expect_raises(DB::Error, "Transaction already closed") do
tx.rollback
end
end
end
end
it "return connection to pool after transaction block in db" do
DummyDriver::DummyConnection.clear_connections
with_dummy do |db|
db.transaction do |tx|
db.pool.is_available?(DummyDriver::DummyConnection.connections.first).should be_false
end
db.pool.is_available?(DummyDriver::DummyConnection.connections.first).should be_true
end
end
it "releasing result_set from within transaction should not return connection to pool" do
cnn = uninitialized DB::Connection
with_dummy do |db|
db.transaction do |tx|
cnn = tx.connection
cnn.scalar "1"
db.pool.is_available?(cnn).should be_false
end
db.pool.is_available?(cnn).should be_true
end
end
end

View file

@ -143,7 +143,10 @@ require "./db/session_methods"
require "./db/disposable"
require "./db/driver"
require "./db/statement"
require "./db/begin_transaction"
require "./db/connection"
require "./db/transaction"
require "./db/statement"
require "./db/pool_statement"
require "./db/database"
require "./db/pool_prepared_statement"

View file

@ -0,0 +1,17 @@
module DB
module BeginTransaction
abstract def begin_transaction : Transaction
def transaction
tx = begin_transaction
begin
yield tx
rescue e
tx.rollback
raise e unless e.is_a?(DB::Rollback)
else
tx.commit unless tx.closed?
end
end
end
end

View file

@ -21,10 +21,12 @@ module DB
abstract class Connection
include Disposable
include SessionMethods(Connection, Statement)
include BeginTransaction
# :nodoc:
getter database
@statements_cache = StringKeyCache(Statement).new
@transaction = false
getter? prepared_statements : Bool
def initialize(@database : Database)
@ -42,10 +44,45 @@ module DB
# :nodoc:
abstract def build_unprepared_statement(query) : Statement
def begin_transaction
raise DB::Error.new("There is an existing transaction in this connection") if @transaction
@transaction = true
create_transaction
end
protected def create_transaction : Transaction
TopLevelTransaction.new(self)
end
protected def do_close
@statements_cache.each_value &.close
@statements_cache.clear
@database.pool.delete self
end
# :nodoc:
def release_from_statement
@database.return_to_pool(self) unless @transaction
end
# :nodoc:
def release_from_transaction
@transaction = false
end
# :nodoc:
def perform_begin_transaction
self.unprepared.exec "BEGIN"
end
# :nodoc:
def perform_commit_transaction
self.unprepared.exec "COMMIT"
end
# :nodoc:
def perform_rollback_transaction
self.unprepared.exec "ROLLBACK"
end
end
end

View file

@ -105,6 +105,14 @@ module DB
end
end
def transaction
using_connection do |cnn|
cnn.transaction do |tx|
yield tx
end
end
end
# :nodoc:
def retry
@pool.retry do

View file

@ -17,4 +17,7 @@ module DB
def initialize(@connection)
end
end
class Rollback < Exception
end
end

View file

@ -59,7 +59,7 @@ module DB
end
def release_connection
@connection.database.return_to_pool(@connection)
@connection.release_from_statement
end
# See `QueryMethods#exec`

43
src/db/transaction.cr Normal file
View file

@ -0,0 +1,43 @@
module DB
abstract class Transaction
include Disposable
abstract def connection : Connection
def commit
close!
end
def rollback
close!
end
private def close!
raise DB::Error.new("Transaction already closed") if closed?
close
end
end
class TopLevelTransaction < Transaction
# :nodoc:
getter connection
def initialize(@connection : Connection)
@connection.perform_begin_transaction
end
def commit
@connection.perform_commit_transaction
close!
end
def rollback
@connection.perform_rollback_transaction
close!
end
protected def do_close
connection.release_from_transaction
end
end
end