packages feed

orville-postgresql-1.0.0.0: test/Test/Plan.hs

{-# LANGUAGE CPP #-}

#if defined(MIN_VERSION_GLASGOW_HASKELL)
#if MIN_VERSION_GLASGOW_HASKELL(9,0,1,0)
{-# LANGUAGE QualifiedDo #-}
#endif
#endif
module Test.Plan
  ( planTests
  )
where

import qualified Control.Exception as Exception
import qualified Control.Monad.IO.Class as MIO
import qualified Data.Either as Either
import Data.Foldable (traverse_)
import qualified Data.List as List
import qualified Data.List.NonEmpty as NEL
import qualified Data.String as String
import Hedgehog ((===))
import qualified Hedgehog as HH
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

import qualified Orville.PostgreSQL as Orville
import qualified Orville.PostgreSQL.Plan as Plan
import qualified Orville.PostgreSQL.Plan.Many as Many

#if defined(MIN_VERSION_GLASGOW_HASKELL)
#if MIN_VERSION_GLASGOW_HASKELL(9,0,1,0)
import qualified Orville.PostgreSQL.Plan.Syntax as PlanSyntax
#endif
#endif

import qualified Test.Entities.Foo as Foo
import qualified Test.Entities.FooChild as FooChild
import qualified Test.Property as Property

{- ORMOLU_DISABLE -}
{- disable formatting so fourmolu doesn't go haywire with the cpp within the list -}
planTests :: Orville.ConnectionPool -> Property.Group
planTests pool =
  Property.group "Plan" $
    [ prop_askParam pool
    , prop_findMaybeOne pool
    , prop_findMaybeOneWhere pool
    , prop_findAll pool
    , prop_findAllWhere pool
    , prop_planMany_findMaybeOne pool
    , prop_planMany_findMaybeOneWhere pool
    , prop_planMany_findAll pool
    , prop_planMany_findAllWhere pool
    , prop_planEither pool
    , prop_planMany_planEither pool
    , prop_bindAndUse pool
    , prop_planMany_bindAndUse pool
    , prop_planMany_findOne_dedupesInClauses pool
#if defined(MIN_VERSION_GLASGOW_HASKELL)
#if MIN_VERSION_GLASGOW_HASKELL(9,0,1,0)
    , prop_bindAndUse_qualifiedDo pool
    , prop_planMany_bindAndUse_qualifiedDo pool
#endif
#endif
    , prop_assert pool
    , prop_explain
    ]
{- ORMOLU_ENABLE -}

prop_askParam :: Property.NamedDBProperty
prop_askParam =
  Property.namedDBProperty "askParam returns the plan's parameter" $ \pool -> do
    let
      plan :: Plan.Plan scope Int Int
      plan = Plan.askParam

    inputParam <- HH.forAll $ Gen.integral (Range.constant minBound maxBound)
    resultParam <- MIO.liftIO $ Orville.runOrville pool (Plan.execute plan inputParam)
    resultParam === inputParam

prop_findMaybeOne :: Property.NamedDBProperty
prop_findMaybeOne =
  Property.namedDBProperty "findMaybeOne finds a row where the field matches (if any)" $ \pool -> do
    let
      plan :: Plan.Plan scope Foo.FooName (Maybe Foo.Foo)
      plan = Plan.findMaybeOne Foo.table Foo.fooNameField

    (targetName, foos) <- HH.forAll generateSearchTargetAndSubjects
    maybeResult <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetName

    let
      isMatch = Foo.hasName targetName

    coverSearchResultCases isMatch foos
    assertMatchIsFromPredicate maybeResult isMatch foos

prop_planMany_findMaybeOne :: Property.NamedDBProperty
prop_planMany_findMaybeOne =
  Property.namedDBProperty "(planMany findMaybeOne) finds a row where the field matches for each input" $ \pool -> do
    let
      plan :: Plan.Plan scope [Foo.FooName] (Many.Many Foo.FooName (Maybe Foo.Foo))
      plan = Plan.planMany $ Plan.findMaybeOne Foo.table Foo.fooNameField

    (targetNames, foos) <- HH.forAll generateSearchTargetListAndSubjects
    results <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetNames

    let
      isMatch foo = elem (Foo.fooName foo) targetNames

    coverSearchResultCases isMatch foos

    assertEachManyResult targetNames results $ \targetName maybeFoo ->
      assertMatchIsFromPredicate
        maybeFoo
        (\foo -> Foo.hasName targetName foo && isMatch foo)
        foos

prop_findMaybeOneWhere :: Property.NamedDBProperty
prop_findMaybeOneWhere =
  Property.namedDBProperty "findMaybeOneWhere finds a row where the field matches (if any), with the given condition" $ \pool -> do
    let
      plan :: Plan.Plan scope Foo.FooName (Maybe Foo.Foo)
      plan =
        Plan.findMaybeOneWhere
          Foo.table
          Foo.fooNameField
          (Orville.fieldGreaterThan Foo.fooAgeField Foo.averageFooAge)

    (targetName, foos) <- HH.forAll generateSearchTargetAndSubjects
    maybeResult <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetName

    let
      isMatch foo = Foo.hasName targetName foo && Foo.fooAge foo > Foo.averageFooAge

    coverSearchResultCases isMatch foos
    assertMatchIsFromPredicate maybeResult isMatch foos

prop_planMany_findMaybeOneWhere :: Property.NamedDBProperty
prop_planMany_findMaybeOneWhere =
  Property.namedDBProperty "(planMany findMaybeOneWhere) finds a row where the field matches for each input, with the given condition" $ \pool -> do
    let
      plan :: Plan.Plan scope [Foo.FooName] (Many.Many Foo.FooName (Maybe Foo.Foo))
      plan =
        Plan.planMany $
          Plan.findMaybeOneWhere
            Foo.table
            Foo.fooNameField
            (Orville.fieldGreaterThan Foo.fooAgeField Foo.averageFooAge)

    (targetNames, foos) <- HH.forAll generateSearchTargetListAndSubjects
    results <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetNames

    let
      isMatch foo =
        elem (Foo.fooName foo) targetNames
          && Foo.fooAge foo > Foo.averageFooAge

    coverSearchResultCases isMatch foos

    assertEachManyResult targetNames results $ \targetName maybeFoo ->
      assertMatchIsFromPredicate
        maybeFoo
        (\foo -> Foo.hasName targetName foo && isMatch foo)
        foos

prop_findAll :: Property.NamedDBProperty
prop_findAll =
  Property.namedDBProperty "findAll finds all rows where the field matches" $ \pool -> do
    let
      plan :: Plan.Plan scope Foo.FooName [Foo.Foo]
      plan = Plan.findAll Foo.table Foo.fooNameField

    (targetName, foos) <- HH.forAll generateSearchTargetAndSubjects
    results <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetName

    let
      isMatch = Foo.hasName targetName

    coverSearchResultCases isMatch foos
    assertAllMatchesFound Foo.fooId results isMatch foos

prop_planMany_findAll :: Property.NamedDBProperty
prop_planMany_findAll =
  Property.namedDBProperty "(planMany findAll) finds all rows where the field matches for each list of inputs" $ \pool -> do
    let
      plan :: Plan.Plan scope [Foo.FooName] (Many.Many Foo.FooName [Foo.Foo])
      plan = Plan.planMany (Plan.findAll Foo.table Foo.fooNameField)

    (targetNames, foos) <- HH.forAll generateSearchTargetListAndSubjects
    results <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetNames

    let
      isMatch foo = elem (Foo.fooName foo) targetNames

    coverSearchResultCases isMatch foos
    assertEachManyResult targetNames results $ \targetName foundFoos ->
      assertAllMatchesFound Foo.fooId foundFoos (\foo -> Foo.hasName targetName foo && isMatch foo) foos

prop_findAllWhere :: Property.NamedDBProperty
prop_findAllWhere =
  Property.namedDBProperty "findAllWhere finds all rows where the field matches, with the given condition" $ \pool -> do
    let
      plan :: Plan.Plan scope Foo.FooName [Foo.Foo]
      plan =
        Plan.findAllWhere
          Foo.table
          Foo.fooNameField
          (Orville.fieldGreaterThan Foo.fooAgeField Foo.averageFooAge)

    (targetName, foos) <- HH.forAll generateSearchTargetAndSubjects
    results <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetName

    let
      isMatch foo = Foo.hasName targetName foo && Foo.fooAge foo > Foo.averageFooAge

    coverSearchResultCases isMatch foos
    assertAllMatchesFound Foo.fooId results isMatch foos

prop_planMany_findAllWhere :: Property.NamedDBProperty
prop_planMany_findAllWhere =
  Property.namedDBProperty "(planMany findAllWhere) finds all rows where the field matches for each list of inputs, with the given condition" $ \pool -> do
    let
      plan :: Plan.Plan scope [Foo.FooName] (Many.Many Foo.FooName [Foo.Foo])
      plan =
        Plan.planMany $
          Plan.findAllWhere
            Foo.table
            Foo.fooNameField
            (Orville.fieldGreaterThan Foo.fooAgeField Foo.averageFooAge)

    (targetNames, foos) <- HH.forAll generateSearchTargetListAndSubjects
    results <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan targetNames

    let
      isMatch foo = elem (Foo.fooName foo) targetNames && Foo.fooAge foo > Foo.averageFooAge

    coverSearchResultCases isMatch foos
    assertEachManyResult targetNames results $ \targetName foundFoos ->
      assertAllMatchesFound Foo.fooId foundFoos (\foo -> Foo.hasName targetName foo && isMatch foo) foos

prop_planEither :: Property.NamedDBProperty
prop_planEither =
  Property.namedDBProperty "planEither executes either the right or left plan based on input" $ \pool -> do
    let
      plan :: Plan.Plan scope (Either Foo.FooId Foo.FooName) Foo.Foo
      plan =
        either id id
          <$> Plan.planEither
            (Plan.findOne Foo.table Foo.fooIdField)
            (Plan.findOne Foo.table Foo.fooNameField)

    foo <- HH.forAll Foo.generate
    param <-
      HH.forAll . Gen.element $
        [ Left (Foo.fooId foo)
        , Right (Foo.fooName foo)
        ]

    foundFoo <-
      Foo.withTable pool $ do
        _ <- Orville.insertEntity Foo.table foo
        Plan.execute plan param

    foundFoo === foo

prop_planMany_planEither :: Property.NamedDBProperty
prop_planMany_planEither =
  Property.namedDBProperty "(planMany planEither) executes either the right and left plans with appropriate inputs" $ \pool -> do
    let
      plan :: Plan.Plan scope [Either Foo.FooId Foo.FooName] (Many.Many (Either Foo.FooId Foo.FooName) Foo.Foo)
      plan =
        Plan.planMany $
          either id id
            <$> Plan.planEither
              (Plan.findOne Foo.table Foo.fooIdField)
              (Plan.findOne Foo.table Foo.fooNameField)

    foos <- HH.forAll $ Foo.generateList (Range.linear 0 10)

    let
      pickInput :: Foo.Foo -> HH.PropertyT IO (Either Foo.FooId Foo.FooName)
      pickInput foo =
        HH.forAll . Gen.element $
          [ Left (Foo.fooId foo)
          , Right (Foo.fooName foo)
          ]

    params <- traverse pickInput foos

    foundFoos <-
      Foo.withTable pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty foos)
        Plan.execute plan params

    assertEachManyResult params foundFoos $ \input foo ->
      case input of
        Left fooId ->
          Foo.fooId foo === fooId
        Right fooName ->
          Foo.fooName foo === fooName

prop_bindAndUse :: Property.NamedDBProperty
prop_bindAndUse =
  Property.namedDBProperty "bind/use allows caller to use plan output as input for another plan" $ \pool -> do
    let
      plan :: Plan.Plan scope Foo.FooName (Foo.Foo, [FooChild.FooChild])
      plan =
        Plan.bind (Plan.findOne Foo.table Foo.fooNameField) $ \foo ->
          let
            fooId = Foo.fooId <$> foo
          in
            (,)
              <$> Plan.use foo
              <*> Plan.using fooId (Plan.findAll FooChild.table FooChild.fooChildFooIdField)

    foo <- HH.forAll Foo.generate
    fooChild <- HH.forAll $ FooChild.generate [foo]

    result <-
      FooChild.withTables pool $ do
        _ <- Orville.insertEntity Foo.table foo
        _ <- Orville.insertEntity FooChild.table fooChild
        Plan.execute plan (Foo.fooName foo)

    result === (foo, [fooChild])

prop_planMany_bindAndUse :: Property.NamedDBProperty
prop_planMany_bindAndUse =
  Property.namedDBProperty "(planMany bind/use) allows caller to use plan output as input for another plan" $ \pool -> do
    let
      plan :: Plan.Plan scope [Foo.FooName] (Many.Many Foo.FooName (Foo.Foo, [FooChild.FooChild]))
      plan =
        Plan.planMany $
          Plan.bind (Plan.findOne Foo.table Foo.fooNameField) $ \foo ->
            let
              fooId = Foo.fooId <$> foo
            in
              (,)
                <$> Plan.use foo
                <*> Plan.using fooId (Plan.findAll FooChild.table FooChild.fooChildFooIdField)

    allFoos <- HH.forAll $ Foo.generateList (Range.linear 1 5)
    allChildren <- HH.forAll $ FooChild.generateList (Range.linear 0 20) allFoos

    let
      allFooNames = Foo.fooName <$> allFoos

    results <-
      FooChild.withTables pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty allFoos)
        traverse_ (Orville.insertEntities FooChild.table) (NEL.nonEmpty allChildren)
        Plan.execute plan allFooNames

    assertEachManyResult allFooNames results $ \fooName (foo, children) -> do
      Foo.fooName foo === fooName
      assertAllMatchesFound FooChild.fooChildId children (FooChild.isChildOf foo) allChildren

prop_planMany_findOne_dedupesInClauses :: Property.NamedDBProperty
prop_planMany_findOne_dedupesInClauses =
  Property.singletonNamedDBProperty "planMany/findOne dedupes in clause to avoid PostgreSQL parameter limit" $ \pool -> do
    let
      plan :: Plan.Plan scope [Foo.FooId] (Many.Many Foo.FooId Foo.Foo)
      plan =
        Plan.planMany (Plan.findOne Foo.table Foo.fooIdField)

    unsavedFoo <- HH.forAll Foo.generate

    results <-
      FooChild.withTables pool $ do
        savedFoo <- Orville.insertAndReturnEntity Foo.table unsavedFoo
        let
          fooIds =
            replicate 65536 (Foo.fooId savedFoo)

        Plan.execute plan fooIds

    length (Many.elems results) === 65536

#if defined(MIN_VERSION_GLASGOW_HASKELL)
#if MIN_VERSION_GLASGOW_HASKELL(9,0,1,0)
prop_bindAndUse_qualifiedDo :: Property.NamedDBProperty
prop_bindAndUse_qualifiedDo =
  Property.namedDBProperty "bind/use allows caller to use plan output as input for another plan (QualifiedDo version)" $ \pool -> do
    let plan :: Plan.Plan scope Foo.FooName (Foo.Foo, [FooChild.FooChild])
        plan = PlanSyntax.do
          foo <- Plan.findOne Foo.table Foo.fooNameField

          let fooId = Foo.fooId <$> foo

          children <-
            Plan.using fooId $
              Plan.findAll FooChild.table FooChild.fooChildFooIdField

          (,)
            <$> Plan.use foo
            <*> Plan.use children

    foo <- HH.forAll Foo.generate
    fooChild <- HH.forAll $ FooChild.generate [foo]

    result <-
      FooChild.withTables pool $ do
        _ <- Orville.insertEntity Foo.table foo
        _ <- Orville.insertEntity FooChild.table fooChild
        Plan.execute plan (Foo.fooName foo)

    result === (foo, [fooChild])

prop_planMany_bindAndUse_qualifiedDo :: Property.NamedDBProperty
prop_planMany_bindAndUse_qualifiedDo =
  Property.namedDBProperty "(planMany bind/use) allows caller to use plan output as input for another plan (QualifiedDo version)" $ \pool -> do
    let plan :: Plan.Plan scope [Foo.FooName] (Many.Many Foo.FooName (Foo.Foo, [FooChild.FooChild]))
        plan =
          Plan.planMany $ PlanSyntax.do
            foo <- Plan.findOne Foo.table Foo.fooNameField

            let fooId = Foo.fooId <$> foo

            children <-
              Plan.using fooId $
                Plan.findAll FooChild.table FooChild.fooChildFooIdField

            (,)
              <$> Plan.use foo
              <*> Plan.use children

    allFoos <- HH.forAll $ Foo.generateList (Range.linear 1 5)
    allChildren <- HH.forAll $ FooChild.generateList (Range.linear 0 20) allFoos

    let allFooNames = Foo.fooName <$> allFoos

    results <-
      FooChild.withTables pool $ do
        traverse_ (Orville.insertEntities Foo.table) (NEL.nonEmpty allFoos)
        traverse_ (Orville.insertEntities FooChild.table) (NEL.nonEmpty allChildren)
        Plan.execute plan allFooNames

    assertEachManyResult allFooNames results $ \fooName (foo, children) -> do
      Foo.fooName foo === fooName
      assertAllMatchesFound FooChild.fooChildId children (FooChild.isChildOf foo) allChildren
#endif
#endif

prop_assert :: Property.NamedDBProperty
prop_assert =
  Property.namedDBProperty "assert raises an AssertionError in IO" $ \pool -> do
    let
      plan :: Plan.Plan scope (Either String ()) ()
      plan =
        Plan.assert (\_ value -> value) Plan.askParam

    input <- HH.forAll $ Gen.element [Left "Test Error", Right ()]
    result <- MIO.liftIO $ Exception.try $ Orville.runOrville pool (Plan.execute plan input)
    Either.isLeft (result :: Either Plan.AssertionFailed ()) === Either.isLeft input

prop_explain :: Property.NamedProperty
prop_explain =
  Property.namedProperty "explain shows the SQL steps that will be executed" $ do
    let
      plan :: Plan.Plan scope Foo.FooName [FooChild.FooChild]
      plan =
        Plan.chain
          (Plan.findOne Foo.table Foo.fooNameField)
          (Plan.focusParam Foo.fooId $ Plan.findAll FooChild.table FooChild.fooChildFooIdField)

      explanation =
        Plan.explain plan

    explanation
      === [ "SELECT \"name\",\"id\",\"name\",\"age\" FROM \"foo\" WHERE (\"name\") = ($1)"
          , "SELECT \"foo_id\",\"id\",\"foo_id\" FROM \"foo_child\" WHERE (\"foo_id\") = ($1)"
          ]

{- |
  Generates a list of Foos that along with FooName that could plausibly be
  found in the list zero, one or more times.

@since 1.0.0.0
-}
generateSearchTargetAndSubjects :: HH.Gen (Foo.FooName, [Foo.Foo])
generateSearchTargetAndSubjects = do
  targetName <- Foo.generateFooName

  let
    generatePossibleTargetFoo =
      Gen.choice
        [ Foo.generateFooWithName targetName
        , Foo.generate
        ]

  foos <- Foo.generateListUsing (Range.linear 0 5) generatePossibleTargetFoo
  pure (targetName, foos)

{- |
  Generates a list of Foos that along with FooName that could plausibly be
  found in the list zero, one or more times.

@since 1.0.0.0
-}
generateSearchTargetListAndSubjects :: HH.Gen ([Foo.FooName], [Foo.Foo])
generateSearchTargetListAndSubjects = do
  targetNames <- Gen.list (Range.linear 0 5) Foo.generateFooName

  let
    generatePossibleTargetFoo =
      Gen.choice (Foo.generate : fmap Foo.generateFooWithName targetNames)

  foos <- Foo.generateListUsing (Range.linear 0 5) generatePossibleTargetFoo
  pure (targetNames, foos)

{- |
  Uses Hedgehog's cover function to make sure common edge cases are covered
  for tests that are conducting a search. The predicate given should indicate
  whether the item would be expected to match the search being tested. The
  list of items should be the list that will be searced against.

@since 1.0.0.0
-}
coverSearchResultCases :: HH.MonadTest m => (a -> Bool) -> [a] -> m ()
coverSearchResultCases predicate subjects = do
  HH.cover 1 (String.fromString "no search subjects") (null subjects)
  HH.cover 1 (String.fromString "subjects present with no matches") (not (null subjects) && not (any predicate subjects))
  HH.cover 1 (String.fromString "at least 1 match") (length (filter predicate subjects) >= 1)

{- |
  Asserts that the given result is one that could have been produced by apply
  the predicate to the given list to find a single result. This assertion
  doesn't care which particular item from the list was found, as long as it the
  result matches the predicate and it is one from the list, or that nothing in
  the list matches if the 'Nothing' was found.

@since 1.0.0.0
-}
assertMatchIsFromPredicate ::
  (HH.MonadTest m, Eq entity) =>
  Maybe entity ->
  (entity -> Bool) ->
  [entity] ->
  m ()
assertMatchIsFromPredicate maybeEntity predicate subjects =
  case maybeEntity of
    Nothing ->
      HH.assert (not $ any predicate subjects)
    Just entity ->
      HH.assert (predicate entity && elem entity subjects)

{- |
  Asserts that the found items are all of those from the list that match the
  predicate, but without caring about order.

@since 1.0.0.0
-}
assertAllMatchesFound ::
  (HH.MonadTest m, Ord key, Eq entity, Show entity) =>
  -- | Key value to sort on to eliminate order dependencies
  (entity -> key) ->
  -- | the actual matches
  [entity] ->
  -- | the predicate to use for matching
  (entity -> Bool) ->
  -- | the full search space
  [entity] ->
  m ()
assertAllMatchesFound keyAttr foundEntities predicate allEntities =
  List.sortOn keyAttr foundEntities === List.sortOn keyAttr (filter predicate allEntities)

{- |
  Applies the given assertion for every key in the list. If any of the key
  is not found in the provided 'Many.Many' value, this assertion will fail.

@since 1.0.0.0
-}
assertEachManyResult ::
  (Show key, HH.MonadTest m) =>
  [key] ->
  Many.Many key value ->
  (key -> value -> m ()) ->
  m ()
assertEachManyResult expectedKeys manyValues assertion = do
  let
    checkResult key =
      case Many.lookup key manyValues of
        Left Many.NotAKey -> do
          HH.footnote $ "Expected " <> show key <> " to be a key in results, but it was not"
          HH.failure
        Right value ->
          assertion key value

  traverse_ checkResult expectedKeys