mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 12:31:52 +03:00
7e334e08a4
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8947 GitOrigin-RevId: 18e52c928e1df535579e2077b4af6c2ce92bdcef
178 lines
6.3 KiB
Haskell
178 lines
6.3 KiB
Haskell
{-# LANGUAGE DeriveAnyClass #-}
|
|
|
|
-- | Postgres Execute Prepare
|
|
--
|
|
-- Deals with translating (session) variables to SQL expressions. Uses a state
|
|
-- monad to keep track of things like variables and generating fresh variable
|
|
-- names.
|
|
--
|
|
-- See 'Hasura.Backends.Postgres.Instances.Execute'.
|
|
module Hasura.Backends.Postgres.Execute.Prepare
|
|
( PlanVariables,
|
|
PrepArgMap,
|
|
PlanningSt (..),
|
|
ExecutionPlan,
|
|
ExecutionStep (..),
|
|
initPlanningSt,
|
|
prepareWithPlan,
|
|
prepareWithoutPlan,
|
|
withUserVars,
|
|
)
|
|
where
|
|
|
|
import Data.Aeson qualified as J
|
|
import Data.HashMap.Strict qualified as HashMap
|
|
import Data.IntMap qualified as IntMap
|
|
import Data.Text.Extended
|
|
import Database.PG.Query qualified as PG
|
|
import Hasura.Backends.Postgres.Connection.MonadTx
|
|
import Hasura.Backends.Postgres.SQL.DML qualified as S
|
|
import Hasura.Backends.Postgres.SQL.Value
|
|
import Hasura.Backends.Postgres.Translate.Column
|
|
import Hasura.Backends.Postgres.Types.Column
|
|
import Hasura.Base.Error
|
|
( Code (NotFound),
|
|
QErr,
|
|
throw400,
|
|
)
|
|
import Hasura.GraphQL.Execute.Backend
|
|
import Hasura.GraphQL.Parser.Names
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.IR.Value
|
|
import Hasura.RQL.Types.BackendType
|
|
import Hasura.RQL.Types.Column
|
|
import Hasura.Session
|
|
( SessionVariables,
|
|
UserInfo (_uiSession),
|
|
getSessionVariableValue,
|
|
sessionVariableToText,
|
|
)
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
|
|
|
type PlanVariables = HashMap.HashMap PlanVariable Int
|
|
|
|
-- | A variable used within the 'PlanVariables' map. We make the distinction
|
|
-- between internal (i.e. generated within `graphql-engine`) and external (i.e.
|
|
-- generated by users or as part of a query) variables, which avoids accidental
|
|
-- name collisions between the two.
|
|
data PlanVariable
|
|
= -- | An external GQL API name
|
|
External G.Name
|
|
| -- | An internal HGE name
|
|
Internal Text
|
|
deriving stock (Eq, Generic, Show)
|
|
deriving anyclass (Hashable)
|
|
|
|
-- | The value is (PG.PrepArg, PGScalarValue) because we want to log the human-readable value of the
|
|
-- prepared argument and not the binary encoding in PG format
|
|
type PrepArgMap = IntMap.IntMap (PG.PrepArg, PGScalarValue)
|
|
|
|
data PlanningSt = PlanningSt
|
|
{ _psArgNumber :: Int,
|
|
_psVariables :: PlanVariables,
|
|
_psPrepped :: PrepArgMap
|
|
}
|
|
deriving stock (Eq, Show)
|
|
|
|
initPlanningSt :: PlanningSt
|
|
initPlanningSt = PlanningSt 2 HashMap.empty IntMap.empty
|
|
|
|
-- | If we're preparing a value with planning state, we favour referring to
|
|
-- values by their prepared argument index. If the value refers to a session
|
|
-- value, we look for it in prepared value (1) and access the particular keys
|
|
-- using the JSONB @->>@ accessor.
|
|
prepareWithPlan ::
|
|
( MonadState PlanningSt m,
|
|
MonadError QErr m
|
|
) =>
|
|
UserInfo ->
|
|
UnpreparedValue ('Postgres pgKind) ->
|
|
m S.SQLExp
|
|
prepareWithPlan userInfo = \case
|
|
UVParameter provenance ColumnValue {..} -> do
|
|
argNum <- case provenance of
|
|
FromInternal name -> getVarArgNum (Internal name)
|
|
FromGraphQL varInfo -> getVarArgNum (External (getName varInfo))
|
|
Unknown -> getNextArgNum
|
|
addPrepArg argNum (binEncoder cvValue, cvValue)
|
|
return $ toPrepParam argNum (unsafePGColumnToBackend cvType)
|
|
UVSessionVar ty sessVar -> do
|
|
-- For queries, we need to make sure the session variables are passed. However,
|
|
-- we want to keep them as variables in the resulting SQL in order to keep
|
|
-- hitting query caching for similar queries.
|
|
_ <-
|
|
getSessionVariableValue sessVar (_uiSession userInfo)
|
|
`onNothing` throw400
|
|
NotFound
|
|
("missing session variable: " <>> sessionVariableToText sessVar)
|
|
let sessVarVal =
|
|
S.SEOpApp
|
|
(S.SQLOp "->>")
|
|
[currentSessionExp, S.SELit $ sessionVariableToText sessVar]
|
|
pure $ withTypeAnn ty sessVarVal
|
|
UVLiteral sqlExp -> pure sqlExp
|
|
UVSession -> pure currentSessionExp
|
|
where
|
|
currentSessionExp = S.SEPrep 1
|
|
|
|
-- | If we're /not/ using a prepared statement, substitution is pretty naïve:
|
|
-- we resolve session variable names, ignore parameter names, and substitute
|
|
-- into the 'S.SQLExp'.
|
|
prepareWithoutPlan ::
|
|
(MonadError QErr m) =>
|
|
UserInfo ->
|
|
UnpreparedValue ('Postgres pgKind) ->
|
|
m S.SQLExp
|
|
prepareWithoutPlan userInfo = \case
|
|
UVParameter _ cv -> pure $ toTxtValue cv
|
|
UVLiteral sqlExp -> pure sqlExp
|
|
UVSession -> pure $ sessionInfoJsonExp $ _uiSession userInfo
|
|
UVSessionVar ty sessVar -> do
|
|
let maybeSessionVariableValue =
|
|
getSessionVariableValue sessVar (_uiSession userInfo)
|
|
sessionVariableValue <-
|
|
fmap S.SELit
|
|
<$> onNothing maybeSessionVariableValue
|
|
$ throw400 NotFound
|
|
$ "missing session variable: " <>> sessionVariableToText sessVar
|
|
pure $ withTypeAnn ty sessionVariableValue
|
|
|
|
-- | The map of user session variables is always given the number (1) as its
|
|
-- variable argument number (see 'getVarArgNum'). If we want to refer to a
|
|
-- particular variable in this map, we use JSONB functions to interrogate
|
|
-- variable (1).
|
|
withUserVars :: SessionVariables -> PrepArgMap -> PrepArgMap
|
|
withUserVars usrVars list =
|
|
let usrVarsAsPgScalar = PGValJSON $ PG.JSON $ J.toJSON usrVars
|
|
prepArg = PG.toPrepVal (PG.ViaJSON usrVars)
|
|
in IntMap.insert 1 (prepArg, usrVarsAsPgScalar) list
|
|
|
|
-- | In prepared statements, we refer to variables by a number, not their name.
|
|
-- If the statement already refers to a variable, then we'll already have a
|
|
-- number for it, and so we just return that. Otherwise, we produce a new
|
|
-- number, and that will refer to the variable from now on.
|
|
getVarArgNum :: (MonadState PlanningSt m) => PlanVariable -> m Int
|
|
getVarArgNum var = do
|
|
PlanningSt curArgNum vars prepped <- get
|
|
HashMap.lookup var vars `onNothing` do
|
|
put $ PlanningSt (curArgNum + 1) (HashMap.insert var curArgNum vars) prepped
|
|
pure curArgNum
|
|
|
|
-- | Add a prepared argument to the prepared argument map. These are keyed by
|
|
-- the variable argument numbers, which can be computed using 'getVarArgNum'.
|
|
addPrepArg ::
|
|
(MonadState PlanningSt m) =>
|
|
Int ->
|
|
(PG.PrepArg, PGScalarValue) ->
|
|
m ()
|
|
addPrepArg argNum arg = modify \s ->
|
|
s {_psPrepped = IntMap.insert argNum arg (_psPrepped s)}
|
|
|
|
-- | Get '_psArgNumber' from inside the 'PlanningSt' and increment it for the
|
|
-- next operation. Think of this as a pure analogue to 'Data.Unique.newUnique'.
|
|
getNextArgNum :: (MonadState PlanningSt m) => m Int
|
|
getNextArgNum = state \s ->
|
|
( _psArgNumber s,
|
|
s {_psArgNumber = _psArgNumber s + 1}
|
|
)
|