mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
5f274b5527
If returning field contains nested selections then mutation is performed in two steps 1. Mutation is performed with returning columns of any primary key and unique constraints 2. returning fields are queried on rows returned by selecting from table by filtering with column values returned in Step 1. Since mutation takes two courses based on selecting relations in returning field, it is hard to maintain sequence of prepared arguments (PrepArg) generated while resolving returning field. So, we're using txtConverter instead of prepare to resolve mutation fields.
157 lines
5.7 KiB
Haskell
157 lines
5.7 KiB
Haskell
module Hasura.GraphQL.Explain
|
|
( explainGQLQuery
|
|
, GQLExplain
|
|
) where
|
|
|
|
import Data.Has (getter)
|
|
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.Casing as J
|
|
import qualified Data.Aeson.TH as J
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Database.PG.Query as Q
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
import qualified Text.Builder as TB
|
|
|
|
import Hasura.GraphQL.Context
|
|
import Hasura.GraphQL.Resolve.Context
|
|
import Hasura.GraphQL.Schema
|
|
import Hasura.GraphQL.Validate.Field
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.DML.Internal
|
|
import Hasura.RQL.Types
|
|
import Hasura.SQL.Types
|
|
|
|
import qualified Hasura.GraphQL.Resolve.Select as RS
|
|
import qualified Hasura.GraphQL.Transport.HTTP as TH
|
|
import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
|
|
import qualified Hasura.GraphQL.Validate as GV
|
|
import qualified Hasura.GraphQL.Validate.Types as VT
|
|
import qualified Hasura.RQL.DML.Select as RS
|
|
|
|
data GQLExplain
|
|
= GQLExplain
|
|
{ _gqeQuery :: !GH.GraphQLRequest
|
|
, _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 =
|
|
(ReaderT (FieldMap, OrdByCtx, SQLGenCtx) (Except QErr))
|
|
|
|
runExplain
|
|
:: (MonadError QErr m)
|
|
=> (FieldMap, OrdByCtx, SQLGenCtx) -> Explain a -> m a
|
|
runExplain ctx m =
|
|
either throwError return $ runExcept $ runReaderT m ctx
|
|
|
|
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
|
|
opCxt <- getOpCtx fName
|
|
builderSQL <- runExplain (fldMap, orderByCtx, sqlGenCtx) $
|
|
case opCxt of
|
|
OCSelect (SelOpCtx tn hdrs permFilter permLimit) -> do
|
|
validateHdrs hdrs
|
|
toSQL . RS.mkSQLSelect False <$>
|
|
RS.fromField txtConverter tn permFilter permLimit fld
|
|
OCSelectPkey (SelPkOpCtx tn hdrs permFilter argMap) -> do
|
|
validateHdrs hdrs
|
|
toSQL . RS.mkSQLSelect True <$>
|
|
RS.fromFieldByPKey txtConverter tn argMap permFilter fld
|
|
OCSelectAgg (SelOpCtx tn hdrs permFilter permLimit) -> do
|
|
validateHdrs hdrs
|
|
toSQL . RS.mkAggSelect <$>
|
|
RS.fromAggField txtConverter tn permFilter permLimit fld
|
|
OCFuncQuery (FuncQOpCtx tn hdrs permFilter permLimit fn argSeq) ->
|
|
procFuncQuery tn fn permFilter permLimit hdrs argSeq False
|
|
OCFuncAggQuery (FuncQOpCtx tn hdrs permFilter permLimit fn argSeq) ->
|
|
procFuncQuery tn fn permFilter permLimit hdrs argSeq True
|
|
_ -> throw500 "unexpected mut field info for explain"
|
|
|
|
let txtSQL = TB.run builderSQL
|
|
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
|
|
|
|
getOpCtx f =
|
|
onNothing (Map.lookup f opCtxMap) $ throw500 $
|
|
"lookup failed: opctx: " <> showName f
|
|
|
|
procFuncQuery tn fn permFilter permLimit hdrs argSeq isAgg = do
|
|
validateHdrs hdrs
|
|
(tabArgs, eSel, frmItem) <-
|
|
RS.fromFuncQueryField txtConverter fn argSeq isAgg fld
|
|
strfyNum <- stringifyNum <$> asks getter
|
|
return $ toSQL $
|
|
RS.mkFuncSelectWith fn tn
|
|
(RS.TablePerm permFilter permLimit) tabArgs strfyNum eSel frmItem
|
|
|
|
validateHdrs hdrs = do
|
|
let receivedHdrs = userVars userInfo
|
|
forM_ hdrs $ \hdr ->
|
|
unless (isJust $ getVarVal hdr receivedHdrs) $
|
|
throw400 NotFound $ hdr <<> " header is expected but not found"
|
|
|
|
explainGQLQuery
|
|
:: (MonadError QErr m, MonadIO m)
|
|
=> Q.PGPool
|
|
-> Q.TxIsolation
|
|
-> SchemaCache
|
|
-> SQLGenCtx
|
|
-> GQLExplain
|
|
-> m BL.ByteString
|
|
explainGQLQuery pool iso sc sqlGenCtx (GQLExplain query userVarsRaw)= do
|
|
(gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxMap
|
|
queryParts <- runReaderT (GV.getQueryParts query) gCtx
|
|
let topLevelNodes = TH.getTopLevelNodes (GV.qpOpDef queryParts)
|
|
|
|
unless (allHasuraNodes gCtx topLevelNodes) $
|
|
throw400 InvalidParams "only hasura queries can be explained"
|
|
|
|
(opTy, selSet) <- runReaderT (GV.validateGQ queryParts) gCtx
|
|
unless (opTy == G.OperationTypeQuery) $
|
|
throw400 InvalidParams "only queries can be explained"
|
|
let tx = mapM (explainField userInfo gCtx sqlGenCtx) (toList selSet)
|
|
plans <- liftIO (runExceptT $ runTx tx) >>= liftEither
|
|
return $ J.encode plans
|
|
where
|
|
gCtxMap = scGCtxMap sc
|
|
usrVars = mkUserVars $ maybe [] Map.toList userVarsRaw
|
|
userInfo = mkUserInfo (fromMaybe adminRole $ roleFromVars usrVars) usrVars
|
|
|
|
runTx tx = runLazyTx pool iso $ withUserInfo userInfo tx
|
|
|
|
allHasuraNodes gCtx nodes =
|
|
let typeLocs = TH.gatherTypeLocs gCtx nodes
|
|
isHasuraNode = \case
|
|
VT.HasuraType -> True
|
|
VT.RemoteType _ _ -> False
|
|
in all isHasuraNode typeLocs
|