packages feed

valiant-0.1.0.0: src/Valiant/Fold.hs

{-# OPTIONS_GHC -fno-full-laziness #-}

-- | Constant-memory row processing via strict folds.
--
-- Process large result sets without buffering all rows in memory
-- and without requiring a transaction or cursor.
--
-- Folds require exclusive access to the connection's wire so that rows
-- can be processed one-at-a-time directly from the socket.
--
-- @
-- -- Count and sum in one pass, constant memory:
-- (count, total) <- executeWithFold conn stmt params $
--   RowFold (0, 0) (\\(!c, !t) (_, _, score) -> (c + 1, t + score))
-- @
module Valiant.Fold
  ( RowFold (..)
  , executeWithFold
  ) where

import Control.Exception (SomeException, catch, throwIO)
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as BS8
import Data.IORef
import Data.Vector (Vector)
import Data.Vector qualified as V
import PgWire.Async (submitExclusive)
import PgWire.Connection (Connection (..))
import PgWire.Error (PgWireError (..), throwPgWire)
import PgWire.Protocol.Backend
import PgWire.Protocol.Frontend
import PgWire.Wire (WireConn, recvBackendMsg, sendFrontendMsgs)
import Valiant.Execute (ensurePrepared)
import Valiant.Statement (Statement (..))

-- | A strict left fold over result rows. Processes rows in constant
-- memory as they arrive from the wire — no list, no buffering.
data RowFold a b = RowFold
  { foldInit :: !b
  -- ^ Initial accumulator value.
  , foldStep :: !(b -> a -> b)
  -- ^ Strict step function. Apply bang patterns to accumulator components.
  }

-- | Execute a statement and fold over the results in constant memory.
--
-- Each row is decoded and fed to the fold as it arrives from the wire.
-- The entire result set is never held in memory.
--
-- @
-- total <- executeWithFold conn countStmt () $
--   RowFold 0 (\\acc (Only n) -> acc + n)
-- @
executeWithFold :: Connection -> Statement p r -> p -> RowFold r b -> IO b
executeWithFold conn stmt params (RowFold z0 step) = do
  stmtName <- ensurePrepared conn stmt
  let encodedParams = stmtEncode stmt params
  submitExclusive (connAsync conn) $ \wc txRef -> do
    sendFrontendMsgs wc
      [ Bind "" stmtName (V.singleton BinaryFormat) encodedParams (V.singleton BinaryFormat)
      , Execute "" 0
      , Sync
      ]
    collectFold wc txRef (stmtDecode stmt) z0 step
      `catch` \(e :: SomeException) -> do
        -- Drain remaining messages up to ReadyForQuery so the connection
        -- is left in a clean state after a fold exception (e.g., decode
        -- error or user step function throwing).
        drainUntilReady wc txRef
          `catch` \(_ :: SomeException) -> pure ()
        throwIO e

collectFold :: WireConn -> IORef TxStatus -> (Vector (Maybe ByteString) -> Either String r) -> b -> (b -> r -> b) -> IO b
collectFold wc txRef decode = go
  where
    go !acc step' = do
      msg <- recvBackendMsg wc
      case msg of
        BindComplete -> go acc step'
        DataRow vals -> case decode vals of
          Left err -> throwPgWire (DecodeError (BS8.pack err))
          Right !val -> go (step' acc val) step'
        CommandComplete _ -> go acc step'
        EmptyQueryResponse -> go acc step'
        ReadyForQuery status -> do
          writeIORef txRef status
          pure acc
        ErrorResponse err -> throwPgWire (QueryError err)
        NoticeResponse _ -> go acc step'
        other -> throwPgWire (ProtocolError ("Unexpected in fold: " <> BS8.pack (show other)))

-- | Drain messages until ReadyForQuery, updating transaction status.
-- Used for cleanup after fold exceptions.
drainUntilReady :: WireConn -> IORef TxStatus -> IO ()
drainUntilReady wc txRef = do
  msg <- recvBackendMsg wc
  case msg of
    ReadyForQuery status -> writeIORef txRef status
    _ -> drainUntilReady wc txRef