diff --git a/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/DirectiveName.hs b/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/DirectiveName.hs index 63dffaada5e37..8010c11683660 100644 --- a/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/DirectiveName.hs +++ b/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/DirectiveName.hs @@ -8,6 +8,9 @@ module Hasura.GraphQL.Parser.DirectiveName _skip, _ttl, __multiple_top_level_fields, + _overrideTo, + _lateral, + _nullable, ) where @@ -34,3 +37,12 @@ _ttl = [G.name|ttl|] __multiple_top_level_fields :: G.Name __multiple_top_level_fields = [G.name|_multiple_top_level_fields|] + +_overrideTo :: G.Name +_overrideTo = [G.name|override_to|] + +_lateral :: G.Name +_lateral = [G.name|lateral|] + +_nullable :: G.Name +_nullable = [G.name|nullable|] diff --git a/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Directives.hs b/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Directives.hs index 2fbff885fe0db..96836da528ba8 100644 --- a/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Directives.hs +++ b/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Directives.hs @@ -6,12 +6,17 @@ module Hasura.GraphQL.Parser.Directives customDirectives, -- Custom Directive Types CachedDirective (..), + extractDirectives, + getDirective, DirectiveMap, + ParsedDirectives, -- lookup keys for directives include, skip, cached, multipleRootFields, + nullableJoin, + lateralJoin, -- parsing utilities parseDirectives, withDirective, @@ -22,6 +27,8 @@ module Hasura.GraphQL.Parser.Directives includeDirective, cachedDirective, multipleRootFieldsDirective, + nullableDirective, + lateralDirective, ) where @@ -35,9 +42,12 @@ import Data.Functor.Identity (Identity (..)) import Data.GADT.Compare.Extended import Data.HashMap.Strict qualified as HashMap import Data.HashSet qualified as S +import Data.Hashable (Hashable) import Data.List qualified as L +import Data.Maybe (mapMaybe) import Data.Traversable (for) -import Data.Typeable (eqT) +import Data.Typeable (Typeable, cast, eqT) +import GHC.Generics (Generic) import Hasura.Base.ToErrorValue import Hasura.GraphQL.Parser.Class import Hasura.GraphQL.Parser.DirectiveName qualified as Name @@ -46,7 +56,7 @@ import Hasura.GraphQL.Parser.Internal.Scalars import Hasura.GraphQL.Parser.Schema import Hasura.GraphQL.Parser.Variable import Language.GraphQL.Draft.Syntax qualified as G -import Type.Reflection (Typeable, typeRep, (:~:) (Refl)) +import Type.Reflection (typeRep, (:~:) (Refl)) import Witherable (catMaybes) import Prelude @@ -91,7 +101,7 @@ inclusionDirectives :: forall m origin. (MonadParse m) => [Directive origin m] inclusionDirectives = [includeDirective @m, skipDirective @m] customDirectives :: forall m origin. (MonadParse m) => [Directive origin m] -customDirectives = [cachedDirective @m, multipleRootFieldsDirective @m] +customDirectives = [cachedDirective @m, multipleRootFieldsDirective @m, nullableDirective @m, lateralDirective @m] -- | Parses directives, given a location. Ensures that all directives are known -- and match the location; subsequently builds a dependent map of the results, @@ -241,6 +251,36 @@ include = DirectiveKey Name._include ifArgument :: (MonadParse m) => InputFieldsParser origin m Bool ifArgument = field Name._if Nothing boolean +-- Table relationships customization +nullableDirective :: forall m origin. (MonadParse m) => Directive origin m +nullableDirective = + mkDirective + Name._nullable -- Directive name + (Just "whether the JOIN allows null values (LEFT JOIN) or not (INNER JOIN)") -- Description + True -- Advertised in schema + [G.DLExecutable G.EDLFIELD] + (overrideToArgument False) + False -- Not repeatable + +lateralDirective :: forall m origin. (MonadParse m) => Directive origin m +lateralDirective = + mkDirective + Name._lateral -- Directive name + (Just "whether the JOIN is LATERAL. Only works on Object Relationships") -- Description + True -- Advertised in schema + [G.DLExecutable G.EDLFIELD] + (overrideToArgument False) + False -- Not repeatable + +nullableJoin :: DirectiveKey Bool +nullableJoin = DirectiveKey Name._nullable + +lateralJoin :: DirectiveKey Bool +lateralJoin = DirectiveKey Name._lateral + +overrideToArgument :: (MonadParse m) => Bool -> InputFieldsParser origin m Bool +overrideToArgument defaultValue = fieldWithDefault Name._overrideTo Nothing (G.VBoolean defaultValue) boolean + -- Parser type for directives. data Directive origin m where @@ -275,6 +315,34 @@ instance GCompare DirectiveKey where type DirectiveMap = DM.DMap DirectiveKey Identity +type ParsedDirectives = HashMap.HashMap G.Name DirectiveData + +data DirectiveData + = NullableData Bool + | LateralData Bool + deriving (Generic, Show) + +instance Hashable DirectiveData + +deriving instance Eq DirectiveData + +extractDirectives :: DirectiveMap -> ParsedDirectives +extractDirectives dmap = HashMap.fromList $ mapMaybe extractPair $ DM.toList dmap + where + extractPair :: DSum DirectiveKey Identity -> Maybe (G.Name, DirectiveData) + extractPair (DirectiveKey name :=> Identity value) = + case cast value of + Just nullableVal | name == Name._nullable -> Just (name, NullableData nullableVal) + Just lateralVal | name == Name._lateral -> Just (name, LateralData lateralVal) + _ -> Nothing + +getDirective :: (Typeable a) => G.Name -> ParsedDirectives -> Maybe a +getDirective name directives = do + directiveData <- HashMap.lookup name directives + case directiveData of + NullableData val -> cast val + LateralData val -> cast val + mkDirective :: (MonadParse m, Typeable a) => G.Name -> diff --git a/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Internal/Parser.hs b/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Internal/Parser.hs index d9abf26b6f2d6..dd87e77b074c9 100644 --- a/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Internal/Parser.hs +++ b/server/lib/schema-parsers/src/Hasura/GraphQL/Parser/Internal/Parser.hs @@ -397,7 +397,19 @@ rawSelection name description argumentsParser resultParser = { fDefinition = Definition name description Nothing [] $ FieldInfo (ifDefinitions argumentsParser) (pType resultParser), - fParser = \Field {_fAlias, _fArguments, _fSelectionSet} -> do + fParser = \Field {_fAlias, _fArguments, _fSelectionSet, _fDirectives} -> do + dirMap <- parseDirectives customDirectives (DLExecutable EDLFIELD) _fDirectives + + -- If the directive is present but this is not a selectionSet (Array or Object type), throw an error + withDirective dirMap nullableJoin $ \maybeDirective -> + case maybeDirective of + Just _ -> parseError $ "The @nullable directive can only be used on fields with selection sets (objects or arrays)" + Nothing -> pure () + withDirective dirMap lateralJoin $ \maybeDirective -> + case maybeDirective of + Just _ -> parseError $ "The @lateral directive can only be used on object relationship fields" + Nothing -> pure () + unless (null _fSelectionSet) $ parseError "unexpected subselection set for non-object field" -- check for extraneous arguments here, since the InputFieldsParser just @@ -434,11 +446,11 @@ subselection :: InputFieldsParser origin m a -> -- | parser for the subselection set Parser origin 'Output m b -> - FieldParser origin m (a, b) + FieldParser origin m (a, b, ParsedDirectives) {-# INLINE subselection #-} subselection name description argumentsParser bodyParser = rawSubselection name description argumentsParser bodyParser - <&> \(_alias, _args, a, b) -> (a, b) + <&> \(_alias, _args, a, b, directives) -> (a, b, directives) rawSubselection :: forall m origin a b. @@ -449,21 +461,35 @@ rawSubselection :: InputFieldsParser origin m a -> -- | parser for the subselection set Parser origin 'Output m b -> - FieldParser origin m (Maybe Name, HashMap Name (Value Variable), a, b) + FieldParser origin m (Maybe Name, HashMap Name (Value Variable), a, b, ParsedDirectives) {-# INLINE rawSubselection #-} rawSubselection name description argumentsParser bodyParser = FieldParser { fDefinition = Definition name description Nothing [] $ FieldInfo (ifDefinitions argumentsParser) (pType bodyParser), - fParser = \Field {_fAlias, _fArguments, _fSelectionSet} -> do + fParser = \Field {_fAlias, _fArguments, _fSelectionSet, _fDirectives} -> do + dirMap <- parseDirectives customDirectives (DLExecutable EDLFIELD) _fDirectives + + hasLateralDirective <- withDirective dirMap lateralJoin $ pure . Maybe.isJust + when hasLateralDirective $ do + let fieldType = pType bodyParser + let isArrayType = case fieldType of + TList _ _ -> True + _ -> False + + when isArrayType $ + parseError $ + "The @lateral directive cannot be used on array relationship fields" + -- check for extraneous arguments here, since the InputFieldsParser just -- handles parsing the fields it cares about for_ (HashMap.keys _fArguments) \argumentName -> unless (argumentName `S.member` argumentNames) $ parseError $ toErrorValue name <> " has no argument named " <> toErrorValue argumentName - (_fAlias,_fArguments,,) + let parsedDirectives = extractDirectives dirMap + (_fAlias,_fArguments,,,parsedDirectives) <$> withKey (Key "args") (ifParser argumentsParser $ GraphQLValue <$> _fArguments) <*> pParser bodyParser _fSelectionSet } @@ -488,7 +514,8 @@ subselection_ :: Maybe Description -> -- | parser for the subselection set Parser origin 'Output m a -> - FieldParser origin m a + FieldParser origin m (a, ParsedDirectives) {-# INLINE subselection_ #-} subselection_ name description bodyParser = - snd <$> subselection name description (pure ()) bodyParser + rawSubselection name description (pure ()) bodyParser + <&> \(_alias, _args, (), result, directives) -> (result, directives) diff --git a/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs b/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs index 3062263d9b2cf..2c19caba4b758 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs @@ -429,7 +429,7 @@ bqComputedField ComputedFieldInfo {..} tableName tableInfo = runMaybeT do let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser pure $ P.subselection fieldName fieldDescription fieldArgsParser selectionSetParser - <&> \((functionArgs', args), fields) -> + <&> \((functionArgs', args), fields, directives) -> IR.AFComputedField _cfiXComputedFieldInfo _cfiName $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG @@ -437,6 +437,7 @@ bqComputedField ComputedFieldInfo {..} tableName tableInfo = runMaybeT do IR._asnFrom = IR.FromFunction (_cffName _cfiFunction) functionArgs' Nothing, IR._asnPerm = tablePermissionsInfo returnTablePermissions, IR._asnArgs = args, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Nothing } @@ -456,7 +457,7 @@ bqComputedField ComputedFieldInfo {..} tableName tableInfo = runMaybeT do <&> parsedSelectionsToFields IR.AFExpression pure $ P.subselection fieldName fieldDescription functionArgsParser selectionSetParser - <&> \(functionArgs', fields) -> + <&> \(functionArgs', fields, directives) -> IR.AFComputedField _cfiXComputedFieldInfo _cfiName $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG @@ -464,6 +465,7 @@ bqComputedField ComputedFieldInfo {..} tableName tableInfo = runMaybeT do IR._asnFrom = IR.FromFunction (_cffName _cfiFunction) functionArgs' Nothing, IR._asnPerm = IR.noTablePermissions, IR._asnArgs = IR.noSelectArgs, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Nothing } diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Schema.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Schema.hs index ed221446178a8..6d0bbbe1b47a7 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Schema.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Schema.hs @@ -205,12 +205,13 @@ selectFunction mkRootFieldName fi@RQL.FunctionInfo {..} description = runMaybeT functionFieldName = RQL.runMkRootFieldName mkRootFieldName _fiGQLName pure $ P.subselection functionFieldName description argsParser selectionSetParser - <&> \((funcArgs, tableArgs''), fields) -> + <&> \((funcArgs, tableArgs''), fields, directives) -> IR.AnnSelectG { IR._asnFields = fields, IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing, IR._asnPerm = GS.C.tablePermissionsInfo selectPermissions, IR._asnArgs = tableArgs'', + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -439,7 +440,7 @@ updateCustomOp (API.UpdateColumnOperatorName operatorName) operatorUsages = GS.U argumentType <- ( HashMap.lookup columnScalarType operatorUsages <&> (\API.UpdateColumnOperatorDefinition {..} -> RQL.ColumnScalar $ Witch.from _ucodArgumentType) - ) + ) -- This shouldn't happen 😬 because updateOperatorApplicableColumn should protect this -- parser from being used with unsupported column types `onNothing` throw500 ("updateOperatorParser: Unable to find argument type for update column operator " <> toTxt operatorName <> " used with column scalar type " <> toTxt columnScalarType) diff --git a/server/src-lib/Hasura/Backends/MSSQL/FromIr/MutationResponse.hs b/server/src-lib/Hasura/Backends/MSSQL/FromIr/MutationResponse.hs index c34e44397debb..fd286c6dec487 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/FromIr/MutationResponse.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/FromIr/MutationResponse.hs @@ -50,7 +50,7 @@ mkMutationOutputSelect stringifyNum withAlias = \case IR.Fields (IR.AnnFieldG 'MSSQL Void Expression) -> FromIr Select mkSelect jsonAggSelect annFields = do - let annSelect = IR.AnnSelectG annFields (IR.FromIdentifier $ IR.FIIdentifier withAlias) IR.noTablePermissions IR.noSelectArgs stringifyNum Nothing + let annSelect = IR.AnnSelectG annFields (IR.FromIdentifier $ IR.FIIdentifier withAlias) IR.noTablePermissions IR.noSelectArgs Nothing stringifyNum Nothing fromSelect jsonAggSelect annSelect -- SELECT COUNT(*) AS "count" FROM [with_alias] diff --git a/server/src-lib/Hasura/Backends/MSSQL/FromIr/Query.hs b/server/src-lib/Hasura/Backends/MSSQL/FromIr/Query.hs index fc813be425c95..09d657a0d4d46 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/FromIr/Query.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/FromIr/Query.hs @@ -205,6 +205,7 @@ fromRemoteRelationFieldsG existingJoins joinColumns (IR.FieldName name, field) = (IR.RelName $ mkNonEmptyTextUnsafe name) joinColumns IR.Nullable + Nothing annotatedRelationship -- | Top/root-level 'Select'. All descendent/sub-translations are collected to produce a root TSQL.Select. @@ -280,7 +281,7 @@ mkNodesSelect Args {..} foreignKeyConditions filterExpression permissionBasedTop aliasedAlias = IR.getFieldNameTxt fieldName } ) - | (index, (fieldName, fieldSources)) <- nodes + | (index, (fieldName, fieldSources)) <- nodes ] -- @@ -335,7 +336,7 @@ mkAggregateSelect Args {..} foreignKeyConditions filterExpression selectFrom agg aliasedAlias = IR.getFieldNameTxt fieldName } ) - | (index, (fieldName, projections)) <- aggregates + | (index, (fieldName, projections)) <- aggregates ] fromNativeQuery :: IR.NativeQuery 'MSSQL Expression -> FromIr TSQL.From diff --git a/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs b/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs index 5bbc8f53350c0..29e8da1dfffdc 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs @@ -312,7 +312,7 @@ mutateAndFetchCols userInfo qt cols (cte, p) strfyNum tCase = do <$> mkSQLSelect userInfo JASMultipleRows - ( AnnSelectG selFlds tabFrom tabPerm noSelectArgs strfyNum tCase + ( AnnSelectG selFlds tabFrom tabPerm noSelectArgs Nothing strfyNum tCase ) let selectWith = S.SelectWith @@ -473,9 +473,9 @@ validateDeleteMutation env manager logger userInfo resolvedWebHook confHeaders t -- id -- } -- } - do - let deleteInputValByPk = J.object ["pk_columns" J..= deleteInputVal] - return (J.object ["input" J..= [deleteInputValByPk]]) + do + let deleteInputValByPk = J.object ["pk_columns" J..= deleteInputVal] + return (J.object ["input" J..= [deleteInputValByPk]]) else return (J.object ["input" J..= [deleteInputVal]]) Nothing -> return J.Null validateMutation env manager logger userInfo resolvedWebHook confHeaders timeout forwardClientHeaders reqHeaders inputData headerPrecedence diff --git a/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs b/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs index 985005a2be6be..74ea2f1ef5d44 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Schema/Select.hs @@ -80,12 +80,13 @@ selectFunction mkRootFieldName fi@FunctionInfo {..} description = runMaybeT do functionFieldName = runMkRootFieldName mkRootFieldName _fiGQLName pure $ P.subselection functionFieldName description argsParser selectionSetParser - <&> \((funcArgs, tableArgs'), fields) -> + <&> \((funcArgs, tableArgs'), fields, directives) -> IR.AnnSelectG { IR._asnFields = fields, IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing, IR._asnPerm = tablePermissionsInfo selectPermissions, IR._asnArgs = tableArgs', + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -134,17 +135,18 @@ selectFunctionAggregate mkRootFieldName fi@FunctionInfo {..} description = runMa $ P.selectionSet selectionName Nothing - [ IR.TAFNodes xNodesAgg <$> P.subselection_ Name._nodes Nothing nodesParser, - IR.TAFAgg <$> P.subselection_ Name._aggregate Nothing aggregateParser + [ IR.TAFNodes xNodesAgg <$> (fst <$> P.subselection_ Name._nodes Nothing nodesParser), + IR.TAFAgg <$> (fst <$> P.subselection_ Name._aggregate Nothing aggregateParser) ] pure $ P.subselection aggregateFieldName description argsParser aggregationParser - <&> \((funcArgs, tableArgs'), fields) -> + <&> \((funcArgs, tableArgs'), fields, directives) -> IR.AnnSelectG { IR._asnFields = fields, IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing, IR._asnPerm = tablePermissionsInfo selectPermissions, IR._asnArgs = tableArgs', + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -181,7 +183,7 @@ selectFunctionConnection mkRootFieldName fi@FunctionInfo {..} description pkeyCo let argsParser = liftA2 (,) functionArgsParser tableConnectionArgsParser pure $ P.subselection fieldName description argsParser selectionSetParser - <&> \((funcArgs, (args, split, slice)), fields) -> + <&> \((funcArgs, (args, split, slice)), fields, directives) -> IR.ConnectionSelect { IR._csXRelay = xRelayInfo, IR._csPrimaryKeyColumns = pkeyColumns, @@ -193,6 +195,7 @@ selectFunctionConnection mkRootFieldName fi@FunctionInfo {..} description pkeyCo IR._asnFrom = IR.FromFunction _fiSQLName funcArgs Nothing, IR._asnPerm = tablePermissionsInfo selectPermissions, IR._asnArgs = args, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -248,7 +251,7 @@ computedFieldPG ComputedFieldInfo {..} parentTable tableInfo = runMaybeT do let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser pure $ P.subselection fieldName fieldDescription fieldArgsParser selectionSetParser - <&> \((functionArgs', args), fields) -> + <&> \((functionArgs', args), fields, directives) -> IR.AFComputedField _cfiXComputedFieldInfo _cfiName $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG @@ -256,6 +259,7 @@ computedFieldPG ComputedFieldInfo {..} parentTable tableInfo = runMaybeT do IR._asnFrom = IR.FromFunction (_cffName _cfiFunction) functionArgs' Nothing, IR._asnPerm = tablePermissionsInfo remotePerms, IR._asnArgs = args, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs index 177ad6057eb96..94ac08adf96f1 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs @@ -114,7 +114,7 @@ mkMutFldExp userInfo cteAlias preCalAffRows strfyNum tCase = \case <$> mkSQLSelect userInfo JASMultipleRows - ( AnnSelectG selFlds tabFrom tabPerm noSelectArgs strfyNum tCase + ( AnnSelectG selFlds tabFrom tabPerm noSelectArgs Nothing strfyNum tCase ) toFIIdentifier :: TableIdentifier -> FIIdentifier @@ -213,7 +213,7 @@ mkMutationOutputExp userInfo qt allCols preCalAffRows cte mutOutput strfyNum tCa <$> mkSQLSelect userInfo JASSingleObject - ( AnnSelectG annFlds tabFrom tabPerm noSelectArgs strfyNum tCase + ( AnnSelectG annFlds tabFrom tabPerm noSelectArgs Nothing strfyNum tCase ) mkCheckErrorExp :: TableIdentifier -> S.SQLExp diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/GenerateSelect.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/GenerateSelect.hs index 0e1eee659d19d..0762a1a229212 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/GenerateSelect.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/GenerateSelect.hs @@ -32,6 +32,8 @@ import Hasura.Backends.Postgres.Translate.Select.Internal.Helpers startCursorIdentifier, ) import Hasura.Backends.Postgres.Translate.Types +import Hasura.GraphQL.Parser.DirectiveName (_lateral, _nullable) +import Hasura.GraphQL.Parser.Directives (ParsedDirectives, getDirective) import Hasura.Prelude import Hasura.RQL.IR.Select (ConnectionSlice (SliceFirst, SliceLast)) import Hasura.RQL.Types.BackendType (PostgresKind (..)) @@ -156,6 +158,33 @@ applyCockroachDistinctOnWorkaround select = replaceOrderByItem replacementExtractors orderByItem = orderByItem {S.oExpression = replaceExp replacementExtractors (S.oExpression orderByItem)} +-- | Given an optional ParsedDirectives HashMap and a Nullable flag, +-- extract the join type and lateral flag. +extractJoinTypeAndLateral :: + Maybe ParsedDirectives -> Nullable -> (S.JoinType, Bool) +extractJoinTypeAndLateral maybeDirectives nullable = + let -- Extract nullable directive value (default to Nothing if not present) + nullableDirectiveValue = case maybeDirectives of + Just parsedDirectives -> getDirective _nullable parsedDirectives + Nothing -> Nothing + -- Extract lateral directive value (default to Nothing if not present) + lateralDirectiveValue = case maybeDirectives of + Just parsedDirectives -> getDirective _lateral parsedDirectives + Nothing -> Nothing + -- Determine join type based on nullable directive or default behavior + joinType = case nullableDirectiveValue of + Just True -> S.LeftOuter + Just False -> S.Inner + Nothing -> + case nullable of + Nullable -> S.LeftOuter + NotNullable -> S.Inner + -- Determine lateral based on lateral directive (default to True) + lateral = case lateralDirectiveValue of + Just lateralValue -> lateralValue + Nothing -> True -- Default to LATERAL JOIN + in (joinType, lateral) + defaultGenerateSQLSelect :: forall pgKind. (PostgresGenerateSQLSelect pgKind) => @@ -216,11 +245,13 @@ defaultGenerateSQLSelect selectRewriter joinCondition selectSource selectNode = S.WhereFrag $ S.simplifyBoolExp $ S.BEBin S.AndOp joinCond whereCond -- function to create a joined from item from two from items + leftOuterJoin :: S.FromItem -> (S.FromItem, S.JoinType) -> S.FromItem + leftOuterJoin current (S.FIJoin (S.JoinExpr _ joinType rhs joinCond), _) = + -- If the new item is already a JoinExpr create a new join with the current item as LHS + S.FIJoin $ S.JoinExpr current joinType rhs joinCond leftOuterJoin current (new, joinType) = - S.FIJoin - $ S.JoinExpr current joinType new - $ S.JoinOn - $ S.BELit True + -- For regular items or lateral joins, create a simple join with TRUE condition + S.FIJoin $ S.JoinExpr current joinType new $ S.JoinOn $ S.BELit True -- this is the from eexp for the final select joinedFrom :: S.FromItem @@ -236,26 +267,64 @@ defaultGenerateSQLSelect selectRewriter joinCondition selectSource selectNode = objectRelationToFromItem (objectRelationSource, node) = let ObjectRelationSource { _orsRelationMapping = colMapping, - _orsSelectSource = objectSelectSource, - _orsNullable = nullable + _orsSelectSource = objSelectSource, + _orsNullable = nullable, + _orsDirectives = directives } = objectRelationSource - alias = S.toTableAlias $ _ossPrefix objectSelectSource - source = objectSelectSourceToSelectSource objectSelectSource - select = generateSQLSelect @pgKind (mkJoinCond baseSelectIdentifier colMapping) source node - joinType = case nullable of - Nullable -> S.LeftOuter - NotNullable -> S.Inner - in (S.mkLateralFromItem select alias, joinType) + -- Extract the components from ObjectSelectSource properly with different variable names + ObjectSelectSource objPrefix _ _ = objSelectSource + alias = S.toTableAlias objPrefix + + (joinType, lateral) = extractJoinTypeAndLateral directives nullable + in case lateral of + False -> + let -- Use joinTableIdentifier for the join condition, not the base alias + joinTableIdentifier = S.tableAliasToIdentifier alias + joinCond = mkJoinCond baseSelectIdentifier joinTableIdentifier colMapping + source = objectSelectSourceToSelectSourceWithLimit objSelectSource NoLimit + + -- For the subquery that extracts columns, still use the base table alias + sourceBaseAlias = mkBaseTableAlias (S.toTableAlias $ _ssPrefix source) + + originalSelect = generateSQLSelect @pgKind (S.BELit True) source node + + -- Use the base alias for adding dynamic extractors + select = addDynamicExtractors sourceBaseAlias colMapping originalSelect + selectFromItem = S.mkSelFromItem select alias + joinItem = + S.FIJoin + $ S.JoinExpr + (S.FIIdentifier baseSelectIdentifier) + joinType + selectFromItem + (S.JoinOn joinCond) + in (joinItem, joinType) + True -> + -- For lateral joins, use the base table alias similarly + -- Make sure to use te limit 1 + let source = objectSelectSourceToSelectSource objSelectSource + joinCondWithBaseAlias = mkJoinCondWithoutPrefix baseSelectIdentifier colMapping + select = generateSQLSelect @pgKind joinCondWithBaseAlias source node + in (S.mkLateralFromItem select alias, joinType) arrayRelationToFromItem :: (ArrayRelationSource, MultiRowSelectNode) -> (S.FromItem, S.JoinType) arrayRelationToFromItem (arrayRelationSource, arraySelectNode) = - let ArrayRelationSource _ colMapping source = arrayRelationSource + let ArrayRelationSource _ colMapping source nullable directives = arrayRelationSource alias = S.toTableAlias $ _ssPrefix source - select = - generateSQLSelectFromArrayNode @pgKind source arraySelectNode - $ mkJoinCond baseSelectIdentifier colMapping - in (S.mkLateralFromItem select alias, S.LeftOuter) + + joinType = case directives of + Just parsedDirectives -> + case getDirective _nullable parsedDirectives of + Just True -> S.LeftOuter + Just False -> S.Inner + Nothing -> if nullable == Nullable then S.LeftOuter else S.Inner + Nothing -> + if nullable == Nullable then S.LeftOuter else S.Inner + + joinCondWithBaseAlias = mkJoinCondWithoutPrefix baseSelectIdentifier colMapping + select = generateSQLSelectFromArrayNode @pgKind source arraySelectNode joinCondWithBaseAlias + in (S.mkLateralFromItem select alias, joinType) arrayConnectionToFromItem :: (ArrayConnectionSource, MultiRowSelectNode) -> (S.FromItem, S.JoinType) @@ -298,12 +367,67 @@ generateSQLSelectFromArrayNode selectSource (MultiRowSelectNode topExtractors se ] } -mkJoinCond :: S.TableIdentifier -> HashMap PGCol PGCol -> S.BoolExp -mkJoinCond baseTablepfx colMapn = +mkJoinCond :: + -- | Base table identifier + S.TableIdentifier -> + -- | Joined table identifier (to qualify the right-hand side) + S.TableIdentifier -> + HashMap PGCol PGCol -> + S.BoolExp +mkJoinCond baseTable joinTable colMapn = + foldl' (S.BEBin S.AndOp) (S.BELit True) + $ flip map (HashMap.toList colMapn) + $ \(lCol, rCol) -> + S.BECompare + S.SEQ + (S.mkQIdenExp baseTable lCol) + (S.mkQIdenExp joinTable rCol) + +mkJoinCondWithoutPrefix :: + -- | Base table identifier + S.TableIdentifier -> + HashMap PGCol PGCol -> + S.BoolExp +mkJoinCondWithoutPrefix baseTable colMapn = foldl' (S.BEBin S.AndOp) (S.BELit True) $ flip map (HashMap.toList colMapn) $ \(lCol, rCol) -> - S.BECompare S.SEQ (S.mkQIdenExp baseTablepfx lCol) (S.mkSIdenExp rCol) + S.BECompare + S.SEQ + (S.mkQIdenExp baseTable lCol) + (S.mkSIdenExp rCol) + +addDynamicExtractors :: + -- | The alias for the source subquery (e.g. "_root.or.firstAppearedInBlock.base") + S.TableAlias -> + -- | The join mapping (left column, right column) + HashMap PGCol PGCol -> + -- | The original select for the relation + S.Select -> + -- | The select with extra extractors added + S.Select +addDynamicExtractors baseAlias colMapping select = + let extraExtractors = + mapMaybe + ( \(_lCol, rCol) -> + let colAlias = S.toColumnAlias rCol + exists = + any + ( \(S.Extractor _ maybeAlias) -> + maybe False (== colAlias) maybeAlias + ) + (S.selExtr select) + in if exists + then Nothing + else + Just + ( S.Extractor + (S.mkQIdenExp (S.tableAliasToIdentifier baseAlias) rCol) + (Just colAlias) + ) + ) + (HashMap.toList colMapping) + in select {S.selExtr = S.selExtr select <> extraExtractors} connectionToSelectWith :: forall pgKind. @@ -350,7 +474,7 @@ connectionToSelectWith rootSelectAlias arrayConnectionSource arraySelectNode = endRowNumberExp = mkLastElementExp $ S.SEIdentifier rowNumberIdentifier fromBaseSelections = - let joinCond = mkJoinCond rootSelectIdentifier columnMapping + let joinCond = mkJoinCond rootSelectIdentifier (S.tableAliasToIdentifier baseSelectAlias) columnMapping baseSelectFrom = S.mkSelFromItem (generateSQLSelect @pgKind joinCond selectSource selectNode) diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/OrderBy.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/OrderBy.hs index cca9d0b7352cd..be57459507579 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/OrderBy.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Internal/OrderBy.hs @@ -133,9 +133,11 @@ processOrderByItems userInfo sourcePrefix' selectSourceQual fieldAlias' similarA $ S.mkQIdenExp baseTableIdentifier $ ciColumn pgColInfo AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do - let RelInfo {riName = relName, riMapping = RelMapping colMapping, riTarget = relTarget} = relInfo + let RelInfo {riName = relName, riMapping = RelMapping colMapping, riTarget = relTarget, riManualNullable = manualNullable, riIsManual = isManual} = relInfo relSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName fieldName = mkOrderByFieldName relName + -- Determine nullability based on manual relationship settings + nullable = if isManual && not manualNullable then NotNullable else Nullable case relTarget of RelTargetNativeQuery _ -> error "processAnnotatedOrderByElement RelTargetNativeQuery (AOCObjectRelation)" RelTargetTable relTable -> do @@ -147,14 +149,16 @@ processOrderByItems userInfo sourcePrefix' selectSourceQual fieldAlias' similarA (tableIdentifierToIdentifier relSourcePrefix) (S.FISimple relTable Nothing) boolExp - relSource = ObjectRelationSource relName colMapping selectSource Nullable + relSource = ObjectRelationSource relName colMapping selectSource nullable Nothing pure ( relSource, InsOrdHashMap.singleton relOrderByAlias relOrdByExp, S.mkQIdenExp relSourcePrefix relOrderByAlias ) AOCArrayAggregation relInfo relFilter aggOrderBy -> withWriteArrayRelation $ do - let RelInfo {riName = relName, riMapping = RelMapping colMapping, riTarget = relTarget} = relInfo + let RelInfo {riName = relName, riMapping = RelMapping colMapping, riTarget = relTarget, riManualNullable = manualNullable, riIsManual = isManual} = relInfo + -- Determine nullability based on manual relationship settings + nullable = if isManual && not manualNullable then NotNullable else Nullable case relTarget of RelTargetNativeQuery _ -> error "processAnnotatedOrderByElement RelTargetNativeQuery (AOCArrayAggregation)" RelTargetTable relTable -> do @@ -174,7 +178,7 @@ processOrderByItems userInfo sourcePrefix' selectSourceQual fieldAlias' similarA (S.FISimple relTable Nothing) boolExp noSortingAndSlicing - relSource = ArrayRelationSource relAlias colMapping selectSource + relSource = ArrayRelationSource relAlias colMapping selectSource nullable Nothing extractorExps <- aggregateFieldsToExtractorExps relSourcePrefix userInfo fields pure ( relSource, 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 be2fd5df42ac9..27790ad74789a 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 @@ -273,7 +273,7 @@ processAnnAggregateSelect userInfo sourcePrefixes fieldAlias annAggSel = do pure (selectSource, nodeExtractors, topLevelExtractor) where - AnnSelectG aggSelFields tableFrom tablePermissions tableArgs strfyNum tCase = annAggSel + AnnSelectG aggSelFields tableFrom tablePermissions tableArgs _ strfyNum tCase = annAggSel permLimit = _tpLimit tablePermissions orderBy = _saOrderBy tableArgs permLimitSubQuery = mkPermissionLimitSubQuery permLimit aggSelFields orderBy @@ -335,7 +335,7 @@ processAnnFields userInfo sourcePrefix fieldAlias annFields tCase = do AFNodeId _ sn tn pKeys -> pure $ mkNodeId sn tn pKeys AFColumn c -> toSQLCol c AFObjectRelation objSel -> withWriteObjectRelation $ do - let AnnRelationSelectG relName relMapping nullable annObjSel = objSel + let AnnRelationSelectG relName relMapping nullable directives annObjSel = objSel AnnObjectSelectG objAnnFields target targetFilter = annObjSel (objRelSourcePrefix, ident, filterExp) <- case target of FromNativeQuery nq -> do @@ -367,7 +367,7 @@ processAnnFields userInfo sourcePrefix fieldAlias annFields tCase = do other -> error $ "processAnnFields: " <> show other let sourcePrefixes = mkSourcePrefixes objRelSourcePrefix selectSource = ObjectSelectSource (_pfThis sourcePrefixes) ident filterExp - objRelSource = ObjectRelationSource relName relMapping selectSource nullable + objRelSource = ObjectRelationSource relName relMapping selectSource nullable directives annFieldsExtr <- processAnnFields userInfo (identifierToTableIdentifier $ _pfThis sourcePrefixes) fieldName objAnnFields tCase pure ( objRelSource, @@ -510,7 +510,7 @@ processArrayRelation :: processArrayRelation userInfo sourcePrefixes fieldAlias relAlias arrSel _tCase = case arrSel of ASSimple annArrRel -> withWriteArrayRelation $ do - let AnnRelationSelectG _ colMapping _ sel = annArrRel + let AnnRelationSelectG _ colMapping nullable directives sel = annArrRel permLimitSubQuery = maybe PLSQNotRequired PLSQRequired $ _tpLimit $ _asnPerm sel (source, nodeExtractors) <- @@ -522,23 +522,23 @@ processArrayRelation userInfo sourcePrefixes fieldAlias relAlias arrSel _tCase = permLimitSubQuery $ orderByForJsonAgg source pure - ( ArrayRelationSource relAlias colMapping source, + ( ArrayRelationSource relAlias colMapping source nullable directives, topExtr, nodeExtractors, () ) ASAggregate aggSel -> withWriteArrayRelation $ do - let AnnRelationSelectG _ colMapping _ sel = aggSel + let AnnRelationSelectG _ colMapping nullable directives sel = aggSel (source, nodeExtractors, topExtr) <- processAnnAggregateSelect userInfo sourcePrefixes fieldAlias sel pure - ( ArrayRelationSource relAlias colMapping source, + ( ArrayRelationSource relAlias colMapping source nullable directives, topExtr, nodeExtractors, () ) ASConnection connSel -> withWriteArrayConnection $ do - let AnnRelationSelectG _ colMapping _ sel = connSel + let AnnRelationSelectG _ colMapping _ _ sel = connSel (source, topExtractor, nodeExtractors) <- processConnectionSelect userInfo sourcePrefixes fieldAlias relAlias colMapping sel pure @@ -634,7 +634,7 @@ processAnnSimpleSelect userInfo sourcePrefixes fieldAlias permLimitSubQuery annS let allExtractors = InsOrdHashMap.fromList $ annFieldsExtr : orderByAndDistinctExtrs pure (selectSource, allExtractors) where - AnnSelectG annSelFields tableFrom tablePermissions tableArgs _ tCase = annSimpleSel + AnnSelectG annSelFields tableFrom tablePermissions tableArgs _ _ tCase = annSimpleSel similarArrayFields = mkSimilarArrayFields annSelFields $ _saOrderBy tableArgs @@ -695,7 +695,7 @@ processConnectionSelect userInfo sourcePrefixes fieldAlias relAlias colMapping c ) where ConnectionSelect _ primaryKeyColumns maybeSplit maybeSlice select = connectionSelect - AnnSelectG fields selectFrom tablePermissions tableArgs _ tCase = select + AnnSelectG fields selectFrom tablePermissions tableArgs _ _ tCase = select fieldIdentifier = toIdentifier fieldAlias thisPrefix = identifierToTableIdentifier $ _pfThis sourcePrefixes permLimitSubQuery = PLSQNotRequired diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Streaming.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Streaming.hs index b1648c5419bf1..4be8e04eeaaa0 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Streaming.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Select/Streaming.hs @@ -109,7 +109,7 @@ mkStreamSQLSelect userInfo (AnnSelectStreamG () fields from perm args strfyNum) _saOffset = Nothing, _saDistinct = Nothing } - sqlSelect = AnnSelectG fields from perm selectArgs strfyNum Nothing + sqlSelect = AnnSelectG fields from perm selectArgs Nothing strfyNum Nothing permLimitSubQuery = PLSQNotRequired ((selectSource, nodeExtractors), SelectWriter {_swJoinTree = joinTree, _swCustomSQLCTEs = customSQLCTEs}) <- runWriterT diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs index 15ad6ad3ee8c8..54cbca5eec6e2 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Types.hs @@ -27,15 +27,19 @@ module Hasura.Backends.Postgres.Translate.Types SelectWriter (..), applySortingAndSlicing, noSortingAndSlicing, + Limitation (..), objectSelectSourceToSelectSource, + objectSelectSourceToSelectSourceWithLimit, orderByForJsonAgg, ) where import Data.HashMap.Strict qualified as HashMap +import Data.Hashable (Hashable (..)) import Data.Int (Int64) import Hasura.Backends.Postgres.SQL.DML qualified as Postgres import Hasura.Backends.Postgres.SQL.Types qualified as Postgres +import Hasura.GraphQL.Parser.Directives (ParsedDirectives) import Hasura.NativeQuery.Metadata (InterpolatedQuery) import Hasura.Prelude import Hasura.RQL.IR.Select @@ -154,37 +158,75 @@ data ObjectSelectSource = ObjectSelectSource instance Hashable ObjectSelectSource +data Limitation + = NoLimit + | LimitTo Int + deriving (Show, Eq) + objectSelectSourceToSelectSource :: ObjectSelectSource -> SelectSource -objectSelectSourceToSelectSource ObjectSelectSource {..} = +objectSelectSourceToSelectSource objSelectSource = + -- We specify 'LIMIT 1' here to mitigate misconfigured object relationships with an + -- unexpected one-to-many/many-to-many relationship, instead of the expected one-to-one/many-to-one relationship. + -- Because we can't detect this misconfiguration statically (it depends on the data), + -- we force a single (or null) result instead by adding 'LIMIT 1'. + -- Which result is returned might be non-deterministic (though only in misconfigured cases). + -- Proper one-to-one/many-to-one object relationships should not be semantically affected by this. + -- See: https://github.com/hasura/graphql-engine/issues/7936 + objectSelectSourceToSelectSourceWithLimit objSelectSource (LimitTo 1) + +objectSelectSourceToSelectSourceWithLimit :: ObjectSelectSource -> Limitation -> SelectSource +objectSelectSourceToSelectSourceWithLimit objSelectSource limitation = SelectSource _ossPrefix _ossFrom _ossWhere sortingAndSlicing where - sortingAndSlicing = SortingAndSlicing noSorting limit1 + ObjectSelectSource {..} = objSelectSource + sortingAndSlicing = SortingAndSlicing noSorting selectSlicing noSorting = NoSorting Nothing - -- We specify 'LIMIT 1' here to mitigate misconfigured object relationships with an - -- unexpected one-to-many/many-to-many relationship, instead of the expected one-to-one/many-to-one relationship. - -- Because we can't detect this misconfiguration statically (it depends on the data), - -- we force a single (or null) result instead by adding 'LIMIT 1'. - -- Which result is returned might be non-deterministic (though only in misconfigured cases). - -- Proper one-to-one/many-to-one object relationships should not be semantically affected by this. - -- See: https://github.com/hasura/graphql-engine/issues/7936 - limit1 = SelectSlicing (Just 1) Nothing + selectSlicing = SelectSlicing limitValue Nothing + limitValue = case limitation of + NoLimit -> Nothing + LimitTo n -> Just n data ObjectRelationSource = ObjectRelationSource { _orsRelationshipName :: RelName, _orsRelationMapping :: HashMap.HashMap Postgres.PGCol Postgres.PGCol, _orsSelectSource :: ObjectSelectSource, - _orsNullable :: Nullable + _orsNullable :: Nullable, + _orsDirectives :: Maybe ParsedDirectives } deriving (Generic, Show) -instance Hashable ObjectRelationSource - -deriving instance Eq ObjectRelationSource +-- TODO: Rollback if find a way to pass directives into processAnnotatedOrderByElement +-- +-- instance Hashable ObjectRelationSource +-- deriving instance Eq ObjectRelationSource + +-- Directives excluded from comparison +instance Eq ObjectRelationSource where + a == b = + _orsRelationshipName a + == _orsRelationshipName b + && _orsRelationMapping a + == _orsRelationMapping b + && _orsSelectSource a + == _orsSelectSource b + && _orsNullable a + == _orsNullable b + +-- Directives excluded from comparison +instance Hashable ObjectRelationSource where + hashWithSalt salt ors = + salt + `hashWithSalt` _orsRelationshipName ors + `hashWithSalt` _orsRelationMapping ors + `hashWithSalt` _orsSelectSource ors + `hashWithSalt` _orsNullable ors data ArrayRelationSource = ArrayRelationSource { _arsAlias :: Postgres.TableAlias, _arsRelationMapping :: HashMap.HashMap Postgres.PGCol Postgres.PGCol, - _arsSelectSource :: SelectSource + _arsSelectSource :: SelectSource, + _arsNullable :: Nullable, + _arsDirectives :: Maybe ParsedDirectives } deriving (Generic, Show) @@ -237,13 +279,73 @@ data JoinTree = JoinTree } deriving stock (Eq, Show) +-- TODO: Rollback if find a way to pass directives into processAnnotatedOrderByElement +-- The current approach selects the the directives from the left object to no override it into Nothing +-- +-- instance Semigroup JoinTree where +-- JoinTree lObjs lArrs lArrConns lCfts <> JoinTree rObjs rArrs rArrConns rCfts = +-- JoinTree +-- (HashMap.unionWith (<>) lObjs rObjs) +-- (HashMap.unionWith (<>) lArrs rArrs) +-- (HashMap.unionWith (<>) lArrConns rArrConns) +-- (HashMap.unionWith (<>) lCfts rCfts) + instance Semigroup JoinTree where JoinTree lObjs lArrs lArrConns lCfts <> JoinTree rObjs rArrs rArrConns rCfts = JoinTree - (HashMap.unionWith (<>) lObjs rObjs) + (mergeObjectRelations lObjs rObjs) (HashMap.unionWith (<>) lArrs rArrs) (HashMap.unionWith (<>) lArrConns rArrConns) (HashMap.unionWith (<>) lCfts rCfts) + where + -- Custom merge function that preserves directives + mergeObjectRelations :: + HashMap.HashMap ObjectRelationSource SelectNode -> + HashMap.HashMap ObjectRelationSource SelectNode -> + HashMap.HashMap ObjectRelationSource SelectNode + mergeObjectRelations leftMap rightMap = + HashMap.foldlWithKey' + ( \acc rightKey rightNode -> + let mLeftNode = HashMap.lookup rightKey acc + finalAcc = case mLeftNode of + -- If key exists in left map, merge the nodes and preserve directives + Just leftNode -> + let mergedNode = leftNode <> rightNode + -- Get the original left key (with directives) + leftKey = findOriginalKey leftMap rightKey + -- If right key has no directives but left key does, use left key's directives + finalKey = case (_orsDirectives rightKey, _orsDirectives leftKey) of + (Nothing, Just dirs) -> rightKey {_orsDirectives = Just dirs} + _ -> rightKey + in HashMap.insert finalKey mergedNode (HashMap.delete rightKey acc) + -- If key doesn't exist, simply add it + Nothing -> HashMap.insert rightKey rightNode acc + in finalAcc + ) + leftMap -- Start with the left map + rightMap -- Fold over the right map + + -- Helper to find the original key in a map, even with directives difference + findOriginalKey :: + HashMap.HashMap ObjectRelationSource a -> + ObjectRelationSource -> + ObjectRelationSource + findOriginalKey sourceMap key = + HashMap.foldrWithKey + ( \k _ acc -> + if _orsRelationshipName k + == _orsRelationshipName key + && _orsRelationMapping k + == _orsRelationMapping key + && _orsSelectSource k + == _orsSelectSource key + && _orsNullable k + == _orsNullable key + then k -- Found matching key + else acc + ) + key -- Default to original key if not found + sourceMap instance Monoid JoinTree where mempty = JoinTree mempty mempty mempty mempty diff --git a/server/src-lib/Hasura/GraphQL/ApolloFederation.hs b/server/src-lib/Hasura/GraphQL/ApolloFederation.hs index 8e6b2c44ed9f9..33223f25a3366 100644 --- a/server/src-lib/Hasura/GraphQL/ApolloFederation.hs +++ b/server/src-lib/Hasura/GraphQL/ApolloFederation.hs @@ -131,6 +131,7 @@ modifyApolloFedParserFunc IR._asnFrom = IR.FromTable tableName, IR._asnPerm = tableSelPerm, IR._asnArgs = IR.noSelectArgs {IR._saWhere = whereExpr}, + IR._asnDirectives = Nothing, IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = tCase } @@ -163,7 +164,7 @@ mkServiceField = serviceFieldParser sdlField = JO.String . generateSDL <$ P.selection_ Name._sdl (Just "SDL representation of schema") P.string serviceParser = P.nonNullableParser $ P.selectionSet Name.__Service Nothing [sdlField] serviceFieldParser = - P.subselection_ Name.__service Nothing serviceParser `bindField` \selSet -> do + P.subselection_ Name.__service Nothing serviceParser `bindField` \(selSet, _) -> do let partialValue = InsOrdHashMap.map (\ps -> handleTypename (\tName _ -> JO.toOrdered tName) ps) (InsOrdHashMap.mapKeys G.unName selSet) pure \schemaIntrospection -> RFRaw . JO.fromOrderedHashMap $ (partialValue ?? schemaIntrospection) @@ -290,7 +291,7 @@ mkEntityUnionFieldParser apolloFedTableParsers = entityParser = subselection name description representationParser bodyParser - `bindField` \(parsedArgs, parsedBody) -> do + `bindField` \(parsedArgs, parsedBody, _) -> do rootFields <- for parsedArgs diff --git a/server/src-lib/Hasura/GraphQL/Execute/Action.hs b/server/src-lib/Hasura/GraphQL/Execute/Action.hs index b38af6162918a..54925c571a031 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Action.hs @@ -416,7 +416,7 @@ resolveAsyncActionQuery userInfo annAction responseErrorsConfig = { RS._saWhere = Just tableBoolExpression } tablePermissions = RS.TablePerm annBoolExpTrue Nothing - in RS.AnnSelectG annotatedFields tableFromExp tablePermissions tableArguments stringifyNumerics Nothing + in RS.AnnSelectG annotatedFields tableFromExp tablePermissions tableArguments Nothing stringifyNumerics Nothing where mkQErrFromErrorValue :: J.Value -> QErr mkQErrFromErrorValue actionLogResponseError = @@ -799,7 +799,7 @@ processOutputSelectionSet :: Options.StringifyNumbers -> RS.AnnSimpleSelectG ('Postgres 'Vanilla) Void v processOutputSelectionSet tableRowInput actionOutputType definitionList actionFields strfyNum = - RS.AnnSelectG annotatedFields selectFrom RS.noTablePermissions RS.noSelectArgs strfyNum Nothing + RS.AnnSelectG annotatedFields selectFrom RS.noTablePermissions RS.noSelectArgs Nothing strfyNum Nothing where annotatedFields = fmap actionFieldToAnnField <$> actionFields jsonbToPostgresRecordFunction = diff --git a/server/src-lib/Hasura/GraphQL/Execute/Backend.hs b/server/src-lib/Hasura/GraphQL/Execute/Backend.hs index 7f81ef9007e5a..44630230d4c89 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Backend.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Backend.hs @@ -221,11 +221,11 @@ convertRemoteSourceRelationship relationshipField = case relationship of SourceRelationshipObject s -> - AFObjectRelation $ AnnRelationSelectG relName columnMapping Nullable s + AFObjectRelation $ AnnRelationSelectG relName columnMapping Nullable Nothing s SourceRelationshipArray s -> - AFArrayRelation $ ASSimple $ AnnRelationSelectG relName columnMapping Nullable s + AFArrayRelation $ ASSimple $ AnnRelationSelectG relName columnMapping Nullable Nothing s SourceRelationshipArrayAggregate s -> - AFArrayRelation $ ASAggregate $ AnnRelationSelectG relName columnMapping Nullable s + AFArrayRelation $ ASAggregate $ AnnRelationSelectG relName columnMapping Nullable Nothing s argumentIdField = ( fromCol @b argumentIdColumn, @@ -245,6 +245,7 @@ convertRemoteSourceRelationship _asnFrom = selectFrom, _asnPerm = TablePerm annBoolExpTrue Nothing, _asnArgs = noSelectArgs, + _asnDirectives = Nothing, _asnStrfyNum = stringifyNumbers, _asnNamingConvention = Nothing } diff --git a/server/src-lib/Hasura/GraphQL/Namespace.hs b/server/src-lib/Hasura/GraphQL/Namespace.hs index f132d0e987beb..bd35f0600a3d8 100644 --- a/server/src-lib/Hasura/GraphQL/Namespace.hs +++ b/server/src-lib/Hasura/GraphQL/Namespace.hs @@ -93,7 +93,7 @@ customizeNamespace (Just _) _ _ [] = [] -- The nampespace doesn't contain any Fi customizeNamespace (Just namespace) fromParsedSelection mkNamespaceTypename fieldParsers = -- Source or remote schema has a namespace field so wrap the parsers -- in a new namespace field parser. - [P.subselection_ namespace Nothing parser] + [fmap fst $ P.subselection_ namespace Nothing parser] where parser :: Parser 'Output n (NamespacedField a) parser = diff --git a/server/src-lib/Hasura/GraphQL/Schema/Action.hs b/server/src-lib/Hasura/GraphQL/Schema/Action.hs index e57e5dce8194d..c6d0573e54d3f 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Action.hs @@ -76,10 +76,10 @@ actionExecute customTypes actionInfo = runMaybeT do pure $ P.subselection fieldName description inputArguments selectionSet AOTScalar ast -> do let selectionSet = customScalarParser ast - pure $ P.selection fieldName description inputArguments selectionSet <&> (,[]) + pure $ P.selection fieldName description inputArguments selectionSet <&> (,[],mempty) pure $ parserOutput - <&> \(argsJson, fields) -> + <&> \(argsJson, fields, _) -> IR.AnnActionExecution { _aaeName = actionName, _aaeType = _adType definition, @@ -170,7 +170,7 @@ actionAsyncQuery objectTypes actionInfo = runMaybeT do Name._output (Just "the output fields of this action") actionOutputParser - <&> IR.AsyncOutput + <&> \(fields, _) -> IR.AsyncOutput fields in [idField, createdAtField, errorsField, outputField] parserOutput <- case outputObject of AOTObject aot -> do @@ -184,13 +184,13 @@ actionAsyncQuery objectTypes actionInfo = runMaybeT do pure $ P.subselection fieldName description actionIdInputField selectionSet AOTScalar ast -> do let selectionSet = customScalarParser ast - pure $ P.selection fieldName description actionIdInputField selectionSet <&> (,[]) + pure $ P.selection fieldName description actionIdInputField selectionSet <&> (,[],mempty) stringifyNumbers <- retrieve Options.soStringifyNumbers definitionsList <- lift $ mkDefinitionList outputObject pure $ parserOutput - <&> \(idArg, fields) -> + <&> \(idArg, fields, _) -> IR.AnnActionAsyncQuery { _aaaqName = actionName, _aaaqActionId = idArg, @@ -302,7 +302,7 @@ actionOutputFields outputType annotatedObject objectTypes = do AOFTObject objectName -> do def <- HashMap.lookup objectName objectTypes `onNothing` throw500 ("Custom type " <> objectName <<> " not found") parser <- fmap (IR.ACFNestedObject fieldName) <$> actionOutputFields gType def objectTypes - pure $ P.subselection_ fieldName description parser + pure $ P.subselection_ fieldName description parser <&> fst where fieldName = unObjectFieldName name wrapScalar parser = diff --git a/server/src-lib/Hasura/GraphQL/Schema/Introspect.hs b/server/src-lib/Hasura/GraphQL/Schema/Introspect.hs index e5ce5858216a3..7b4c42b0b0ba4 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Introspect.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Introspect.hs @@ -239,7 +239,7 @@ typeIntrospection :: typeIntrospection = do let nameArg :: P.InputFieldsParser n Text nameArg = P.field GName._name Nothing P.string - ~(nameText, printer) <- P.subselection GName.___type Nothing nameArg typeField + ~(nameText, printer, _) <- P.subselection GName.___type Nothing nameArg typeField -- We pass around the GraphQL schema information under the name `partialSchema`, -- because the GraphQL spec forces us to expose a hybrid between the -- specification of valid queries (including introspection) and an @@ -255,7 +255,7 @@ schema :: (MonadParse n) => FieldParser n (Schema -> J.Value) {-# INLINE schema #-} -schema = P.subselection_ GName.___schema Nothing schemaSet +schema = fmap fst $ P.subselection_ GName.___schema Nothing schemaSet {- type __Type { @@ -337,7 +337,7 @@ typeField = fields :: FieldParser n (SomeType -> J.Value) fields = do -- TODO handle the value of includeDeprecated - ~(_includeDeprecated, printer) <- P.subselection GName._fields Nothing includeDeprecated fieldField + ~(_includeDeprecated, printer, _) <- P.subselection GName._fields Nothing includeDeprecated fieldField return $ \case SomeType tp -> @@ -349,7 +349,7 @@ typeField = _ -> J.Null interfaces :: FieldParser n (SomeType -> J.Value) interfaces = do - printer <- P.subselection_ GName._interfaces Nothing typeField + printer <- fmap fst $ P.subselection_ GName._interfaces Nothing typeField return $ \case SomeType tp -> @@ -359,7 +359,7 @@ typeField = _ -> J.Null possibleTypes :: FieldParser n (SomeType -> J.Value) possibleTypes = do - printer <- P.subselection_ GName._possibleTypes Nothing typeField + printer <- fmap fst $ P.subselection_ GName._possibleTypes Nothing typeField return $ \case SomeType tp -> @@ -372,7 +372,7 @@ typeField = enumValues :: FieldParser n (SomeType -> J.Value) enumValues = do -- TODO handle the value of includeDeprecated - ~(_includeDeprecated, printer) <- P.subselection GName._enumValues Nothing includeDeprecated enumValue + ~(_includeDeprecated, printer, _) <- P.subselection GName._enumValues Nothing includeDeprecated enumValue return $ \case SomeType tp -> @@ -382,7 +382,7 @@ typeField = _ -> J.Null inputFields :: FieldParser n (SomeType -> J.Value) inputFields = do - printer <- P.subselection_ GName._inputFields Nothing inputValue + printer <- fmap fst $ P.subselection_ GName._inputFields Nothing inputValue return $ \case SomeType tp -> @@ -393,7 +393,7 @@ typeField = -- ofType peels modalities off of types ofType :: FieldParser n (SomeType -> J.Value) ofType = do - printer <- P.subselection_ GName._ofType Nothing typeField + printer <- fmap fst $ P.subselection_ GName._ofType Nothing typeField return $ \case -- kind = "NON_NULL": !a -> a SomeType (P.TNamed P.NonNullable x) -> @@ -445,7 +445,7 @@ inputValue = . P.dDescription typeF :: FieldParser n (P.Definition P.InputFieldInfo -> J.Value) typeF = do - printer <- P.subselection_ GName._type Nothing typeField + printer <- fmap fst $ P.subselection_ GName._type Nothing typeField return $ \defInfo -> case P.dInfo defInfo of P.InputFieldInfo tp _ -> printer $ SomeType tp defaultValue :: FieldParser n (P.Definition P.InputFieldInfo -> J.Value) @@ -568,11 +568,11 @@ fieldField = Just desc -> J.String (G.unDescription desc) args :: FieldParser n (P.Definition P.FieldInfo -> J.Value) args = do - printer <- P.subselection_ GName._args Nothing inputValue + printer <- fmap fst $ P.subselection_ GName._args Nothing inputValue return $ J.Array . V.fromList . map printer . sortOn P.dName . P.fArguments . P.dInfo typeF :: FieldParser n (P.Definition P.FieldInfo -> J.Value) typeF = do - printer <- P.subselection_ GName._type Nothing typeField + printer <- fmap fst $ P.subselection_ GName._type Nothing typeField return $ printer . (\case P.FieldInfo _ tp -> SomeType tp) . P.dInfo -- TODO We don't seem to track deprecation info isDeprecated :: FieldParser n (P.Definition P.FieldInfo -> J.Value) @@ -624,7 +624,7 @@ directiveSet = $> (J.toOrdered . map showDirLoc . P.diLocations) args :: FieldParser n (P.DirectiveInfo -> J.Value) args = do - printer <- P.subselection_ GName._args Nothing inputValue + printer <- fmap fst $ P.subselection_ GName._args Nothing inputValue pure $ J.array . map printer . P.diArguments isRepeatable :: FieldParser n (P.DirectiveInfo -> J.Value) isRepeatable = @@ -670,7 +670,7 @@ schemaSet = Just s -> J.String $ G.unDescription s types :: FieldParser n (Schema -> J.Value) types = do - printer <- P.subselection_ GName._types Nothing typeField + printer <- fmap fst $ P.subselection_ GName._types Nothing typeField return $ \partialSchema -> J.Array @@ -685,23 +685,23 @@ schemaSet = SomeType $ P.TNamed P.Nullable def queryType :: FieldParser n (Schema -> J.Value) queryType = do - printer <- P.subselection_ GName._queryType Nothing typeField + printer <- fmap fst $ P.subselection_ GName._queryType Nothing typeField return $ \partialSchema -> printer $ SomeType $ sQueryType partialSchema mutationType :: FieldParser n (Schema -> J.Value) mutationType = do - printer <- P.subselection_ GName._mutationType Nothing typeField + printer <- fmap fst $ P.subselection_ GName._mutationType Nothing typeField return $ \partialSchema -> case sMutationType partialSchema of Nothing -> J.Null Just tp -> printer $ SomeType tp subscriptionType :: FieldParser n (Schema -> J.Value) subscriptionType = do - printer <- P.subselection_ GName._subscriptionType Nothing typeField + printer <- fmap fst $ P.subselection_ GName._subscriptionType Nothing typeField return $ \partialSchema -> case sSubscriptionType partialSchema of Nothing -> J.Null Just tp -> printer $ SomeType tp directives :: FieldParser n (Schema -> J.Value) directives = do - printer <- P.subselection_ GName._directives Nothing directiveSet + printer <- fmap fst $ P.subselection_ GName._directives Nothing directiveSet return $ \partialSchema -> J.array $ map printer $ sDirectives partialSchema in applyPrinter <$> P.selectionSet diff --git a/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs b/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs index ecbe91f277fdd..f3213a55c90b9 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Mutation.hs @@ -99,7 +99,7 @@ insertIntoTable backendInsertAction scenario tableInfo fieldName description = r pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description argsParser selectionParser - <&> \(insertObject, output) -> IR.AnnotatedInsert (G.unName fieldName) False insertObject (IR.MOutMultirowFields output) (Just tCase) + <&> \(insertObject, output, _) -> IR.AnnotatedInsert (G.unName fieldName) False insertObject (IR.MOutMultirowFields output) (Just tCase) where mkObjectsArg objectParser = P.field @@ -150,7 +150,7 @@ insertOneIntoTable backendInsertAction scenario tableInfo fieldName description pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description argsParser selectionParser - <&> \(insertObject, output) -> IR.AnnotatedInsert (G.unName fieldName) True insertObject (IR.MOutSinglerowObject output) (Just tCase) + <&> \(insertObject, output, _) -> IR.AnnotatedInsert (G.unName fieldName) True insertObject (IR.MOutSinglerowObject output) (Just tCase) where mkObjectArg objectParser = P.field @@ -420,8 +420,14 @@ deleteFromTable scenario tableInfo fieldName description = runMaybeT $ do pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description whereArg selection - <&> mkDeleteObject (tableInfoName tableInfo) columns deletePerms (Just tCase) False - . fmap IR.MOutMultirowFields + <&> \(whereExp, mutationFields, _) -> + mkDeleteObject + (tableInfoName tableInfo) + columns + deletePerms + (Just tCase) + False + (whereExp, IR.MOutMultirowFields mutationFields) -- | Construct a root field, normally called delete_tablename_by_pk, that can be used to delete an -- individual rows from a DB table, specified by primary key. Select permissions are required, as @@ -457,8 +463,14 @@ deleteFromTableByPk scenario tableInfo fieldName description = runMaybeT $ do pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description pkArgs selection - <&> mkDeleteObject (tableInfoName tableInfo) columns deletePerms (Just tCase) True - . fmap IR.MOutSinglerowObject + <&> \(whereExp, mutationFields, _) -> + mkDeleteObject + (tableInfoName tableInfo) + columns + deletePerms + (Just tCase) + True + (whereExp, IR.MOutSinglerowObject mutationFields) mkDeleteObject :: (Backend b) => @@ -507,7 +519,7 @@ mutationSelectionSet tableInfo = do tableSet <- MaybeT $ tableSelectionList tableInfo let returningName = Name._returning returningDesc = "data from the rows affected by the mutation" - pure $ IR.MRet <$> P.subselection_ returningName (Just returningDesc) tableSet + pure $ fmap (IR.MRet . fst) $ P.subselection_ returningName (Just returningDesc) tableSet let selectionName = mkTypename $ applyTypeNameCaseIdentifier tCase $ mkTableMutationResponseTypeName tableGQLName affectedRowsName = applyFieldNameCaseIdentifier tCase affectedRowsFieldName affectedRowsDesc = "number of rows affected by the mutation" diff --git a/server/src-lib/Hasura/GraphQL/Schema/Relay.hs b/server/src-lib/Hasura/GraphQL/Schema/Relay.hs index 5f5ccb99f244f..84c5adbd2944d 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Relay.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Relay.hs @@ -113,7 +113,7 @@ nodeField sourceCache context options = do RelaySchema nodeBuilder -> runNodeBuilder nodeBuilder context options pure $ P.subselection Name._node Nothing idArgument nodeObject - `P.bindField` \(ident, parseds) -> do + `P.bindField` \(ident, parseds, _) -> do nodeId <- parseNodeId ident case nodeId of NodeIdV1 (V1NodeId tableName pKeys) -> do @@ -185,6 +185,7 @@ nodeField sourceCache context options = do IR._saOffset = Nothing, IR._saDistinct = Nothing }, + IR._asnDirectives = Nothing, IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just $ _rscNamingConvention $ _siCustomization sourceInfo } diff --git a/server/src-lib/Hasura/GraphQL/Schema/Remote.hs b/server/src-lib/Hasura/GraphQL/Schema/Remote.hs index b273a353b1a91..dc816d382cadf 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Remote.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Remote.hs @@ -455,7 +455,6 @@ remoteInputObjectParser schemaDoc defn@(G.InputObjectTypeDefinition desc name _ else -- At least one field is not a preset, meaning we have the guarantee that there will be at least -- one field in the input object. We have to memoize this branch as we might recursively call -- the same parser. - Right <$> P.memoizeOn 'remoteInputObjectParser defn do typename <- asks getter <&> \mkTypename -> runMkTypename mkTypename name @@ -988,7 +987,7 @@ remoteField sdoc remoteRelationships parentTypeName fieldName description argsDe FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField IR.UnpreparedValue) RemoteSchemaVariable) mkFieldParserWithSelectionSet customizedFieldName argsParser outputParser = P.rawSubselection customizedFieldName description argsParser outputParser - <&> \(alias, _, (_, args), selSet) -> mkField alias customizedFieldName args selSet + <&> \(alias, _, (_, args), selSet, _) -> mkField alias customizedFieldName args selSet -- | helper function to get a parser of an object with it's name -- This function is called from 'remoteSchemaInterface' and diff --git a/server/src-lib/Hasura/GraphQL/Schema/RemoteRelationship.hs b/server/src-lib/Hasura/GraphQL/Schema/RemoteRelationship.hs index f95e4cb2adc04..7b685318bf517 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/RemoteRelationship.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/RemoteRelationship.hs @@ -213,7 +213,7 @@ remoteRelationshipToSourceField context options sourceCache RemoteSourceFieldInf Just selectionSetParser -> pure $ P.subselection_ fieldName Nothing selectionSetParser - <&> \fields -> + <&> \(fields, _) -> IR.SourceRelationshipObject $ IR.AnnObjectSelectG fields (IR.FromTable _rsfiTable) $ IR._tpFilter diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index d5ce382cc180c..b243d6c9afe08 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -129,12 +129,13 @@ defaultSelectTable tableInfo fieldName description = runMaybeT do pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description tableArgsParser selectionSetParser - <&> \(args, fields) -> + <&> \(args, fields, directives) -> IR.AnnSelectG { IR._asnFields = fields, IR._asnFrom = IR.FromTable tableName, IR._asnPerm = tablePermissionsInfo selectPermissions, IR._asnArgs = args, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -186,7 +187,7 @@ selectTableConnection tableInfo fieldName description pkeyColumns = runMaybeT do selectArgsParser <- tableConnectionArgs pkeyColumns tableInfo selectPermissions pure $ P.subselection fieldName description selectArgsParser selectionSetParser - <&> \((args, split, slice), fields) -> + <&> \((args, split, slice), fields, directives) -> IR.ConnectionSelect { IR._csXRelay = xRelayInfo, IR._csPrimaryKeyColumns = pkeyColumns, @@ -198,6 +199,7 @@ selectTableConnection tableInfo fieldName description pkeyColumns = runMaybeT do IR._asnFrom = IR.FromTable tableName, IR._asnPerm = tablePermissionsInfo selectPermissions, IR._asnArgs = args, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -249,7 +251,7 @@ selectTableByPk tableInfo fieldName description = runMaybeT do pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description argsParser selectionSetParser - <&> \(boolExpr, fields) -> + <&> \(boolExpr, fields, directives) -> let defaultPerms = tablePermissionsInfo selectPermissions -- Do not account permission limit since the result is just a nullable object permissions = defaultPerms {IR._tpLimit = Nothing} @@ -259,6 +261,7 @@ selectTableByPk tableInfo fieldName description = runMaybeT do IR._asnFrom = IR.FromTable tableName, IR._asnPerm = permissions, IR._asnArgs = IR.noSelectArgs {IR._saWhere = whereExpr}, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -303,8 +306,8 @@ defaultSelectTableAggregate tableInfo fieldName description = runMaybeT $ do aggregateParser <- tableAggregationFields tableInfo groupByParser <- groupBy tableInfo let aggregateFields = - [ IR.TAFNodes xNodesAgg <$> P.subselection_ Name._nodes Nothing nodesParser, - IR.TAFAgg <$> P.subselection_ Name._aggregate Nothing aggregateParser + [ IR.TAFNodes xNodesAgg <$> (fst <$> P.subselection_ Name._nodes Nothing nodesParser), + IR.TAFAgg <$> (fst <$> P.subselection_ Name._aggregate Nothing aggregateParser) ] <> (maybeToList groupByParser) let selectionName = mkTypename $ applyTypeNameCaseIdentifier tCase $ mkTableAggregateTypeName tableGQLName @@ -318,12 +321,13 @@ defaultSelectTableAggregate tableInfo fieldName description = runMaybeT $ do pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description tableArgsParser aggregationParser - <&> \(args, fields) -> + <&> \(args, fields, directives) -> IR.AnnSelectG { IR._asnFields = fields, IR._asnFrom = IR.FromTable tableName, IR._asnPerm = tablePermissionsInfo selectPermissions, IR._asnArgs = args, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -353,7 +357,7 @@ groupBy tableInfo = runMaybeT $ do selectionSetParser <- groupBySelectionSet tableInfo P.subselection groupByFieldName (Just "Groups the table by the specified keys") groupByInputFieldsParser selectionSetParser - <&> (\(keys, fields) -> IR.TAFGroupBy xGroupBy (IR.GroupByG keys fields)) + <&> (\(keys, fields, _) -> IR.TAFGroupBy xGroupBy (IR.GroupByG keys fields)) & pure where guardGroupByFeatureSupported :: MaybeT (SchemaT r m) (XGroupBy b) @@ -436,8 +440,8 @@ groupBySelectionSet tableInfo = do P.selectionSet groupByTypeName (Just groupByDescription) - [ IR.GBFGroupKey <$> P.subselection_ groupKeyName Nothing groupByKeyParser, - IR.GBFAggregate <$> P.subselection_ aggregateFieldName Nothing aggregateParser + [ IR.GBFGroupKey <$> (fst <$> P.subselection_ groupKeyName Nothing groupByKeyParser), + IR.GBFAggregate <$> (fst <$> P.subselection_ aggregateFieldName Nothing aggregateParser) ] <&> parsedSelectionsToFields IR.GBFExp & P.nonNullableParser @@ -691,17 +695,17 @@ tableConnectionSelectionSet tableInfo = runMaybeT do lift $ P.memoizeOn 'tableConnectionSelectionSet (sourceName, tableName) do let connectionTypeName = mkTypename $ tableGQLName <> Name._Connection pageInfo = - P.subselection_ - Name._pageInfo - Nothing - pageInfoSelectionSet - <&> IR.ConnectionPageInfo + (IR.ConnectionPageInfo . fst) + <$> P.subselection_ + Name._pageInfo + Nothing + pageInfoSelectionSet edges = - P.subselection_ - Name._edges - Nothing - edgesParser - <&> IR.ConnectionEdges + (IR.ConnectionEdges . fst) + <$> P.subselection_ + Name._edges + Nothing + edgesParser connectionDescription = G.Description $ "A Relay connection object on " <>> tableName pure $ P.nonNullableParser @@ -758,11 +762,11 @@ tableConnectionSelectionSet tableInfo = runMaybeT do P.string $> IR.EdgeCursor edgeNode = - P.subselection_ - Name._node - Nothing - edgeNodeParser - <&> IR.EdgeNode + (IR.EdgeNode . fst) + <$> P.subselection_ + Name._node + Nothing + edgeNodeParser pure $ nonNullableObjectList $ P.selectionSet edgesType Nothing [cursor, edgeNode] @@ -1234,8 +1238,8 @@ tableAggregationFields tableInfo = do subselectionParser = P.selectionSet setName setDesc columns <&> parsedSelectionsToFields IR.SFExp - in P.subselection_ opFieldName Nothing subselectionParser - <&> IR.AFOp . IR.AggregateOp opText + in (IR.AFOp . IR.AggregateOp opText . fst) + <$> P.subselection_ opFieldName Nothing subselectionParser -- | shared implementation between tables and logical models defaultArgsParser :: @@ -1368,7 +1372,7 @@ fieldSelection logicalModelCache table tableInfo = \case case HashMap.lookup _noiType logicalModelCache of Just objectType -> do parser <- nestedObjectParser _noiSupportsNestedObjects logicalModelCache objectType _noiColumn _noiIsNullable - pure $ P.subselection_ _noiName _noiDescription parser + pure $ fmap fst $ P.subselection_ _noiName _noiDescription parser _ -> throw500 $ "fieldSelection: object type " <> _noiType <<> " not found" outputParserModifier :: Bool -> IP.Parser origin 'Output m a -> IP.Parser origin 'Output m a @@ -1412,7 +1416,7 @@ nestedObjectParser supportsNestedObjects objectTypes LogicalModelInfo {..} colum LogicalModelTypeReference LogicalModelTypeReferenceC {..} -> do objectType' <- HashMap.lookup lmtrReference objectTypes `onNothing` throw500 ("Custom logical model type " <> lmtrReference <<> " not found") parser <- fmap (IR.AFNestedObject @b) <$> nestedObjectParser supportsNestedObjects objectTypes objectType' lmfName lmtrNullable - pure $ P.subselection_ name (G.Description <$> lmfDescription) parser + pure $ fmap fst $ P.subselection_ name (G.Description <$> lmfDescription) parser LogicalModelTypeArray LogicalModelTypeArrayC {..} -> do nestedArrayFieldParser supportsNestedObjects lmtaNullable <$> go lmtaArray go lmfType @@ -1608,6 +1612,8 @@ relationshipField table ri@RelInfo {riTarget = RelTargetTable otherTableName} = -- suboptimality is merely that in introspection some fields might get -- marked nullable which are in fact known to always be non-null. nullable <- case (riIsManual ri, riInsertOrder ri) of + -- If this is a manual relationship, use NotNullable when riManualNullable is False + (True, _) -> pure (if not (riManualNullable ri) then NotNullable else Nullable) -- Automatically generated forward relationship (False, BeforeParent) -> do let columns = fmap (getColumnPathColumn @b) $ HashMap.keys $ unRelMapping $ riMapping ri @@ -1619,15 +1625,15 @@ relationshipField table ri@RelInfo {riTarget = RelTargetTable otherTableName} = traverse findColumn columns `onNothing` throw500 "could not find column info in schema cache" pure $ boolToNullable $ any ciIsNullable colInfo - -- Manual or reverse relationships are always nullable - _ -> pure Nullable + -- Reverse relationships, respect riManualNullable setting + _ -> pure (if not (riManualNullable ri) then NotNullable else Nullable) pure $ pure $ case nullable of Nullable -> id; NotNullable -> IP.nonNullableField $ P.subselection_ relFieldName desc selectionSetParser - <&> \fields -> + <&> \(fields, directives) -> IR.AFObjectRelation - $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable + $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) nullable (Just directives) $ IR.AnnObjectSelectG fields (IR.FromTable otherTableName) $ deduplicatePermissions $ IR._tpFilter @@ -1635,11 +1641,15 @@ relationshipField table ri@RelInfo {riTarget = RelTargetTable otherTableName} = ArrRel -> do let arrayRelDesc = Just $ G.Description "An array relationship" otherTableParser <- MaybeT $ selectTable otherTableInfo relFieldName arrayRelDesc + + -- For array relationships, determine nullability based on riNullable when it's a manual relationship + let innerNullability = if riIsManual ri && not (riManualNullable ri) then NotNullable else Nullable + let arrayRelField = otherTableParser <&> \selectExp -> IR.AFArrayRelation $ IR.ASSimple - $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable + $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability (IR._asnDirectives selectExp) $ deduplicatePermissions' selectExp relAggFieldName = applyFieldNameCaseCust tCase $ relFieldName <> Name.__aggregate relAggDesc = Just $ G.Description "An aggregate relationship" @@ -1658,8 +1668,8 @@ relationshipField table ri@RelInfo {riTarget = RelTargetTable otherTableName} = pure $ catMaybes [ Just arrayRelField, - fmap (IR.AFArrayRelation . IR.ASAggregate . IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable) <$> remoteAggField, - fmap (IR.AFArrayRelation . IR.ASConnection . IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable) <$> remoteConnectionField + fmap (IR.AFArrayRelation . IR.ASAggregate . IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability Nothing) <$> remoteAggField, + fmap (IR.AFArrayRelation . IR.ASConnection . IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability Nothing) <$> remoteConnectionField ] relationshipField _table ri@RelInfo {riTarget = RelTargetNativeQuery nativeQueryName} = runMaybeT do relFieldName <- lift $ textToName $ relNameToTxt $ riName ri @@ -1674,19 +1684,20 @@ relationshipField _table ri@RelInfo {riTarget = RelTargetNativeQuery nativeQuery MaybeT $ selectNativeQueryObject nativeQueryInfo relFieldName objectRelDesc -- this only affects the generated GraphQL type - let nullability = Nullable + -- Determine nullability + let nullability = if riIsManual ri && not (riManualNullable ri) then NotNullable else Nullable pure $ pure $ nativeQueryParser - <&> \selectExp -> - IR.AFObjectRelation (IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) nullability selectExp) + <&> \(selectExp) -> + IR.AFObjectRelation (IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) nullability Nothing selectExp) ArrRel -> do nativeQueryInfo <- askNativeQueryInfo nativeQueryName let objectRelDesc = Just $ G.Description "An array relationship" arrayNullability = Nullable - innerNullability = Nullable + innerNullability = if riIsManual ri && not (riManualNullable ri) then NotNullable else Nullable nativeQueryParser <- MaybeT $ selectNativeQuery nativeQueryInfo relFieldName arrayNullability objectRelDesc @@ -1694,7 +1705,7 @@ relationshipField _table ri@RelInfo {riTarget = RelTargetNativeQuery nativeQuery pure $ pure $ nativeQueryParser - <&> \selectExp -> + <&> \(selectExp) -> IR.AFArrayRelation $ IR.ASSimple - $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability selectExp + $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability Nothing selectExp diff --git a/server/src-lib/Hasura/GraphQL/Schema/SubscriptionStream.hs b/server/src-lib/Hasura/GraphQL/Schema/SubscriptionStream.hs index 39d8c6a5304ec..4ab5f052eece9 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/SubscriptionStream.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/SubscriptionStream.hs @@ -81,7 +81,7 @@ cursorOrderingArgParser = do [ ( define enumNameVal, snd enumNameVal ) - | enumNameVal <- [(Name._ASC, COAscending), (Name._DESC, CODescending)] + | enumNameVal <- [(Name._ASC, COAscending), (Name._DESC, CODescending)] ] where define (name, val) = @@ -271,7 +271,7 @@ selectStreamTable tableInfo fieldName description = runMaybeT $ do pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $ P.subselection fieldName description tableStreamArgsParser selectionSetParser - <&> \(args, fields) -> + <&> \(args, fields, _) -> IR.AnnSelectStreamG { IR._assnXStreamingSubscription = xStreamSubscription, IR._assnFields = fields, diff --git a/server/src-lib/Hasura/GraphQL/Schema/Update/Batch.hs b/server/src-lib/Hasura/GraphQL/Schema/Update/Batch.hs index bc30ceebde805..16a9785c154ab 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Update/Batch.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Update/Batch.hs @@ -69,7 +69,15 @@ buildAnnotatedUpdateGField scenario tableInfo fieldName description parseOutput updateVariantParser <- mkUpdateVariantParser updatePerms pure $ P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) - $ mkAnnotatedUpdateG tableName columns updatePerms (Just tCase) validateInput + $ ( \(updateVariant, output, _) -> + mkAnnotatedUpdateG + tableName + columns + updatePerms + (Just tCase) + validateInput + (updateVariant, output) + ) <$> P.subselection fieldName description updateVariantParser outputParser -- | Construct a root field, normally called update_tablename, that can be used diff --git a/server/src-lib/Hasura/NativeQuery/Schema.hs b/server/src-lib/Hasura/NativeQuery/Schema.hs index 4cc4442411c45..167c95a2a2c7d 100644 --- a/server/src-lib/Hasura/NativeQuery/Schema.hs +++ b/server/src-lib/Hasura/NativeQuery/Schema.hs @@ -109,7 +109,7 @@ defaultSelectNativeQueryObject nqi@NativeQueryInfo {..} fieldName description = description nativeQueryArgsParser selectionSetParser - <&> \(nqArgs, fields) -> + <&> \(nqArgs, fields, _) -> IR.AnnObjectSelectG fields ( IR.FromNativeQuery @@ -205,7 +205,7 @@ defaultSelectNativeQuery nqi@NativeQueryInfo {..} fieldName nullability descript <*> nativeQueryArgsParser ) selectionListParser - <&> \((lmArgs, nqArgs), fields) -> + <&> \((lmArgs, nqArgs), fields, directives) -> IR.AnnSelectG { IR._asnFields = fields, IR._asnFrom = @@ -217,6 +217,7 @@ defaultSelectNativeQuery nqi@NativeQueryInfo {..} fieldName nullability descript }, IR._asnPerm = logicalModelPermissions, IR._asnArgs = lmArgs, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } @@ -328,7 +329,7 @@ nativeQueryRelationshipField ri | riType ri == ObjRel = runMaybeT do pure $ nativeQueryParser <&> \selectExp -> - IR.AFObjectRelation (IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) nullability selectExp) + IR.AFObjectRelation (IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) nullability Nothing selectExp) RelTargetTable otherTableName -> do let desc = Just $ G.Description "An object relationship" roleName <- retrieve scRole @@ -337,9 +338,9 @@ nativeQueryRelationshipField ri | riType ri == ObjRel = runMaybeT do selectionSetParser <- MaybeT $ tableSelectionSet otherTableInfo pure $ P.subselection_ relFieldName desc selectionSetParser - <&> \fields -> + <&> \(fields, directives) -> IR.AFObjectRelation - $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable + $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable (Just directives) $ IR.AnnObjectSelectG fields (IR.FromTable otherTableName) $ IR._tpFilter $ tablePermissionsInfo remotePerms @@ -361,7 +362,7 @@ nativeQueryRelationshipField ri = do <&> \selectExp -> IR.AFArrayRelation $ IR.ASSimple - $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability selectExp + $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) innerNullability Nothing selectExp RelTargetTable otherTableName -> runMaybeT $ do let arrayRelDesc = Just $ G.Description "An array relationship" @@ -371,6 +372,6 @@ nativeQueryRelationshipField ri = do otherTableParser <&> \selectExp -> IR.AFArrayRelation $ IR.ASSimple - $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable + $ IR.AnnRelationSelectG (riName ri) (unRelMapping $ riMapping ri) Nullable Nothing $ selectExp pure arrayRelField diff --git a/server/src-lib/Hasura/RQL/DDL/Relationship.hs b/server/src-lib/Hasura/RQL/DDL/Relationship.hs index 71c157cf0ec47..3725e6709db37 100644 --- a/server/src-lib/Hasura/RQL/DDL/Relationship.hs +++ b/server/src-lib/Hasura/RQL/DDL/Relationship.hs @@ -137,6 +137,7 @@ defaultBuildObjectRelationshipInfo source foreignKeys qt (RelDef rn ru _) = case RUManual (RelManualNativeQueryConfig (RelManualNativeQueryConfigC {rmnNativeQueryName = refqt, rmnCommon = common})) -> do let (lCols, rCols) = unzip $ HashMap.toList $ unRelMapping $ rmColumns common io = fromMaybe BeforeParent $ rmInsertOrder common + nullable = rmManualNullable common mkNativeQueryDependency nativeQueryName reason col = SchemaDependency ( SOSourceObj source @@ -158,10 +159,11 @@ defaultBuildObjectRelationshipInfo source foreignKeys qt (RelDef rn ru _) = case dependencies = (mkDependency qt DRLeftColumn <$> Seq.fromList lCols) <> (mkNativeQueryDependency refqt DRRightColumn <$> Seq.fromList rCols) - pure (RelInfo rn ObjRel (rmColumns common) (RelTargetNativeQuery refqt) True io, dependencies) + pure (RelInfo rn ObjRel (rmColumns common) (RelTargetNativeQuery refqt) True io nullable, dependencies) RUManual (RelManualTableConfig (RelManualTableConfigC {rmtTable = refqt, rmtCommon = common})) -> do let (lCols, rCols) = unzip $ HashMap.toList $ unRelMapping $ rmColumns common io = fromMaybe BeforeParent $ rmInsertOrder common + nullable = rmManualNullable common mkDependency tableName reason col = SchemaDependency ( SOSourceObj source @@ -174,7 +176,7 @@ defaultBuildObjectRelationshipInfo source foreignKeys qt (RelDef rn ru _) = case dependencies = (mkDependency qt DRLeftColumn <$> Seq.fromList lCols) <> (mkDependency refqt DRRightColumn <$> Seq.fromList rCols) - pure (RelInfo rn ObjRel (rmColumns common) (RelTargetTable refqt) True io, dependencies) + pure (RelInfo rn ObjRel (rmColumns common) (RelTargetTable refqt) True io nullable, dependencies) RUFKeyOn (SameTable columns) -> do foreignTableForeignKeys <- HashMap.lookup qt foreignKeys @@ -199,7 +201,7 @@ defaultBuildObjectRelationshipInfo source foreignKeys qt (RelDef rn ru _) = case DRRemoteTable ] <> (drUsingColumnDep @b source qt <$> Seq.fromList (toList $ getColumnPathColumn @b <$> columns)) - pure (RelInfo rn ObjRel (RelMapping $ NEHashMap.toHashMap colMap) (RelTargetTable foreignTable) False BeforeParent, dependencies) + pure (RelInfo rn ObjRel (RelMapping $ NEHashMap.toHashMap colMap) (RelTargetTable foreignTable) False BeforeParent True, dependencies) RUFKeyOn (RemoteTable remoteTable remoteCols) -> mkFkeyRel ObjRel AfterParent source rn qt remoteTable remoteCols foreignKeys @@ -260,7 +262,7 @@ nativeQueryRelationshipSetup sourceName nativeQueryName relType (RelDef relName (Seq.fromList lCols) ) <> fmap createRHSSchemaDependency (Seq.fromList rCols) - pure (RelInfo relName relType (rmColumns common) relTarget True io, deps) + pure (RelInfo relName relType (rmColumns common) relTarget True io (rmManualNullable common), deps) defaultBuildArrayRelationshipInfo :: forall b m. @@ -299,7 +301,7 @@ defaultBuildArrayRelationshipInfo source foreignKeys qt (RelDef rn ru _) = case DRRightColumn ) (Seq.fromList rCols) - pure (RelInfo rn ArrRel (rmColumns common) (RelTargetNativeQuery refqt) True AfterParent, deps) + pure (RelInfo rn ArrRel (rmColumns common) (RelTargetNativeQuery refqt) True AfterParent (rmManualNullable common), deps) RUManual (RelManualTableConfig (RelManualTableConfigC {rmtTable = refqt, rmtCommon = common})) -> do let (lCols, rCols) = unzip $ HashMap.toList $ unRelMapping $ rmColumns common deps = @@ -328,7 +330,7 @@ defaultBuildArrayRelationshipInfo source foreignKeys qt (RelDef rn ru _) = case DRRightColumn ) (Seq.fromList rCols) - pure (RelInfo rn ArrRel (rmColumns common) (RelTargetTable refqt) True AfterParent, deps) + pure (RelInfo rn ArrRel (rmColumns common) (RelTargetTable refqt) True AfterParent (rmManualNullable common), deps) RUFKeyOn (ArrRelUsingFKeyOn refqt refCols) -> mkFkeyRel ArrRel AfterParent source rn qt refqt refCols foreignKeys @@ -370,7 +372,7 @@ mkFkeyRel relType io source rn sourceTable remoteTable remoteColumns foreignKeys <> ( drUsingColumnDep @b source remoteTable <$> Seq.fromList (toList $ getColumnPathColumn @b <$> remoteColumns) ) - pure (RelInfo rn relType (RelMapping $ reverseMap $ NEHashMap.toHashMap colMap) (RelTargetTable remoteTable) False io, dependencies) + pure (RelInfo rn relType (RelMapping $ reverseMap $ NEHashMap.toHashMap colMap) (RelTargetTable remoteTable) False io True, dependencies) where reverseMap :: (Hashable y) => HashMap x y -> HashMap y x reverseMap = HashMap.fromList . fmap swap . HashMap.toList diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs b/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs index 0ea5a70a70d8e..d1fe42358c12d 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs @@ -933,4 +933,4 @@ recreateSystemMetadata = do $ RelManualTableConfig $ RelManualTableConfigC (QualifiedObject schemaName tableName) - (RelManualCommon (RelMapping $ HashMap.fromList columns) Nothing) + (RelManualCommon (RelMapping $ HashMap.fromList columns) Nothing True) diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs index b452710ce037c..f8a05bdcec431 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Rename.hs @@ -302,9 +302,9 @@ updateRelDefs source qt rn renameTable = do updateObjRelDef (oldQT, newQT) = rdUsing %~ \case RUFKeyOn fk -> RUFKeyOn fk - RUManual (RelManualTableConfig (RelManualTableConfigC origQT (RelManualCommon rmCols rmIO))) -> + RUManual (RelManualTableConfig (RelManualTableConfigC origQT (RelManualCommon rmCols rmIO rmManualNullable))) -> let updQT = bool origQT newQT $ oldQT == origQT - in RUManual $ RelManualTableConfig $ RelManualTableConfigC updQT (RelManualCommon rmCols rmIO) + in RUManual $ RelManualTableConfig $ RelManualTableConfigC updQT (RelManualCommon rmCols rmIO rmManualNullable) RUManual (RelManualNativeQueryConfig nqc) -> RUManual (RelManualNativeQueryConfig nqc) @@ -314,9 +314,9 @@ updateRelDefs source qt rn renameTable = do RUFKeyOn (ArrRelUsingFKeyOn origQT c) -> let updQT = getUpdQT origQT in RUFKeyOn $ ArrRelUsingFKeyOn updQT c - RUManual (RelManualTableConfig (RelManualTableConfigC origQT (RelManualCommon rmCols rmIO))) -> + RUManual (RelManualTableConfig (RelManualTableConfigC origQT (RelManualCommon rmCols rmIO rmManualNullable))) -> let updQT = getUpdQT origQT - in RUManual $ RelManualTableConfig $ RelManualTableConfigC updQT (RelManualCommon rmCols rmIO) + in RUManual $ RelManualTableConfig $ RelManualTableConfigC updQT (RelManualCommon rmCols rmIO rmManualNullable) RUManual (RelManualNativeQueryConfig nqc) -> RUManual (RelManualNativeQueryConfig nqc) where @@ -746,10 +746,10 @@ updateRelManualConfig :: RenameCol b -> RelManualConfig b -> RelManualConfig b -updateRelManualConfig fromQT toQT rnCol (RelManualTableConfig (RelManualTableConfigC tn (RelManualCommon colMap io))) = - RelManualTableConfig (RelManualTableConfigC tn (RelManualCommon (updateColMap fromQT toQT rnCol colMap) io)) -updateRelManualConfig fromQT toQT rnCol (RelManualNativeQueryConfig (RelManualNativeQueryConfigC nqn (RelManualCommon colMap io))) = - RelManualNativeQueryConfig (RelManualNativeQueryConfigC nqn (RelManualCommon (updateColMap fromQT toQT rnCol colMap) io)) +updateRelManualConfig fromQT toQT rnCol (RelManualTableConfig (RelManualTableConfigC tn (RelManualCommon colMap io rmManualNullable))) = + RelManualTableConfig (RelManualTableConfigC tn (RelManualCommon (updateColMap fromQT toQT rnCol colMap) io rmManualNullable)) +updateRelManualConfig fromQT toQT rnCol (RelManualNativeQueryConfig (RelManualNativeQueryConfigC nqn (RelManualCommon colMap io rmManualNullable))) = + RelManualNativeQueryConfig (RelManualNativeQueryConfigC nqn (RelManualCommon (updateColMap fromQT toQT rnCol colMap) io rmManualNullable)) updateColMap :: forall b. diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index 3f43425278ff2..2f86f66e2b826 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -248,7 +248,7 @@ convSelectQ sqlGen table fieldInfoMap selPermInfo selQ sessVarBldr prepValBldr = tabPerm = TablePerm resolvedSelFltr mPermLimit tabArgs = SelectArgs wClause annOrdByM mQueryLimit (fromIntegral <$> mQueryOffset) Nothing strfyNum = stringifyNum sqlGen - pure $ AnnSelectG annFlds tabFrom tabPerm tabArgs strfyNum Nothing + pure $ AnnSelectG annFlds tabFrom tabPerm tabArgs Nothing strfyNum Nothing where mQueryOffset = sqOffset selQ mQueryLimit = sqLimit selQ @@ -297,7 +297,7 @@ convExtRel sqlGen fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do when misused $ throw400 UnexpectedPayload objRelMisuseMsg pure $ Left - $ AnnRelationSelectG (fromMaybe relName mAlias) colMapping Nullable + $ AnnRelationSelectG (fromMaybe relName mAlias) colMapping Nullable Nothing $ AnnObjectSelectG (_asnFields annSel) (FromTable relTableName) $ _tpFilter $ _asnPerm annSel @@ -309,6 +309,7 @@ convExtRel sqlGen fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do (fromMaybe relName mAlias) colMapping Nullable + Nothing annSel where pgWhenRelErr = "only relationships can be expanded" diff --git a/server/src-lib/Hasura/RQL/IR/Select/AnnSelectG.hs b/server/src-lib/Hasura/RQL/IR/Select/AnnSelectG.hs index bb69fde8c832c..2441c03bf0661 100644 --- a/server/src-lib/Hasura/RQL/IR/Select/AnnSelectG.hs +++ b/server/src-lib/Hasura/RQL/IR/Select/AnnSelectG.hs @@ -11,6 +11,7 @@ where import Data.Bifoldable import Data.Kind (Type) +import Hasura.GraphQL.Parser.Directives (ParsedDirectives) import Hasura.Prelude import Hasura.RQL.IR.Select.Args import Hasura.RQL.IR.Select.From @@ -28,6 +29,7 @@ data AnnSelectG (b :: BackendType) (f :: Type -> Type) (v :: Type) = AnnSelectG _asnFrom :: SelectFromG b v, _asnPerm :: TablePermG b v, _asnArgs :: SelectArgsG b v, + _asnDirectives :: Maybe ParsedDirectives, _asnStrfyNum :: StringifyNumbers, _asnNamingConvention :: Maybe NamingCase } @@ -52,7 +54,8 @@ data AnnSelectStreamG (b :: BackendType) (f :: Type -> Type) - (v :: Type) = AnnSelectStreamG + (v :: Type) + = AnnSelectStreamG { -- | type to indicate if streaming subscription has been enabled in the `BackendType`. -- This type helps avoiding missing case match patterns for backends where it's disabled. _assnXStreamingSubscription :: XStreamingSubscription b, diff --git a/server/src-lib/Hasura/RQL/IR/Select/Lenses.hs b/server/src-lib/Hasura/RQL/IR/Select/Lenses.hs index a443fcc69bbb9..97e451a4f2fb4 100644 --- a/server/src-lib/Hasura/RQL/IR/Select/Lenses.hs +++ b/server/src-lib/Hasura/RQL/IR/Select/Lenses.hs @@ -18,6 +18,7 @@ module Hasura.RQL.IR.Select.Lenses aarColumnMapping, aarRelationshipName, aarNullable, + aarDirectives, anosSupportsNestedObjects, anosColumn, anosFields, diff --git a/server/src-lib/Hasura/RQL/IR/Select/RelationSelect.hs b/server/src-lib/Hasura/RQL/IR/Select/RelationSelect.hs index 9888186eb7650..ce773e3426fb6 100644 --- a/server/src-lib/Hasura/RQL/IR/Select/RelationSelect.hs +++ b/server/src-lib/Hasura/RQL/IR/Select/RelationSelect.hs @@ -7,6 +7,7 @@ module Hasura.RQL.IR.Select.RelationSelect ) where +import Hasura.GraphQL.Parser.Directives (ParsedDirectives) import Hasura.Prelude import Hasura.RQL.Types.Backend import Hasura.RQL.Types.BackendType @@ -19,6 +20,7 @@ data AnnRelationSelectG (b :: BackendType) a = AnnRelationSelectG { _aarRelationshipName :: RelName, -- Relationship name _aarColumnMapping :: HashMap (ColumnPath b) (ColumnPath b), -- Column of left table to join with _aarNullable :: Nullable, -- is the target object allowed to be missing? + _aarDirectives :: Maybe ParsedDirectives, -- Directives on the relationship field _aarAnnSelect :: a -- Current table. Almost ~ to SQL Select } deriving stock (Functor, Foldable, Traversable) diff --git a/server/src-lib/Hasura/RQL/Types/Relationships/Local.hs b/server/src-lib/Hasura/RQL/Types/Relationships/Local.hs index 40eea7f1a8f2f..7d44502913ed3 100644 --- a/server/src-lib/Hasura/RQL/Types/Relationships/Local.hs +++ b/server/src-lib/Hasura/RQL/Types/Relationships/Local.hs @@ -117,7 +117,8 @@ deriving instance (Backend b) => Show (RelManualNativeQueryConfig b) data RelManualCommon (b :: BackendType) = RelManualCommon { rmColumns :: RelMapping b, - rmInsertOrder :: Maybe InsertOrder + rmInsertOrder :: Maybe InsertOrder, + rmManualNullable :: Bool } deriving (Generic) @@ -150,6 +151,10 @@ instance (Backend b) => AC.HasObjectCodec (RelManualCommon b) where AC..= rmColumns <*> optionalFieldOrIncludedNull' "insertion_order" AC..= rmInsertOrder + <*> AC.optionalFieldWithDefault "nullable" True nullableDoc + AC..= rmManualNullable + where + nullableDoc = "Is manual relationship nullable or not?" data RelUsing (b :: BackendType) a = RUFKeyOn a @@ -439,7 +444,8 @@ data RelInfo (b :: BackendType) = RelInfo riMapping :: RelMapping b, riTarget :: RelTarget b, riIsManual :: Bool, - riInsertOrder :: InsertOrder + riInsertOrder :: InsertOrder, + riManualNullable :: Bool } deriving (Generic) diff --git a/server/src-lib/Hasura/StoredProcedure/Schema.hs b/server/src-lib/Hasura/StoredProcedure/Schema.hs index 102105bdffc51..a41bc6ed0961e 100644 --- a/server/src-lib/Hasura/StoredProcedure/Schema.hs +++ b/server/src-lib/Hasura/StoredProcedure/Schema.hs @@ -94,7 +94,7 @@ defaultBuildStoredProcedureRootFields StoredProcedureInfo {..} = runMaybeT $ do <*> storedProcedureArgsParser ) selectionListParser - <&> \((lmArgs, spArgs), fields) -> + <&> \((lmArgs, spArgs), fields, directives) -> QDBMultipleRows $ IR.AnnSelectG { IR._asnFields = fields, @@ -108,6 +108,7 @@ defaultBuildStoredProcedureRootFields StoredProcedureInfo {..} = runMaybeT $ do }, IR._asnPerm = logicalModelPermissions, IR._asnArgs = lmArgs, + IR._asnDirectives = (Just directives), IR._asnStrfyNum = stringifyNumbers, IR._asnNamingConvention = Just tCase } diff --git a/server/src-test/Hasura/Backends/Postgres/RQLGenerator/GenAnnSelectG.hs b/server/src-test/Hasura/Backends/Postgres/RQLGenerator/GenAnnSelectG.hs index 7926bd05990ca..35188eab61e75 100644 --- a/server/src-test/Hasura/Backends/Postgres/RQLGenerator/GenAnnSelectG.hs +++ b/server/src-test/Hasura/Backends/Postgres/RQLGenerator/GenAnnSelectG.hs @@ -3,6 +3,7 @@ module Hasura.Backends.Postgres.RQLGenerator.GenAnnSelectG ) where +import Data.HashMap.Strict qualified as HashMap import Hasura.Backends.Postgres.RQLGenerator.GenSelectArgsG (genSelectArgsG) import Hasura.Backends.Postgres.RQLGenerator.GenSelectFromG (genSelectFromG) import Hasura.Backends.Postgres.RQLGenerator.GenTablePermG (genTablePermG) @@ -24,6 +25,7 @@ genAnnSelectG genA genFA = <*> genSelectFromG genA <*> genTablePermG genA <*> genArgs + <*> genDirectives <*> genStringifyNumbers <*> (pure Nothing) where @@ -32,3 +34,4 @@ genAnnSelectG genA genFA = False -> Options.Don'tStringifyNumbers True -> Options.StringifyNumbers genArgs = genSelectArgsG genA + genDirectives = Gen.maybe (pure HashMap.empty) diff --git a/server/src-test/Hasura/GraphQL/Schema/BoolExp/AggregationPredicatesSpec.hs b/server/src-test/Hasura/GraphQL/Schema/BoolExp/AggregationPredicatesSpec.hs index 6e340925ef125..af02f0b0f865c 100644 --- a/server/src-test/Hasura/GraphQL/Schema/BoolExp/AggregationPredicatesSpec.hs +++ b/server/src-test/Hasura/GraphQL/Schema/BoolExp/AggregationPredicatesSpec.hs @@ -310,7 +310,8 @@ spec = do riMapping = RelMapping $ HashMap.fromList [("id", "album_id")], riTarget = RelTargetTable (mkTable "track"), riIsManual = False, - riInsertOrder = AfterParent + riInsertOrder = AfterParent, + riManualNullable = True } sourceInfo :: SourceInfo ('Postgres 'Vanilla) diff --git a/server/src-test/Hasura/RQL/IR/Generator.hs b/server/src-test/Hasura/RQL/IR/Generator.hs index 1852fbb173d6e..cadf4ea629003 100644 --- a/server/src-test/Hasura/RQL/IR/Generator.hs +++ b/server/src-test/Hasura/RQL/IR/Generator.hs @@ -239,6 +239,7 @@ genRelInfo genTableName genColumn = <*> genRelTarget genTableName <*> bool_ <*> genInsertOrder + <*> bool_ genRelTarget :: (MonadGen m) => m (TableName b) -> m (RelTarget b) genRelTarget genTableName =