@@ -8,7 +8,6 @@ module Database.PostgreSQL.Simple.Transaction
8 , withTransactionMode
8 , withTransactionMode
9 , withTransactionModeRetry
9 , withTransactionModeRetry
10 , withTransactionSerializable
10 , withTransactionSerializable
11− , isSerializationError
12 , TransactionMode(..)
11 , TransactionMode(..)
13 , IsolationLevel(..)
12 , IsolationLevel(..)
14 , ReadWriteMode(..)
13 , ReadWriteMode(..)
@@ -21,13 +20,27 @@ module Database.PostgreSQL.Simple.Transaction
21 , beginMode
20 , beginMode
23 , rollback
22 , rollback
29+ , rollbackToSavepoint
30+ , rollbackToAndReleaseSavepoint
32+ -- * Error predicates
33+ , isSerializationError
34+ , isNoActiveTransactionError
35+ , isFailedTransactionError
26−import Control.Exception hiding (mask)
38+import qualified Control.Exception as E
39+import Data.ByteString (ByteString)
27import qualified Data.ByteString as B
40import qualified Data.ByteString as B
28import Database.PostgreSQL.Simple.Internal
41import Database.PostgreSQL.Simple.Internal
29import Database.PostgreSQL.Simple.Types
42import Database.PostgreSQL.Simple.Types
30−import Database.PostgreSQL.Simple.Compat(mask)
43+import Database.PostgreSQL.Simple.Compat (mask, (<>))
33-- | Of the four isolation levels defined by the SQL standard,
46-- | Of the four isolation levels defined by the SQL standard,
@@ -85,6 +98,8 @@ defaultReadWriteMode = DefaultReadWriteMode
85-- If the action throws /any/ kind of exception (not just a
98-- If the action throws /any/ kind of exception (not just a
86-- PostgreSQL-related exception), the transaction will be rolled back using
99-- PostgreSQL-related exception), the transaction will be rolled back using
87-- 'rollback', then the exception will be rethrown.
100-- 'rollback', then the exception will be rethrown.
102+-- For nesting transactions, see 'withSavepoint'.
88withTransaction :: Connection -> IO a -> IO a
103withTransaction :: Connection -> IO a -> IO a
89withTransaction = withTransactionMode defaultTransactionMode
104withTransaction = withTransactionMode defaultTransactionMode
@@ -107,17 +122,6 @@ withTransactionSerializable =
108 isSerializationError
123 isSerializationError
111−isSerializationError :: SqlError -> Bool
112−isSerializationError exception =
114− SqlError{..} | sqlState == serialization_failure
118− -- http://www.postgresql.org/docs/current/static/errcodes-appendix.html
119− serialization_failure = "40001"
121-- | Execute an action inside a SQL transaction with a given isolation level.
125-- | Execute an action inside a SQL transaction with a given isolation level.
122withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a
126withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a
123withTransactionLevel lvl
127withTransactionLevel lvl
@@ -128,7 +132,7 @@ withTransactionMode :: TransactionMode -> Connection -> IO a -> IO a
128withTransactionMode mode conn act =
132withTransactionMode mode conn act =
129 mask $ \restore -> do
133 mask $ \restore -> do
130 beginMode mode conn
134 beginMode mode conn
131− r <- restore act `onException` rollback conn
135+ r <- restore act `E.onException` rollback conn
132 commit conn
136 commit conn
@@ -142,21 +146,21 @@ withTransactionMode mode conn act =
142withTransactionModeRetry :: TransactionMode -> (SqlError -> Bool) -> Connection -> IO a -> IO a
146withTransactionModeRetry :: TransactionMode -> (SqlError -> Bool) -> Connection -> IO a -> IO a
143withTransactionModeRetry mode shouldRetry conn act =
147withTransactionModeRetry mode shouldRetry conn act =
144 mask $ \restore ->
148 mask $ \restore ->
145− retryLoop $ try $ do
149+ retryLoop $ E.try $ do
146 a <- restore act
150 a <- restore act
147 commit conn
151 commit conn
150− retryLoop :: IO (Either SomeException a) -> IO a
154+ retryLoop :: IO (Either E.SomeException a) -> IO a
151 retryLoop act' = do
155 retryLoop act' = do
152 beginMode mode conn
156 beginMode mode conn
153 r <- act'
157 r <- act'
154 case r of
158 case r of
155 Left e -> do
159 Left e -> do
156 rollback conn
160 rollback conn
157− case fmap shouldRetry (fromException e) of
161+ case fmap shouldRetry (E.fromException e) of
158 Just True -> retryLoop act'
162 Just True -> retryLoop act'
159− _ -> throwIO e
163+ _ -> E.throwIO e
160 Right a ->
164 Right a ->
@@ -191,3 +195,69 @@ beginMode mode conn = do
191 DefaultReadWriteMode -> ""
195 DefaultReadWriteMode -> ""
192 ReadWrite -> " READ WRITE"
196 ReadWrite -> " READ WRITE"
193 ReadOnly -> " READ ONLY"
197 ReadOnly -> " READ ONLY"
199+------------------------------------------------------------------------
202+-- | Create a savepoint, and roll back to it if an error occurs. This may only
203+-- be used inside of a transaction, and provides a sort of
204+-- \"nested transaction\".
206+-- See <http://www.postgresql.org/docs/current/static/sql-savepoint.html>
207+withSavepoint :: Connection -> IO a -> IO a
208+withSavepoint conn body =
209+ mask $ \restore -> do
210+ sp <- newSavepoint conn
211+ r <- restore body `E.onException` rollbackToAndReleaseSavepoint conn sp
212+ releaseSavepoint conn sp `E.catch` \err ->
213+ if isFailedTransactionError err
214+ then rollbackToAndReleaseSavepoint conn sp
218+-- | Create a new savepoint. This may only be used inside of a transaction.
219+newSavepoint :: Connection -> IO Savepoint
220+newSavepoint conn = do
221+ name <- newTempName conn
222+ _ <- execute_ conn ("SAVEPOINT " <> name)
223+ return (Savepoint name)
225+-- | Destroy a savepoint, but retain its effects.
227+-- Warning: this will throw a 'SqlError' matching 'isFailedTransactionError' if
228+-- the transaction is aborted due to an error. 'commit' would merely warn and
230+releaseSavepoint :: Connection -> Savepoint -> IO ()
231+releaseSavepoint conn (Savepoint name) =
232+ execute_ conn ("RELEASE SAVEPOINT " <> name) >> return ()
234+-- | Roll back to a savepoint. This will not release the savepoint.
235+rollbackToSavepoint :: Connection -> Savepoint -> IO ()
236+rollbackToSavepoint conn (Savepoint name) =
237+ execute_ conn ("ROLLBACK TO SAVEPOINT " <> name) >> return ()
239+-- | Roll back to a savepoint and release it. This is like calling
240+-- 'rollbackToSavepoint' followed by 'releaseSavepoint', but avoids a
241+-- round trip to the database server.
242+rollbackToAndReleaseSavepoint :: Connection -> Savepoint -> IO ()
243+rollbackToAndReleaseSavepoint conn (Savepoint name) =
244+ execute_ conn sql >> return ()
246+ sql = "ROLLBACK TO SAVEPOINT " <> name <> "; RELEASE SAVEPOINT " <> name
248+------------------------------------------------------------------------
251+-- http://www.postgresql.org/docs/current/static/errcodes-appendix.html
253+isSerializationError :: SqlError -> Bool
254+isSerializationError = isSqlState "40001"
256+isNoActiveTransactionError :: SqlError -> Bool
257+isNoActiveTransactionError = isSqlState "25P01"
259+isFailedTransactionError :: SqlError -> Bool
260+isFailedTransactionError = isSqlState "25P02"
262+isSqlState :: ByteString -> SqlError -> Bool
263+isSqlState s SqlError{..} = sqlState == s