packages feed

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

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

import Control.Monad (void, when)
import Data.Int (Int32)
import Data.Maybe (Maybe(..))
import Data.List qualified as L
import Data.Pool as Pool
import Data.Text (Text)
import Database.PostgreSQL.Simple (Connection)
import Hedgehog (Property, discard, evalIO, forAll, property, withTests)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import PgSchema.DML
import Utils

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

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

-- | Upsert by composite unique @(code, grp)@ when PK @id@ is omitted; second call
-- updates the same row.
prop_upsert_json_by_composite_unique :: Pool Connection -> Property
prop_upsert_json_by_composite_unique pool = withTests 5 $ property do
  code <- forAll (Gen.text (Range.linear 3 24) Gen.alphaNum)
  name1 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)
  name2 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)
  when (name1 == name2) discard
  let grp = 4242 :: Int32
  evalIO $ Pool.withResource pool \conn -> do
    void $ delByCond "root" conn ("code" =? code)
    let
      row1 :: RootRec
      row1 = "code" =: code :. "grp" =: grp :. "name" =: name1 :. "someEmpty" =: ()
      row2 :: RootRec
      row2 = "code" =: code :. "grp" =: grp :. "name" =: name2 :. "someEmpty" =: ()
    void $ upsJSON_ "root" conn [row1]
    void $ upsJSON_ "root" conn [row2]
    (rows :: [RootSel], _) <- selSch "root" conn $
      qRoot $ qWhere $ "code" =? code &&& "grp" =? grp
    case rows of
      [_ :. _ :. n] ->
        when (unPgTag n /= name2) $
          fail $ "expected updated name " <> show name2 <> ", got " <> show (unPgTag n)
      _ -> fail $ "expected 1 row, got " <> show (L.length rows)

-- | Upsert by nullable unique @(code, suffix)@ when table has no NOT NULL UK.
prop_upsert_json_by_nullable_unique :: Pool Connection -> Property
prop_upsert_json_by_nullable_unique pool = withTests 5 $ property do
  code <- forAll (Gen.text (Range.linear 3 24) Gen.alphaNum)
  suffix <- forAll (Gen.text (Range.linear 1 12) Gen.alphaNum)
  name1 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)
  name2 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)
  when (name1 == name2) discard
  let sfx = Just suffix
  evalIO $ Pool.withResource pool \conn -> do
    void $ delByCond "nullable_uq_row" conn ("code" =? code)
    let
      row1 = "code" =: code :. "suffix" =: sfx :. "name" =: name1
        :. "note" =: (Nothing :: Maybe Text) :. "someEmpty" =: ()
      row2 = "code" =: code :. "suffix" =: sfx :. "name" =: name2
        :. "note" =: (Nothing :: Maybe Text) :. "someEmpty" =: ()
    void $ upsJSON_ "nullable_uq_row" conn [row1]
    void $ upsJSON_ "nullable_uq_row" conn [row2]
    (rows :: ["name" := Text], _) <- selSch "nullable_uq_row" conn $
      qRoot $ qWhere $ "code" =? code &&& "suffix" =?? sfx
    case rows of
      [n] ->  when (unPgTag n /= name2) $ fail
        $ "expected updated name " <> show name2 <> ", got " <> show (unPgTag n)
      _ -> fail $ "expected 1 row, got " <> show (L.length rows)

-- | UPDATE branch: key only + optional field, mandatory @name@ omitted.
prop_upsert_json_update_by_nullable_unique :: Pool Connection -> Property
prop_upsert_json_update_by_nullable_unique pool = withTests 5 $ property do
  code <- forAll (Gen.text (Range.linear 3 24) Gen.alphaNum)
  suffix <- forAll (Gen.text (Range.linear 1 12) Gen.alphaNum)
  note1 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)
  note2 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)
  when (note1 == note2) discard
  let
    sfx = Just suffix
    fullName = "seed-name"
  evalIO $ Pool.withResource pool \conn -> do
    void $ delByCond "nullable_uq_row" conn ("code" =? code)
    let
      seed =
        "code" =: code :. "suffix" =: sfx :. "name" =: fullName
          :. "note" =: Just note1 :. "someEmpty" =: ()
      patch =
        "code" =: code :. "suffix" =: sfx :. "note" =: Just note2
          :. "someEmpty" =: ()
    void $ upsJSON_ "nullable_uq_row" conn [seed]
    void $ upsJSON_ "nullable_uq_row" conn [patch]
    (rows :: ["name" := Text :. "note" := Maybe Text], _) <-
      selSch "nullable_uq_row" conn
        $ qRoot $ qWhere $ "code" =? code &&& "suffix" =?? sfx
    case rows of
      [nm :. note] -> do
        when (unPgTag note /= Just note2) $
          fail $ "expected note " <> show note2 <> ", got " <> show (unPgTag note)
        when (unPgTag nm /= fullName) $
          fail $ "name should be unchanged, got " <> show (unPgTag nm)
      _ -> fail $ "expected 1 row, got " <> show (L.length rows)

-- | NULL in nullable key column: classic UNIQUE allows duplicate keys.
prop_upsert_json_nullable_key_null_inserts_twice :: Pool Connection -> Property
prop_upsert_json_nullable_key_null_inserts_twice pool = withTests 3 $ property do
  code <- forAll (Gen.text (Range.linear 3 24) Gen.alphaNum)
  name1 <- forAll (Gen.text (Range.linear 1 16) Gen.alphaNum)
  name2 <- forAll (Gen.text (Range.linear 1 16) Gen.alphaNum)
  when (name1 == name2) discard
  evalIO $ Pool.withResource pool \conn -> do
    void $ delByCond "nullable_uq_row" conn ("code" =? code)
    let
      row nm =
        "code" =: code :. "suffix" =: (Nothing :: Maybe Text) :. "name" =: nm
          :. "note" =: (Nothing :: Maybe Text) :. "someEmpty" =: ()
    void $ upsJSON_ "nullable_uq_row" conn [row name1]
    void $ upsJSON_ "nullable_uq_row" conn [row name2]
    (rows :: ["code" := Text], _) <- selSch "nullable_uq_row" conn $
      qRoot $ qWhere $ "code" =? code &&& pnull "suffix"
    when (L.length rows /= 2) $
      fail $ "expected 2 rows with null suffix, got " <> show (L.length rows)