graphql-engine/server/src-lib/Hasura/RQL/DML/Update.hs
Auke Booij 4c8ea8e865 Import pg-client-hs as PG
Result of executing the following commands:
```shell
# replace "as Q" imports with "as PG" (in retrospect this didn't need a regex)
git grep -lE 'as Q($|[^a-zA-Z])' -- '*.hs' | xargs sed -i -E 's/as Q($|[^a-zA-Z])/as PG\1/'
# replace " Q." with " PG."
git grep -lE ' Q\.' -- '*.hs' | xargs sed -i 's/ Q\./ PG./g'
# replace "(Q." with "(PG."
git grep -lE '\(Q\.' -- '*.hs' | xargs sed -i 's/(Q\./(PG./g'
# ditto, but for [, |, { and !
git grep -lE '\[Q\.' -- '*.hs' | xargs sed -i 's/\[Q\./\[PG./g'
git grep -l '|Q\.' -- '*.hs' | xargs sed -i 's/|Q\./|PG./g'
git grep -l '{Q\.' -- '*.hs' | xargs sed -i 's/{Q\./{PG./g'
git grep -l '!Q\.' -- '*.hs' | xargs sed -i 's/!Q\./!PG./g'
```
(Doing the `grep -l` before the `sed`, instead of `sed` on the entire codebase, reduces the number of `mtime` updates, and so reduces how many times a file gets recompiled while checking intermediate results.)

Finally, I manually removed a broken and unused `Arbitrary` instance in `Hasura.RQL.Network`. (It used an `import Test.QuickCheck.Arbitrary as Q` statement, which was erroneously caught by the first find-replace command.)

After this PR, `Q` is no longer used as an import qualifier. That was not the goal of this PR, but perhaps it's a useful fact for future efforts.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5933
GitOrigin-RevId: 8c84c59d57789111d40f5d3322c5a885dcfbf40e
2022-09-20 19:55:51 +00:00

234 lines
7.0 KiB
Haskell

module Hasura.RQL.DML.Update
( runUpdate,
)
where
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Aeson.Types
import Data.HashMap.Strict qualified as M
import Data.HashMap.Strict qualified as Map
import Data.Sequence qualified as DS
import Data.Text.Extended
import Database.PG.Query qualified as PG
import Hasura.Backends.Postgres.Connection
import Hasura.Backends.Postgres.Execute.Mutation
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.Returning
import Hasura.Backends.Postgres.Types.Table
import Hasura.Backends.Postgres.Types.Update
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.Prelude
import Hasura.QueryTags
import Hasura.RQL.DML.Internal
import Hasura.RQL.DML.Types
import Hasura.RQL.IR.BoolExp
import Hasura.RQL.IR.Update
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Metadata
import Hasura.RQL.Types.Permission
import Hasura.RQL.Types.SchemaCache
import Hasura.RQL.Types.Table
import Hasura.SQL.Backend
import Hasura.SQL.Types
import Hasura.Server.Types
import Hasura.Session
import Hasura.Tracing qualified as Tracing
convInc ::
(QErrM m) =>
ValueParser ('Postgres 'Vanilla) m S.SQLExp ->
PGCol ->
ColumnType ('Postgres 'Vanilla) ->
Value ->
m (PGCol, S.SQLExp)
convInc f col colType val = do
prepExp <- f (CollectableTypeScalar colType) val
return (col, S.SEOpApp S.incOp [S.mkSIdenExp col, prepExp])
convMul ::
(QErrM m) =>
ValueParser ('Postgres 'Vanilla) m S.SQLExp ->
PGCol ->
ColumnType ('Postgres 'Vanilla) ->
Value ->
m (PGCol, S.SQLExp)
convMul f col colType val = do
prepExp <- f (CollectableTypeScalar colType) val
return (col, S.SEOpApp S.mulOp [S.mkSIdenExp col, prepExp])
convSet ::
(QErrM m) =>
ValueParser ('Postgres 'Vanilla) m S.SQLExp ->
PGCol ->
ColumnType ('Postgres 'Vanilla) ->
Value ->
m (PGCol, S.SQLExp)
convSet f col colType val = do
prepExp <- f (CollectableTypeScalar colType) val
return (col, prepExp)
convDefault :: (Monad m) => PGCol -> ColumnType ('Postgres 'Vanilla) -> () -> m (PGCol, S.SQLExp)
convDefault col _ _ = return (col, S.SEUnsafe "DEFAULT")
convOp ::
(UserInfoM m, QErrM m) =>
FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) ->
[PGCol] ->
UpdPermInfo ('Postgres 'Vanilla) ->
[(PGCol, a)] ->
(PGCol -> ColumnType ('Postgres 'Vanilla) -> a -> m (PGCol, S.SQLExp)) ->
m [(PGCol, S.SQLExp)]
convOp fieldInfoMap preSetCols updPerm objs conv =
forM objs $ \(pgCol, a) -> do
-- if column has predefined value then throw error
when (pgCol `elem` preSetCols) $ throwNotUpdErr pgCol
checkPermOnCol PTUpdate allowedCols pgCol
colType <- askColumnType fieldInfoMap pgCol relWhenPgErr
res <- conv pgCol colType a
-- build a set expression's entry
withPathK (getPGColTxt pgCol) $ return res
where
allowedCols = upiCols updPerm
relWhenPgErr = "relationships can't be updated"
throwNotUpdErr c = do
roleName <- _uiRole <$> askUserInfo
throw400 NotSupported $
"column " <> c <<> " is not updatable"
<> " for role "
<> roleName <<> "; its value is predefined in permission"
validateUpdateQueryWith ::
(UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m) =>
SessionVariableBuilder m ->
ValueParser ('Postgres 'Vanilla) m S.SQLExp ->
UpdateQuery ->
m (AnnotatedUpdate ('Postgres 'Vanilla))
validateUpdateQueryWith sessVarBldr prepValBldr uq = do
let tableName = uqTable uq
tableInfo <- withPathK "table" $ askTableInfoSource tableName
let coreInfo = _tiCoreInfo tableInfo
-- If it is view then check if it is updatable
mutableView
tableName
viIsUpdatable
(_tciViewInfo coreInfo)
"updatable"
-- Check if the role has update permissions
updPerm <- askUpdPermInfo tableInfo
-- Check if all dependent headers are present
validateHeaders $ upiRequiredHeaders updPerm
-- Check if select is allowed
selPerm <-
modifyErr (<> selNecessaryMsg) $
askSelPermInfo tableInfo
let fieldInfoMap = _tciFieldInfoMap coreInfo
allCols = getCols fieldInfoMap
preSetObj = upiSet updPerm
preSetCols = M.keys preSetObj
-- convert the object to SQL set expression
setItems <-
withPathK "$set" $
convOp fieldInfoMap preSetCols updPerm (M.toList $ uqSet uq) $ convSet prepValBldr
incItems <-
withPathK "$inc" $
convOp fieldInfoMap preSetCols updPerm (M.toList $ uqInc uq) $ convInc prepValBldr
mulItems <-
withPathK "$mul" $
convOp fieldInfoMap preSetCols updPerm (M.toList $ uqMul uq) $ convMul prepValBldr
defItems <-
withPathK "$default" $
convOp fieldInfoMap preSetCols updPerm ((,()) <$> uqDefault uq) convDefault
-- convert the returning cols into sql returing exp
mAnnRetCols <- forM mRetCols $ \retCols ->
withPathK "returning" $ checkRetCols fieldInfoMap selPerm retCols
resolvedPreSetItems <-
M.toList
<$> mapM (convPartialSQLExp sessVarBldr) preSetObj
let setExpItems =
resolvedPreSetItems
++ setItems
++ incItems
++ mulItems
++ defItems
when (null setExpItems) $
throw400 UnexpectedPayload "atleast one of $set, $inc, $mul has to be present"
-- convert the where clause
annSQLBoolExp <-
withPathK "where" $
convBoolExp fieldInfoMap selPerm (uqWhere uq) sessVarBldr tableName prepValBldr
resolvedUpdFltr <-
convAnnBoolExpPartialSQL sessVarBldr $
upiFilter updPerm
resolvedUpdCheck <-
fromMaybe gBoolExpTrue
<$> traverse
(convAnnBoolExpPartialSQL sessVarBldr)
(upiCheck updPerm)
return $
AnnotatedUpdateG
tableName
(resolvedUpdFltr, annSQLBoolExp)
resolvedUpdCheck
(BackendUpdate $ Map.fromList $ fmap UpdateSet <$> setExpItems)
(mkDefaultMutFlds mAnnRetCols)
allCols
Nothing
where
mRetCols = uqReturning uq
selNecessaryMsg =
"; \"update\" is only allowed if the role "
<> "has \"select\" permission as \"where\" can't be used "
<> "without \"select\" permission on the table"
validateUpdateQuery ::
(QErrM m, UserInfoM m, CacheRM m) =>
UpdateQuery ->
m (AnnotatedUpdate ('Postgres 'Vanilla), DS.Seq PG.PrepArg)
validateUpdateQuery query = do
let source = uqSource query
tableCache :: TableCache ('Postgres 'Vanilla) <- fold <$> askTableCache source
flip runTableCacheRT (source, tableCache) $
runDMLP1T $
validateUpdateQueryWith sessVarFromCurrentSetting (valueParserWithCollectableType binRHSBuilder) query
runUpdate ::
forall m.
( QErrM m,
UserInfoM m,
CacheRM m,
HasServerConfigCtx m,
MonadBaseControl IO m,
MonadIO m,
Tracing.MonadTrace m,
MetadataM m
) =>
UpdateQuery ->
m EncJSON
runUpdate q = do
sourceConfig <- askSourceConfig @('Postgres 'Vanilla) (uqSource q)
userInfo <- askUserInfo
strfyNum <- stringifyNum . _sccSQLGenCtx <$> askServerConfigCtx
validateUpdateQuery q
>>= runTxWithCtx (_pscExecCtx sourceConfig) PG.ReadWrite
. flip runReaderT emptyQueryTagsComment
. execUpdateQuery strfyNum Nothing userInfo