packages feed

orville-postgresql-1.0.0.0: test/Test/Expr/Cursor.hs

module Test.Expr.Cursor
  ( cursorTests
  )
where

import qualified Control.Exception.Safe as ExSafe
import qualified Control.Monad.IO.Class as MIO
import qualified Data.ByteString.Char8 as B8
import qualified Data.Int as Int
import Hedgehog ((===))
import qualified Hedgehog as HH

import qualified Orville.PostgreSQL as Orville
import qualified Orville.PostgreSQL.Execution as Execution
import qualified Orville.PostgreSQL.Expr as Expr
import qualified Orville.PostgreSQL.Raw.Connection as Conn
import qualified Orville.PostgreSQL.Raw.RawSql as RawSql
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue

import Test.Expr.TestSchema (FooBar, assertEqualFooBarRows, findAllFooBars, mkFooBar, withFooBarData)
import qualified Test.Property as Property

cursorTests :: Orville.ConnectionPool -> Property.Group
cursorTests pool =
  Property.group
    "Expr - Cursor"
    [ prop_cursorInTransaction pool
    , prop_cursorOutsideTransactionWithHold pool
    , prop_cursorCloseAll pool
    , prop_cursorMove pool
    , prop_cursorNoScroll pool
    , prop_cursorFetchAll pool
    , prop_cursorFetchRowCount pool
    , prop_cursorFetchForward pool
    , prop_cursorFetchForwardCount pool
    , prop_cursorFetchForwardAll pool
    , prop_cursorFetchBackward pool
    , prop_cursorFetchBackwardCount pool
    , prop_cursorFetchBackwardAll pool
    , prop_cursorFetchFirstLast pool
    , prop_cursorFetchNextPrior pool
    , prop_cursorFetchAbsolute pool
    , prop_cursorFetchRelative pool
    ]

prop_cursorInTransaction :: Property.NamedDBProperty
prop_cursorInTransaction =
  Property.singletonNamedDBProperty "In transaction" $ \pool -> do
    result <-
      withFooBarData pool [row 1, row 2] $ \connection ->
        withTestTransaction connection $
          withTestCursor connection Nothing Nothing findAllFooBars $ \cursorName -> do
            result <- RawSql.execute connection $ Expr.fetch Nothing cursorName
            Execution.readRows result

    assertEqualFooBarRows result [row 1]

prop_cursorOutsideTransactionWithHold :: Property.NamedDBProperty
prop_cursorOutsideTransactionWithHold =
  Property.singletonNamedDBProperty "Outside transaction (with hold)" $ \pool -> do
    result <-
      withFooBarData pool [row 1, row 2] $ \connection ->
        withTestCursor connection Nothing (Just Expr.withHold) findAllFooBars $ \cursorName -> do
          result <- RawSql.execute connection $ Expr.fetch Nothing cursorName
          Execution.readRows result

    assertEqualFooBarRows result [row 1]

prop_cursorCloseAll :: Property.NamedDBProperty
prop_cursorCloseAll =
  Property.singletonNamedDBProperty "Close all cursors" $ \pool -> do
    MIO.liftIO . Conn.withPoolConnection pool $ \connection -> do
      let
        cursorName :: Expr.CursorName
        cursorName = Expr.fromIdentifier $ Expr.identifier "testcursor"

        declare =
          RawSql.executeVoid connection
            . Expr.declare cursorName Nothing (Just Expr.withHold)
            $ RawSql.unsafeSqlExpression "SELECT 1"

        close =
          RawSql.executeVoid connection $ Expr.close (Left Expr.allCursors)

      -- As long as close doesn't raise an exception, the test passes
      ExSafe.bracket_ declare close (pure ())

prop_cursorMove :: Property.NamedDBProperty
prop_cursorMove =
  Property.singletonNamedDBProperty "Move" $ \pool -> do
    result <-
      withFooBarData pool [row 1, row 2, row 3] $ \connection ->
        withTestCursor connection Nothing (Just Expr.withHold) findAllFooBars $ \cursorName -> do
          RawSql.executeVoid connection $ Expr.move (Just $ Expr.rowCount 2) cursorName
          result <- RawSql.execute connection $ Expr.fetch Nothing cursorName
          Execution.readRows result

    assertEqualFooBarRows result [row 3]

prop_cursorNoScroll :: Property.NamedDBProperty
prop_cursorNoScroll =
  Property.singletonNamedDBProperty "Move" $ \pool -> do
    scrollBackResult <-
      withFooBarData pool [row 1, row 2] $ \connection ->
        withTestCursor connection (Just Expr.noScroll) (Just Expr.withHold) findAllFooBars $ \cursorName -> do
          RawSql.executeVoid connection $ Expr.move (Just Expr.next) cursorName
          ExSafe.try $ RawSql.executeVoid connection $ Expr.move (Just Expr.prior) cursorName

    case scrollBackResult of
      Right () -> do
        HH.footnote "Expected 'executeVoid' to return failure, but it did not"
        HH.failure
      Left err ->
        -- Expected that the execute failed because we tried to scroll backward
        -- on a non-scrollable cursor
        Conn.sqlExecutionErrorSqlState err === Just (B8.pack "55000")

prop_cursorFetchAll :: Property.NamedDBProperty
prop_cursorFetchAll =
  Property.singletonNamedDBProperty "Fetch all" $ \pool -> do
    [first] <-
      runFetchDirectionsOnData
        pool
        Nothing
        [row 1, row 2]
        [Expr.fetchAll]

    assertEqualFooBarRows first [row 1, row 2]

prop_cursorFetchRowCount :: Property.NamedDBProperty
prop_cursorFetchRowCount =
  Property.singletonNamedDBProperty "Fetch row count" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        Nothing
        [row 1, row 2, row 3]
        [Expr.rowCount 2, Expr.rowCount 2]

    assertEqualFooBarRows first [row 1, row 2]
    assertEqualFooBarRows second [row 3]

prop_cursorFetchForward :: Property.NamedDBProperty
prop_cursorFetchForward =
  Property.singletonNamedDBProperty "Fetch forward" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        Nothing
        [row 1, row 2]
        [Expr.forward, Expr.forward]

    assertEqualFooBarRows first [row 1]
    assertEqualFooBarRows second [row 2]

prop_cursorFetchForwardCount :: Property.NamedDBProperty
prop_cursorFetchForwardCount =
  Property.singletonNamedDBProperty "Fetch forward count" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        Nothing
        [row 1, row 2, row 3]
        [Expr.forwardCount 2, Expr.forwardCount 2]

    assertEqualFooBarRows first [row 1, row 2]
    assertEqualFooBarRows second [row 3]

prop_cursorFetchForwardAll :: Property.NamedDBProperty
prop_cursorFetchForwardAll =
  Property.singletonNamedDBProperty "Fetch forward all" $ \pool -> do
    [first] <-
      runFetchDirectionsOnData
        pool
        Nothing
        [row 1, row 2]
        [Expr.forwardAll]

    assertEqualFooBarRows first [row 1, row 2]

prop_cursorFetchBackward :: Property.NamedDBProperty
prop_cursorFetchBackward =
  Property.singletonNamedDBProperty "Fetch backward" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        (Just Expr.scroll)
        [row 1, row 2]
        [Expr.forwardCount 2, Expr.backward]

    assertEqualFooBarRows first [row 1, row 2]
    assertEqualFooBarRows second [row 1]

prop_cursorFetchBackwardCount :: Property.NamedDBProperty
prop_cursorFetchBackwardCount =
  Property.singletonNamedDBProperty "Fetch backward count" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        (Just Expr.scroll)
        [row 1, row 2, row 3]
        [Expr.forwardCount 3, Expr.backwardCount 2]

    assertEqualFooBarRows first [row 1, row 2, row 3]
    assertEqualFooBarRows second [row 2, row 1]

prop_cursorFetchBackwardAll :: Property.NamedDBProperty
prop_cursorFetchBackwardAll =
  Property.singletonNamedDBProperty "Fetch backward all" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        (Just Expr.scroll)
        [row 1, row 2, row 3]
        [Expr.forwardCount 4, Expr.backwardAll]

    assertEqualFooBarRows first [row 1, row 2, row 3]
    assertEqualFooBarRows second [row 3, row 2, row 1]

prop_cursorFetchFirstLast :: Property.NamedDBProperty
prop_cursorFetchFirstLast =
  Property.singletonNamedDBProperty "Fetch first/last" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        Nothing
        [row 1, row 2, row 3]
        [Expr.first, Expr.last]

    assertEqualFooBarRows first [row 1]
    assertEqualFooBarRows second [row 3]

prop_cursorFetchNextPrior :: Property.NamedDBProperty
prop_cursorFetchNextPrior =
  Property.singletonNamedDBProperty "Fetch next/prior" $ \pool -> do
    [first, second, third] <-
      runFetchDirectionsOnData
        pool
        (Just Expr.scroll)
        [row 1, row 2, row 3]
        [Expr.next, Expr.next, Expr.prior]

    assertEqualFooBarRows first [row 1]
    assertEqualFooBarRows second [row 2]
    assertEqualFooBarRows third [row 1]

prop_cursorFetchAbsolute :: Property.NamedDBProperty
prop_cursorFetchAbsolute =
  Property.singletonNamedDBProperty "Fetch absolute" $ \pool -> do
    [first, second] <-
      runFetchDirectionsOnData
        pool
        (Just Expr.scroll)
        [row 1, row 2, row 3]
        [Expr.absolute 3, Expr.absolute (-1)]

    assertEqualFooBarRows first [row 3]
    assertEqualFooBarRows second [row 3]

prop_cursorFetchRelative :: Property.NamedDBProperty
prop_cursorFetchRelative =
  Property.singletonNamedDBProperty "Fetch relative" $ \pool -> do
    [first, second, third, fourth] <-
      runFetchDirectionsOnData
        pool
        (Just Expr.scroll)
        [row 1, row 2, row 3]
        [Expr.relative 1, Expr.relative 2, Expr.relative (-1), Expr.relative 0]

    assertEqualFooBarRows first [row 1]
    assertEqualFooBarRows second [row 3]
    assertEqualFooBarRows third [row 2]
    assertEqualFooBarRows fourth [row 2]

row :: Int.Int32 -> FooBar
row n = mkFooBar n ("row " <> show n)

runFetchDirectionsOnData ::
  Orville.ConnectionPool ->
  Maybe Expr.ScrollExpr ->
  [FooBar] ->
  [Expr.CursorDirection] ->
  HH.PropertyT IO [[[(Maybe B8.ByteString, SqlValue.SqlValue)]]]
runFetchDirectionsOnData pool scroll fooBars directions =
  withFooBarData pool fooBars $ \connection ->
    withTestCursor connection scroll (Just Expr.withHold) findAllFooBars $ \cursorName ->
      let
        runDirection direction = do
          result <- RawSql.execute connection $ Expr.fetch (Just direction) cursorName
          Execution.readRows result
      in
        traverse runDirection directions

withTestCursor ::
  Orville.Connection ->
  Maybe Expr.ScrollExpr ->
  Maybe Expr.HoldExpr ->
  Expr.QueryExpr ->
  (Expr.CursorName -> IO a) ->
  IO a
withTestCursor connection scroll hold query action =
  let
    cursorName :: Expr.CursorName
    cursorName = Expr.fromIdentifier $ Expr.identifier "testcursor"

    declare =
      RawSql.executeVoid connection $
        Expr.declare cursorName scroll hold query

    close =
      RawSql.executeVoid connection $ Expr.close (Right cursorName)
  in
    ExSafe.bracket_ declare close (action cursorName)

withTestTransaction :: Orville.Connection -> IO a -> IO a
withTestTransaction connection action =
  let
    begin =
      RawSql.executeVoid connection $ Expr.beginTransaction Nothing

    commit =
      RawSql.executeVoid connection $ Expr.commit
  in
    ExSafe.bracket_ begin commit action