packages feed

pqi-conformance-0.0.1.0: src/library/Pqi/Conformance/Scenario.hs

-- | Reusable scenario fragments shared by the per-operation spec modules.
--
-- These are the recurring building blocks — running a query and observing its
-- result, draining the asynchronous result stream, collecting a @COPY OUT@
-- stream, driving a polling loop to its terminal status, and the handful of
-- well-known type OIDs — factored out so each operation module stays focused on
-- the one operation it covers.
module Pqi.Conformance.Scenario
  ( -- * Running queries
    execScenario,
    execAllScenario,
    observed,

    -- * Asynchronous result collection
    drainResults,
    takeResult,
    takeCommandResults,

    -- * Copy and polling loops
    collectCopyOut,
    pollUntilDone,
    flushUntilDone,

    -- * Transactions
    inTransaction,

    -- * Well-known type OIDs
    boolOid,
    byteaOid,
    int2Oid,
    int4Oid,
    int8Oid,
    textOid,
    float8Oid,
  )
where

import Pqi
  ( CopyOutResult (..),
    FlushStatus (..),
    IsConnection (..),
    PollingStatus (..),
  )
import qualified Pqi as Lq
import Pqi.Conformance.Observation
import Pqi.Conformance.Prelude

-- | Run 'Pqi.exec' and observe its result (if any).
execScenario :: (IsConnection c) => ByteString -> c -> IO (Maybe ResultObservation)
execScenario sql connection = exec connection sql >>= traverse observeResult

-- | Run a sequence of statements with 'Pqi.exec', observing every result.
execAllScenario :: (IsConnection c) => [ByteString] -> c -> IO [Maybe ResultObservation]
execAllScenario sqls connection = traverse (`execScenario` connection) sqls

-- | Run 'Pqi.execParams' and observe its result (if any).
observed ::
  (IsConnection c) =>
  ByteString ->
  [Maybe (Word32, ByteString, Lq.Format)] ->
  Lq.Format ->
  c ->
  IO (Maybe ResultObservation)
observed sql params resultFormat connection =
  execParams connection sql params resultFormat >>= traverse observeResult

-- | Collect and observe results with 'Pqi.getResult' until it reports
-- completion with 'Nothing'.
drainResults :: (IsConnection c) => c -> IO [ResultObservation]
drainResults connection = go []
  where
    go acc =
      getResult connection >>= \case
        Nothing -> pure (reverse acc)
        Just result -> do
          observation <- observeResult result
          go (observation : acc)

-- | One 'Pqi.getResult' step, observed.
takeResult :: (IsConnection c) => c -> IO (Maybe ResultObservation)
takeResult connection = getResult connection >>= traverse observeResult

-- | The results of one pipelined command: its result and the 'Nothing'
-- separator that ends it.
takeCommandResults ::
  (IsConnection c) => c -> IO (Maybe ResultObservation, Maybe ResultObservation)
takeCommandResults connection = do
  result <- takeResult connection
  separator <- takeResult connection
  pure (result, separator)

-- | Collect blocking 'Pqi.getCopyData' outcomes until the stream reports
-- anything other than a row (normally 'CopyOutDone'), including that final
-- outcome.
collectCopyOut :: (IsConnection c) => c -> IO [CopyOutResult]
collectCopyOut connection = go (1000 :: Int) []
  where
    go 0 acc = pure (reverse acc)
    go n acc =
      getCopyData connection False >>= \case
        CopyOutRow row -> go (n - 1) (CopyOutRow row : acc)
        terminal -> pure (reverse (terminal : acc))

-- | Drive a polling loop to its terminal status, spinning with a small delay
-- instead of waiting on the socket. Bails out as failed after ten seconds.
pollUntilDone :: IO PollingStatus -> IO PollingStatus
pollUntilDone poll = go (10000 :: Int)
  where
    go 0 = pure PollingFailed
    go n =
      poll >>= \case
        PollingReading -> threadDelay 1000 >> go (n - 1)
        PollingWriting -> threadDelay 1000 >> go (n - 1)
        terminal -> pure terminal

-- | Drive 'Pqi.flush' to a terminal status, spinning while it reports
-- 'FlushWriting'. Bails out as failed after ten seconds.
flushUntilDone :: IO FlushStatus -> IO FlushStatus
flushUntilDone doFlush = go (10000 :: Int)
  where
    go 0 = pure FlushFailed
    go n =
      doFlush >>= \case
        FlushWriting -> threadDelay 1000 >> go (n - 1)
        terminal -> pure terminal

-- | Run an action between @begin@ and @commit@. Large-object operations must
-- run inside a transaction block.
inTransaction :: (IsConnection c) => c -> IO a -> IO a
inTransaction connection action = do
  _ <- exec connection "begin"
  result <- action
  _ <- exec connection "commit"
  pure result

boolOid :: Word32
boolOid = 16

byteaOid :: Word32
byteaOid = 17

int2Oid :: Word32
int2Oid = 21

int4Oid :: Word32
int4Oid = 23

int8Oid :: Word32
int8Oid = 20

textOid :: Word32
textOid = 25

float8Oid :: Word32
float8Oid = 701