@@ -97,6 +97,7 @@ module Database.PostgreSQL.Simple
9797-- , Base.insertID
9898 -- * Transaction handling
9999 , withTransaction
100+ , withTransactionSerializable
100101 , TransactionMode(..)
101102 , IsolationLevel(..)
102103 , ReadWriteMode(..)
@@ -122,7 +123,8 @@ import Blaze.ByteString.Builder.Char8 (fromChar)
122123import Control.Applicative ((<$>), pure)
123124import Control.Concurrent.MVar
124125import Control.Exception
125− ( Exception, onException, throw, throwIO, finally )
126+ ( Exception, onException, throw, throwIO, finally
127+ , try, SomeException, fromException )
126128import Control.Monad (foldM)
127129import Data.ByteString (ByteString)
128130import Data.Int (Int64)
@@ -643,6 +645,51 @@ defaultReadWriteMode = DefaultReadWriteMode
643645withTransaction :: Connection -> IO a -> IO a
644646withTransaction = withTransactionMode defaultTransactionMode
645647
648+-- | Execute an action inside of a 'Serializable' transaction. If a
649+-- serialization failure occurs, roll back the transaction and try again.
650+-- Be warned that this may execute the IO action multiple times.
651+--
652+-- More precisely, if a 'SqlError' arises whose 'sqlState' is @\"40001\"@
653+-- (@serialization_failure@), this will issue a @ROLLBACK@, then try the action
654+-- again. If any other exception arises, this will issue a @ROLLBACK@, but
655+-- will propagate the exception instead of retrying.
656+--
657+-- A 'Serializable' transaction creates the illusion that your program has
658+-- exclusive access to the database. This means that, even in a concurrent
659+-- setting, you can perform queries in sequence without having to worry about
660+-- what might happen between one statement and the next.
661+--
662+-- Think of it as STM, but without @retry@.
663+withTransactionSerializable :: ReadWriteMode -> Connection -> IO a -> IO a
664+withTransactionSerializable readWriteMode conn act =
665+ mask $ \restore ->
666+ retryLoop $ try $ do
667+ a <- restore act
668+ commit conn
669+ return a
670+ where
671+ retryLoop :: IO (Either SomeException a) -> IO a
672+ retryLoop act' = do
673+ beginMode mode conn
674+ r <- act'
675+ case r of
676+ Left e -> do
677+ rollback conn
678+ case fromException e of
679+ Just SqlError{..} | sqlState == serialization_failure
680+ -> retryLoop act'
681+ _ -> throwIO e
682+ Right a ->
683+ return a
684+
685+ mode = TransactionMode
686+ { isolationLevel = Serializable
687+ , readWriteMode
688+ }
689+
690+ -- http://www.postgresql.org/docs/current/static/errcodes-appendix.html
691+ serialization_failure = "40001"
692+
646693-- | Execute an action inside a SQL transaction with a given isolation level.
647694withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a
648695withTransactionLevel lvl