Skip to content

Commit c75ca53

Browse files
jberrymanhasura-bot
authored andcommitted
server: refactor for faster introspection queries
by bypassing intermediate aeson Values. There were a couple places where this didn't work out, and so we need to carry around some extra information. This is significant for UX as e.g. the console runs introspection frequently. Note that e.g. regular postgres graphql queries return json directly from the DB so this work isn't relevant there. GitOrigin-RevId: 2d7d373766ee5b60ba5e46b76ff01ac7d4602475
1 parent d77bfc8 commit c75ca53

13 files changed

Lines changed: 195 additions & 206 deletions

File tree

server/src-lib/Hasura/App.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ import Hasura.RQL.DDL.Schema.Cache.Common
127127
import Hasura.RQL.DDL.Schema.Cache.Config
128128
import Hasura.RQL.DDL.Schema.Catalog
129129
import Hasura.RQL.DDL.SchemaRegistry qualified as SchemaRegistry
130+
import Hasura.RQL.IR.Root (irEncJSON)
130131
import Hasura.RQL.Types.Allowlist
131132
import Hasura.RQL.Types.Backend
132133
import Hasura.RQL.Types.BackendType
@@ -771,8 +772,8 @@ instance MonadGQLExecutionCheck AppM where
771772
checkQueryInAllowlist enableAL AllowlistModeGlobalOnly userInfo req sc
772773
return req
773774

774-
executeIntrospection _ introspectionQuery _ =
775-
pure $ Right $ ExecStepRaw introspectionQuery
775+
executeIntrospection _ introspectionResult _ =
776+
pure $ Right $ ExecStepRaw (irEncJSON introspectionResult)
776777

777778
checkGQLBatchedReqs _ _ _ _ = runExceptT $ pure ()
778779

server/src-lib/Hasura/GraphQL/ApolloFederation.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
2121
import Data.Text qualified as T
2222
import Hasura.Base.Error
2323
import Hasura.Base.ErrorMessage (toErrorMessage)
24+
import Hasura.EncJSON
2425
import Hasura.GraphQL.Parser qualified as P
2526
import Hasura.GraphQL.Schema.Common
2627
import Hasura.GraphQL.Schema.Parser
@@ -165,7 +166,12 @@ mkServiceField = serviceFieldParser
165166
serviceFieldParser =
166167
P.subselection_ Name.__service Nothing serviceParser `bindField` \selSet -> do
167168
let partialValue = InsOrdHashMap.map (\ps -> handleTypename (\tName _ -> JO.toOrdered tName) ps) (InsOrdHashMap.mapKeys G.unName selSet)
168-
pure \schemaIntrospection -> RFRaw . JO.fromOrderedHashMap $ (partialValue ?? schemaIntrospection)
169+
pure \schemaIntrospection ->
170+
RFRaw
171+
. SchemaIntrospection
172+
$ encJFromAssocList
173+
$ map (fmap encJFromOrderedValue)
174+
$ InsOrdHashMap.toList (partialValue ?? schemaIntrospection)
169175

170176
apolloRootFields ::
171177
ApolloFederationStatus ->

server/src-lib/Hasura/GraphQL/Context.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ instance (J.ToJSON a) => J.ToJSON (RoleContext a) where
3030

3131
data GQLContext = GQLContext
3232
{ gqlQueryParser :: ParserFn (RootFieldMap (IR.QueryRootField IR.UnpreparedValue)),
33+
-- | Names of top-level query root fields (used for conflict detection without
34+
-- needing to run/decode an introspection query)
35+
gqlQueryRootFieldNames :: [G.Name],
3336
gqlMutationParser :: Maybe (ParserFn (RootFieldMap (IR.MutationRootField IR.UnpreparedValue))),
3437
gqlSubscriptionParser :: Maybe (ParserFn (RootFieldMap (IR.QueryRootField IR.UnpreparedValue)))
3538
}

server/src-lib/Hasura/GraphQL/Execute.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ buildSubscriptionPlan removeEmptySubscriptionResponses userInfo rootFields param
245245
case JO.asObject oldResponse of
246246
Left (_err :: String) -> oldResponse
247247
Right responseObj ->
248-
JO.Object $ JO.insert (fieldIndex, G.unName $ _rfaAlias gName) val responseObj
248+
JO.Object $ JO.insert (fieldIndex, G.unName $ _rfaAlias gName) (JO.toOrdered (IR.irEncJSON val)) responseObj
249249
pure ((accLiveQueryFields, accStreamingFields), Just (Endo newModifier) <> modifier)
250250
IR.RFMulti _ -> throw400 NotSupported "not supported over subscriptions"
251251
IR.RFDB src e -> do

server/src-lib/Hasura/GraphQL/Execute/Backend.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ where
1414
import Control.Monad.Trans.Control (MonadBaseControl)
1515
import Data.Aeson qualified as J
1616
import Data.Aeson.Casing qualified as J
17-
import Data.Aeson.Ordered qualified as JO
1817
import Data.Environment qualified as Env
1918
import Data.Kind (Type)
2019
import Data.Text.Extended
@@ -333,7 +332,7 @@ data ExecutionStep where
333332
ExecutionStep
334333
-- | Output a plain JSON object
335334
ExecStepRaw ::
336-
JO.Value ->
335+
EncJSON ->
337336
ExecutionStep
338337
ExecStepMulti ::
339338
[ExecutionStep] ->

server/src-lib/Hasura/GraphQL/Execute/Common.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ module Hasura.GraphQL.Execute.Common
33
)
44
where
55

6-
import Data.Aeson.Ordered qualified as JO
76
import Hasura.Authentication.User (UserInfo)
87
import Hasura.Base.Error
98
import Hasura.GraphQL.Execute.Backend
109
import Hasura.GraphQL.Transport.HTTP.Protocol
1110
import Hasura.Prelude
11+
import Hasura.RQL.IR.Root (RFRawPayload)
1212
import Hasura.RQL.Types.GraphqlSchemaIntrospection
1313
import Hasura.RQL.Types.SchemaCache
1414
import Hasura.Server.Init (AllowListStatus)
@@ -43,7 +43,7 @@ class (Monad m) => MonadGQLExecutionCheck m where
4343

4444
executeIntrospection ::
4545
UserInfo ->
46-
JO.Value ->
46+
RFRawPayload ->
4747
SetGraphqlIntrospectionOptions ->
4848
m (Either QErr ExecutionStep)
4949

server/src-lib/Hasura/GraphQL/Schema.hs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import Control.Concurrent.Extended (concurrentlyEIO, forConcurrentlyEIO)
1111
import Control.Concurrent.STM qualified as STM
1212
import Control.Lens hiding (contexts)
1313
import Control.Monad.Memoize
14-
import Data.Aeson.Ordered qualified as JO
1514
import Data.HashMap.Strict qualified as HashMap
1615
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
1716
import Data.HashSet qualified as Set
@@ -23,6 +22,7 @@ import Hasura.Authentication.Role (RoleName, adminRoleName, mkRoleNameSafe)
2322
import Hasura.Base.Error
2423
import Hasura.Base.ErrorMessage
2524
import Hasura.Base.ToErrorValue
25+
import Hasura.EncJSON
2626
import Hasura.Function.Cache
2727
import Hasura.GraphQL.ApolloFederation
2828
import Hasura.GraphQL.Context
@@ -394,15 +394,20 @@ buildRoleContext sampledFeatureFlags options sources remotes actions customTypes
394394
(P.parserType <$> mutationParserFrontend)
395395
(P.parserType <$> subscriptionParser)
396396

397+
let queryRootFieldNamesFrontend = queryRootFieldNamesOf queryParserFrontend
398+
queryRootFieldNamesBackend = queryRootFieldNamesOf queryParserBackend
399+
397400
-- (since we're running this in parallel in caller, be strict)
398401
let !frontendContext =
399402
GQLContext
400403
(finalizeParser queryParserFrontend)
404+
queryRootFieldNamesFrontend
401405
(finalizeParser <$> mutationParserFrontend)
402406
(finalizeParser <$> subscriptionParser)
403407
!backendContext =
404408
GQLContext
405409
(finalizeParser queryParserBackend)
410+
queryRootFieldNamesBackend
406411
(finalizeParser <$> mutationParserBackend)
407412
(finalizeParser <$> subscriptionParser)
408413

@@ -527,14 +532,18 @@ buildRelayRoleContext options sources actions customTypes role expFeatures schem
527532
(P.parserType <$> mutationParserFrontend)
528533
(P.parserType <$> subscriptionParser)
529534

530-
let frontendContext =
535+
let relayFrontendFieldNames = queryRootFieldNamesOf queryParserFrontend
536+
relayBackendFieldNames = queryRootFieldNamesOf queryParserBackend
537+
frontendContext =
531538
GQLContext
532539
(finalizeParser queryParserFrontend)
540+
relayFrontendFieldNames
533541
(finalizeParser <$> mutationParserFrontend)
534542
(finalizeParser <$> subscriptionParser)
535543
backendContext =
536544
GQLContext
537545
(finalizeParser queryParserBackend)
546+
relayBackendFieldNames
538547
(finalizeParser <$> mutationParserBackend)
539548
(finalizeParser <$> subscriptionParser)
540549

@@ -653,7 +662,8 @@ unauthenticatedContext options sources allRemotes expFeatures schemaSampledFeatu
653662
(P.parserType queryParser)
654663
(P.parserType <$> mutationParser)
655664
(P.parserType <$> subscriptionParser)
656-
pure (GQLContext (finalizeParser queryParser) (finalizeParser <$> mutationParser) (finalizeParser <$> subscriptionParser), remoteErrors)
665+
let unauthFieldNames = queryRootFieldNamesOf queryParser
666+
pure (GQLContext (finalizeParser queryParser) unauthFieldNames (finalizeParser <$> mutationParser) (finalizeParser <$> subscriptionParser), remoteErrors)
657667

658668
-------------------------------------------------------------------------------
659669
-- Building parser fields
@@ -1024,13 +1034,14 @@ queryWithIntrospectionHelper basicQueryFP mutationP subscriptionP = do
10241034
-- Those two requirements cannot both be met when a service is mutations-only, and does not
10251035
-- provide any query. In such a case, to meet both of those, we introduce a placeholder query
10261036
-- in the schema.
1027-
placeholderText = "There are no queries available to the current role. Either there are no sources or remote schemas configured, or the current role doesn't have the required permissions."
1028-
placeholderField = NotNamespaced (RFRaw $ JO.String placeholderText) <$ P.selection_ Name._no_queries_available (Just $ G.Description placeholderText) P.string
1037+
placeholderText = "There are no queries available to the current role. Either there are no sources or remote schemas configured, or the current role doesn't have the required permissions." :: Text
1038+
placeholderField = NotNamespaced (RFRaw . TypenameResult $ encJFromJValue placeholderText) <$ P.selection_ Name._no_queries_available (Just $ G.Description placeholderText) P.string
10291039
fixedQueryFP = if null basicQueryFP then [placeholderField] else basicQueryFP
10301040
basicQueryP <- queryRootFromFields fixedQueryFP
10311041
let buildIntrospectionResponse printResponseFromSchema =
10321042
NotNamespaced
10331043
. RFRaw
1044+
. SchemaIntrospection
10341045
. printResponseFromSchema
10351046
<$> parseBuildIntrospectionSchema
10361047
(P.parserType basicQueryP)
@@ -1104,8 +1115,8 @@ customizeFields ::
11041115
(Functor f, MonadParse n) =>
11051116
ResolvedSourceCustomization ->
11061117
MkTypename ->
1107-
f [FieldParser n (RootField db remote action JO.Value)] ->
1108-
f [FieldParser n (NamespacedField (RootField db remote action JO.Value))]
1118+
f [FieldParser n (RootField db remote action RFRawPayload)] ->
1119+
f [FieldParser n (NamespacedField (RootField db remote action RFRawPayload))]
11091120
customizeFields ResolvedSourceCustomization {..} =
11101121
fmap . customizeNamespace _rscRootNamespace (const typenameToRawRF)
11111122

@@ -1166,15 +1177,24 @@ queryRoot = Name._query_root
11661177
finalizeParser :: Parser 'Output P.Parse a -> ParserFn a
11671178
finalizeParser parser = P.toQErr . P.runParse . P.runParser parser
11681179

1180+
-- | Extract the top-level field names from a query root parser's output type,
1181+
-- for storage in 'GQLContext' and use in conflict detection ('checkConflictingNode').
1182+
-- Returns @[]@ if the type isn't a named object, which shouldn't happen for a
1183+
-- well-formed query root but is handled gracefully.
1184+
queryRootFieldNamesOf :: P.Parser 'Output n a -> [G.Name]
1185+
queryRootFieldNamesOf p = case P.parserType p of
1186+
P.TNamed _ (P.Definition _ _ _ _ (P.TIObject oi)) -> map P.dName (P.oiFields oi)
1187+
_ -> []
1188+
11691189
throwOnConflictingDefinitions :: (QErrM m) => Either P.ConflictingDefinitions a -> m a
11701190
throwOnConflictingDefinitions = either (throw500 . fromErrorMessage . toErrorValue) pure
11711191

11721192
typenameToNamespacedRawRF ::
1173-
P.ParsedSelection (NamespacedField (RootField db remote action JO.Value)) ->
1174-
NamespacedField (RootField db remote action JO.Value)
1175-
typenameToNamespacedRawRF = P.handleTypename $ NotNamespaced . RFRaw . JO.String . toTxt
1193+
P.ParsedSelection (NamespacedField (RootField db remote action RFRawPayload)) ->
1194+
NamespacedField (RootField db remote action RFRawPayload)
1195+
typenameToNamespacedRawRF = P.handleTypename $ NotNamespaced . RFRaw . TypenameResult . encJFromJValue . toTxt
11761196

11771197
typenameToRawRF ::
1178-
P.ParsedSelection (RootField db remote action JO.Value) ->
1179-
RootField db remote action JO.Value
1180-
typenameToRawRF = P.handleTypename $ RFRaw . JO.String . toTxt
1198+
P.ParsedSelection (RootField db remote action RFRawPayload) ->
1199+
RootField db remote action RFRawPayload
1200+
typenameToRawRF = P.handleTypename $ RFRaw . TypenameResult . encJFromJValue . toTxt

0 commit comments

Comments
 (0)