graphql-engine/server/src-lib/Hasura/GraphQL/Explain.hs
Vamshi Surabhi ce243f5899
multiplexed subscriptions (#1934)
* add types to represent unparsed http gql requests

This will help when we add caching of frequently used ASTs

* query plan caching

* move livequery to execute

* add multiplexed module

* session variable can be customised depending on the context

Previously the value was always "current_setting('hasura.user')"

* get rid of typemap requirement in reusable plan

* subscriptions are multiplexed when possible

* use lazytx for introspection to avoid acquiring a pg connection

* refactor to make execute a completely decoupled module

* don't issue a transaction for a query

* don't use current setting for explained sql

* move postgres related types to a different module

* validate variableValues on postgres before multiplexing subs

* don't user current_setting for queries over ws

* plan_cache is only visible when developer flag is enabled

* introduce 'batch size' when multiplexing subscriptions

* bump stackage to 13.16

* fix schema_stitching test case error code

* store hashes instead of actual responses for subscriptions

* internal api to dump subscriptions state

* remove PlanCache from SchemaCacheRef

* allow live query options to be configured on server startup

* capture metrics for multiplexed subscriptions

* more metrics captured for multiplexed subs

* switch to tvar based hashmap for faster snapshotting

* livequery modules do not expose internal details

* fix typo in live query env vars

* switch to hasura's pg-client-hs
2019-04-17 15:18:41 +05:30

133 lines
4.3 KiB
Haskell

module Hasura.GraphQL.Explain
( explainGQLQuery
, GQLExplain
) where
import qualified Data.Aeson as J
import qualified Data.Aeson.Casing as J
import qualified Data.Aeson.TH as J
import qualified Data.HashMap.Strict as Map
import qualified Database.PG.Query as Q
import qualified Language.GraphQL.Draft.Syntax as G
import Hasura.EncJSON
import Hasura.GraphQL.Context
import Hasura.GraphQL.Resolve.Context
import Hasura.GraphQL.Validate.Field
import Hasura.Prelude
import Hasura.RQL.DML.Internal
import Hasura.RQL.Types
import Hasura.SQL.Types
import Hasura.SQL.Value
import qualified Hasura.GraphQL.Execute as E
import qualified Hasura.GraphQL.Resolve as RS
import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
import qualified Hasura.GraphQL.Validate as GV
import qualified Hasura.SQL.DML as S
data GQLExplain
= GQLExplain
{ _gqeQuery :: !GH.GQLReqParsed
, _gqeUser :: !(Maybe (Map.HashMap Text Text))
} deriving (Show, Eq)
$(J.deriveJSON (J.aesonDrop 4 J.camelCase){J.omitNothingFields=True}
''GQLExplain
)
data FieldPlan
= FieldPlan
{ _fpField :: !G.Name
, _fpSql :: !(Maybe Text)
, _fpPlan :: !(Maybe [Text])
} deriving (Show, Eq)
$(J.deriveJSON (J.aesonDrop 3 J.camelCase) ''FieldPlan)
type Explain r =
(ReaderT r (Except QErr))
runExplain
:: (MonadError QErr m)
=> r -> Explain r a -> m a
runExplain ctx m =
either throwError return $ runExcept $ runReaderT m ctx
resolveVal
:: (MonadError QErr m)
=> UserInfo -> UnresolvedVal -> m S.SQLExp
resolveVal userInfo = \case
RS.UVPG annPGVal ->
txtConverter annPGVal
RS.UVSessVar colTy sessVar -> do
sessVarVal <- getSessVarVal userInfo sessVar
return $ S.withTyAnn colTy $ withGeoVal colTy $
S.SELit sessVarVal
RS.UVSQL sqlExp -> return sqlExp
getSessVarVal
:: (MonadError QErr m)
=> UserInfo -> SessVar -> m SessVarVal
getSessVarVal userInfo sessVar =
onNothing (getVarVal sessVar usrVars) $
throw400 UnexpectedPayload $
"missing required session variable for role " <> rn <<>
" : " <> sessVar
where
rn = userRole userInfo
usrVars = userVars userInfo
explainField
:: (MonadTx m)
=> UserInfo -> GCtx -> SQLGenCtx -> Field -> m FieldPlan
explainField userInfo gCtx sqlGenCtx fld =
case fName of
"__type" -> return $ FieldPlan fName Nothing Nothing
"__schema" -> return $ FieldPlan fName Nothing Nothing
"__typename" -> return $ FieldPlan fName Nothing Nothing
_ -> do
unresolvedAST <-
runExplain (opCtxMap, userInfo, fldMap, orderByCtx, sqlGenCtx) $
RS.queryFldToPGAST fld
resolvedAST <- RS.traverseQueryRootFldAST (resolveVal userInfo)
unresolvedAST
let txtSQL = Q.getQueryText $ RS.toPGQuery resolvedAST
withExplain = "EXPLAIN (FORMAT TEXT) " <> txtSQL
planLines <- liftTx $ map runIdentity <$>
Q.listQE dmlTxErrorHandler (Q.fromText withExplain) () True
return $ FieldPlan fName (Just txtSQL) $ Just planLines
where
fName = _fName fld
opCtxMap = _gOpCtxMap gCtx
fldMap = _gFields gCtx
orderByCtx = _gOrdByCtx gCtx
explainGQLQuery
:: (MonadError QErr m, MonadIO m)
=> PGExecCtx
-> SchemaCache
-> SQLGenCtx
-> GQLExplain
-> m EncJSON
explainGQLQuery pgExecCtx sc sqlGenCtx (GQLExplain query userVarsRaw)= do
execPlan <- E.getExecPlanPartial userInfo sc query
(gCtx, rootSelSet) <- case execPlan of
E.GExPHasura (gCtx, rootSelSet, _) ->
return (gCtx, rootSelSet)
E.GExPRemote _ _ ->
throw400 InvalidParams "only hasura queries can be explained"
case rootSelSet of
GV.RQuery selSet -> do
let tx = mapM (explainField userInfo gCtx sqlGenCtx) (toList selSet)
plans <- liftIO (runExceptT $ runLazyTx pgExecCtx tx) >>= liftEither
return $ encJFromJValue plans
GV.RMutation _ ->
throw400 InvalidParams "only queries can be explained"
GV.RSubscription _ ->
throw400 InvalidParams "only queries can be explained"
where
usrVars = mkUserVars $ maybe [] Map.toList userVarsRaw
userInfo = mkUserInfo (fromMaybe adminRole $ roleFromVars usrVars) usrVars