diff --git a/server/lib/api-tests/api-tests.cabal b/server/lib/api-tests/api-tests.cabal index 53efa5c5721..deb21d49fbc 100644 --- a/server/lib/api-tests/api-tests.cabal +++ b/server/lib/api-tests/api-tests.cabal @@ -117,6 +117,7 @@ executable api-tests Test.Queries.Directives.IncludeSpec Test.Queries.Directives.SkipSpec Test.Queries.DirectivesSpec + Test.Queries.DistinctSpec Test.Queries.ExplainSpec Test.Queries.FilterSearchSpec Test.Queries.FragmentsSpec diff --git a/server/lib/api-tests/test/Test/Queries/DistinctSpec.hs b/server/lib/api-tests/test/Test/Queries/DistinctSpec.hs new file mode 100644 index 00000000000..a75b98e303d --- /dev/null +++ b/server/lib/api-tests/test/Test/Queries/DistinctSpec.hs @@ -0,0 +1,167 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- | +-- Tests for @distinct@ queries. +-- +-- https://hasura.io/docs/latest/queries/postgres/distinct-queries/ +module Test.Queries.DistinctSpec where + +import Data.Aeson (Value) +import Data.List.NonEmpty qualified as NE +import Harness.Backend.BigQuery qualified as BigQuery +import Harness.Backend.Citus qualified as Citus +import Harness.Backend.Postgres qualified as Postgres +import Harness.GraphqlEngine (postGraphql) +import Harness.Quoter.Graphql (graphql) +import Harness.Quoter.Yaml (interpolateYaml) +import Harness.Test.Fixture qualified as Fixture +import Harness.Test.Schema (Table (..), table) +import Harness.Test.Schema qualified as Schema +import Harness.TestEnvironment (TestEnvironment) +import Harness.Yaml (shouldReturnYaml) +import Hasura.Prelude +import Test.Hspec (SpecWith, it) + +spec :: SpecWith TestEnvironment +spec = do + Fixture.run + ( NE.fromList + [ (Fixture.fixture $ Fixture.Backend Fixture.Postgres) + { Fixture.setupTeardown = \(testEnv, _) -> + [ Postgres.setupTablesAction schema testEnv + ] + }, + (Fixture.fixture $ Fixture.Backend Fixture.Citus) + { Fixture.setupTeardown = \(testEnv, _) -> + [ Citus.setupTablesAction schema testEnv + ] + }, + (Fixture.fixture $ Fixture.Backend Fixture.BigQuery) + { Fixture.setupTeardown = \(testEnv, _) -> + [ BigQuery.setupTablesAction schema testEnv + ], + Fixture.customOptions = + Just $ + Fixture.defaultOptions + { Fixture.stringifyNumbers = True + } + } + ] + ) + tests + +-------------------------------------------------------------------------------- +-- Schema + +schema :: [Schema.Table] +schema = + [ (table "author") + { tableColumns = + [ Schema.column "id" Schema.TInt, + Schema.column "name" Schema.TStr, + Schema.columnNull "genre" Schema.TStr, + Schema.column "age" Schema.TInt + ], + tablePrimaryKey = ["id"], + tableData = + [ [Schema.VInt 1, Schema.VStr "Alice", Schema.VStr "Action", Schema.VInt 25], + [Schema.VInt 2, Schema.VStr "Bob", Schema.VStr "Biography", Schema.VInt 40], + [Schema.VInt 3, Schema.VStr "Carol", Schema.VStr "Crime", Schema.VInt 35], + [Schema.VInt 4, Schema.VStr "Dave", Schema.VStr "Action", Schema.VInt 30], + [Schema.VInt 5, Schema.VStr "Eve", Schema.VStr "Crime", Schema.VInt 25], + [Schema.VInt 6, Schema.VStr "Bart", Schema.VNull, Schema.VInt 25] + ] + } + ] + +-------------------------------------------------------------------------------- +-- Tests + +-- We specify the nulls ordering because the behaviour is not consistent between +-- postgres and cockroach. +-- +-- For postgres and citus: +-- - @asc_nulls_last@ is the behaviour of @asc@ +-- - @desc_nulls_first@ is the behaviour of @desc@ +-- +-- For cockroach, it's the opposite: +-- - @asc_nulls_first@ is the behaviour of @asc@ +-- - @desc_nulls_last@ is the behaviour of @desc@ +tests :: Fixture.Options -> SpecWith TestEnvironment +tests opts = do + let shouldBe :: IO Value -> Value -> IO () + shouldBe = shouldReturnYaml opts + + it "Find the oldest writer of each genre - nulls last" \testEnvironment -> do + let schemaName :: Schema.SchemaName + schemaName = Schema.getSchemaName testEnvironment + + actual :: IO Value + actual = + postGraphql + testEnvironment + [graphql| + query { + #{schemaName}_author ( + distinct_on: [genre], + order_by: [ { genre: asc_nulls_last }, { age: desc }, { id: asc } ] + ) { + id + name + } + } + |] + + expected :: Value + expected = + [interpolateYaml| + data: + #{schemaName}_author: + - id: 4 + name: Dave + - id: 2 + name: Bob + - id: 3 + name: Carol + - id: 6 + name: Bart + |] + + actual `shouldBe` expected + + it "Find the oldest writer of each genre - nulls first" \testEnvironment -> do + let schemaName :: Schema.SchemaName + schemaName = Schema.getSchemaName testEnvironment + + actual :: IO Value + actual = + postGraphql + testEnvironment + [graphql| + query { + #{schemaName}_author ( + distinct_on: [genre], + order_by: [ { genre: asc_nulls_first }, { age: desc }, { id: asc } ] + ) { + id + name + } + } + |] + + expected :: Value + expected = + [interpolateYaml| + data: + #{schemaName}_author: + - id: 6 + name: Bart + - id: 4 + name: Dave + - id: 2 + name: Bob + - id: 3 + name: Carol + |] + + actual `shouldBe` expected diff --git a/server/lib/test-harness/src/Harness/Backend/BigQuery.hs b/server/lib/test-harness/src/Harness/Backend/BigQuery.hs index 7287cb5bb71..974836a613f 100644 --- a/server/lib/test-harness/src/Harness/Backend/BigQuery.hs +++ b/server/lib/test-harness/src/Harness/Backend/BigQuery.hs @@ -61,7 +61,13 @@ run_ :: HasCallStack => String -> IO () run_ query = do void $ runWithRetry - (\conn -> (Execute.executeBigQuery conn Execute.BigQuery {Execute.query = fromString query, Execute.parameters = mempty})) + ( \conn -> do + res <- + Execute.executeBigQuery + conn + Execute.BigQuery {Execute.query = fromString query, Execute.parameters = mempty} + onLeft res \x -> liftIO (bigQueryError x query) + ) bigQueryError :: HasCallStack => Execute.ExecuteProblem -> String -> IO a bigQueryError e query = @@ -85,7 +91,7 @@ removeDataset schemaName = void $ runWithRetry (\conn -> Execute.deleteDataset conn $ unSchemaName schemaName) -- | Serialize Table into a SQL statement, as needed, and execute it on the BigQuery backend -createTable :: SchemaName -> Schema.Table -> IO () +createTable :: HasCallStack => SchemaName -> Schema.Table -> IO () createTable schemaName table@Schema.Table {tableName, tableColumns} = do run_ $ T.unpack $ @@ -149,7 +155,7 @@ serialize = \case VCustomValue bsv -> Schema.formatBackendScalarValueType $ Schema.backendScalarValue bsv bsvBigQuery -- | Serialize Table into an SQL DROP statement and execute it -dropTable :: SchemaName -> Schema.Table -> IO () +dropTable :: HasCallStack => SchemaName -> Schema.Table -> IO () dropTable schemaName Schema.Table {tableName} = do run_ $ T.unpack $ @@ -193,7 +199,7 @@ untrackTable testEnvironment schemaName Schema.Table {tableName} = do -- | Setup the schema in the most expected way. -- NOTE: Certain test modules may warrant having their own local version. -setup :: [Schema.Table] -> (TestEnvironment, ()) -> IO () +setup :: HasCallStack => [Schema.Table] -> (TestEnvironment, ()) -> IO () setup tables' (testEnvironment, _) = do let source = defaultSource BigQuery backendType = defaultBackendTypeString BigQuery @@ -249,24 +255,24 @@ teardown (reverse -> tables) (testEnvironment, _) = do -- remove test dataset (removeDataset schemaName) -setupTablesAction :: [Schema.Table] -> TestEnvironment -> SetupAction +setupTablesAction :: HasCallStack => [Schema.Table] -> TestEnvironment -> SetupAction setupTablesAction ts env = SetupAction (setup ts (env, ())) (const $ teardown ts (env, ())) -setupPermissionsAction :: [Permissions.Permission] -> TestEnvironment -> SetupAction +setupPermissionsAction :: HasCallStack => [Permissions.Permission] -> TestEnvironment -> SetupAction setupPermissionsAction permissions env = SetupAction (setupPermissions permissions env) (const $ teardownPermissions permissions env) -- | Setup the given permissions to the graphql engine in a TestEnvironment. -setupPermissions :: [Permissions.Permission] -> TestEnvironment -> IO () +setupPermissions :: HasCallStack => [Permissions.Permission] -> TestEnvironment -> IO () setupPermissions permissions env = Permissions.setup BigQuery permissions env -- | Remove the given permissions from the graphql engine in a TestEnvironment. -teardownPermissions :: [Permissions.Permission] -> TestEnvironment -> IO () +teardownPermissions :: HasCallStack => [Permissions.Permission] -> TestEnvironment -> IO () teardownPermissions permissions env = Permissions.teardown BigQuery permissions env -- | We get @jobRateLimitExceeded@ errors from BigQuery if we run too many DML operations in short intervals.