module Database.PostgreSQL.Store.Errand (
ErrandError (..),
ErrorCode (..),
Errand,
runErrand,
execute,
execute',
query,
queryWith,
prepare,
beginTransaction,
commitTransaction,
saveTransaction,
rollbackTransaction,
rollbackTransactionTo,
withTransaction,
ErrandQuery (..),
) where
import GHC.TypeLits
import Control.Applicative
import Control.Monad.Except
import Control.Monad.Reader
import Data.Attoparsec.ByteString.Char8
import qualified Data.ByteString as B
import Data.Maybe
import qualified Database.PostgreSQL.LibPQ as P
import Database.PostgreSQL.Store.Entity
import Database.PostgreSQL.Store.Query
import Database.PostgreSQL.Store.RowParser
import Database.PostgreSQL.Store.Tuple
import Database.PostgreSQL.Store.Types
data ErrandError
= NoResult
| UserError String
| ExecError P.ExecStatus ErrorCode B.ByteString B.ByteString B.ByteString
| ParseError RowError
deriving (Show, Eq)
instance Monoid ErrandError where
mempty = UserError "mempty"
mappend _ e = e
data ErrorCode
= UnknownErrorCause B.ByteString
| IntegrityViolation
| RestrictViolation
| NotNullViolation
| ForeignKeyViolation
| UniqueViolation
| CheckViolation
| ExclusionViolation
deriving (Show, Eq)
newtype Errand a = Errand (ReaderT P.Connection (ExceptT ErrandError IO) a)
deriving (Functor, Applicative, Monad, Alternative, MonadIO, MonadError ErrandError)
runErrand :: P.Connection -> Errand a -> IO (Either ErrandError a)
runErrand con (Errand errand) = runExceptT (runReaderT errand con)
validateResult :: P.Result -> Errand ()
validateResult res = do
status <- liftIO (P.resultStatus res)
case status of
P.CommandOk -> pure ()
P.TuplesOk -> pure ()
P.SingleTuple -> pure ()
other -> do
(state, msg, detail, hint) <- liftIO $
(,,,) <$> P.resultErrorField res P.DiagSqlstate
<*> P.resultErrorField res P.DiagMessagePrimary
<*> P.resultErrorField res P.DiagMessageDetail
<*> P.resultErrorField res P.DiagMessageHint
let cause =
case fromMaybe B.empty state of
"23000" -> IntegrityViolation
"23001" -> RestrictViolation
"23502" -> NotNullViolation
"23503" -> ForeignKeyViolation
"23505" -> UniqueViolation
"23514" -> CheckViolation
"23P01" -> ExclusionViolation
code -> UnknownErrorCause code
throwError (ExecError other
cause
(fromMaybe B.empty msg)
(fromMaybe B.empty detail)
(fromMaybe B.empty hint))
countAffectedRows :: P.Result -> Errand Int
countAffectedRows res =
fmap (\ numTuples -> fromMaybe 0 (numTuples >>= maybeResult . endResult . parse decimal))
(liftIO (P.cmdTuples res))
where
endResult (Partial f) = f B.empty
endResult x = x
transformResult :: Maybe P.Result -> Errand P.Result
transformResult = maybe (throwError NoResult) pure
class ErrandQuery q r where
type ErrandResult q r
executeWith :: (P.Result -> Errand r) -> q x -> ErrandResult q r
acceptResult :: IO (Maybe P.Result) -> Errand P.Result
acceptResult action = do
mbRes <- liftIO action
res <- transformResult mbRes
res <$ validateResult res
instance ErrandQuery Statement r where
type ErrandResult Statement r = Errand r
executeWith end (Statement stmt) = do
con <- Errand ask
res <- acceptResult (P.execParams con stmt [] P.Text)
end res
instance ErrandQuery Query r where
type ErrandResult Query r = Errand r
executeWith end (Query stmt params) = do
con <- Errand ask
res <- acceptResult (P.execParams con stmt params P.Text)
end res
instance (WithTuple ts) => ErrandQuery (PrepQuery ts) r where
type ErrandResult (PrepQuery ts) r = Function ts (Errand r)
executeWith end (PrepQuery name _ _ gens) =
withTuple $ \ params -> do
con <- Errand ask
mbRes <- liftIO (P.execPrepared con name (gens params) P.Text)
res <- transformResult mbRes
validateResult res
end res
execute :: (ErrandQuery q P.Result) => q r -> ErrandResult q P.Result
execute = executeWith pure
execute' :: (ErrandQuery q Int) => q r -> ErrandResult q Int
execute' = executeWith countAffectedRows
queryWith :: (ErrandQuery q [r], KnownNat n) => RowParser n r -> q r -> ErrandResult q [r]
queryWith parser =
executeWith $ \ result ->
Errand (lift (withExceptT ParseError (processResultWith result parser)))
query :: (Entity r, ErrandQuery q [r]) => q r -> ErrandResult q [r]
query = queryWith parseEntity
prepare :: PrepQuery a r -> Errand ()
prepare (PrepQuery name stmt oids _) = do
con <- Errand ask
mbRes <- liftIO (P.prepare con name stmt (Just oids))
res <- transformResult mbRes
validateResult res
beginTransaction :: Errand ()
beginTransaction = () <$ execute (Statement "BEGIN")
commitTransaction :: Errand ()
commitTransaction = () <$ execute (Statement "COMMIT")
saveTransaction :: B.ByteString -> Errand ()
saveTransaction name = () <$ execute [pgQuery| SAVEPOINT $(genIdentifier name) |]
rollbackTransaction :: Errand ()
rollbackTransaction = () <$ execute (Statement "ROLLBACK")
rollbackTransactionTo :: B.ByteString -> Errand ()
rollbackTransactionTo name = () <$ execute [pgQuery| ROLLBACK TO $(genIdentifier name) |]
withTransaction :: Errand a -> Errand ()
withTransaction trans = do
beginTransaction
(trans >> commitTransaction) <|> rollbackTransaction