{-# LANGUAGE MagicHash #-}
module Hmssql.Transaction
( IsolationLevel (..)
, withTransaction
, withTransactionLevel
, TransactionError (..)
, begin
, commit
, rollback
, inTransaction
) where
import Control.Exception
import Control.Monad (void, when)
import Data.IORef
import Data.Maybe (isJust, isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word8)
import Hmssql.Connection.Internal
import Hmssql.Errors (UnusableReason (..))
import Hmssql.Internal.Derived
import Hmssql.Query
import Hmssql.Query.Types (Request (..), ResponseReader (..))
import Hmssql.Tds.Client (tmBeginTransactionBuilder)
import Hmssql.Tds.PacketWriter (writeBuilder)
import Hmssql.Tds.Context
import Hmssql.Tds.Packet (PacketType (..))
data IsolationLevel
= ReadUncommitted
| ReadCommitted
| RepeatableRead
| Snapshot
| Serializable
deriving stock (IsolationLevel -> IsolationLevel -> Bool
(IsolationLevel -> IsolationLevel -> Bool)
-> (IsolationLevel -> IsolationLevel -> Bool) -> Eq IsolationLevel
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: IsolationLevel -> IsolationLevel -> Bool
== :: IsolationLevel -> IsolationLevel -> Bool
$c/= :: IsolationLevel -> IsolationLevel -> Bool
/= :: IsolationLevel -> IsolationLevel -> Bool
Eq, Int -> IsolationLevel -> ShowS
[IsolationLevel] -> ShowS
IsolationLevel -> String
(Int -> IsolationLevel -> ShowS)
-> (IsolationLevel -> String)
-> ([IsolationLevel] -> ShowS)
-> Show IsolationLevel
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> IsolationLevel -> ShowS
showsPrec :: Int -> IsolationLevel -> ShowS
$cshow :: IsolationLevel -> String
show :: IsolationLevel -> String
$cshowList :: [IsolationLevel] -> ShowS
showList :: [IsolationLevel] -> ShowS
Show, Int -> IsolationLevel
IsolationLevel -> Int
IsolationLevel -> [IsolationLevel]
IsolationLevel -> IsolationLevel
IsolationLevel -> IsolationLevel -> [IsolationLevel]
IsolationLevel
-> IsolationLevel -> IsolationLevel -> [IsolationLevel]
(IsolationLevel -> IsolationLevel)
-> (IsolationLevel -> IsolationLevel)
-> (Int -> IsolationLevel)
-> (IsolationLevel -> Int)
-> (IsolationLevel -> [IsolationLevel])
-> (IsolationLevel -> IsolationLevel -> [IsolationLevel])
-> (IsolationLevel -> IsolationLevel -> [IsolationLevel])
-> (IsolationLevel
-> IsolationLevel -> IsolationLevel -> [IsolationLevel])
-> Enum IsolationLevel
forall a.
(a -> a)
-> (a -> a)
-> (Int -> a)
-> (a -> Int)
-> (a -> [a])
-> (a -> a -> [a])
-> (a -> a -> [a])
-> (a -> a -> a -> [a])
-> Enum a
$csucc :: IsolationLevel -> IsolationLevel
succ :: IsolationLevel -> IsolationLevel
$cpred :: IsolationLevel -> IsolationLevel
pred :: IsolationLevel -> IsolationLevel
$ctoEnum :: Int -> IsolationLevel
toEnum :: Int -> IsolationLevel
$cfromEnum :: IsolationLevel -> Int
fromEnum :: IsolationLevel -> Int
$cenumFrom :: IsolationLevel -> [IsolationLevel]
enumFrom :: IsolationLevel -> [IsolationLevel]
$cenumFromThen :: IsolationLevel -> IsolationLevel -> [IsolationLevel]
enumFromThen :: IsolationLevel -> IsolationLevel -> [IsolationLevel]
$cenumFromTo :: IsolationLevel -> IsolationLevel -> [IsolationLevel]
enumFromTo :: IsolationLevel -> IsolationLevel -> [IsolationLevel]
$cenumFromThenTo :: IsolationLevel
-> IsolationLevel -> IsolationLevel -> [IsolationLevel]
enumFromThenTo :: IsolationLevel
-> IsolationLevel -> IsolationLevel -> [IsolationLevel]
Enum, IsolationLevel
IsolationLevel -> IsolationLevel -> Bounded IsolationLevel
forall a. a -> a -> Bounded a
$cminBound :: IsolationLevel
minBound :: IsolationLevel
$cmaxBound :: IsolationLevel
maxBound :: IsolationLevel
Bounded)
isolationByte :: IsolationLevel -> Word8
isolationByte :: IsolationLevel -> Word8
isolationByte = \case
IsolationLevel
ReadUncommitted -> Word8
1
IsolationLevel
ReadCommitted -> Word8
2
IsolationLevel
RepeatableRead -> Word8
3
IsolationLevel
Serializable -> Word8
4
IsolationLevel
Snapshot -> Word8
5
data TransactionError
= TransactionAlreadyActive
| TransactionEndedEarly
| TransactionRequestsActive [Text]
| TransactionStillOpen
instance Show TransactionError where
showsPrec :: Int -> TransactionError -> ShowS
showsPrec Int
p = \case
TransactionError
TransactionAlreadyActive -> String -> ShowS
showString String
"TransactionAlreadyActive"
TransactionError
TransactionEndedEarly -> String -> ShowS
showString String
"TransactionEndedEarly"
TransactionRequestsActive [Text]
a -> Int -> Addr# -> [Text] -> ShowS
forall a. Show a => Int -> Addr# -> a -> ShowS
showCon1 Int
p Addr#
"TransactionRequestsActive"# [Text]
a
TransactionError
TransactionStillOpen -> String -> ShowS
showString String
"TransactionStillOpen"
{-# OPAQUE showsPrec #-}
instance Exception TransactionError where
displayException :: TransactionError -> String
displayException = \case
TransactionError
TransactionAlreadyActive ->
String
"hmssql: withTransaction: a transaction is already active on this connection \
\(nested transactions are not supported)"
TransactionError
TransactionEndedEarly ->
String
"hmssql: withTransaction: the transaction ended before the body returned \
\(rolled back by the server, or ended by the body); nothing was committed"
TransactionRequestsActive [Text]
stmts ->
String
"hmssql: a transaction can't begin or be committed while other responses on the \
\connection are unfinished (read them to the end or cancel them first): "
String -> ShowS
forall a. [a] -> [a] -> [a]
++ Text -> String
T.unpack (Text -> [Text] -> Text
T.intercalate Text
"; " [Text]
stmts)
TransactionError
TransactionStillOpen ->
String
"hmssql: withTransaction: the connection was still in a transaction after the COMMIT \
\(the body began a nested transaction and did not end it); everything was rolled back"
inTransaction :: Connection -> IO Bool
inTransaction :: Connection -> IO Bool
inTransaction Connection
conn = Maybe TransactionDescriptor -> Bool
forall a. Maybe a -> Bool
isJust (Maybe TransactionDescriptor -> Bool)
-> (TdsContext -> Maybe TransactionDescriptor)
-> TdsContext
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. TdsContext -> Maybe TransactionDescriptor
tdsTransaction (TdsContext -> Bool) -> IO TdsContext -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Connection -> IO TdsContext
connectionContext Connection
conn
begin :: Connection -> IO ()
begin :: Connection -> IO ()
begin Connection
conn = Connection -> IO () -> IO ()
forall a. Connection -> IO a -> IO a
withConnectionLock Connection
conn (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Connection -> IO ()
refuseActiveRequests Connection
conn
Connection -> Maybe IsolationLevel -> IO ()
beginAtLevel Connection
conn Maybe IsolationLevel
forall a. Maybe a
Nothing
refuseActiveRequests :: Connection -> IO ()
refuseActiveRequests :: Connection -> IO ()
refuseActiveRequests Connection
conn = Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Maybe Mars -> Bool
forall a. Maybe a -> Bool
isJust (Connection -> Maybe Mars
connMars Connection
conn)) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ Connection -> IO () -> IO ()
forall a. Connection -> IO a -> IO a
withConnectionLock Connection
conn (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Connection -> IO ()
refuseUnusable Connection
conn
Connection -> IO ()
drainCancelled Connection
conn
busy <- Connection -> IO [Text]
busyStatements Connection
conn
when (not (null busy)) $ throwIO (TransactionRequestsActive busy)
beginAtLevel :: Connection -> Maybe IsolationLevel -> IO ()
beginAtLevel :: Connection -> Maybe IsolationLevel -> IO ()
beginAtLevel Connection
conn Maybe IsolationLevel
mlevel = ((forall a. IO a -> IO a) -> IO ()) -> IO ()
forall b. ((forall a. IO a -> IO a) -> IO b) -> IO b
mask (((forall a. IO a -> IO a) -> IO ()) -> IO ())
-> ((forall a. IO a -> IO a) -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \forall a. IO a -> IO a
restore -> do
resp <- Connection -> Request -> IO ResponseReader
startResponse Connection
conn Request
req
readingToEnd conn (respActive resp) (restore (drainResponse resp))
where
req :: Request
req = PacketType
-> (TdsContext -> PacketWriter -> IO ()) -> Maybe Text -> Request
Request PacketType
PTTransactionManager
(\TdsContext
ctx PacketWriter
writer -> PacketWriter -> Builder -> IO ()
writeBuilder PacketWriter
writer (TdsContext -> Word8 -> Builder
tmBeginTransactionBuilder TdsContext
ctx (Word8 -> (IsolationLevel -> Word8) -> Maybe IsolationLevel -> Word8
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Word8
0 IsolationLevel -> Word8
isolationByte Maybe IsolationLevel
mlevel)))
(Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"BEGIN TRANSACTION")
commit :: Connection -> IO ()
commit :: Connection -> IO ()
commit Connection
conn = Connection -> IO () -> IO ()
forall a. Connection -> IO a -> IO a
withConnectionLock Connection
conn (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Connection -> IO ()
refuseActiveRequests Connection
conn
IO Int64 -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO Int64 -> IO ()) -> IO Int64 -> IO ()
forall a b. (a -> b) -> a -> b
$ Connection -> Query -> IO Int64
execute Connection
conn Query
"COMMIT TRANSACTION"
rollback :: Connection -> IO ()
rollback :: Connection -> IO ()
rollback Connection
conn = Connection -> IO () -> IO ()
forall a. Connection -> IO a -> IO a
withConnectionLock Connection
conn (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
Connection -> IO ()
finishResponse Connection
conn
active <- Connection -> IO Bool
inTransaction Connection
conn
when active $ void $ execute conn "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"
withTransaction :: Connection -> IO a -> IO a
withTransaction :: forall a. Connection -> IO a -> IO a
withTransaction Connection
conn = Connection -> Maybe IsolationLevel -> IO a -> IO a
forall a. Connection -> Maybe IsolationLevel -> IO a -> IO a
runTransaction Connection
conn Maybe IsolationLevel
forall a. Maybe a
Nothing
withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a
withTransactionLevel :: forall a. IsolationLevel -> Connection -> IO a -> IO a
withTransactionLevel IsolationLevel
level Connection
conn = Connection -> Maybe IsolationLevel -> IO a -> IO a
forall a. Connection -> Maybe IsolationLevel -> IO a -> IO a
runTransaction Connection
conn (IsolationLevel -> Maybe IsolationLevel
forall a. a -> Maybe a
Just IsolationLevel
level)
runTransaction :: Connection -> Maybe IsolationLevel -> IO a -> IO a
runTransaction :: forall a. Connection -> Maybe IsolationLevel -> IO a -> IO a
runTransaction Connection
conn Maybe IsolationLevel
mlevel IO a
act = ((forall a. IO a -> IO a) -> IO a) -> IO a
forall b. ((forall a. IO a -> IO a) -> IO b) -> IO b
mask (((forall a. IO a -> IO a) -> IO a) -> IO a)
-> ((forall a. IO a -> IO a) -> IO a) -> IO a
forall a b. (a -> b) -> a -> b
$ \forall a. IO a -> IO a
restore -> Connection -> IO a -> IO a
forall a. Connection -> IO a -> IO a
withConnectionLock Connection
conn (IO a -> IO a) -> IO a -> IO a
forall a b. (a -> b) -> a -> b
$ do
if Maybe Mars -> Bool
forall a. Maybe a -> Bool
isJust (Connection -> Maybe Mars
connMars Connection
conn) then Connection -> IO ()
refuseActiveRequests Connection
conn else Connection -> IO ()
finishResponse Connection
conn
active <- Connection -> IO Bool
inTransaction Connection
conn
when active $ throwIO TransactionAlreadyActive
let restoreSql = case Maybe IsolationLevel
mlevel of
Just IsolationLevel
_ -> Query
"; SET TRANSACTION ISOLATION LEVEL READ COMMITTED"
Maybe IsolationLevel
Nothing -> Query
""
cleanup = do
Connection -> IO ()
finishResponse Connection
conn
IO Int64 -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO Int64 -> IO ()) -> IO Int64 -> IO ()
forall a b. (a -> b) -> a -> b
$ Connection -> Query -> IO Int64
execute Connection
conn (Query
"IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION" Query -> Query -> Query
forall a. Semigroup a => a -> a -> a
<> Query
restoreSql)
cleanupOrBreak = IO ()
cleanup IO () -> (SomeException -> IO ()) -> IO ()
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` \SomeException
e -> do
Connection -> UnusableReason -> IO ()
markBroken Connection
conn (SomeException -> UnusableReason
RollbackFailed SomeException
e)
case SomeException -> Maybe SomeAsyncException
forall e. Exception e => SomeException -> Maybe e
fromException SomeException
e of
Just (SomeAsyncException
_ :: SomeAsyncException) -> SomeException -> IO ()
forall e a. (HasCallStack, Exception e) => e -> IO a
throwIO SomeException
e
Maybe SomeAsyncException
Nothing -> () -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
beginAtLevel conn mlevel `onException` cleanupOrBreak
started <- tdsTransaction <$> connectionContext conn
r <- restore act `onException` cleanupOrBreak
flip onException cleanupOrBreak $ do
if isJust (connMars conn) then refuseActiveRequests conn else finishResponse conn
now <- tdsTransaction <$> connectionContext conn
resetPending <- readIORef (connResetPending conn)
when (isNothing now || now /= started || resetPending) $ throwIO TransactionEndedEarly
void $ execute conn ("COMMIT TRANSACTION" <> restoreSql)
stillOpen <- inTransaction conn
when stillOpen $ throwIO TransactionStillOpen
pure r