packages feed

psql-0.0.0: lib/PostgreSQL/Result.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RankNTypes #-}

-- | This module is dedicated to dealing with query results.
module PostgreSQL.Result
  ( Result
  , runResultPq

    -- * Combinators
  , ignored
  , single
  , first
  , many
  , affectedRows

    -- * Validation
  , checkForError
  )
where

import           Control.Monad (when)
import qualified Control.Monad.Except as Except
import           Control.Monad.IO.Class (MonadIO, liftIO)
import qualified Data.Bifunctor as Bifunctor
import qualified Data.ByteString as ByteString
import           Data.Foldable (for_)
import           Data.Maybe (fromMaybe)
import qualified Data.Text as Text
import           Data.Text.Encoding (decodeUtf8')
import qualified Data.Vector as Vector
import qualified Database.PostgreSQL.LibPQ as PQ
import qualified PostgreSQL.Result.Row as Row
import qualified PostgreSQL.Types as Types
import           Text.Read (readEither)

data ResultF a where
  IgnoreResult :: ResultF ()

  SingleRow :: Row.Row a -> ResultF a

  FirstRow :: Row.Row a -> ResultF a

  ManyRows :: Row.Row a -> ResultF (Vector.Vector a)

  AffectedRows :: ResultF Integer

-- | Query result
--
-- @since 0.0.0
data Result a where
  Result :: (b -> a) -> ResultF b -> Result a

-- | @since 0.0.0
instance Functor Result where
  fmap f (Result g inner) = Result (f . g) inner

  {-# INLINE fmap #-}

-- | Process libpq's 'PQ.Result'.
--
-- @since 0.0.0
runResultPq :: MonadIO m => PQ.Result -> Result a -> m (Either Types.Errors a)
runResultPq result (Result f basis) = Except.runExceptT $ fmap f $
  case basis of
    IgnoreResult ->
      pure ()

    SingleRow row -> do
      rows <- liftIO (PQ.ntuples result)
      when (rows /= 1) $
        Except.throwError [Types.ErrorDuringValidation $ Types.MultipleRows $ Types.RowNum rows]
      runRow <- importErrors (Row.runRowPq result row)
      importErrors (runRow 0)

    FirstRow row -> do
      rows <- liftIO (PQ.ntuples result)
      when (rows < 1) $ Except.throwError [Types.ErrorDuringValidation Types.NoRows]
      runRow <- importErrors (Row.runRowPq result row)
      importErrors (runRow 0)

    ManyRows row -> do
      runRow <- importErrors (Row.runRowPq result row)
      PQ.Row rows <- liftIO (PQ.ntuples result)
      Vector.generateM (fromIntegral rows) (importErrors . runRow . fromIntegral)

    AffectedRows -> do
      tuples <- liftIO (PQ.cmdTuples result)
      case tuples of
        Nothing -> pure 0
        Just tuples ->
          Except.liftEither
          $ Bifunctor.first
            (pure . Types.ErrorDuringValidation . Types.FailedToParseAffectedRows . Text.pack)
          $ do
            tuples <- Bifunctor.first show (decodeUtf8' tuples)
            readEither (Text.unpack tuples)
  where
    importErrors = Except.withExceptT (fmap Types.ErrorDuringProcessing)


-- | Ignore the result set.
--
-- @since 0.0.0
ignored :: Result ()
ignored = Result id IgnoreResult

-- | Process exactly 1 row.
--
-- @since 0.0.0
single :: Row.Row a -> Result a
single row = Result id (SingleRow row)

-- | Process only the first row. There may be more rows in the result set, but they won't be
-- touched.
--
-- @since 0.0.0
first :: Row.Row a -> Result a
first row = Result id (FirstRow row)

-- | Process 0 or more rows.
--
-- @since 0.0.0
many :: Row.Row a -> Result (Vector.Vector a)
many row = Result id (ManyRows row)


-- | Get the number of affected rows.
--
-- @since 0.0.0
affectedRows :: Result Integer
affectedRows = Result id AffectedRows

---

-- | Check the result, if any, and the connection for errors.
--
-- @since 0.0.0
checkForError
  :: (MonadIO m, Except.MonadError Types.ResultErrors m)
  => Types.Connection
  -> Maybe PQ.Result
  -> m PQ.Result
checkForError conn mbResult = do
  result <-
    case mbResult of
      Nothing -> do
        connError <- liftIO $ PQ.errorMessage conn
        Except.throwError [Types.BadResultStatus (fromMaybe mempty connError)]

      Just result -> pure result

  resultError <- liftIO $ PQ.resultErrorMessage result
  for_ resultError $ \error ->
    when (ByteString.length error > 0) $
      Except.throwError [Types.BadResultStatus error]

  pure result

{-# INLINE checkForError #-}