diff --git a/server/lib/api-tests/src/Test/Schema/ComputedFields/TableSpec.hs b/server/lib/api-tests/src/Test/Schema/ComputedFields/TableSpec.hs index a2c357bbb4cf1..af2c0686575f2 100644 --- a/server/lib/api-tests/src/Test/Schema/ComputedFields/TableSpec.hs +++ b/server/lib/api-tests/src/Test/Schema/ComputedFields/TableSpec.hs @@ -16,6 +16,7 @@ import Harness.Quoter.Graphql (graphql) import Harness.Quoter.Yaml (interpolateYaml, yaml) import Harness.Test.BackendType qualified as BackendType import Harness.Test.Fixture qualified as Fixture +import Harness.Test.FixtureName (backendTypesForFixture) import Harness.Test.Permissions (Permission (SelectPermission), SelectPermissionDetails (..), selectPermission) import Harness.Test.Permissions qualified as Permission import Harness.Test.Schema (SchemaName (..), Table (..), table) @@ -102,6 +103,11 @@ articleTable = Schema.VStr "Article 3 Title", Schema.VStr "Article 3 by Author 2, has search keyword", Schema.VInt 2 + ], + [ Schema.VInt 4, + Schema.VStr "Article 4 Title", + Schema.VStr "Article 4 by unknown author", + Schema.VInt 3 ] ] } @@ -112,6 +118,7 @@ postgresSetupFunctions :: TestEnvironment -> [Fixture.SetupAction] postgresSetupFunctions testEnv = let schemaName = Schema.getSchemaName testEnv articleTableSQL = unSchemaName schemaName <> ".article" + authorTableSQL = unSchemaName schemaName <> ".author" in [ Fixture.SetupAction { Fixture.setupAction = Postgres.run_ testEnv $ @@ -140,6 +147,34 @@ postgresSetupFunctions testEnv = $$ LANGUAGE sql STABLE; |], Fixture.teardownAction = \_ -> pure () + }, + Fixture.SetupAction + { Fixture.setupAction = + Postgres.run_ testEnv $ + [i| + CREATE FUNCTION #{ fetch_author schemaName }(article_row article, filter_author_id int) + RETURNS author AS $$ + SELECT * + FROM #{ authorTableSQL } + WHERE id = article_row.author_id AND id = filter_author_id + LIMIT 1 + $$ LANGUAGE sql STABLE; + |], + Fixture.teardownAction = \_ -> pure () + }, + Fixture.SetupAction + { Fixture.setupAction = + Postgres.run_ testEnv $ + [i| + CREATE FUNCTION #{ fetch_author_no_user_args schemaName }(article_row article) + RETURNS author AS $$ + SELECT * + FROM #{ authorTableSQL } + WHERE id = article_row.author_id + LIMIT 1 + $$ LANGUAGE sql STABLE; + |], + Fixture.teardownAction = \_ -> pure () } ] @@ -147,6 +182,7 @@ bigquerySetupFunctions :: TestEnvironment -> [Fixture.SetupAction] bigquerySetupFunctions testEnv = let schemaName = Schema.getSchemaName testEnv articleTableSQL = unSchemaName schemaName <> ".article" + authorTableSQL = unSchemaName schemaName <> ".author" in [ Fixture.SetupAction { Fixture.setupAction = BigQuery.run_ $ @@ -174,6 +210,39 @@ bigquerySetupFunctions testEnv = ) |], Fixture.teardownAction = \_ -> pure () + }, + Fixture.SetupAction + { Fixture.setupAction = + BigQuery.run_ $ + [i| + CREATE TABLE FUNCTION + #{ fetch_author schemaName }(a_id INT64, filter_author_id INT64) + AS + ( + SELECT au.* + FROM #{ authorTableSQL } as au + JOIN #{ articleTableSQL } as ar + ON ar.author_id = au.id + WHERE ar.id = a_id AND au.id = filter_author_id + ) + |], + Fixture.teardownAction = \_ -> pure () + }, + Fixture.SetupAction + { Fixture.setupAction = + BigQuery.run_ $ + [i| + CREATE TABLE FUNCTION + #{ fetch_author_no_user_args schemaName }(a_id INT64) + AS + ( + SELECT au.* FROM #{ authorTableSQL } AS au + JOIN #{ articleTableSQL } as ar + ON ar.author_id = au.id + WHERE ar.id = a_id + ) + |], + Fixture.teardownAction = \_ -> pure () } ] @@ -185,6 +254,14 @@ fetch_articles_no_user_args :: SchemaName -> T.Text fetch_articles_no_user_args schemaName = unSchemaName schemaName <> ".fetch_articles_no_user_args" +fetch_author :: SchemaName -> T.Text +fetch_author schemaName = + unSchemaName schemaName <> ".fetch_author" + +fetch_author_no_user_args :: SchemaName -> T.Text +fetch_author_no_user_args schemaName = + unSchemaName schemaName <> ".fetch_author_no_user_args" + setupMetadata :: TestEnvironment -> [Fixture.SetupAction] setupMetadata testEnvironment = let backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnvironment @@ -203,9 +280,9 @@ setupMetadata testEnvironment = "search_articles" [yaml| a_id: id |] [yaml| - name: article - dataset: *schemaName - |] + name: article + dataset: *schemaName + |] testEnvironment, Fixture.teardownAction = \_ -> pure () }, @@ -262,6 +339,36 @@ setupMetadata testEnvironment = selectPermissionColumns = (["id", "name"] :: [Text]) }, Fixture.teardownAction = \_ -> pure () + }, + Fixture.SetupAction + { Fixture.setupAction = + Schema.trackComputedField + source + articleTable + "fetch_author" + "author" + [yaml| a_id: id |] + [yaml| + name: author + dataset: *schemaName + |] + testEnvironment, + Fixture.teardownAction = \_ -> pure () + }, + Fixture.SetupAction + { Fixture.setupAction = + Schema.trackComputedField + source + articleTable + "fetch_author_no_user_args" + "author_no_args" + [yaml| a_id: id |] + [yaml| + name: author + dataset: *schemaName + |] + testEnvironment, + Fixture.teardownAction = \_ -> pure () } ] @@ -473,3 +580,95 @@ tests opts = do id: 2 title: Article 2 Title |] + + it "Query single nullable value for non-SETOF function" $ \testEnv -> do + let schemaName = Schema.getSchemaName testEnv + TestEnvironment { fixtureName } = testEnv + + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnv + [graphql| + query { + #{schemaName}_article(order_by: {id: desc} limit: 2) { + id + title + author(args: {filter_author_id: 1}) { + id + name + } + } + } + |] + ) + if Fixture.Postgres `elem` backendTypesForFixture fixtureName then + [interpolateYaml| + data: + #{schemaName}_article: + - id: 4 + title: Article 4 Title + author: null + - id: 3 + title: Article 3 Title + author: null + |] + else + [interpolateYaml| + data: + #{schemaName}_article: + - id: 4 + title: Article 4 Title + author: [] + - id: 3 + title: Article 3 Title + author: [] + |] + + it "Query single nullable value for non-SETOF function without arguments" $ \testEnv -> do + let schemaName = Schema.getSchemaName testEnv + TestEnvironment { fixtureName } = testEnv + + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnv + [graphql| + query { + #{schemaName}_article(order_by: {id: desc} limit: 2) { + id + title + author_no_args { + id + name + } + } + } + |] + ) + if Fixture.Postgres `elem` backendTypesForFixture fixtureName then + [interpolateYaml| + data: + #{schemaName}_article: + - id: 4 + title: Article 4 Title + author_no_args: null + - id: 3 + title: Article 3 Title + author_no_args: + id: 2 + name: Author 2 + |] + else + [interpolateYaml| + data: + #{schemaName}_article: + - id: 4 + title: Article 4 Title + author_no_args: [] + - id: 3 + title: Article 3 Title + author_no_args: + - id: 2 + name: Author 2 + |] diff --git a/server/src-lib/Hasura/Backends/Postgres/DDL/BoolExp.hs b/server/src-lib/Hasura/Backends/Postgres/DDL/BoolExp.hs index c6260b02a193a..07e0d65175169 100644 --- a/server/src-lib/Hasura/Backends/Postgres/DDL/BoolExp.hs +++ b/server/src-lib/Hasura/Backends/Postgres/DDL/BoolExp.hs @@ -313,16 +313,18 @@ buildComputedFieldBooleanExp boolExpResolver rhsParser rootFieldInfoMap colInfoM [] -> do let hasuraSession = _berpSessionValue rhsParser computedFieldFunctionArgs = flip FunctionArgsExp mempty $ PG.fromComputedFieldImplicitArguments hasuraSession _cffComputedFieldImplicitArgs + cfbeFromTable tableName = do + tableBoolExp <- decodeValue colVal + tableFieldInfoMap <- askFieldInfoMapSource tableName + annTableBoolExp <- (getBoolExpResolver boolExpResolver) rhsParser tableFieldInfoMap tableFieldInfoMap $ unBoolExp tableBoolExp + pure $ CFBETable tableName annTableBoolExp AnnComputedFieldBoolExp _cfiXComputedFieldInfo _cfiName _cffName computedFieldFunctionArgs <$> case _cfiReturnType of CFRScalar scalarType -> CFBEScalar <$> parseBoolExpOperations (_berpValueParser rhsParser) rootFieldInfoMap colInfoMap (ColumnReferenceComputedField _cfiName scalarType) colVal - CFRSetofTable table -> do - tableBoolExp <- decodeValue colVal - tableFieldInfoMap <- askFieldInfoMapSource table - annTableBoolExp <- (getBoolExpResolver boolExpResolver) rhsParser tableFieldInfoMap tableFieldInfoMap $ unBoolExp tableBoolExp - pure $ CFBETable table annTableBoolExp + CFRTable t -> cfbeFromTable t + CFRSetofTable t -> cfbeFromTable t _ -> throw400 UnexpectedPayload diff --git a/server/src-lib/Hasura/Backends/Postgres/DDL/ComputedField.hs b/server/src-lib/Hasura/Backends/Postgres/DDL/ComputedField.hs index c731bcbb44228..3b7052cf20877 100644 --- a/server/src-lib/Hasura/Backends/Postgres/DDL/ComputedField.hs +++ b/server/src-lib/Hasura/Backends/Postgres/DDL/ComputedField.hs @@ -133,7 +133,9 @@ buildComputedFieldInfo trackedTables table _tableColumns computedField definitio MV.dispute $ pure $ CFVEReturnTableNotFound returnTable - pure $ PG.CFRSetofTable returnTable + pure $ if rfiReturnsSet rawFunctionInfo + then PG.CFRSetofTable returnTable + else PG.CFRTable returnTable else do let scalarType = _qptName functionReturnType unless (isBaseType functionReturnType) $ diff --git a/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs b/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs index 8bb57b54de012..cfb5558800b6e 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs @@ -146,6 +146,7 @@ instance computedFieldReturnType = \case Postgres.CFRScalar scalarType -> ReturnsScalar scalarType Postgres.CFRSetofTable table -> ReturnsTable table + Postgres.CFRTable table -> ReturnsTable table fromComputedFieldImplicitArguments = Postgres.fromComputedFieldImplicitArguments tableGraphQLName = Postgres.qualifiedObjectToName diff --git a/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs b/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs index 15fbf86194d77..6c9480b3be19f 100644 --- a/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs +++ b/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs @@ -14,10 +14,10 @@ module Hasura.Backends.Postgres.SQL.DML Extractor (..), FromExp (..), FromItem (..), - FunctionAlias (FunctionAlias), + FunctionAlias (FunctionAlias, _faIdentifier), FunctionDefinitionListItem (..), FunctionArgs (FunctionArgs), - FunctionExp (FunctionExp), + FunctionExp (FunctionExp, feAlias), GroupByExp (GroupByExp), HavingExp (HavingExp), JoinCond (..), diff --git a/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs b/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs index 09f6cfbf12df2..d19c14fc67b00 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs @@ -240,10 +240,28 @@ computedFieldPG ComputedFieldInfo {..} parentTable tableInfo = runMaybeT do ) dummyParser <- lift $ columnParser @('Postgres pgKind) (ColumnScalar scalarReturnType) (G.Nullability True) pure $ P.selection fieldName fieldDescription fieldArgsParser dummyParser + Postgres.CFRTable tableName -> do + otherTableInfo <- lift $ askTableInfo tableName + remotePerms <- hoistMaybe $ tableSelectPermissions roleName otherTableInfo + selectionSetParser <- MaybeT $ tableSelectionSet otherTableInfo + let fieldArgsParser = functionArgsParser + pure $ + P.subselection fieldName fieldDescription fieldArgsParser selectionSetParser + <&> \(functionArgs', fields) -> + IR.AFComputedField _cfiXComputedFieldInfo _cfiName $ + IR.CFSTable JASSingleObject $ + IR.AnnSelectG + { IR._asnFields = fields, + IR._asnFrom = IR.FromFunction (_cffName _cfiFunction) functionArgs' Nothing, + IR._asnPerm = tablePermissionsInfo remotePerms, + IR._asnArgs = noSelectArgs, + IR._asnStrfyNum = stringifyNumbers, + IR._asnNamingConvention = Just tCase + } Postgres.CFRSetofTable tableName -> do otherTableInfo <- lift $ askTableInfo tableName remotePerms <- hoistMaybe $ tableSelectPermissions roleName otherTableInfo - selectionSetParser <- MaybeT (fmap (P.multiple . P.nonNullableParser) <$> tableSelectionSet otherTableInfo) + selectionSetParser <- MaybeT (fmap (P.nonNullableParser . P.multiple . P.nonNullableParser) <$> tableSelectionSet otherTableInfo) selectArgsParser <- lift $ tableArguments otherTableInfo let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser pure $ diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/Process.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/Process.hs index ad83eb2618695..5cbdc6f7d49d0 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/Process.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/Process.hs @@ -342,10 +342,15 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields tCase = do fieldName PLSQNotRequired sel - let computedFieldTableSetSource = ComputedFieldTableSetSource fieldName selectSource + let selectSourceWithoutNulls = + case selectSource of + SelectSource {_ssFrom = S.FIFunc (S.FunctionExp {S.feAlias = Just (S.FunctionAlias {S._faIdentifier = fnIden})})} -> + selectSource {_ssWhere = S.BENotNull $ S.SERowIdentifier $ S.getTableAlias fnIden} + _ -> selectSource + computedFieldTableSetSource = ComputedFieldTableSetSource fieldName selectSourceWithoutNulls extractor = asJsonAggExtr selectTy (S.toColumnAlias fieldName) PLSQNotRequired $ - orderByForJsonAgg selectSource + orderByForJsonAgg selectSourceWithoutNulls pure ( computedFieldTableSetSource, extractor, diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs index fb25bd79545b8..4d0bdf68d436b 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs @@ -18,7 +18,7 @@ module Hasura.Backends.Postgres.Translate.Types SelectNode (SelectNode), SelectSlicing (SelectSlicing, _ssLimit, _ssOffset), SelectSorting (..), - SelectSource (SelectSource, _ssPrefix), + SelectSource (SelectSource, _ssPrefix, _ssWhere, _ssFrom), SortingAndSlicing (SortingAndSlicing), SourcePrefixes (..), SimilarArrayFields, diff --git a/server/src-lib/Hasura/Backends/Postgres/Types/ComputedField.hs b/server/src-lib/Hasura/Backends/Postgres/Types/ComputedField.hs index 9fa2ae3b4bb35..719d7e5776a3e 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Types/ComputedField.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Types/ComputedField.hs @@ -9,6 +9,7 @@ module Hasura.Backends.Postgres.Types.ComputedField fromComputedFieldImplicitArguments, ComputedFieldReturn (..), _CFRScalar, + _CFRTable, _CFRSetofTable, ) where @@ -108,6 +109,7 @@ fromComputedFieldImplicitArguments sess _ = [AESession sess, AETableRow] data ComputedFieldReturn = CFRScalar PGScalarType + | CFRTable QualifiedTable | CFRSetofTable QualifiedTable deriving (Show, Eq, Generic) diff --git a/server/src-lib/Hasura/RQL/IR/BoolExp.hs b/server/src-lib/Hasura/RQL/IR/BoolExp.hs index bd8cc818328ca..d15c42435df4d 100644 --- a/server/src-lib/Hasura/RQL/IR/BoolExp.hs +++ b/server/src-lib/Hasura/RQL/IR/BoolExp.hs @@ -392,7 +392,7 @@ opExpDepCol = \case data ComputedFieldBoolExp (backend :: BackendType) scalar = -- | SQL function returning a scalar CFBEScalar [OpExpG backend scalar] - | -- | SQL function returning SET OF table + | -- | SQL function returning table or SETOF table CFBETable (TableName backend) (AnnBoolExp backend scalar) deriving (Functor, Foldable, Traversable, Generic)