graphql-engine/server/src-test/Hasura/GraphQL/Schema/BoolExp/AggregationPredicatesSpec.hs
Philip Lykke Carlsen 8cb14a592d server: Add an ExperimentalFeatureFlag for Aggregation Predicates
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6523
GitOrigin-RevId: 76861a1bf0d9895901564935b0778e7bda75c6a9
2022-10-25 13:21:40 +00:00

333 lines
12 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
-- | This module contain unit tests of the schema of the default implementation
-- of aggregation predicates.
module Hasura.GraphQL.Schema.BoolExp.AggregationPredicatesSpec (spec) where
import Data.Aeson.QQ (aesonQQ)
import Data.Has (Has (..))
import Data.HashMap.Strict qualified as HM
import Data.Text.NonEmpty (nonEmptyTextQQ)
import Hasura.Backends.Postgres.Instances.Schema ()
import Hasura.Backends.Postgres.SQL.Types
( PGScalarType (PGInteger, PGText),
QualifiedTable,
)
import Hasura.Backends.Postgres.SQL.Value (PGScalarValue (..))
import Hasura.GraphQL.Parser.Internal.Input (ifParser)
import Hasura.GraphQL.Schema.BoolExp.AggregationPredicates
( ArgumentsSignature (..),
FunctionSignature (..),
defaultAggregationPredicatesParser,
)
import Hasura.GraphQL.Schema.Introspection (queryInputFieldsParserIntrospection)
import Hasura.GraphQL.Schema.NamingCase (NamingCase (..))
import Hasura.GraphQL.Schema.Options qualified as Options
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp (GBoolExp (..), OpExpG (AEQ))
import Hasura.RQL.IR.BoolExp.AggregationPredicates
import Hasura.RQL.IR.Value (UnpreparedValue (UVParameter))
import Hasura.RQL.Types.Column (ColumnType (ColumnScalar), ColumnValue (..))
import Hasura.RQL.Types.Common (InsertOrder (..), RelName (..), RelType (..), SourceName (..))
import Hasura.RQL.Types.Relationships.Local (RelInfo (..))
import Hasura.RQL.Types.Source (SourceInfo (..))
import Hasura.RQL.Types.SourceCustomization (ResolvedSourceCustomization (..))
import Hasura.RQL.Types.Table
( TableCoreInfoG (_tciName),
TableInfo (_tiCoreInfo),
)
import Hasura.SQL.Backend (BackendType (Postgres), PostgresKind (Vanilla))
import Language.GraphQL.Draft.Syntax qualified as G
import Language.GraphQL.Draft.Syntax.QQ qualified as G
import Test.Aeson.Expectation (shouldBeSubsetOf)
import Test.Hspec
import Test.Hspec.Extended
import Test.Parser.Field qualified as GQL
import Test.Parser.Internal
( ColumnInfoBuilder
( ColumnInfoBuilder,
cibIsPrimaryKey,
cibName,
cibNullable,
cibType
),
TableInfoBuilder (columns, relations),
buildTableInfo,
mkTable,
tableInfoBuilder,
)
import Test.Parser.Monad
( ParserTest (runParserTest),
notImplementedYet,
runSchemaTest,
)
import Type.Reflection (Typeable, typeRep)
{- Notes:
AggregationPredicates are defined as a standalone feature. It should be possible
to test them without reference to an existing backend.
We cannot do that however, since backends have the closed datakind `BackendType`.
-}
newtype Unshowable a = Unshowable {unUnshowable :: a}
deriving (Eq, Ord)
instance Typeable a => Show (Unshowable a) where
show _ = "Unshowable<" ++ show (typeRep @a) ++ ">"
spec :: Spec
spec = do
describe "Aggregation Predicates Schema Parsers" do
describe "When no aggregation functions are given" do
it "Yields no parsers" do
let maybeParser =
runSchemaTest $
defaultAggregationPredicatesParser @('Postgres 'Vanilla) @_ @_ @ParserTest
[]
sourceInfo
albumTableInfo
(Unshowable maybeParser) `shouldSatisfy` (isNothing . unUnshowable)
describe "When some aggregation functions are given" do
let maybeParser =
runSchemaTest $
defaultAggregationPredicatesParser @('Postgres 'Vanilla) @_ @_ @ParserTest
[ FunctionSignature
{ fnName = "count",
fnGQLName = [G.name|count|],
fnArguments = ArgumentsStar,
fnReturnType = PGInteger
}
]
sourceInfo
albumTableInfo
it "Positively yields a parser" do
(Unshowable maybeParser) `shouldSatisfy` (isJust . unUnshowable)
dependentSpec maybeParser $ do
it "Defines the expected GraphQL types" \parser -> do
introspectionResult <-
queryInputFieldsParserIntrospection
parser
[GQL.field|
__schema {
types {
name
fields {
name
type { name }
}
inputFields {
name
type { name }
}
}
} |]
let expectedTopLevel =
[aesonQQ|
{ "types": [
{
"name": "track_aggregate_bool_exp",
"fields": null,
"inputFields": [
{
"name": "count",
"type": {
"name": "track_aggregate_bool_exp_count"
}
}
]
}
]
}
|]
let expectedCountType =
[aesonQQ|
{ "types": [
{
"name": "track_aggregate_bool_exp_count",
"fields": null,
"inputFields": [
{
"name": "arguments",
"type": {
"name": null
}
},
{
"name": "distinct",
"type": {
"name": "Boolean"
}
},
{
"name": "filter",
"type": {
"name": "track_bool_exp"
}
},
{
"name": "predicate",
"type": {
"name": null
}
}
]
}
]
}
|]
expectedTopLevel `shouldBeSubsetOf` introspectionResult
expectedCountType `shouldBeSubsetOf` introspectionResult
it "Parses an example field" \parser -> do
let input =
[GQL.inputfields|
tracks_aggregate: {
count: {
arguments: [],
predicate: {_eq : 42 },
distinct: true
}
}
|]
(actual : _) <- runParserTest $ ifParser parser input
let expected :: AggregationPredicatesImplementation ('Postgres 'Vanilla) (UnpreparedValue ('Postgres 'Vanilla))
expected =
AggregationPredicatesImplementation
{ aggRelation = tracksRel,
aggRowPermission = BoolAnd [],
aggPredicate =
AggregationPredicate
{ aggPredFunctionName = "count",
aggPredDistinct = True,
aggPredFilter = Nothing,
aggPredArguments = AggregationPredicateArgumentsStar,
aggPredPredicate =
[ AEQ
True
( UVParameter
Nothing
ColumnValue
{ cvType = ColumnScalar PGInteger,
cvValue = PGValInteger 42
}
)
]
}
}
-- Permissions aren't in scope for this test.
actual {aggRowPermission = BoolAnd []} `shouldBe` expected
describe "When SchemaOptions dictate exclusion of aggregation predicates" do
it "Yields no parsers" do
let maybeParser =
runSchemaTest $
local
( modifier
( \so ->
so
{ Options.soIncludeAggregationPredicates = Options.Don'tIncludeAggregationPredicates
}
)
)
$ defaultAggregationPredicatesParser @('Postgres 'Vanilla) @_ @_ @ParserTest
[ FunctionSignature
{ fnName = "count",
fnGQLName = [G.name|count|],
fnArguments = ArgumentsStar,
fnReturnType = PGInteger
}
]
sourceInfo
albumTableInfo
(Unshowable maybeParser) `shouldSatisfy` (isNothing . unUnshowable)
where
albumTableInfo :: TableInfo ('Postgres 'Vanilla)
albumTableInfo =
buildTableInfo
( (tableInfoBuilder (mkTable "album"))
{ columns =
[ ColumnInfoBuilder
{ cibName = "id",
cibType = ColumnScalar PGInteger,
cibNullable = False,
cibIsPrimaryKey = True
},
ColumnInfoBuilder
{ cibName = "title",
cibType = ColumnScalar PGText,
cibNullable = False,
cibIsPrimaryKey = False
}
],
relations = [tracksRel]
}
)
trackTableInfo :: TableInfo ('Postgres 'Vanilla)
trackTableInfo =
buildTableInfo
( (tableInfoBuilder (mkTable "track"))
{ columns =
[ ColumnInfoBuilder
{ cibName = "id",
cibType = ColumnScalar PGInteger,
cibNullable = False,
cibIsPrimaryKey = True
},
ColumnInfoBuilder
{ cibName = "title",
cibType = ColumnScalar PGText,
cibNullable = False,
cibIsPrimaryKey = False
},
ColumnInfoBuilder
{ cibName = "duration_seconds",
cibType = ColumnScalar PGInteger,
cibNullable = False,
cibIsPrimaryKey = False
},
ColumnInfoBuilder
{ cibName = "album_id",
cibType = ColumnScalar PGInteger,
cibNullable = False,
cibIsPrimaryKey = False
}
]
}
)
tracksRel :: RelInfo ('Postgres 'Vanilla)
tracksRel =
RelInfo
{ riName = RelName [nonEmptyTextQQ|tracks|],
riType = ArrRel,
riMapping = HM.fromList [("id", "album_id")],
riRTable = (mkTable "track"),
riIsManual = False,
riInsertOrder = AfterParent
}
sourceInfo :: SourceInfo ('Postgres 'Vanilla)
sourceInfo =
SourceInfo
{ _siName = SNDefault,
_siTables = makeTableCache [albumTableInfo, trackTableInfo],
_siFunctions = mempty,
_siConfiguration = notImplementedYet "SourceConfig",
_siQueryTagsConfig = Nothing,
_siCustomization = ResolvedSourceCustomization mempty mempty HasuraCase Nothing
}
makeTableCache :: [TableInfo ('Postgres 'Vanilla)] -> HashMap QualifiedTable (TableInfo ('Postgres 'Vanilla))
makeTableCache tables = HM.fromList [(_tciName $ _tiCoreInfo ti, ti) | ti <- tables]