packages feed

pg-schema-0.8.0.0: test-pgs/Tests/Conditions.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}

module Tests.Conditions where

import Control.Monad (forM_)
import Control.Monad.IO.Class
import Data.Functor
import Data.Function
import Data.List qualified as L
import Data.Ord
import Data.Pool as Pool
import Data.Text as T
import Database.PostgreSQL.Simple
import GHC.Generics (Generic)
import GHC.Int
import GHC.Stack (HasCallStack)
import Hedgehog
import PgSchema.DML
import Utils


data LeafI = MkLeafI
  { leafNo :: Int32
  , value :: Double }
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass GenDefault

data Mid2I = MkMid2I
  { seq :: Int32
  , kind :: Text
  , flag :: Bool
  , priority :: Int32 }
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass GenDefault

data Mid1I = MkMid1I
  { pos :: Int32
  , flag :: Bool
  , sortKey :: Int32
  , payload :: Maybe Text }
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass GenDefault

data RootI = MkRootI
  { code :: Text
  , grp :: Int32
  , someEmpty :: ()
  , name :: Text }
  deriving stock (Show, Generic)
  deriving anyclass GenDefault

type InsData = "mid1_root_fk" := [Mid1I]
  :. "mid2_root_fk" := ["leaf_mid2_fk" := [LeafI] :. Mid2I]
  :. RootI

-- | Two edges in the result type that renamer maps to the same DB fk
-- ('mid1_root_fk2' -> 'mid1_root_fk'); each branch gets its own 'qPath' filter.
data RootDualMid1 = MkRootDualMid1
  { code :: Text
  , grp :: Int32
  , someEmpty :: ()
  , name :: Text
  , mid1RootFk :: [Mid1I] -- Renamer renames it to "mid1_root_fk"
  , mid1_root_fk2 :: [Mid1I] -- Renamer renames it to "mid1_root_fk"
  }
  deriving stock (Show, Generic)
  deriving anyclass GenDefault

eqRoot :: RootI -> RootI -> Bool
eqRoot r1 r2 = (r1.code, r1.grp) == (r2.code, r2.grp)

insData :: MonadIO m => Pool Connection -> PropertyT m [InsData]
insData pool = do
  rootsIn <- forAll (L.nubBy eqRoot <$> genData' RootI 1 200)
  mid1In <- forAll (genData' Mid1I 0 10)
  mid2In <- forAll (L.nubBy ((==) `on` (.seq)) <$> genData' Mid2I 0 10)
  leafIn <- forAll (L.nubBy ((==) `on` (.leafNo)) <$> genData' LeafI 0 10)
  let
    inIns = rootsIn <&> \r -> "mid1_root_fk" =: mid1In
      :. "mid2_root_fk" =: (mid2In <&> \m2 -> "leaf_mid2_fk" =: leafIn :. m2)
      :. r
  evalIO $ withResource pool \conn -> do
    delByCond "leaf" conn mempty
    delByCond "mid1" conn mempty
    delByCond "mid2" conn mempty
    delByCond "root" conn mempty
    void $ insJSON_ "root" conn inIns
  pure inIns

prop_cond_query :: Pool Connection -> Property
prop_cond_query pool = withTests 30 $ property do
  inIns <- insData pool
  (res :: [InsData], _) <- evalIO $ withResource pool \conn ->
    selSch "root" conn $ qRoot do
      qWhere $ ("grp" >? (100::Int32) ||| pinArr "grp" ([0..70]::[Int32]))
        &&& pchild (TS "mid1_root_fk")
          defTabParam { order = [ascf "pos"], lo = LO (Just 2) Nothing }
          ("sort_key" <? (100::Int32))
      qPath "mid1_root_fk" do
        qDistinctOn [ascf "root_id"]
        qWhere $ pnull "payload"
      qPath "mid2_root_fk" do
        qPath "leaf_mid2_fk" do
          qWhere $ pparent (TS "leaf_mid2_fk")
            $ pparent (TS "mid2_root_fk") $ "grp" >? (0::Int32)
        qOrderBy [descf "kind"]
  L.length (L.filter rootMatches inIns) === L.length res
  where
    rootMatches (PgTag m1s :. _ :. r) =
      (r.grp > 100 || r.grp `L.elem` [0..70])
      && L.any ((<100) . (.sortKey))
        (L.take 2 $ L.sortBy (comparing (.pos)) m1s)

-- | 'qPath' / 'qWhere' on 'mid1_root_fk' vs 'mid1_root_fk2' must not be merged
-- just because both edges share one fk in the database.
prop_renamer_alias_dual_fields :: Pool Connection -> Property
prop_renamer_alias_dual_fields pool = withTests 30 $ property do
  inIns <- insData pool
  (sel :: [RootDualMid1], _) <- evalIO $ withResource pool \conn ->
    selSch "root" conn $ qRoot do
      qPath "mid1RootFk" do
        qWhere $ "flag" =? True
      qPath "mid1_root_fk2" do
        qWhere $ "pos" >? (5 :: Int32)
  forM_ sel \root -> do
    let
      expectFk = [ m1
        | PgTag m1s :. _ :. r <- inIns
        , (r.code, r.grp) == (root.code, root.grp)
        , m1 <- m1s
        , m1.flag
        ]
      expectFk2 = [ m1
        | PgTag m1s :. _ :. r <- inIns
        , (r.code, r.grp) == (root.code, root.grp)
        , m1 <- m1s
        , m1.pos > 5
        ]
    L.sort root.mid1RootFk === L.sort expectFk
    L.sort root.mid1_root_fk2 === L.sort expectFk2

-- | 'qPath' by shared db fk name applies one 'qWhere' to every matching branch.
prop_renamer_broadcast_dual_fields :: Pool Connection -> Property
prop_renamer_broadcast_dual_fields pool = withTests 30 $ property do
  inIns <- insData pool
  (sel :: [RootDualMid1], _) <- evalIO $ withResource pool \conn ->
    selSch "root" conn $ qRoot do
      qPath "mid1_root_fk" do
        qWhere $ "flag" =? True
  forM_ sel \root -> do
    let
      expectFlag = [ m1
        | PgTag m1s :. _ :. r <- inIns
        , (r.code, r.grp) == (root.code, root.grp)
        , m1 <- m1s
        , m1.flag
        ]
    L.sort root.mid1RootFk === L.sort expectFlag
    L.sort root.mid1_root_fk2 === L.sort expectFlag

-- | Duplicate 'mid2_root_fk' in the result type: filters on the path
-- and on nested 'leaf_mid2_fk' apply per branch and may diverge.
prop_cond_by_dup_path :: Pool Connection -> Property
prop_cond_by_dup_path pool = withTests 30 $ property do
  inIns <- insData pool
  (sel :: ["mid2_root_fk" := [Mid2I] :. InsData], _) <- evalIO $ withResource pool \conn ->
    selSch "root" conn $ qRoot
      $ qPath "mid2_root_fk" do -- there are two "mid2_root_fk" branches
        qWhere $ "flag" =? True
        qPath "leaf_mid2_fk" do -- only one "leaf_mid2_fk" branch
          qWhere $ "leaf_no" >? (97::Int32)
  L.sort [m2 | (PgTag m2s :. _) <- sel, m2 <- m2s]
    === L.sort [m2 | (_ :. PgTag m2s :. _) <- inIns, (_ :. m2) <- m2s, m2.flag]
  L.sort [(m2, L.length ls) | (_ :. _ :. PgTag m2s :. _) <- sel, (PgTag ls :. m2) <- m2s]
    === L.sort [(m2, L.length $ L.filter ((>97) . (.leafNo)) ls)
      | (_ :. PgTag m2s :. _) <- inIns, (PgTag ls :. m2) <- m2s, m2.flag]