packages feed

orville-postgresql-1.0.0.0: src/Orville/PostgreSQL/Execution/ExecutionResult.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

{- |
Copyright : Flipstone Technology Partners 2023
License   : MIT
Stability : Stable

@since 1.0.0.0
-}
module Orville.PostgreSQL.Execution.ExecutionResult
  ( ExecutionResult (..)
  , Column (..)
  , Row (..)
  , readRows
  , FakeLibPQResult
  , mkFakeLibPQResult
  )
where

import qualified Data.ByteString as BS
import qualified Data.Map.Strict as Map
import qualified Data.Maybe as Maybe
import qualified Database.PostgreSQL.LibPQ as LibPQ

import Orville.PostgreSQL.Raw.SqlValue (SqlValue)
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue

{- |
  A trivial wrapper for `Int` to help keep track of column vs row number.

@since 1.0.0.0
-}
newtype Column
  = Column Int
  deriving
    ( -- | @since 1.0.0.0
      Eq
    , -- | @since 1.0.0.0
      Ord
    , -- | @since 1.0.0.0
      Enum
    , -- | @since 1.0.0.0
      Num
    )

{- |
  A trivial wrapper for `Int` to help keep track of column vs row number.

@since 1.0.0.0
-}
newtype Row
  = Row Int
  deriving
    ( -- | @since 1.0.0.0
      Eq
    , -- | @since 1.0.0.0
      Ord
    , -- | @since 1.0.0.0
      Enum
    , -- | @since 1.0.0.0
      Num
    )

{- |
  'ExecutionResult' is a common interface for types that represent a result set
  returned from the database. For real, live database interactions, the
  concrete type will be a 'LibPQ.Result', but the 'FakeLibPQResult' may be
  useful as well if you are writing custom code for decoding result sets and
  want to test aspects of the decoding that don't require a real database.

@since 1.0.0.0
-}
class ExecutionResult result where
  maxRowNumber :: result -> IO (Maybe Row)
  maxColumnNumber :: result -> IO (Maybe Column)
  columnName :: result -> Column -> IO (Maybe BS.ByteString)
  getValue :: result -> Row -> Column -> IO SqlValue

{- |
  Reads the rows of an 'ExecutionResult' as a list of column name, 'SqlValue'
  pairs. You're almost always better off using a
  'Orville.PostgreSQL.SqlMarshaller' instead, but this function is provided for
  cases where you really want to decode the rows yourself but don't want to use
  the 'ExecutionResult' API to read each row of each column directly.

@since 1.0.0.0
-}
readRows ::
  ExecutionResult result =>
  result ->
  IO [[(Maybe BS.ByteString, SqlValue)]]
readRows res = do
  mbMaxRow <- maxRowNumber res
  mbMaxColumn <- maxColumnNumber res

  let
    rowIndices =
      case mbMaxRow of
        Nothing -> []
        Just maxRow -> [0 .. maxRow]

    columnIndices =
      case mbMaxColumn of
        Nothing -> []
        Just maxColumn -> [0 .. maxColumn]

    readValue rowIndex columnIndex = do
      name <- columnName res columnIndex
      value <- getValue res rowIndex columnIndex
      pure $ (name, value)

    readRow rowIndex =
      traverse (readValue rowIndex) columnIndices

  traverse readRow rowIndices

-- | @since 1.0.0.0
instance ExecutionResult LibPQ.Result where
  maxRowNumber result = do
    rowCount <- fmap fromEnum (LibPQ.ntuples result)
    pure $
      if rowCount > 0
        then Just $ Row (rowCount - 1)
        else Nothing

  maxColumnNumber result = do
    columnCount <- fmap fromEnum (LibPQ.nfields result)
    pure $
      if columnCount > 0
        then Just $ Column (columnCount - 1)
        else Nothing

  columnName result =
    LibPQ.fname result . LibPQ.toColumn . fromEnum

  getValue result (Row row) (Column column) =
    -- N.B. the usage of `getvalue'` here is important as this version returns a
    -- _copy_ of the data in the 'Result' rather than a _reference_.
    -- This allows the 'Result' to be garbage collected instead of being held onto indefinitely.
    SqlValue.fromRawBytesNullable
      <$> LibPQ.getvalue' result (LibPQ.toRow row) (LibPQ.toColumn column)

{- |
  `FakeLibPQResult` provides a fake, in-memory implementation of
  `ExecutionResult`.  This is mostly useful for writing automated tests that
  can assume a result set has been loaded and you just need to test decoding
  the results.

@since 1.0.0.0
-}
data FakeLibPQResult = FakeLibPQResult
  { fakeLibPQColumns :: Map.Map Column BS.ByteString
  , fakeLibPQRows :: Map.Map Row (Map.Map Column SqlValue)
  }

-- | @since 1.0.0.0
instance ExecutionResult FakeLibPQResult where
  maxRowNumber = pure . fakeLibPQMaxRow
  maxColumnNumber = pure . fakeLibPQMaxColumn
  columnName result = pure . fakeLibPQColumnName result
  getValue result column = pure . fakeLibPQGetValue result column

{- |
  Constructs a `FakeLibPQResult`. The column names given are associated with
  the values for each row by their position in-list. Any missing values (e.g.
  because a row is shorter than the header list) are treated as a SQL Null
  value.

@since 1.0.0.0
-}
mkFakeLibPQResult ::
  -- | The column names for the result set
  [BS.ByteString] ->
  -- | The row data for the result set
  [[SqlValue]] ->
  FakeLibPQResult
mkFakeLibPQResult columnList valuesList =
  let
    indexedRows = do
      (rowNumber, row) <- zip [Row 0 ..] valuesList

      let
        indexedColumns = zip [Column 0 ..] row

      pure (rowNumber, Map.fromList indexedColumns)
  in
    FakeLibPQResult
      { fakeLibPQColumns = Map.fromList (zip [Column 0 ..] columnList)
      , fakeLibPQRows = Map.fromList indexedRows
      }

fakeLibPQMaxRow :: FakeLibPQResult -> Maybe Row
fakeLibPQMaxRow =
  fmap fst . Map.lookupMax . fakeLibPQRows

fakeLibPQMaxColumn :: FakeLibPQResult -> Maybe Column
fakeLibPQMaxColumn result =
  let
    maxColumnsByRow =
      map fst
        . Maybe.mapMaybe Map.lookupMax
        . Map.elems
        . fakeLibPQRows
        $ result
  in
    case maxColumnsByRow of
      [] ->
        Nothing
      _ ->
        Just (maximum maxColumnsByRow)

fakeLibPQColumnName :: FakeLibPQResult -> Column -> (Maybe BS.ByteString)
fakeLibPQColumnName result column =
  Map.lookup column (fakeLibPQColumns result)

fakeLibPQGetValue :: FakeLibPQResult -> Row -> Column -> SqlValue
fakeLibPQGetValue result rowNumber columnNumber =
  Maybe.fromMaybe SqlValue.sqlNull $ do
    row <- Map.lookup rowNumber (fakeLibPQRows result)
    Map.lookup columnNumber row