summaryrefslogtreecommitdiff
path: root/sql/transaction.lisp
diff options
context:
space:
mode:
Diffstat (limited to 'sql/transaction.lisp')
-rw-r--r--sql/transaction.lisp152
1 files changed, 152 insertions, 0 deletions
diff --git a/sql/transaction.lisp b/sql/transaction.lisp
new file mode 100644
index 0000000..24465d1
--- /dev/null
+++ b/sql/transaction.lisp
@@ -0,0 +1,152 @@
+;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*-
+;;;; *************************************************************************
+;;;;
+;;;; Transaction support
+;;;;
+;;;; This file is part of CLSQL.
+;;;;
+;;;; CLSQL users are granted the rights to distribute and use this software
+;;;; as governed by the terms of the Lisp Lesser GNU Public License
+;;;; (http://opensource.franz.com/preamble.html), also known as the LLGPL.
+;;;; *************************************************************************
+
+(in-package #:clsql-sys)
+
+(defclass transaction ()
+ ((commit-hooks :initform () :accessor commit-hooks)
+ (rollback-hooks :initform () :accessor rollback-hooks)
+ (previous-autocommit :initarg :previous-autocommit
+ :reader previous-autocommit)
+ (status :initform nil :accessor transaction-status
+ :documentation "nil or :committed")))
+
+(defun add-transaction-commit-hook (commit-hook &key
+ (database *default-database*))
+ "Adds COMMIT-HOOK, which should a designator for a function
+with no required arguments, to the list of hooks run when COMMIT
+is called on DATABASE which defaults to *DEFAULT-DATABASE*."
+ (when (transaction database)
+ (push commit-hook (commit-hooks (transaction database)))))
+
+(defun add-transaction-rollback-hook (rollback-hook
+ &key (database *default-database*))
+ "Adds ROLLBACK-HOOK, which should a designator for a function
+with no required arguments, to the list of hooks run when ROLLBACK
+is called on DATABASE which defaults to *DEFAULT-DATABASE*."
+ (when (transaction database)
+ (push rollback-hook (rollback-hooks (transaction database)))))
+
+(defmethod database-start-transaction ((database database))
+ (unless (transaction database)
+ (setf (transaction database)
+ (make-instance 'transaction :previous-autocommit
+ (database-autocommit database))))
+ ;; TODO: database-autocommit might get lost in some scenarios
+ ;; when pooling connections
+ (setf (database-autocommit database) nil)
+ (when (= (incf (transaction-level database)) 1)
+ (let ((transaction (transaction database)))
+ (setf (commit-hooks transaction) nil
+ (rollback-hooks transaction) nil
+ (transaction-status transaction) nil)
+ (case (database-underlying-type database)
+ (:oracle nil)
+ (:mssql (execute-command "BEGIN TRANSACTION" :database database))
+ (t (execute-command "BEGIN" :database database))))))
+
+;;ODBC should potentially be using the following scheme for transactions:
+;; turn off autocommit for begin. then use sqlendtran (or maybe sqltransact)
+;; whatever is appropriate for this version of odbc.
+(defmethod database-commit-transaction ((database database))
+ (with-slots (transaction transaction-level autocommit) database
+ (if (plusp transaction-level)
+ (if (zerop (decf transaction-level))
+ (progn
+ (case (database-underlying-type database)
+ (:mssql (execute-command "COMMIT TRANSACTION" :database database))
+ (t (execute-command "COMMIT" :database database)))
+ (setf autocommit (previous-autocommit transaction))
+ (map nil #'funcall (commit-hooks transaction)))
+ (setf (transaction-status (transaction database)) nil))
+ (warn 'sql-warning
+ :message
+ (format nil "Cannot commit transaction against ~A because there is no transaction in progress."
+ database)))))
+
+(defmethod database-abort-transaction ((database database))
+ (with-slots (transaction transaction-level autocommit) database
+ (if (plusp transaction-level)
+ (when (zerop (decf transaction-level))
+ (unwind-protect
+ (case (database-underlying-type database)
+ (:mssql (execute-command "ROLLBACK TRANSACTION" :database database))
+ (t (execute-command "ROLLBACK" :database database)))
+ (setf autocommit (previous-autocommit transaction))
+ (map nil #'funcall (rollback-hooks transaction))))
+ (warn 'sql-warning
+ :message
+ (format nil "Cannot abort transaction against ~A because there is no transaction in progress."
+ database)))))
+
+(defun mark-transaction-committed (database)
+ (when (and (transaction database)
+ (not (transaction-status (transaction database))))
+ (setf (transaction-status (transaction database)) :committed)))
+
+(defmacro with-transaction ((&key (database '*default-database*)) &body body)
+ "Starts a transaction in the database specified by DATABASE,
+which is *DEFAULT-DATABASE* by default, and executes BODY within
+that transaction. If BODY aborts or throws, DATABASE is rolled
+back and otherwise the transaction is committed."
+ (let ((db (gensym "db-")))
+ `(let ((,db ,database))
+ (unwind-protect
+ (prog2
+ (database-start-transaction ,db)
+ (progn
+ ,@body)
+ (mark-transaction-committed ,db))
+ (if (eq (transaction-status (transaction ,db)) :committed)
+ (database-commit-transaction ,db)
+ (database-abort-transaction ,db))))))
+
+(defun commit (&key (database *default-database*))
+ "If DATABASE, which defaults to *DEFAULT-DATABASE*, is
+currently within the scope of a transaction, commits changes made
+since the transaction began."
+ (database-commit-transaction database)
+ nil)
+
+(defun rollback (&key (database *default-database*))
+ "If DATABASE, which defaults to *DEFAULT-DATABASE*, is
+currently within the scope of a transaction, rolls back changes
+made since the transaction began."
+ (database-abort-transaction database)
+ nil)
+
+(defun start-transaction (&key (database *default-database*))
+ "Starts a transaction block on DATABASE which defaults to
+*DEFAULT-DATABASE* and which continues until ROLLBACK or COMMIT
+are called."
+ (unless (in-transaction-p :database database)
+ (database-start-transaction database))
+ nil)
+
+(defun in-transaction-p (&key (database *default-database*))
+ "A predicate to test whether DATABASE, which defaults to
+*DEFAULT-DATABASE*, is currently within the scope of a
+transaction."
+ (and database (transaction database) (= (transaction-level database) 1)))
+
+(defun set-autocommit (value &key (database *default-database*))
+ "Turns autocommit off for DATABASE if VALUE is NIL, and
+otherwise turns it on. Returns the old value of autocommit flag.
+For RDBMS (such as Oracle) which don't automatically commit
+changes, turning autocommit on has the effect of explicitly
+committing changes made whenever SQL statements are executed.
+Autocommit is turned on by default."
+ (let ((old-value (database-autocommit database)))
+ (setf (database-autocommit database) value)
+ (database-autocommit database)
+ old-value))
+