module Database.PostgreSQL.Store.Errand (
ErrandError (..),
ErrorCode (..),
Errand,
runErrand,
execute,
execute',
query,
queryWith,
insert,
insertMany,
deleteAll,
findAll,
create
) where
import Control.Monad.Trans
import Control.Monad.Except
import Control.Monad.Reader
import Data.Proxy
import Data.Maybe
import qualified Data.ByteString as B
import Data.Attoparsec.ByteString.Char8
import qualified Database.PostgreSQL.LibPQ as P
import Database.PostgreSQL.Store.Types
import Database.PostgreSQL.Store.Table
import Database.PostgreSQL.Store.Entity
import Database.PostgreSQL.Store.RowParser
import Database.PostgreSQL.Store.Query.Builder
data ErrandError
= NoResult
| UserError String
| ExecError P.ExecStatus ErrorCode B.ByteString B.ByteString B.ByteString
| ParseError RowError
deriving (Show, Eq)
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, MonadIO, MonadError ErrandError)
runErrand :: P.Connection -> Errand a -> IO (Either ErrandError a)
runErrand con (Errand errand) =
runExceptT (runReaderT errand con)
execute :: Query a -> Errand P.Result
execute (Query statement params) = Errand . ReaderT $ \ con -> do
res <- ExceptT (transformResult <$> P.execParams con statement (map transformParam params) P.Text)
status <- lift (P.resultStatus res)
case status of
P.CommandOk -> pure res
P.TuplesOk -> pure res
P.SingleTuple -> pure res
other -> do
(state, msg, detail, hint) <- lift $
(,,,) <$> 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))
where
transformResult =
maybe (throwError NoResult) pure
transformParam (TypedValue typ mbValue) =
(\ (Value value) -> (typ, value, P.Text)) <$> mbValue
execute' :: Query a -> Errand Int
execute' =
countAffectedRows <=< execute
query :: (Entity a) => Query a -> Errand [a]
query qry =
queryWith qry parseEntity
queryWith :: Query a -> RowParser a -> Errand [a]
queryWith qry parser = do
result <- execute qry
Errand (lift (withExceptT ParseError (parseResult result parser)))
countAffectedRows :: P.Result -> Errand Int
countAffectedRows res = do
fmap (\ numTuples -> fromMaybe 0 (numTuples >>= maybeResult . endResult . parse decimal))
(liftIO (P.cmdTuples res))
where
endResult (Partial f) = f B.empty
endResult x = x
insert :: (TableEntity a) => a -> Errand Bool
insert row = do
fmap (> 0) . execute' $ buildQuery $ do
insertCode "INSERT INTO "
insertName name
insertCode "("
insertCommaSeperated (map (\ (Column colName _) -> insertName colName) cols)
insertCode ") VALUES ("
insertEntity row
insertCode ")"
where
Table name cols =
describeTableType ((const Proxy :: a -> Proxy a) row)
insertMany :: (TableEntity a) => [a] -> Errand Int
insertMany [] = pure 0
insertMany rows =
execute' $ buildQuery $ do
insertCode "INSERT INTO "
insertName name
insertCode "("
insertCommaSeperated (map (\ (Column colName _) -> insertName colName) cols)
insertCode ") VALUES "
insertCommaSeperated (map insertRowValue rows)
where
Table name cols =
describeTableType ((const Proxy :: [a] -> Proxy a) rows)
insertRowValue row = do
insertCode "("
insertEntity row
insertCode ")"
deleteAll :: (TableEntity a) => proxy a -> Errand Int
deleteAll proxy =
execute' $ buildQuery $ do
insertCode "DELETE FROM "
insertName (tableName (describeTableType proxy))
findAll :: (TableEntity a) => Errand [a]
findAll =
query (findAllQuery Proxy)
where
findAllQuery :: (TableEntity a) => Proxy a -> Query a
findAllQuery proxy =
buildQuery $ do
insertCode "SELECT "
insertColumns (describeTableType proxy)
insertCode " FROM "
insertName (tableName (describeTableType proxy))
create :: (TableEntity a) => proxy a -> Errand ()
create proxy =
() <$ execute (buildQuery (buildTableSchema (describeTableType proxy)))