hasql-1.10: src/library/Hasql/Comms/Session.hs
module Hasql.Comms.Session
( Session,
-- * Constructors
cleanUpAfterInterruption,
-- * Executors
toHandler,
)
where
import Hasql.Comms.Roundtrip qualified as Roundtrip
import Hasql.Platform.Prelude
import Hasql.Pq qualified as Pq
-- | Serial execution of commands in the scope of a connection.
newtype Session a = Session (Pq.Connection -> IO (Either Error a))
deriving
(Functor, Applicative, Monad, MonadError Error)
via (ExceptT Error (ReaderT Pq.Connection IO))
type Error = Text
-- * Constructors
-- | Bring the connection to a clean state after an interruption.
--
-- This includes:
-- - Leaving pipeline mode if we are in it.
-- - Bringing the transaction status to idle if we are in a transaction.
-- - Deallocating all prepared statements.
cleanUpAfterInterruption :: Session ()
cleanUpAfterInterruption = do
drainResults
cancel
drainResults
-- Ensure we are out of pipeline mode.
leavePipeline
-- Ensure we are in idle transaction state.
bringTransactionStatusToIdle
deallocateAllPreparedStatements
bringTransactionStatusToIdle :: Session ()
bringTransactionStatusToIdle = do
transactionStatus <- getTransactionStatus
case transactionStatus of
Pq.TransIdle -> pure ()
Pq.TransInTrans -> do
runScript "ABORT"
Pq.TransActive -> do
-- A command is still in progress.
drainResults
-- Check status again after draining.
transactionStatus <- getTransactionStatus
case transactionStatus of
Pq.TransIdle -> pure ()
Pq.TransInTrans -> do
runScript "ABORT"
Pq.TransActive -> do
-- If we're still active, there's not much we can do.
-- The connection is probably in a bad state.
throwError "Failed to bring transaction status to idle after draining results"
Pq.TransInError -> do
runScript "ABORT"
Pq.TransUnknown -> do
-- Unknown state (connection issue), there's not much we can do.
throwError "Transaction status is unknown, connection is corrupted"
Pq.TransInError -> do
-- Transaction is in error state, we need to abort it.
runScript "ABORT"
Pq.TransUnknown -> do
-- Unknown state (connection issue), there's not much we can do.
throwError "Transaction status is unknown, connection is corrupted"
leavePipeline :: Session ()
leavePipeline = do
pipelineStatus <- getPipelineStatus
when (pipelineStatus == Pq.PipelineOn) do
-- Try to exit pipeline mode.
-- This might fail if there are pending results that need to be consumed.
success <- exitPipelineMode
unless success do
-- If exit failed, drain results and try again.
drainResults
success <- exitPipelineMode
unless success do
-- If it still fails, there's not much we can do.
-- The connection is probably in a bad state.
errorMessage <- getErrorMessage
let message = case errorMessage of
Nothing -> "Failed to exit pipeline mode after draining results"
Just details -> "Failed to exit pipeline mode after draining results: " <> decodeUtf8Lenient details
throwError message
deallocateAllPreparedStatements :: Session ()
deallocateAllPreparedStatements =
runScript "DEALLOCATE ALL"
cancel :: Session ()
cancel = Session \connection -> do
mCancel <- Pq.getCancel connection
case mCancel of
Just cancel -> do
result <- Pq.cancel cancel
case result of
Left errorMessage ->
pure (Left ("Failed to cancel: " <> decodeUtf8Lenient errorMessage))
Right () ->
pure (Right ())
Nothing -> pure (Right ())
getErrorMessage :: Session (Maybe ByteString)
getErrorMessage = Session \connection -> do
Right <$> Pq.errorMessage connection
getTransactionStatus :: Session Pq.TransactionStatus
getTransactionStatus = Session \connection -> do
Right <$> Pq.transactionStatus connection
getPipelineStatus :: Session Pq.PipelineStatus
getPipelineStatus = Session \connection -> do
Right <$> Pq.pipelineStatus connection
exitPipelineMode :: Session Bool
exitPipelineMode = Session \connection -> do
Right <$> Pq.exitPipelineMode connection
-- Drain all pending results from the connection.
drainResults :: Session ()
drainResults = Session \connection ->
let go = do
mResult <- Pq.getResult connection
case mResult of
Nothing -> pure ()
Just _ -> go
in go $> Right ()
runScript :: ByteString -> Session ()
runScript script = runRoundtrip (Roundtrip.query () script)
runRoundtrip :: Roundtrip.Roundtrip () a -> Session a
runRoundtrip roundtrip = Session \connection -> do
result <- Roundtrip.toSerialIO roundtrip connection
case result of
Left err ->
let message = case err of
Roundtrip.ClientError () Nothing ->
"Unknown client error occurred"
Roundtrip.ClientError () (Just details) ->
"Client error occurred: " <> decodeUtf8Lenient details
Roundtrip.ServerError recvError ->
"Server error occurred: " <> fromString (show recvError)
in pure (Left message)
Right value -> pure (Right value)
-- * Executors
toHandler :: Session a -> Pq.Connection -> IO (Either Text a)
toHandler (Session run) = run