orville-postgresql-1.1.0.0: src/Orville/PostgreSQL/Execution/Cursor.hs
{-# LANGUAGE GADTs #-}
{- |
Copyright : Flipstone Technology Partners 2023
License : MIT
Stability : Stable
Functions and types for working with PostgreSQL cursors. You can use cursors to
execute a query and consume rows from the result set incrementally. Rows that
you do not consume will never be sent from the database to the client.
@since 1.0.0.0
-}
module Orville.PostgreSQL.Execution.Cursor
( Cursor
, withCursor
, declareCursor
, closeCursor
, fetch
, move
, Expr.CursorDirection
, Expr.next
, Expr.prior
, Expr.first
, Expr.last
, Expr.absolute
, Expr.relative
, Expr.count
, Expr.fetchAll
, Expr.forward
, Expr.forwardCount
, Expr.forwardAll
, Expr.backward
, Expr.backwardCount
, Expr.backwardAll
)
where
import Control.Monad.IO.Class (MonadIO (liftIO))
import qualified Data.Time as Time
import qualified Data.Time.Clock.POSIX as POSIXTime
import qualified Data.Word as Word
import qualified System.Random as Random
import qualified Text.Printf as Printf
import qualified Orville.PostgreSQL.Execution.Execute as Execute
import qualified Orville.PostgreSQL.Execution.QueryType as QueryType
import Orville.PostgreSQL.Execution.Select (Select, useSelect)
import qualified Orville.PostgreSQL.Expr as Expr
import qualified Orville.PostgreSQL.Internal.Bracket as Bracket
import Orville.PostgreSQL.Marshall (AnnotatedSqlMarshaller)
import qualified Orville.PostgreSQL.Monad as Monad
{- | A 'Cursor' allows you to fetch rows incrementally from PostgreSQL. Using
a cursor will allow you to execute a query that returns a very large
result set without the entire result set being loaded in memory in your
application and instead pulling rows as you're able to process them.
See 'withCursor', 'fetch' and 'move' for details on creating and using
'Cursor' values.
@since 1.0.0.0
-}
data Cursor readEntity where
Cursor ::
AnnotatedSqlMarshaller writeEntity readEntity ->
Expr.CursorName ->
Cursor readEntity
{- | Declares a @CURSOR@ in PostgreSQL that is available for the duration of the
action passed to 'withCursor' and closes the cursor when that action
completes (or raises an exception).
See @https://www.postgresql.org/docs/current/sql-declare.html@ for details
about the 'Expr.ScrollExpr' and 'Expr.HoldExpr' parameters and how cursors
behave in general.
We recommend you use this instead of 'declareCursor' and 'closeCursor'
unless you need to control the cursor resource acquisition and release
yourself and can do so safely.
@since 1.0.0.0
-}
withCursor ::
Monad.MonadOrville m =>
Maybe Expr.ScrollExpr ->
Maybe Expr.HoldExpr ->
Select readEntity ->
(Cursor readEntity -> m a) ->
m a
withCursor scrollExpr holdExpr select =
Bracket.bracketWithResult
(declareCursor scrollExpr holdExpr select)
(\cursor _bracketResult -> closeCursor cursor)
{- | Declares a @CURSOR@ in PostgreSQL and returns it for you to use. The cursor
must be closed via 'closeCursor' (or another means) when you are done using
it. Generally you should use 'withCursor' instead of 'declareCursor' to
ensure that the cursor gets closed properly.
See @https://www.postgresql.org/docs/current/sql-declare.html@ for details
about the 'Expr.ScrollExpr' and 'Expr.HoldExpr' parameters and how cursors
behave in general.
@since 1.0.0.0
-}
declareCursor ::
Monad.MonadOrville m =>
Maybe Expr.ScrollExpr ->
Maybe Expr.HoldExpr ->
Select readEntity ->
m (Cursor readEntity)
declareCursor scrollExpr holdExpr =
useSelect $ \queryExpr marshaller -> do
cursorName <- newCursorName
let
declareExpr =
Expr.declare cursorName scrollExpr holdExpr queryExpr
_ <- Execute.executeVoid QueryType.CursorQuery declareExpr
pure (Cursor marshaller cursorName)
{- | Closes a @CURSOR@ in PostgreSQL that was previously declared.
This should be used to close any cursors you open via 'declareCursor',
though we recommend you use 'withCursor' instead to ensure that any
opened cursors are closed in the event of an exception.
@since 1.0.0.0
-}
closeCursor ::
Monad.MonadOrville m =>
Cursor readEntity ->
m ()
closeCursor (Cursor _ cursorName) =
Execute.executeVoid QueryType.CursorQuery
. Expr.close
. Right
$ cursorName
{- | Fetch rows from a cursor according to the 'Expr.CursorDirection' given. See
@https://www.postgresql.org/docs/current/sql-fetch.html@ for details about
the effects of fetch and the meanings of cursor directions to PostgreSQL.
@since 1.0.0.0
-}
fetch ::
Monad.MonadOrville m =>
Maybe Expr.CursorDirection ->
Cursor readEntity ->
m [readEntity]
fetch direction (Cursor marshaller cursorName) =
Execute.executeAndDecode
QueryType.CursorQuery
(Expr.fetch direction cursorName)
marshaller
{- | Moves a cursor according to the 'Expr.CursorDirection' given. See
@https://www.postgresql.org/docs/current/sql-fetch.html@ for details about
the effect of move and the meanings of cursor directions to PostgreSQL.
@since 1.0.0.0
-}
move ::
Monad.MonadOrville m =>
Maybe Expr.CursorDirection ->
Cursor readEntity ->
m ()
move direction (Cursor _ cursorName) =
Execute.executeVoid
QueryType.CursorQuery
(Expr.move direction cursorName)
{- | INTERNAL - Generates a unique (or very nearly guaranteed to be) cursor name.
Cursor names only need to be unique among the currently-open cursors on the
current connection, so using POSIX time plus a 32-bit random tag should be
more than sufficient to ensure conflicts are not seen in practice.
@since 1.0.0.0
-}
newCursorName :: MonadIO m => m Expr.CursorName
newCursorName =
liftIO $ do
now <- POSIXTime.getPOSIXTime
randomWord <- Random.randomIO
let
nowAsInteger =
fromEnum . Time.nominalDiffTimeToSeconds $ now
pure . Expr.cursorName $
Printf.printf
"orville_cursor_%x_%08x"
nowAsInteger
(randomWord :: Word.Word32)