Breaking up the Postgres implementation of the update-schema into reusable components

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2889
GitOrigin-RevId: 49c5d59a6f817832f11b1773b078aa24cc650ab5
This commit is contained in:
Philip Lykke Carlsen 2021-11-18 19:02:58 +01:00 committed by hasura-bot
parent f7e13cb7c9
commit 84027dad04
18 changed files with 511 additions and 200 deletions

View File

@ -424,9 +424,10 @@ library
, Hasura.Backends.Postgres.Translate.Types
, Hasura.Backends.Postgres.Translate.Update
, Hasura.Backends.Postgres.Types.BoolExp
, Hasura.Backends.Postgres.Types.CitusExtraTableMetadata
, Hasura.Backends.Postgres.Types.Column
, Hasura.Backends.Postgres.Types.Table
, Hasura.Backends.Postgres.Types.CitusExtraTableMetadata
, Hasura.Backends.Postgres.Types.Update
, Hasura.Backends.MySQL.DataLoader.Execute
, Hasura.Backends.MySQL.DataLoader.Plan

View File

@ -226,7 +226,6 @@ msDBMutationPlan ::
msDBMutationPlan userInfo stringifyNum sourceName sourceConfig mrf = do
go <$> case mrf of
MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert
MDBUpdate _annUpdate -> throw400 NotSupported "update mutations are not supported in MSSQL"
MDBDelete _annDelete -> throw400 NotSupported "delete mutations are not supported in MSSQL"
MDBFunction {} -> throw400 NotSupported "function mutations are not supported in MSSQL"
where

View File

@ -109,7 +109,7 @@ execUpdateQuery ::
) =>
Bool ->
UserInfo ->
(AnnUpd ('Postgres pgKind), DS.Seq Q.PrepArg) ->
(AnnotatedUpdateNode ('Postgres pgKind), DS.Seq Q.PrepArg) ->
m EncJSON
execUpdateQuery strfyNum userInfo (u, p) =
runMutation

View File

@ -32,6 +32,7 @@ import Hasura.Backends.Postgres.SQL.Types qualified as PG
import Hasura.Backends.Postgres.SQL.Value qualified as PG
import Hasura.Backends.Postgres.Translate.Select (PostgresAnnotatedFieldJSON)
import Hasura.Backends.Postgres.Translate.Select qualified as DS
import Hasura.Backends.Postgres.Types.Update
import Hasura.Base.Error (QErr)
import Hasura.EncJSON (EncJSON, encJFromJValue)
import Hasura.GraphQL.Execute.Backend
@ -196,13 +197,13 @@ convertUpdate ::
PostgresAnnotatedFieldJSON pgKind
) =>
UserInfo ->
IR.AnnUpdG ('Postgres pgKind) (Const Void) (UnpreparedValue ('Postgres pgKind)) ->
IR.AnnotatedUpdateNodeG ('Postgres pgKind) (Const Void) (UnpreparedValue ('Postgres pgKind)) ->
Bool ->
QueryTagsComment ->
m (Tracing.TraceT (Q.TxET QErr IO) EncJSON)
convertUpdate userInfo updateOperation stringifyNum queryTags = do
preparedUpdate <- traverse (prepareWithoutPlan userInfo) updateOperation
if null $ IR.uqp1OpExps updateOperation
if null $ updateOperations . IR.uqp1BackendIR $ updateOperation
then pure $ pure $ IR.buildEmptyMutResp $ IR.uqp1Output preparedUpdate
else
pure $

View File

@ -10,16 +10,16 @@ import Data.Aeson qualified as J
import Data.Has
import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.Extended qualified as M
import Data.HashMap.Strict.InsOrd.Extended qualified as OMap
import Data.List.NonEmpty qualified as NE
import Data.Parser.JSONPath
import Data.Text qualified as T
import Data.Text.Extended
import Hasura.Backends.Postgres.SQL.DML as PG hiding (CountType)
import Hasura.Backends.Postgres.SQL.DML as PG hiding (CountType, incOp)
import Hasura.Backends.Postgres.SQL.Types as PG hiding (FunctionName, TableName)
import Hasura.Backends.Postgres.SQL.Value as PG
import Hasura.Backends.Postgres.Types.BoolExp
import Hasura.Backends.Postgres.Types.Column
import Hasura.Backends.Postgres.Types.Update as PGIR
import Hasura.Base.Error
import Hasura.GraphQL.Parser hiding (EnumValueInfo, field)
import Hasura.GraphQL.Parser qualified as P
@ -39,7 +39,6 @@ import Hasura.GraphQL.Schema.Table
import Hasura.Prelude
import Hasura.RQL.IR
import Hasura.RQL.IR.Select qualified as IR
import Hasura.RQL.IR.Update qualified as IR
import Hasura.RQL.Types
import Hasura.SQL.AnyBackend qualified as AB
import Hasura.SQL.Types
@ -52,8 +51,11 @@ import Language.GraphQL.Draft.Syntax qualified as G
-- Some functions of 'BackendSchema' differ across different Postgres "kinds",
-- or call to functions (such as those related to Relay) that have not been
-- generalized to all kinds of Postgres and still explicitly work on Vanilla
-- Postgres. This class alllows each "kind" to specify its own specific
-- Postgres. This class allows each "kind" to specify its own specific
-- implementation. All common code is directly part of `BackendSchema`.
--
-- Note: Users shouldn't ever put this as a constraint. Use `BackendSchema
-- ('Postgres pgKind)` instead.
class PostgresSchema (pgKind :: PostgresKind) where
pgkBuildTableRelayQueryFields ::
BS.MonadBuildSchema ('Postgres pgKind) r m n =>
@ -121,7 +123,7 @@ instance
buildTableQueryFields = GSB.buildTableQueryFields
buildTableRelayQueryFields = pgkBuildTableRelayQueryFields
buildTableInsertMutationFields = GSB.buildTableInsertMutationFields
buildTableUpdateMutationFields = GSB.buildTableUpdateMutationFields updateOperators
buildTableUpdateMutationFields = GSB.buildTableUpdateMutationFields (\ti updP -> fmap BackendUpdate <$> updateOperators ti updP) -- TODO: simplify this!
buildTableDeleteMutationFields = GSB.buildTableDeleteMutationFields
buildFunctionQueryFields = GSB.buildFunctionQueryFields
buildFunctionRelayQueryFields = pgkBuildFunctionRelayQueryFields
@ -645,140 +647,407 @@ mkCountType _ Nothing = PG.CTStar
mkCountType (Just True) (Just cols) = PG.CTDistinct cols
mkCountType _ (Just cols) = PG.CTSimple cols
-- | @UpdateOperator b m n t@ represents one single update operator for a
-- backend @b@, parsing a value of type @t@. @UpdateOperator b m n@ is a
-- @Functor@, which (apart from the type variable @b@) is what enables
-- multi-backend support.
--
-- Use the 'Functor (UpdateOperator b m n)' instance to inject the
-- @UpdateOperator b m n (UnpreparedValue b)@ operators into backend-specific
-- IR types that encode update operators.
data UpdateOperator b m n t = UpdateOperator
{ updateOperatorApplicableColumn :: ColumnInfo b -> Bool,
updateOperatorParser ::
G.Name ->
TableName b ->
NonEmpty (ColumnInfo b) ->
m (InputFieldsParser n (HashMap (Column b) t))
}
deriving (Functor)
-- | The top-level component for building update operators parsers.
--
-- * It implements the 'preset' functionality from Update Permissions (see
-- <https://hasura.io/docs/latest/graphql/core/auth/authorization/permission-rules.html#column-presets
-- Permissions user docs>)
-- * It validates that that the update fields parsed are sound when taken as a
-- whole, i.e. that some changes are actually specified (either in the
-- mutation query text or in update preset columns) and that each column is
-- only used in one operator.
buildUpdateOperators ::
forall b n t m.
(BackendSchema b, MonadSchema n m, MonadError QErr m) =>
-- | Columns with @preset@ expressions
(HashMap (Column b) t) ->
-- | Update operators to include in the Schema
[UpdateOperator b m n t] ->
TableInfo b ->
UpdPermInfo b ->
m (InputFieldsParser n (HashMap (Column b) t))
buildUpdateOperators presetCols ops tableInfo updatePermissions = do
parsers :: InputFieldsParser n [HashMap (Column b) t] <-
sequenceA . catMaybes <$> traverse (runUpdateOperator tableInfo updatePermissions) ops
pure $
parsers
`P.bindFields` ( \opExps -> do
let withPreset = presetCols : opExps
mergeDisjoint @b withPreset
)
-- | The columns that have 'preset' definitions applied to them. (see
-- <https://hasura.io/docs/latest/graphql/core/auth/authorization/permission-rules.html#column-presets
-- Permissions user docs>)
presetColumns :: UpdPermInfo b -> HashMap (Column b) (UnpreparedValue b)
presetColumns = fmap partialSQLExpToUnpreparedValue . upiSet
-- | Produce an InputFieldsParser from an UpdateOperator, but only if the operator
-- applies to the table (i.e., it admits a non-empty column set).
runUpdateOperator ::
forall b m n t.
(Backend b, MonadSchema n m, MonadError QErr m) =>
TableInfo b ->
UpdPermInfo b ->
UpdateOperator b m n t ->
m
( Maybe
( InputFieldsParser
n
(HashMap (Column b) t)
)
)
runUpdateOperator tableInfo updatePermissions UpdateOperator {..} = do
let tableName = tableInfoName tableInfo
tableGQLName <- getTableGQLName tableInfo
columns <- tableUpdateColumns tableInfo updatePermissions
let applicableCols :: Maybe (NonEmpty (ColumnInfo b)) =
nonEmpty . filter updateOperatorApplicableColumn $ columns
(sequenceA :: Maybe (m a) -> m (Maybe a))
(applicableCols <&> updateOperatorParser tableGQLName tableName)
-- | Ensure that /some/ updates have been specified in a mutation.
ensureNonEmpty ::
forall b m t.
(MonadParse m, Backend b) =>
[Text] ->
[HashMap (Column b) t] ->
m ()
ensureNonEmpty allowedOperators parsedResults =
when (null $ M.unions parsedResults) $
parseError $
"At least any one of "
<> commaSeparated allowedOperators
<> " is expected"
-- | Merge the results of parsed update operators. Throws an error if the same
-- column has been specified in multiple operators.
mergeDisjoint ::
forall b m t.
(Backend b, MonadParse m) =>
[HashMap (Column b) t] ->
m (HashMap (Column b) t)
mergeDisjoint parsedResults = do
let unioned = M.unionsAll parsedResults
duplicates =
M.keys $
M.filter
( \case
_ :| [] -> False
_ -> True
)
unioned
unless (null duplicates) $
parseError
( "Column found in multiple operators: "
<> commaSeparated (map dquote duplicates)
<> "."
)
return $ M.map NE.head unioned
setOp ::
forall b n r m.
( BackendSchema b,
MonadReader r m,
Has MkTypename r,
MonadError QErr m,
MonadSchema n m
) =>
UpdateOperator b m n (UnpreparedValue b)
setOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = const True
updateOperatorParser tableGQLName tableName columns = do
let typedParser columnInfo =
fmap P.mkParameter
<$> BS.columnParser
(pgiType columnInfo)
(G.Nullability $ pgiIsNullable columnInfo)
updateOperator
tableGQLName
$$(G.litName "_set")
typedParser
columns
"sets the columns of the filtered rows to the given values"
(G.Description $ "input type for updating data in table " <>> tableName)
incOp ::
forall b m n r.
( Backend b,
MonadReader r m,
MonadError QErr m,
MonadSchema n m,
BackendSchema b,
Has MkTypename r
) =>
UpdateOperator b m n (UnpreparedValue b)
incOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = isNumCol
updateOperatorParser tableGQLName tableName columns = do
let typedParser columnInfo =
fmap P.mkParameter
<$> BS.columnParser
(pgiType columnInfo)
(G.Nullability $ pgiIsNullable columnInfo)
updateOperator
tableGQLName
$$(G.litName "_inc")
typedParser
columns
"increments the numeric columns with given value of the filtered values"
(G.Description $ "input type for incrementing numeric columns in table " <>> tableName)
-- | Update operator that prepends a value to a column containing jsonb arrays.
--
-- Note: Currently this is Postgres specific because json columns have not been ported
-- to other backends yet.
prependOp ::
forall pgKind m n r.
( BackendSchema ('Postgres pgKind),
MonadReader r m,
MonadError QErr m,
MonadSchema n m,
Has MkTypename r
) =>
UpdateOperator ('Postgres pgKind) m n (UnpreparedValue ('Postgres pgKind))
prependOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = (isScalarColumnWhere (== PGJSONB) . pgiType)
updateOperatorParser tableGQLName tableName columns = do
let typedParser columnInfo =
fmap P.mkParameter
<$> BS.columnParser
(pgiType columnInfo)
(G.Nullability $ pgiIsNullable columnInfo)
desc = "prepend existing jsonb value of filtered columns with new jsonb value"
updateOperator
tableGQLName
$$(G.litName "_prepend")
typedParser
columns
desc
desc
-- | Update operator that appends a value to a column containing jsonb arrays.
--
-- Note: Currently this is Postgres specific because json columns have not been ported
-- to other backends yet.
appendOp ::
forall pgKind m n r.
( BackendSchema ('Postgres pgKind),
MonadReader r m,
MonadError QErr m,
MonadSchema n m,
Has MkTypename r
) =>
UpdateOperator ('Postgres pgKind) m n (UnpreparedValue ('Postgres pgKind))
appendOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = (isScalarColumnWhere (== PGJSONB) . pgiType)
updateOperatorParser tableGQLName tableName columns = do
let typedParser columnInfo =
fmap P.mkParameter
<$> BS.columnParser
(pgiType columnInfo)
(G.Nullability $ pgiIsNullable columnInfo)
desc = "append existing jsonb value of filtered columns with new jsonb value"
updateOperator
tableGQLName
$$(G.litName "_append")
typedParser
columns
desc
desc
-- | Update operator that deletes a value at a specified key from a column
-- containing jsonb objects.
--
-- Note: Currently this is Postgres specific because json columns have not been ported
-- to other backends yet.
deleteKeyOp ::
forall pgKind m n r.
( BackendSchema ('Postgres pgKind),
MonadReader r m,
MonadError QErr m,
MonadSchema n m,
Has MkTypename r
) =>
UpdateOperator ('Postgres pgKind) m n (UnpreparedValue ('Postgres pgKind))
deleteKeyOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = (isScalarColumnWhere (== PGJSONB) . pgiType)
updateOperatorParser tableGQLName tableName columns = do
let nullableTextParser _ = fmap P.mkParameter <$> columnParser (ColumnScalar PGText) (G.Nullability True)
desc = "delete key/value pair or string element. key/value pairs are matched based on their key value"
updateOperator
tableGQLName
$$(G.litName "_delete_key")
nullableTextParser
columns
desc
desc
-- | Update operator that deletes a value at a specific index from a column
-- containing jsonb arrays.
--
-- Note: Currently this is Postgres specific because json columns have not been ported
-- to other backends yet.
deleteElemOp ::
forall pgKind m n r.
( BackendSchema ('Postgres pgKind),
MonadReader r m,
MonadError QErr m,
MonadSchema n m,
Has MkTypename r
) =>
UpdateOperator ('Postgres pgKind) m n (UnpreparedValue ('Postgres pgKind))
deleteElemOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = (isScalarColumnWhere (== PGJSONB) . pgiType)
updateOperatorParser tableGQLName tableName columns = do
let nonNullableIntParser _ = fmap P.mkParameter <$> columnParser (ColumnScalar PGInteger) (G.Nullability False)
desc =
"delete the array element with specified index (negative integers count from the end). "
<> "throws an error if top level container is not an array"
updateOperator
tableGQLName
$$(G.litName "_delete_elem")
nonNullableIntParser
columns
desc
desc
-- | Update operator that deletes a field at a certan path from a column
-- containing jsonb objects.
--
-- Note: Currently this is Postgres specific because json columns have not been ported
-- to other backends yet.
deleteAtPathOp ::
forall pgKind m n r.
( BackendSchema ('Postgres pgKind),
MonadReader r m,
MonadError QErr m,
MonadSchema n m,
Has MkTypename r
) =>
UpdateOperator ('Postgres pgKind) m n [UnpreparedValue ('Postgres pgKind)]
deleteAtPathOp = UpdateOperator {..}
where
updateOperatorApplicableColumn = (isScalarColumnWhere (== PGJSONB) . pgiType)
updateOperatorParser tableGQLName tableName columns = do
let nonNullableTextListParser _ = P.list . fmap (P.mkParameter) <$> columnParser (ColumnScalar PGText) (G.Nullability False)
desc = "delete the field or element with specified path (for JSON arrays, negative integers count from the end)"
updateOperator
tableGQLName
$$(G.litName "_delete_at_path")
nonNullableTextListParser
columns
desc
desc
-- | Construct a parser for a single update operator.
--
-- @updateOperator _ "op" fp MkOp ["col1","col2"]@ gives a parser that accepts
-- objects in the shape of:
--
-- > op: {
-- > col1: "x",
-- > col2: "y"
-- > }
--
-- And (morally) parses into values:
--
-- > M.fromList [("col1", MkOp (fp "x")), ("col2", MkOp (fp "y"))]
updateOperator ::
forall n r m b a.
(MonadParse n, MonadReader r m, Has MkTypename r, Backend b) =>
G.Name ->
G.Name ->
(ColumnInfo b -> m (Parser 'Both n a)) ->
NonEmpty (ColumnInfo b) -> -- TODO: Should actually be a nonempty set - do we have a lib for that?
G.Description ->
G.Description ->
m (InputFieldsParser n (HashMap (Column b) a))
updateOperator tableGQLName opName mkParser columns opDesc objDesc = do
fieldParsers :: NonEmpty (InputFieldsParser n (Maybe (Column b, a))) <-
for columns \columnInfo -> do
let fieldName = pgiName columnInfo
fieldDesc = pgiDescription columnInfo
fieldParser <- mkParser columnInfo
pure $
P.fieldOptional fieldName fieldDesc fieldParser
`mapField` \value -> (pgiColumn columnInfo, value)
objName <- P.mkTypename $ tableGQLName <> opName <> $$(G.litName "_input")
pure $
fmap (M.fromList . (fold :: Maybe [(Column b, a)] -> [(Column b, a)])) $
P.fieldOptional opName (Just opDesc) $
P.object objName (Just objDesc) $
(catMaybes . toList) <$> sequenceA fieldParsers
{-# ANN updateOperator ("HLint: ignore Use tuple-section" :: String) #-}
-- | Various update operators
updateOperators ::
forall pgKind m n r.
(BackendSchema ('Postgres pgKind), MonadSchema n m, MonadTableInfo r m, Has MkTypename r) =>
-- | table info
( MonadParse n,
MonadReader r m,
Has MkTypename r,
MonadError QErr m,
MonadSchema n m,
BackendSchema ('Postgres pgKind)
) =>
TableInfo ('Postgres pgKind) ->
-- | update permissions of the table
UpdPermInfo ('Postgres pgKind) ->
m (InputFieldsParser n [(Column ('Postgres pgKind), IR.UpdOpExpG (UnpreparedValue ('Postgres pgKind)))])
updateOperators tableInfo updatePermissions = do
tableGQLName <- getTableGQLName tableInfo
columns <- tableUpdateColumns tableInfo updatePermissions
let numericCols = onlyNumCols columns
jsonCols = onlyJSONBCols columns
parsers <-
catMaybes
<$> sequenceA
[ updateOperator
tableGQLName
$$(G.litName "_set")
typedParser
IR.UpdSet
columns
"sets the columns of the filtered rows to the given values"
(G.Description $ "input type for updating data in table " <>> tableName),
updateOperator
tableGQLName
$$(G.litName "_inc")
typedParser
IR.UpdInc
numericCols
"increments the numeric columns with given value of the filtered values"
(G.Description $ "input type for incrementing numeric columns in table " <>> tableName),
let desc = "prepend existing jsonb value of filtered columns with new jsonb value"
in updateOperator
tableGQLName
$$(G.litName "_prepend")
typedParser
IR.UpdPrepend
jsonCols
desc
desc,
let desc = "append existing jsonb value of filtered columns with new jsonb value"
in updateOperator
tableGQLName
$$(G.litName "_append")
typedParser
IR.UpdAppend
jsonCols
desc
desc,
let desc = "delete key/value pair or string element. key/value pairs are matched based on their key value"
in updateOperator
tableGQLName
$$(G.litName "_delete_key")
nullableTextParser
IR.UpdDeleteKey
jsonCols
desc
desc,
let desc =
"delete the array element with specified index (negative integers count from the end). "
<> "throws an error if top level container is not an array"
in updateOperator
tableGQLName
$$(G.litName "_delete_elem")
nonNullableIntParser
IR.UpdDeleteElem
jsonCols
desc
desc,
let desc = "delete the field or element with specified path (for JSON arrays, negative integers count from the end)"
in updateOperator
tableGQLName
$$(G.litName "_delete_at_path")
(fmap P.list . nonNullableTextParser)
IR.UpdDeleteAtPath
jsonCols
desc
desc
]
let allowedOperators = fst <$> parsers
pure $
fmap catMaybes (sequenceA $ snd <$> parsers)
`P.bindFields` \opExps -> do
-- there needs to be at least one operator in the update, even if it is empty
let presetColumns = Map.toList $ IR.UpdSet . partialSQLExpToUnpreparedValue <$> upiSet updatePermissions
when (null opExps && null presetColumns) $
parseError $
"at least any one of " <> commaSeparated allowedOperators <> " is expected"
-- no column should appear twice
let flattenedExps = concat opExps
erroneousExps = OMap.filter ((> 1) . length) $ OMap.groupTuples flattenedExps
unless (OMap.null erroneousExps) $
parseError $
"column found in multiple operators; "
<> T.intercalate
". "
[ dquote columnName <> " in " <> commaSeparated (IR.updateOperatorText <$> ops)
| (columnName, ops) <- OMap.toList erroneousExps
]
pure $ presetColumns <> flattenedExps
where
tableName = tableInfoName tableInfo
typedParser columnInfo = fmap P.mkParameter <$> columnParser (pgiType columnInfo) (G.Nullability $ pgiIsNullable columnInfo)
nonNullableTextParser _ = fmap P.mkParameter <$> columnParser (ColumnScalar PGText) (G.Nullability False)
nullableTextParser _ = fmap P.mkParameter <$> columnParser (ColumnScalar PGText) (G.Nullability True)
nonNullableIntParser _ = fmap P.mkParameter <$> columnParser (ColumnScalar PGInteger) (G.Nullability False)
onlyJSONBCols = filter (isScalarColumnWhere (== PGJSONB) . pgiType)
updateOperator ::
G.Name ->
G.Name ->
(ColumnInfo b -> m (Parser 'Both n a)) ->
(a -> IR.UpdOpExpG (UnpreparedValue b)) ->
[ColumnInfo b] ->
G.Description ->
G.Description ->
m (Maybe (Text, InputFieldsParser n (Maybe [(Column b, IR.UpdOpExpG (UnpreparedValue b))])))
updateOperator tableGQLName opName mkParser updOpExp columns opDesc objDesc =
whenMaybe (not $ null columns) do
fields <- for columns \columnInfo -> do
let fieldName = pgiName columnInfo
fieldDesc = pgiDescription columnInfo
fieldParser <- mkParser columnInfo
pure $
P.fieldOptional fieldName fieldDesc fieldParser
`mapField` \value -> (pgiColumn columnInfo, updOpExp value)
objName <- P.mkTypename $ tableGQLName <> opName <> $$(G.litName "_input")
pure $
(G.unName opName,) $
P.fieldOptional opName (Just opDesc) $
P.object objName (Just objDesc) $
catMaybes <$> sequenceA fields
m (InputFieldsParser n (HashMap (Column ('Postgres pgKind)) (UpdOpExpG (UnpreparedValue ('Postgres pgKind)))))
updateOperators tableInfo updatePermissions =
buildUpdateOperators
(PGIR.UpdSet <$> presetColumns updatePermissions)
[ PGIR.UpdSet <$> setOp,
PGIR.UpdInc <$> incOp,
PGIR.UpdPrepend <$> prependOp,
PGIR.UpdAppend <$> appendOp,
PGIR.UpdDeleteKey <$> deleteKeyOp,
PGIR.UpdDeleteElem <$> deleteElemOp,
PGIR.UpdDeleteAtPath <$> deleteAtPathOp
]
tableInfo
updatePermissions

View File

@ -15,6 +15,7 @@ import Hasura.Backends.Postgres.SQL.Types qualified as PG
import Hasura.Backends.Postgres.SQL.Value qualified as PG
import Hasura.Backends.Postgres.Types.BoolExp qualified as PG
import Hasura.Backends.Postgres.Types.CitusExtraTableMetadata qualified as Citus
import Hasura.Backends.Postgres.Types.Update qualified as PG
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.Types.Backend
@ -28,6 +29,9 @@ import Hasura.SQL.Tag
-- Some types of 'Backend' differ across different Postgres "kinds". This
-- class alllows each "kind" to specify its own specific implementation. All
-- common code is directly part of the `Backend` instance.
--
-- Note: Users shouldn't ever put this as a constraint. Use `Backend ('Postgres
-- pgKind)` instead.
class
( Representable (PgExtraTableMetadata pgKind),
J.ToJSON (PgExtraTableMetadata pgKind),
@ -71,6 +75,8 @@ instance
type SQLExpression ('Postgres pgKind) = PG.SQLExp
type SQLOperator ('Postgres pgKind) = PG.SQLOp
type BackendUpdate ('Postgres pgKind) = PG.BackendUpdate
type ExtraTableMetadata ('Postgres pgKind) = PgExtraTableMetadata pgKind
type ExtraInsertData ('Postgres pgKind) = ()

View File

@ -3,11 +3,13 @@ module Hasura.Backends.Postgres.Translate.Update
)
where
import Data.HashMap.Strict qualified as Map
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.BoolExp
import Hasura.Backends.Postgres.Translate.Insert
import Hasura.Backends.Postgres.Translate.Returning
import Hasura.Backends.Postgres.Types.Update
import Hasura.Prelude
import Hasura.RQL.IR.Update
import Hasura.RQL.Types
@ -15,9 +17,9 @@ import Hasura.SQL.Types
mkUpdateCTE ::
Backend ('Postgres pgKind) =>
AnnUpd ('Postgres pgKind) ->
AnnotatedUpdateNode ('Postgres pgKind) ->
S.CTE
mkUpdateCTE (AnnUpd tn opExps (permFltr, wc) chk _ columnsInfo) =
mkUpdateCTE (AnnotatedUpdateNode tn (permFltr, wc) chk (BackendUpdate opExps) _ columnsInfo) =
S.CTEUpdate update
where
update =
@ -27,7 +29,7 @@ mkUpdateCTE (AnnUpd tn opExps (permFltr, wc) chk _ columnsInfo) =
$ [ S.selectStar,
asCheckErrorExtractor $ insertCheckConstraint checkExpr
]
setExp = S.SetExp $ map (expandOperator columnsInfo) opExps
setExp = S.SetExp $ map (expandOperator columnsInfo) (Map.toList opExps)
tableFltr = Just $ S.WhereFrag tableFltrExpr
tableFltrExpr = toSQLBoolExp (S.QualTable tn) $ andAnnBoolExps permFltr wc
checkExpr = toSQLBoolExp (S.QualTable tn) chk

View File

@ -0,0 +1,34 @@
-- | This module defines the Update-related IR types specific to Postgres.
module Hasura.Backends.Postgres.Types.Update
( BackendUpdate (..),
UpdOpExpG (..),
)
where
import Hasura.Backends.Postgres.SQL.Types (PGCol)
import Hasura.Prelude
-- | The PostgreSQL-specific data of an Update expression.
--
-- This is parameterised over @v@ which enables different phases of IR
-- transformation to maintain the overall structure while enriching/transforming
-- the data at the leaves.
data BackendUpdate v = BackendUpdate
{ -- | The update operations to perform on each colum.
updateOperations :: !(HashMap PGCol (UpdOpExpG v))
}
deriving (Functor, Foldable, Traversable, Generic, Data)
-- | The various @update operators@ supported by PostgreSQL,
-- i.e. the @_set@, @_inc@ operators that appear in the schema.
--
-- See <https://hasura.io/docs/latest/graphql/core/databases/postgres/mutations/update.html#postgres-update-mutation Update Mutations User docs>
data UpdOpExpG v
= UpdSet !v
| UpdInc !v
| UpdAppend !v
| UpdPrepend !v
| UpdDeleteKey !v
| UpdDeleteElem !v
| UpdDeleteAtPath ![v]
deriving (Functor, Foldable, Traversable, Generic, Data)

View File

@ -83,7 +83,9 @@ boolExp sourceName tableInfo selectPermissions = memoizeOn 'boolExp (sourceName,
FIRelationship relationshipInfo -> do
remoteTableInfo <- askTableInfo sourceName $ riRTable relationshipInfo
remotePermissions <- lift $ tableSelectPermissions remoteTableInfo
let remoteTableFilter = (fmap . fmap) partialSQLExpToUnpreparedValue $ maybe annBoolExpTrue spiFilter remotePermissions
let remoteTableFilter =
fmap partialSQLExpToUnpreparedValue
<$> maybe annBoolExpTrue spiFilter remotePermissions
remoteBoolExp <- lift $ boolExp sourceName remoteTableInfo remotePermissions
pure $ fmap (AVRelationship relationshipInfo . andAnnBoolExps remoteTableFilter) remoteBoolExp
FIComputedField ComputedFieldInfo {..} -> do

View File

@ -174,11 +174,11 @@ buildTableInsertMutationFields
buildTableUpdateMutationFields ::
forall b r m n.
MonadBuildSchema b r m n =>
-- | Action that builds the @update operators@ the backend supports
-- | TODO: Docs. WAS: Action that builds the @update operators@ the backend supports
( TableInfo b ->
UpdPermInfo b ->
m
(InputFieldsParser n [(Column b, UpdOpExpG (UnpreparedValue b))])
(InputFieldsParser n (BackendUpdate b (UnpreparedValue b)))
) ->
-- | The source that the table lives in
SourceName ->

View File

@ -407,7 +407,7 @@ updateTable ::
( TableInfo b ->
UpdPermInfo b ->
m
(InputFieldsParser n [(Column b, UpdOpExpG (UnpreparedValue b))])
(InputFieldsParser n (BackendUpdate b (UnpreparedValue b)))
) ->
-- | table source
SourceName ->
@ -421,13 +421,13 @@ updateTable ::
UpdPermInfo b ->
-- | select permissions of the table (if any)
Maybe (SelPermInfo b) ->
m (FieldParser n (IR.AnnUpdG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b)))
updateTable updateOperators sourceName tableInfo fieldName description updatePerms selectPerms = do
m (FieldParser n (IR.AnnotatedUpdateNodeG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b)))
updateTable updateIR sourceName tableInfo fieldName description updatePerms selectPerms = do
let tableName = tableInfoName tableInfo
columns = tableColumns tableInfo
whereName = $$(G.litName "where")
whereDesc = "filter the rows which have to be updated"
opArgs <- updateOperators tableInfo updatePerms
opArgs <- updateIR tableInfo updatePerms
whereArg <- P.field whereName (Just whereDesc) <$> boolExp sourceName tableInfo selectPerms
selection <- mutationSelectionSet sourceName tableInfo selectPerms
let argsParser = liftA2 (,) opArgs whereArg
@ -445,7 +445,7 @@ updateTableByPk ::
-- | Update Operators
( TableInfo b ->
UpdPermInfo b ->
m (InputFieldsParser n [(Column b, UpdOpExpG (UnpreparedValue b))])
m (InputFieldsParser n (BackendUpdate b (UnpreparedValue b)))
) ->
-- | table source
SourceName ->
@ -459,14 +459,14 @@ updateTableByPk ::
UpdPermInfo b ->
-- | select permissions of the table
SelPermInfo b ->
m (Maybe (FieldParser n (IR.AnnUpdG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b))))
updateTableByPk updateOperators sourceName tableInfo fieldName description updatePerms selectPerms = runMaybeT $ do
m (Maybe (FieldParser n (IR.AnnotatedUpdateNodeG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b))))
updateTableByPk backendIR sourceName tableInfo fieldName description updatePerms selectPerms = runMaybeT $ do
let columns = tableColumns tableInfo
tableName = tableInfoName tableInfo
tableGQLName <- getTableGQLName tableInfo
pkArgs <- MaybeT $ primaryKeysArguments tableInfo selectPerms
lift $ do
opArgs <- updateOperators tableInfo updatePerms
opArgs <- backendIR tableInfo updatePerms
selection <- tableSelectionSet sourceName tableInfo selectPerms
pkObjectName <- P.mkTypename $ tableGQLName <> $$(G.litName "_pk_columns_input")
let pkFieldName = $$(G.litName "pk_columns")
@ -485,16 +485,16 @@ mkUpdateObject ::
TableName b ->
[ColumnInfo b] ->
UpdPermInfo b ->
( ( [(Column b, IR.UpdOpExpG (UnpreparedValue b))],
( ( BackendUpdate b (UnpreparedValue b),
AnnBoolExp b (UnpreparedValue b)
),
IR.MutationOutputG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b)
) ->
IR.AnnUpdG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b)
mkUpdateObject table columns updatePerms ((opExps, whereExp), mutationOutput) =
IR.AnnUpd
IR.AnnotatedUpdateNodeG b (IR.RemoteSelect UnpreparedValue) (UnpreparedValue b)
mkUpdateObject table columns updatePerms ((backendIR, whereExp), mutationOutput) =
IR.AnnotatedUpdateNode
{ IR.uqp1Table = table,
IR.uqp1OpExps = opExps,
IR.uqp1BackendIR = backendIR,
IR.uqp1Where = (permissionFilter, whereExp),
IR.uqp1Check = checkExp,
IR.uqp1Output = mutationOutput,

View File

@ -163,6 +163,8 @@ tableColumns tableInfo =
columnInfo (FIColumn ci) = Just ci
columnInfo _ = Nothing
-- | Get the columns of a table that my be selected under the given select
-- permissions.
tableSelectColumns ::
forall m n r b.
(Backend b, MonadSchema n m, MonadTableInfo r m, MonadRole r m) =>
@ -176,6 +178,8 @@ tableSelectColumns sourceName tableInfo permissions =
columnInfo (FIColumn ci) = Just ci
columnInfo _ = Nothing
-- | Get the columns of a table that my be updated under the given update
-- permissions.
tableUpdateColumns ::
forall m n b.
(Backend b, MonadSchema n m) =>

View File

@ -8,6 +8,7 @@ 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 Q
@ -17,6 +18,7 @@ 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
@ -98,7 +100,7 @@ validateUpdateQueryWith ::
SessionVariableBuilder ('Postgres 'Vanilla) m ->
ValueParser ('Postgres 'Vanilla) m S.SQLExp ->
UpdateQuery ->
m (AnnUpd ('Postgres 'Vanilla))
m (AnnotatedUpdateNode ('Postgres 'Vanilla))
validateUpdateQueryWith sessVarBldr prepValBldr uq = do
let tableName = uqTable uq
tableInfo <- withPathK "table" $ askTabInfoSource tableName
@ -177,11 +179,11 @@ validateUpdateQueryWith sessVarBldr prepValBldr uq = do
(upiCheck updPerm)
return $
AnnUpd
AnnotatedUpdateNode
tableName
(fmap UpdSet <$> setExpItems)
(resolvedUpdFltr, annSQLBoolExp)
resolvedUpdCheck
(BackendUpdate $ Map.fromList $ fmap UpdSet <$> setExpItems)
(mkDefaultMutFlds mAnnRetCols)
allCols
where
@ -194,7 +196,7 @@ validateUpdateQueryWith sessVarBldr prepValBldr uq = do
validateUpdateQuery ::
(QErrM m, UserInfoM m, CacheRM m) =>
UpdateQuery ->
m (AnnUpd ('Postgres 'Vanilla), DS.Seq Q.PrepArg)
m (AnnotatedUpdateNode ('Postgres 'Vanilla), DS.Seq Q.PrepArg)
validateUpdateQuery query = do
let source = uqSource query
tableCache :: TableCache ('Postgres 'Vanilla) <- askTableCache source

View File

@ -41,7 +41,7 @@ data RootField (db :: BackendType -> Type) remote action raw where
data MutationDB (b :: BackendType) (r :: BackendType -> Type) v
= MDBInsert (AnnInsert b r v)
| MDBUpdate (AnnUpdG b r v)
| MDBUpdate (AnnotatedUpdateNodeG b r v)
| MDBDelete (AnnDelG b r v)
| -- | This represents a VOLATILE function, and is AnnSimpleSelG for easy
-- re-use of non-VOLATILE function tracking code.

View File

@ -1,8 +1,6 @@
module Hasura.RQL.IR.Update
( AnnUpd,
AnnUpdG (..),
UpdOpExpG (..),
updateOperatorText,
( AnnotatedUpdateNode,
AnnotatedUpdateNodeG (..),
)
where
@ -14,11 +12,12 @@ import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.SQL.Backend
data AnnUpdG (b :: BackendType) (r :: BackendType -> Type) v = AnnUpd
data AnnotatedUpdateNodeG (b :: BackendType) (r :: BackendType -> Type) v = AnnotatedUpdateNode
{ uqp1Table :: !(TableName b),
uqp1OpExps :: ![(Column b, UpdOpExpG v)],
uqp1Where :: !(AnnBoolExp b v, AnnBoolExp b v),
uqp1Check :: !(AnnBoolExp b v),
-- | All the backend-specific data related to an update mutation
uqp1BackendIR :: BackendUpdate b v,
-- we don't prepare the arguments for returning
-- however the session variable can still be
-- converted as desired
@ -27,28 +26,4 @@ data AnnUpdG (b :: BackendType) (r :: BackendType -> Type) v = AnnUpd
}
deriving (Functor, Foldable, Traversable)
type AnnUpd b = AnnUpdG b (Const Void) (SQLExpression b)
data UpdOpExpG v
= UpdSet !v
| UpdInc !v
| UpdAppend !v
| UpdPrepend !v
| UpdDeleteKey !v
| UpdDeleteElem !v
| UpdDeleteAtPath ![v]
deriving (Functor, Foldable, Traversable, Generic, Data)
-- NOTE: This function can be improved, because we use
-- the literal values defined below in the 'updateOperators'
-- function in 'Hasura.GraphQL.Schema.Mutation'. It would
-- be nice if we could avoid duplicating the string literal
-- values
updateOperatorText :: UpdOpExpG a -> Text
updateOperatorText (UpdSet _) = "_set"
updateOperatorText (UpdInc _) = "_inc"
updateOperatorText (UpdAppend _) = "_append"
updateOperatorText (UpdPrepend _) = "_prepend"
updateOperatorText (UpdDeleteKey _) = "_delete_key"
updateOperatorText (UpdDeleteElem _) = "_delete_elem"
updateOperatorText (UpdDeleteAtPath _) = "_delete_at_path"
type AnnotatedUpdateNode b = AnnotatedUpdateNodeG b (Const Void) (SQLExpression b)

View File

@ -104,7 +104,11 @@ class
Eq (XNodesAgg b),
Show (XNodesAgg b),
Eq (XRelay b),
Show (XRelay b)
Show (XRelay b),
-- Intermediate Representations
Functor (BackendUpdate b),
Foldable (BackendUpdate b),
Traversable (BackendUpdate b)
) =>
Backend (b :: BackendType)
where
@ -129,6 +133,14 @@ class
type ExtraTableMetadata b :: Type
-- Backend-specific IR types
-- | Intermediate Representation of Update Mutations.
-- The default implementation makes update expressions uninstantiable.
type BackendUpdate b :: Type -> Type
type BackendUpdate b = Const Void
-- | Extra backend specific context needed for insert mutations.
type ExtraInsertData b :: Type

View File

@ -7,6 +7,7 @@ module Hasura.RQL.Types.Column
isScalarColumnWhere,
ValueParser,
onlyNumCols,
isNumCol,
onlyComparableCols,
parseScalarValueColumnType,
parseScalarValuesColumnType,
@ -218,7 +219,10 @@ instance Backend b => ToJSON (ColumnInfo b) where
type PrimaryKeyColumns b = NESeq (ColumnInfo b)
onlyNumCols :: forall b. Backend b => [ColumnInfo b] -> [ColumnInfo b]
onlyNumCols = filter (isScalarColumnWhere (isNumType @b) . pgiType)
onlyNumCols = filter isNumCol
isNumCol :: forall b. Backend b => ColumnInfo b -> Bool
isNumCol = isScalarColumnWhere (isNumType @b) . pgiType
onlyComparableCols :: forall b. Backend b => [ColumnInfo b] -> [ColumnInfo b]
onlyComparableCols = filter (isScalarColumnWhere (isComparableType @b) . pgiType)

View File

@ -6,7 +6,7 @@ response:
- extensions:
path: "$.selectionSet.update_article.args"
code: validation-failed
message: column found in multiple operators; "author_id" in _set, _inc. "id" in _set, _inc
message: 'Column found in multiple operators: "author_id", "id".'
query:
query: |