graphql-engine/server/src-lib/Hasura/Backends/Postgres/Execute/Prepare.hs
Tom Harding 7e334e08a4 Import HashMap, not HM, Map, M...
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8947
GitOrigin-RevId: 18e52c928e1df535579e2077b4af6c2ce92bdcef
2023-04-26 15:43:44 +00:00

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}
)