packages feed

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

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

Low-level functions for executing 'RawSql.SqlExpression' values directly.

@since 1.0.0.0
-}
module Orville.PostgreSQL.Execution.Execute
  ( executeAndDecode
  , executeAndReturnAffectedRows
  , executeVoid
  , executeAndDecodeIO
  , executeAndReturnAffectedRowsIO
  , executeVoidIO
  , AffectedRowsDecodingError
  )
where

import Control.Exception (Exception, throwIO)
import Control.Monad (void)
import Control.Monad.IO.Class (liftIO)
import qualified Database.PostgreSQL.LibPQ as LibPQ

import Orville.PostgreSQL.Execution.QueryType (QueryType)
import qualified Orville.PostgreSQL.Marshall.SqlMarshaller as SqlMarshaller
import Orville.PostgreSQL.Monad (MonadOrville, askOrvilleState, withConnection)
import Orville.PostgreSQL.OrvilleState (OrvilleState, orvilleErrorDetailLevel, orvilleSqlCommenterAttributes, orvilleSqlExecutionCallback)
import Orville.PostgreSQL.Raw.Connection (Connection)
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
import qualified Orville.PostgreSQL.Raw.SqlCommenter as SqlCommenter
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue

{- |
  Executes a SQL query and decodes the result set using the provided
  marshaller. Any SQL Execution callbacks that have been added to the
  'OrvilleState' will be called.

  If the query fails or if any row is unable to be decoded by the marshaller,
  an exception will be raised.

@since 1.0.0.0
-}
executeAndDecode ::
  (MonadOrville m, RawSql.SqlExpression sql) =>
  QueryType ->
  sql ->
  SqlMarshaller.AnnotatedSqlMarshaller writeEntity readEntity ->
  m [readEntity]
executeAndDecode queryType sql marshaller = do
  orvilleState <- askOrvilleState
  withConnection (liftIO . executeAndDecodeIO queryType sql marshaller orvilleState)

{- |
  Executes a SQL query and returns the number of rows affected by the query.
  Any SQL Execution callbacks that have been added to the 'OrvilleState' will
  be called.

  This function can only be used for the execution of a SELECT, CREATE
  TABLE AS, INSERT, UPDATE, DELETE, MOVE, FETCH, or COPY statement, or an
  EXECUTE of a prepared query that contains an INSERT, UPDATE, or DELETE
  statement. If the query is anything else, an 'AffectedRowsDecodingError'
  wil be raised after the query is executed when the result is read.

  If the query fails, an exception will be raised.

@since 1.0.0.0
-}
executeAndReturnAffectedRows ::
  (MonadOrville m, RawSql.SqlExpression sql) =>
  QueryType ->
  sql ->
  m Int
executeAndReturnAffectedRows queryType sql = do
  orvilleState <- askOrvilleState
  withConnection (liftIO . executeAndReturnAffectedRowsIO queryType sql orvilleState)

{- |
  Executes a SQL query and ignores the result. Any SQL Execution callbacks
  that have been added to the 'OrvilleState' will be called.

  If the query fails an exception will be raised.

@since 1.0.0.0
-}
executeVoid ::
  (MonadOrville m, RawSql.SqlExpression sql) =>
  QueryType ->
  sql ->
  m ()
executeVoid queryType sql = do
  orvilleState <- askOrvilleState
  withConnection (liftIO . executeVoidIO queryType sql orvilleState)

{- |
  Executes a SQL query and decodes the result set using the provided
  marshaller. Any SQL Execution callbacks that have been added to the
  'OrvilleState' will be called.

  If the query fails or if any row is unable to be decoded by the marshaller,
  an exception will be raised.

@since 1.0.0.0
-}
executeAndDecodeIO ::
  RawSql.SqlExpression sql =>
  QueryType ->
  sql ->
  SqlMarshaller.AnnotatedSqlMarshaller writeEntity readEntity ->
  OrvilleState ->
  Connection ->
  IO [readEntity]
executeAndDecodeIO queryType sql marshaller orvilleState conn = do
  libPqResult <- executeWithCallbacksIO queryType sql orvilleState conn

  let
    errorDetailLevel = orvilleErrorDetailLevel orvilleState

  decodingResult <-
    SqlMarshaller.marshallResultFromSql
      errorDetailLevel
      marshaller
      libPqResult

  case decodingResult of
    Left err ->
      throwIO err
    Right entities ->
      pure entities

{- |
  Executes a SQL query and returns the number of rows affected by the query.
  Any SQL Execution callbacks that have been added to the 'OrvilleState' will
  be called.

  This function can only be used for the execution of a SELECT, CREATE
  TABLE AS, INSERT, UPDATE, DELETE, MOVE, FETCH, or COPY statement, or an
  EXECUTE of a prepared query that contains an INSERT, UPDATE, or DELETE
  statement. If the query is anything else, an 'AffectedRowsDecodingError'
  wil be raised after the query is executed when the result is read.

  If the query fails, an exception will be raised.

@since 1.0.0.0
-}
executeAndReturnAffectedRowsIO ::
  RawSql.SqlExpression sql =>
  QueryType ->
  sql ->
  OrvilleState ->
  Connection ->
  IO Int
executeAndReturnAffectedRowsIO queryType sql orvilleState conn = do
  libPqResult <- executeWithCallbacksIO queryType sql orvilleState conn
  mbTupleCount <- LibPQ.cmdTuples libPqResult
  case mbTupleCount of
    Nothing ->
      throwIO
        . AffectedRowsDecodingError
        $ "No affected row count was produced by the query"
    Just bs ->
      case SqlValue.toInt (SqlValue.fromRawBytes bs) of
        Left err ->
          throwIO . AffectedRowsDecodingError $ err
        Right n ->
          pure n

{- |
  Executes a SQL query and ignores the result. Any SQL Execution callbacks
  that have been added to the 'OrvilleState' will be called.

  If the query fails, an exception will be raised.

@since 1.0.0.0
-}
executeVoidIO ::
  RawSql.SqlExpression sql =>
  QueryType ->
  sql ->
  OrvilleState ->
  Connection ->
  IO ()
executeVoidIO queryType sql orvilleState =
  void . executeWithCallbacksIO queryType sql orvilleState

executeWithCallbacksIO ::
  RawSql.SqlExpression sql =>
  QueryType ->
  sql ->
  OrvilleState ->
  Connection ->
  IO LibPQ.Result
executeWithCallbacksIO queryType sql orvilleState conn =
  let
    rawSql =
      case orvilleSqlCommenterAttributes orvilleState of
        Nothing ->
          RawSql.toRawSql sql
        Just sqlCommenterAttributes ->
          SqlCommenter.addSqlCommenterAttributes sqlCommenterAttributes $ RawSql.toRawSql sql
  in
    orvilleSqlExecutionCallback
      orvilleState
      queryType
      rawSql
      (RawSql.execute conn rawSql)

{- |
  Thrown by 'executeAndReturnAffectedRows' and 'executeAndReturnAffectedRowsIO'
  if the number of affected rows cannot be successfully read from the LibPQ
  command result.

@since 1.0.0.0
-}
newtype AffectedRowsDecodingError
  = AffectedRowsDecodingError String
  deriving
    ( -- | @since 1.0.0.0
      Show
    )

-- | @since 1.0.0.0
instance Exception AffectedRowsDecodingError