packages feed

pg-schema-0.7.1.0: test-pgs/Tests/InsertJSONTransaction.hs

{-# LANGUAGE BlockArguments #-}
module Tests.InsertJSONTransaction where

import Control.Exception (SomeException, try)
import Control.Monad (void, when)
import Data.Functor
import Data.Int (Int32, Int64)
import Data.List qualified as L
import Data.Pool as Pool
import Data.Text (Text)
import Database.PostgreSQL.Simple
import Hedgehog
import Utils
import PgSchema.DML

type RootRec = "code" := Text :. "grp" := Int32 :. "name" := Text :. "someEmpty" := ()

type RootSel = "code" := Text :. "grp" := Int32 :. "name" := Text

eqRoot :: RootRec -> RootRec -> Bool
eqRoot (a1 :. b1 :. c1) (a2 :. b2 :. c2) = (a1, b1) == (a2, b2)

countRoots :: Connection -> IO Int64
countRoots conn = do
  [cnt] <- fst <$> selSch "root" conn qpEmpty
  pure $ unAggr @ACount $ unPgTag @"cnt" cnt

countRootsCode :: Connection -> Text -> IO Int64
countRootsCode conn code = do
  [cnt] <- fst <$> selSch "root" conn (qRoot $ qWhere $ "code" =? code)
  pure $ unAggr @ACount $ unPgTag @"cnt" cnt

clearRoots :: Connection -> IO ()
clearRoots conn = void $ delByCond "root" conn mempty

prop_insertJSON_standalone_commits :: Pool Connection -> Property
prop_insertJSON_standalone_commits pool = property do
  rootsIn <- forAll (L.nubBy eqRoot <$> genData' RootRec 1 5)
  evalIO do
    Pool.withResource pool clearRoots
    Pool.withResource pool \conn -> void $ insJSON_ "root" conn rootsIn
    Pool.withResource pool \conn -> do
      n <- countRoots conn
      when (n /= fromIntegral (length rootsIn)) $
        fail $ "expected " <> show (length rootsIn) <> " rows, got " <> show n

prop_insertJSON_respects_outer_transaction :: Pool Connection -> Property
prop_insertJSON_respects_outer_transaction pool = property do
  rootsIn <- forAll (L.nubBy eqRoot <$> genData' RootRec 1 5)
  evalIO do
    Pool.withResource pool clearRoots
    Pool.withResource pool \conn -> do
      execute_ conn "begin"
      void $ insJSON_ "root" conn rootsIn
      n <- countRoots conn
      when (n /= fromIntegral (length rootsIn)) $
        fail $ "expected " <> show (length rootsIn) <> " rows in session, got "
          <> show n
      Pool.withResource pool \conn2 -> do
        n2 <- countRoots conn2
        when (n2 /= 0) $
          fail $ "uncommitted rows visible in other session: " <> show n2
      rollback conn
    Pool.withResource pool \conn -> do
      n <- countRoots conn
      when (n /= 0) $
        fail $ "rows remained after rollback: " <> show n

prop_insertJSON_begin_without_prior_writes :: Pool Connection -> Property
prop_insertJSON_begin_without_prior_writes pool = property do
  rootsIn <- forAll (L.nubBy eqRoot <$> genData' RootRec 1 5)
  evalIO do
    Pool.withResource pool clearRoots
    Pool.withResource pool \conn -> do
      execute_ conn "begin"
      void $ insJSON_ "root" conn rootsIn
      rollback conn
    Pool.withResource pool \conn -> do
      n <- countRoots conn
      when (n /= 0) $
        fail $ "rows remained after rollback from empty begin: " <> show n

prop_insertJSON_rolls_back_on_failure :: Pool Connection -> Property
prop_insertJSON_rolls_back_on_failure pool = property do
  evalIO do
    Pool.withResource pool clearRoots
    let
      row :: RootRec
      row = "code" =: "dup_tx" :. "grp" =: (1 :: Int32)
        :. "name" =: "x" :. "someEmpty" =: ()
    Pool.withResource pool \conn -> do
      res <- try @SomeException $ insJSON_ "root" conn [row, row]
      case res of
        Left _ -> pure ()
        Right _ -> fail "expected unique violation"
      n <- countRootsCode conn "dup_tx"
      when (n /= 0) $
        fail $ "partial insert remained after failure: " <> show n
    Pool.withResource pool \conn -> do
      n <- countRootsCode conn "dup_tx"
      when (n /= 0) $
        fail $ "failed insert visible in other session: " <> show n