server: avoid raising mutation check constraint violation in pg procedure (#6123)

https://github.com/hasura/graphql-engine/pull/6123
This commit is contained in:
Rakesh Emmadi 2020-11-06 18:52:22 +05:30 committed by GitHub
parent 01c4cd2ff3
commit ca47c92f50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 226 additions and 166 deletions

View File

@ -279,7 +279,7 @@ Now, we can create a role ``user`` and add an insert validation rule as follows:
:alt: validation using permission: title cannot be empty :alt: validation using permission: title cannot be empty
.. tab:: CLI .. tab:: CLI
You can add roles and permissions in the ``tables.yaml`` file inside the ``metadata`` directory: You can add roles and permissions in the ``tables.yaml`` file inside the ``metadata`` directory:
.. code-block:: yaml .. code-block:: yaml
@ -345,7 +345,7 @@ If we try to insert an article with ``title = ""``, we will get a ``permission-e
{ {
"errors": [ "errors": [
{ {
"message": "Check constraint violation. insert check constraint failed", "message": "check constraint of an insert/update permission has failed",
"extensions": { "extensions": {
"path": "$.selectionSet.insert_article.args.objects", "path": "$.selectionSet.insert_article.args.objects",
"code": "permission-error" "code": "permission-error"
@ -449,7 +449,7 @@ will receive a ``permission-error`` :
{ {
"errors": [ "errors": [
{ {
"message": "Check constraint violation. insert check constraint failed", "message": "check constraint of an insert/update permission has failed",
"extensions": { "extensions": {
"path": "$.selectionSet.insert_article.args.objects", "path": "$.selectionSet.insert_article.args.objects",
"code": "permission-error" "code": "permission-error"
@ -545,7 +545,7 @@ we get the following error message:
InsertAuthor(author: { name: "Thanos" }) { InsertAuthor(author: { name: "Thanos" }) {
id id
} }
} }
:response: :response:
{ {
"errors": [ "errors": [

View File

@ -35,16 +35,16 @@ import Hasura.Backends.Postgres.Translate.Select
import Hasura.Backends.Postgres.Translate.Update import Hasura.Backends.Postgres.Translate.Update
import Hasura.EncJSON import Hasura.EncJSON
import Hasura.RQL.DML.Internal import Hasura.RQL.DML.Internal
import Hasura.RQL.Instances ()
import Hasura.RQL.IR.Delete import Hasura.RQL.IR.Delete
import Hasura.RQL.IR.Insert import Hasura.RQL.IR.Insert
import Hasura.RQL.IR.Returning import Hasura.RQL.IR.Returning
import Hasura.RQL.IR.Select import Hasura.RQL.IR.Select
import Hasura.RQL.IR.Update import Hasura.RQL.IR.Update
import Hasura.RQL.Instances ()
import Hasura.RQL.Types import Hasura.RQL.Types
import Hasura.SQL.Types
import Hasura.Server.Version (HasVersion) import Hasura.Server.Version (HasVersion)
import Hasura.Session import Hasura.Session
import Hasura.SQL.Types
type MutationRemoteJoinCtx = (HTTP.Manager, [N.Header], UserInfo) type MutationRemoteJoinCtx = (HTTP.Manager, [N.Header], UserInfo)
@ -52,7 +52,7 @@ type MutationRemoteJoinCtx = (HTTP.Manager, [N.Header], UserInfo)
data Mutation (b :: Backend) data Mutation (b :: Backend)
= Mutation = Mutation
{ _mTable :: !QualifiedTable { _mTable :: !QualifiedTable
, _mQuery :: !(S.CTE, DS.Seq Q.PrepArg) , _mQuery :: !(MutationCTE, DS.Seq Q.PrepArg)
, _mOutput :: !(MutationOutput b) , _mOutput :: !(MutationOutput b)
, _mCols :: ![ColumnInfo b] , _mCols :: ![ColumnInfo b]
, _mRemoteJoins :: !(Maybe (RemoteJoins b, MutationRemoteJoinCtx)) , _mRemoteJoins :: !(Maybe (RemoteJoins b, MutationRemoteJoinCtx))
@ -62,7 +62,7 @@ data Mutation (b :: Backend)
mkMutation mkMutation
:: Maybe MutationRemoteJoinCtx :: Maybe MutationRemoteJoinCtx
-> QualifiedTable -> QualifiedTable
-> (S.CTE, DS.Seq Q.PrepArg) -> (MutationCTE, DS.Seq Q.PrepArg)
-> MutationOutput 'Postgres -> MutationOutput 'Postgres
-> [ColumnInfo 'Postgres] -> [ColumnInfo 'Postgres]
-> Bool -> Bool
@ -97,10 +97,7 @@ mutateAndReturn
-> Mutation 'Postgres -> Mutation 'Postgres
-> m EncJSON -> m EncJSON
mutateAndReturn env (Mutation qt (cte, p) mutationOutput allCols remoteJoins strfyNum) = mutateAndReturn env (Mutation qt (cte, p) mutationOutput allCols remoteJoins strfyNum) =
executeMutationOutputQuery env sqlQuery (toList p) remoteJoins executeMutationOutputQuery env qt allCols Nothing cte mutationOutput strfyNum (toList p) remoteJoins
where
sqlQuery = Q.fromBuilder $ toSQL $
mkMutationOutputExp qt allCols Nothing cte mutationOutput strfyNum
execUpdateQuery execUpdateQuery
@ -116,7 +113,7 @@ execUpdateQuery
-> (AnnUpd 'Postgres, DS.Seq Q.PrepArg) -> (AnnUpd 'Postgres, DS.Seq Q.PrepArg)
-> m EncJSON -> m EncJSON
execUpdateQuery env strfyNum remoteJoinCtx (u, p) = execUpdateQuery env strfyNum remoteJoinCtx (u, p) =
runMutation env $ mkMutation remoteJoinCtx (uqp1Table u) (updateCTE, p) runMutation env $ mkMutation remoteJoinCtx (uqp1Table u) (MCCheckConstraint updateCTE, p)
(uqp1Output u) (uqp1AllCols u) strfyNum (uqp1Output u) (uqp1AllCols u) strfyNum
where where
updateCTE = mkUpdateCTE u updateCTE = mkUpdateCTE u
@ -134,10 +131,10 @@ execDeleteQuery
-> (AnnDel 'Postgres, DS.Seq Q.PrepArg) -> (AnnDel 'Postgres, DS.Seq Q.PrepArg)
-> m EncJSON -> m EncJSON
execDeleteQuery env strfyNum remoteJoinCtx (u, p) = execDeleteQuery env strfyNum remoteJoinCtx (u, p) =
runMutation env $ mkMutation remoteJoinCtx (dqp1Table u) (deleteCTE, p) runMutation env $ mkMutation remoteJoinCtx (dqp1Table u) (MCDelete delete, p)
(dqp1Output u) (dqp1AllCols u) strfyNum (dqp1Output u) (dqp1AllCols u) strfyNum
where where
deleteCTE = mkDeleteCTE u delete = mkDelete u
execInsertQuery execInsertQuery
:: ( HasVersion :: ( HasVersion
@ -152,7 +149,7 @@ execInsertQuery
-> m EncJSON -> m EncJSON
execInsertQuery env strfyNum remoteJoinCtx (u, p) = execInsertQuery env strfyNum remoteJoinCtx (u, p) =
runMutation env runMutation env
$ mkMutation remoteJoinCtx (iqp1Table u) (insertCTE, p) $ mkMutation remoteJoinCtx (iqp1Table u) (MCCheckConstraint insertCTE, p)
(iqp1Output u) (iqp1AllCols u) strfyNum (iqp1Output u) (iqp1AllCols u) strfyNum
where where
insertCTE = mkInsertCTE u insertCTE = mkInsertCTE u
@ -186,41 +183,67 @@ mutateAndSel
mutateAndSel env (Mutation qt q mutationOutput allCols remoteJoins strfyNum) = do mutateAndSel env (Mutation qt q mutationOutput allCols remoteJoins strfyNum) = do
-- Perform mutation and fetch unique columns -- Perform mutation and fetch unique columns
MutateResp _ columnVals <- liftTx $ mutateAndFetchCols qt allCols q strfyNum MutateResp _ columnVals <- liftTx $ mutateAndFetchCols qt allCols q strfyNum
selCTE <- mkSelCTEFromColVals qt allCols columnVals select <- mkSelectExpFromColumnValues qt allCols columnVals
let selWith = mkMutationOutputExp qt allCols Nothing selCTE mutationOutput strfyNum
-- Perform select query and fetch returning fields -- Perform select query and fetch returning fields
executeMutationOutputQuery env (Q.fromBuilder $ toSQL selWith) [] remoteJoins executeMutationOutputQuery env qt allCols Nothing
(MCSelectValues select) mutationOutput strfyNum [] remoteJoins
withCheckPermission :: (MonadError QErr m) => m (a, Bool) -> m a
withCheckPermission sqlTx = do
(rawResponse, checkConstraint) <- sqlTx
unless checkConstraint $ throw400 PermissionError $
"check constraint of an insert/update permission has failed"
pure rawResponse
executeMutationOutputQuery executeMutationOutputQuery
:: :: forall m.
( HasVersion ( HasVersion
, MonadTx m , MonadTx m
, MonadIO m , MonadIO m
, Tracing.MonadTrace m , Tracing.MonadTrace m
) )
=> Env.Environment => Env.Environment
-> Q.Query -- ^ SQL query -> QualifiedTable
-> [ColumnInfo 'Postgres]
-> Maybe Int
-> MutationCTE
-> MutationOutput 'Postgres
-> Bool
-> [Q.PrepArg] -- ^ Prepared params -> [Q.PrepArg] -- ^ Prepared params
-> Maybe (RemoteJoins 'Postgres, MutationRemoteJoinCtx) -- ^ Remote joins context -> Maybe (RemoteJoins 'Postgres, MutationRemoteJoinCtx) -- ^ Remote joins context
-> m EncJSON -> m EncJSON
executeMutationOutputQuery env query prepArgs = \case executeMutationOutputQuery env qt allCols preCalAffRows cte mutOutput strfyNum prepArgs maybeRJ = do
Nothing -> let queryTx :: Q.FromRes a => m a
runIdentity . Q.getRow queryTx = do
-- See Note [Prepared statements in Mutations] let selectWith = mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum
<$> liftTx (Q.rawQE dmlTxErrorHandler query prepArgs False) query = Q.fromBuilder $ toSQL selectWith
Just (remoteJoins, (httpManager, reqHeaders, userInfo)) -> -- See Note [Prepared statements in Mutations]
executeQueryWithRemoteJoins env httpManager reqHeaders userInfo query prepArgs remoteJoins liftTx (Q.rawQE dmlTxErrorHandler query prepArgs False)
rawResponse <-
if checkPermissionRequired cte
then withCheckPermission $ Q.getRow <$> queryTx
else (runIdentity . Q.getRow) <$> queryTx
case maybeRJ of
Nothing -> pure $ encJFromLBS rawResponse
Just (remoteJoins, (httpManager, reqHeaders, userInfo)) ->
processRemoteJoins env httpManager reqHeaders userInfo rawResponse remoteJoins
mutateAndFetchCols mutateAndFetchCols
:: QualifiedTable :: QualifiedTable
-> [ColumnInfo 'Postgres] -> [ColumnInfo 'Postgres]
-> (S.CTE, DS.Seq Q.PrepArg) -> (MutationCTE, DS.Seq Q.PrepArg)
-> Bool -> Bool
-> Q.TxE QErr (MutateResp TxtEncodedPGVal) -> Q.TxE QErr (MutateResp TxtEncodedPGVal)
mutateAndFetchCols qt cols (cte, p) strfyNum = mutateAndFetchCols qt cols (cte, p) strfyNum = do
Q.getAltJ . runIdentity . Q.getRow let mutationTx :: Q.FromRes a => Q.TxE QErr a
-- See Note [Prepared statements in Mutations] mutationTx =
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) (toList p) False -- See Note [Prepared statements in Mutations]
Q.rawQE dmlTxErrorHandler sqlText (toList p) False
if checkPermissionRequired cte
then withCheckPermission $ (first Q.getAltJ . Q.getRow) <$> mutationTx
else (Q.getAltJ . runIdentity . Q.getRow) <$> mutationTx
where where
aliasIdentifier = Identifier $ qualifiedObjectToText qt <> "__mutation_result" aliasIdentifier = Identifier $ qualifiedObjectToText qt <> "__mutation_result"
tabFrom = FromIdentifier aliasIdentifier tabFrom = FromIdentifier aliasIdentifier
@ -228,9 +251,12 @@ mutateAndFetchCols qt cols (cte, p) strfyNum =
selFlds = flip map cols $ selFlds = flip map cols $
\ci -> (fromPGCol $ pgiColumn ci, mkAnnColumnFieldAsText ci) \ci -> (fromPGCol $ pgiColumn ci, mkAnnColumnFieldAsText ci)
sql = toSQL selectWith sqlText = Q.fromBuilder $ toSQL selectWith
selectWith = S.SelectWith [(S.Alias aliasIdentifier, cte)] select selectWith = S.SelectWith [(S.Alias aliasIdentifier, getMutationCTE cte)] select
select = S.mkSelect {S.selExtr = [S.Extractor extrExp Nothing]} select = S.mkSelect { S.selExtr = S.Extractor extrExp Nothing
: bool [] [S.Extractor checkErrExp Nothing] (checkPermissionRequired cte)
}
checkErrExp = mkCheckErrorExp aliasIdentifier
extrExp = S.applyJsonBuildObj extrExp = S.applyJsonBuildObj
[ S.SELit "affected_rows", affRowsSel [ S.SELit "affected_rows", affRowsSel
, S.SELit "returning_columns", colSel , S.SELit "returning_columns", colSel

View File

@ -8,12 +8,14 @@ module Hasura.Backends.Postgres.Execute.RemoteJoin
, FieldPath(..) , FieldPath(..)
, RemoteJoin(..) , RemoteJoin(..)
, executeQueryWithRemoteJoins , executeQueryWithRemoteJoins
, processRemoteJoins
) where ) where
import Hasura.Prelude import Hasura.Prelude
import qualified Data.Aeson as A import qualified Data.Aeson as A
import qualified Data.Aeson.Ordered as AO import qualified Data.Aeson.Ordered as AO
import qualified Data.ByteString.Lazy as BL
import qualified Data.Environment as Env import qualified Data.Environment as Env
import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict as Map
import qualified Data.HashMap.Strict.Extended as Map import qualified Data.HashMap.Strict.Extended as Map
@ -64,8 +66,26 @@ executeQueryWithRemoteJoins
-> RemoteJoins 'Postgres -> RemoteJoins 'Postgres
-> m EncJSON -> m EncJSON
executeQueryWithRemoteJoins env manager reqHdrs userInfo q prepArgs rjs = do executeQueryWithRemoteJoins env manager reqHdrs userInfo q prepArgs rjs = do
-- Step 1: Perform the query on database and fetch the response -- Perform the query on database and fetch the response
pgRes <- runIdentity . Q.getRow <$> Tracing.trace "Postgres" (liftTx (Q.rawQE dmlTxErrorHandler q prepArgs True)) pgRes <- runIdentity . Q.getRow <$> Tracing.trace "Postgres" (liftTx (Q.rawQE dmlTxErrorHandler q prepArgs True))
-- Process remote joins in the response
processRemoteJoins env manager reqHdrs userInfo pgRes rjs
processRemoteJoins
:: ( HasVersion
, MonadTx m
, MonadIO m
, Tracing.MonadTrace m
)
=> Env.Environment
-> HTTP.Manager
-> [N.Header]
-> UserInfo
-> BL.ByteString
-> RemoteJoins 'Postgres
-> m EncJSON
processRemoteJoins env manager reqHdrs userInfo pgRes rjs = do
-- Step 1: Decode the given bytestring as a JSON value
jsonRes <- onLeft (AO.eitherDecode pgRes) (throw500 . T.pack) jsonRes <- onLeft (AO.eitherDecode pgRes) (throw500 . T.pack)
-- Step 2: Traverse through the JSON obtained in above step and generate composite JSON value with remote joins -- Step 2: Traverse through the JSON obtained in above step and generate composite JSON value with remote joins
compositeJson <- traverseQueryResponseJSON rjMap jsonRes compositeJson <- traverseQueryResponseJSON rjMap jsonRes

View File

@ -2,17 +2,17 @@ module Hasura.Backends.Postgres.SQL.DML where
import Hasura.Prelude import Hasura.Prelude
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T import qualified Data.Text as T
import qualified Text.Builder as TB import qualified Text.Builder as TB
import Data.String (fromString) import Data.String (fromString)
import Data.Text.Extended import Data.Text.Extended
import Language.Haskell.TH.Syntax (Lift) import Language.Haskell.TH.Syntax (Lift)
import Hasura.Backends.Postgres.SQL.Types import Hasura.Backends.Postgres.SQL.Types
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.SQL.Types import Hasura.SQL.Types

View File

@ -1,5 +1,5 @@
module Hasura.Backends.Postgres.Translate.Delete module Hasura.Backends.Postgres.Translate.Delete
( mkDeleteCTE ( mkDelete
) where ) where
import Hasura.Prelude import Hasura.Prelude
@ -12,12 +12,9 @@ import Hasura.Backends.Postgres.Translate.BoolExp
import Hasura.RQL.IR.Delete import Hasura.RQL.IR.Delete
import Hasura.RQL.Types import Hasura.RQL.Types
mkDelete :: AnnDel 'Postgres -> S.SQLDelete
mkDeleteCTE mkDelete (AnnDel tn (fltr, wc) _ _) =
:: AnnDel 'Postgres -> S.CTE S.SQLDelete tn Nothing tableFltr $ Just S.returningStar
mkDeleteCTE (AnnDel tn (fltr, wc) _ _) =
S.CTEDelete delete
where where
delete = S.SQLDelete tn Nothing tableFltr $ Just S.returningStar
tableFltr = Just $ S.WhereFrag $ tableFltr = Just $ S.WhereFrag $
toSQLBoolExp (S.QualTable tn) $ andAnnBoolExps fltr wc toSQLBoolExp (S.QualTable tn) $ andAnnBoolExps fltr wc

View File

@ -1,22 +1,23 @@
module Hasura.Backends.Postgres.Translate.Insert module Hasura.Backends.Postgres.Translate.Insert
( mkInsertCTE ( mkInsertCTE
, insertCheckExpr
, buildConflictClause , buildConflictClause
, toSQLConflict , toSQLConflict
, insertCheckConstraint
, insertOrUpdateCheckExpr , insertOrUpdateCheckExpr
) where ) where
import Hasura.Prelude import Hasura.Prelude
import qualified Data.HashSet as HS import qualified Data.HashSet as HS
import Data.Text.Extended import Data.Text.Extended
import Instances.TH.Lift () import Instances.TH.Lift ()
import qualified Hasura.Backends.Postgres.SQL.DML as S import qualified Hasura.Backends.Postgres.SQL.DML as S
import Hasura.Backends.Postgres.SQL.Types import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.BoolExp import Hasura.Backends.Postgres.Translate.BoolExp
import Hasura.Backends.Postgres.Translate.Returning
import Hasura.RQL.DML.Internal import Hasura.RQL.DML.Internal
import Hasura.RQL.IR.Insert import Hasura.RQL.IR.Insert
import Hasura.RQL.Types import Hasura.RQL.Types
@ -32,11 +33,9 @@ mkInsertCTE (InsertQueryP1 tn cols vals conflict (insCheck, updCheck) _ _) =
. Just . Just
. S.RetExp . S.RetExp
$ [ S.selectStar $ [ S.selectStar
, S.Extractor , insertOrUpdateCheckExpr tn conflict
(insertOrUpdateCheckExpr tn conflict (toSQLBool insCheck)
(toSQLBool insCheck) (fmap toSQLBool updCheck)
(fmap toSQLBool updCheck))
Nothing
] ]
toSQLBool = toSQLBoolExp $ S.QualTable tn toSQLBool = toSQLBoolExp $ S.QualTable tn
@ -117,28 +116,11 @@ buildConflictClause sessVarBldr tableInfo inpCols (OnConflict mTCol mTCons act)
validateInpCols inpCols updCols validateInpCols inpCols updCols
return (updFiltr, preSet) return (updFiltr, preSet)
-- | Create an expression which will fail with a check constraint violation error -- | Annotates the check constraint expression with @boolean@
-- if the condition is not met on any of the inserted rows. -- (<check-condition>)::boolean
-- insertCheckConstraint :: S.BoolExp -> S.SQLExp
-- The resulting SQL will look something like this: insertCheckConstraint boolExp =
-- S.SETyAnn (S.SEBool boolExp) S.boolTypeAnn
-- > INSERT INTO
-- > ...
-- > RETURNING
-- > *,
-- > CASE WHEN {cond}
-- > THEN NULL
-- > ELSE hdb_catalog.check_violation('insert check constraint failed')
-- > END
insertCheckExpr :: Text -> S.BoolExp -> S.SQLExp
insertCheckExpr errorMessage condExpr =
S.SECond condExpr S.SENull
(S.SEFunction
(S.FunctionExp
(QualifiedObject (SchemaName "hdb_catalog") (FunctionName "check_violation"))
(S.FunctionArgs [S.SELit errorMessage] mempty)
Nothing)
)
-- | When inserting data, we might need to also enforce the update -- | When inserting data, we might need to also enforce the update
-- check condition, because we might fall back to an update via an -- check condition, because we might fall back to an update via an
@ -153,15 +135,10 @@ insertCheckExpr errorMessage condExpr =
-- > RETURNING -- > RETURNING
-- > *, -- > *,
-- > CASE WHEN xmax = 0 -- > CASE WHEN xmax = 0
-- > THEN CASE WHEN {insert_cond} -- > THEN {insert_cond}
-- > THEN NULL -- > ELSE {update_cond}
-- > ELSE hdb_catalog.check_violation('insert check constraint failed')
-- > END
-- > ELSE CASE WHEN {update_cond}
-- > THEN NULL
-- > ELSE hdb_catalog.check_violation('update check constraint failed')
-- > END
-- > END -- > END
-- > AS "check__constraint"
-- --
-- See @https://stackoverflow.com/q/34762732@ for more information on the use of -- See @https://stackoverflow.com/q/34762732@ for more information on the use of
-- the @xmax@ system column. -- the @xmax@ system column.
@ -170,15 +147,16 @@ insertOrUpdateCheckExpr
-> Maybe (ConflictClauseP1 'Postgres S.SQLExp) -> Maybe (ConflictClauseP1 'Postgres S.SQLExp)
-> S.BoolExp -> S.BoolExp
-> Maybe S.BoolExp -> Maybe S.BoolExp
-> S.SQLExp -> S.Extractor
insertOrUpdateCheckExpr qt (Just _conflict) insCheck (Just updCheck) = insertOrUpdateCheckExpr qt (Just _conflict) insCheck (Just updCheck) =
asCheckErrorExtractor $
S.SECond S.SECond
(S.BECompare (S.BECompare
S.SEQ S.SEQ
(S.SEQIdentifier (S.QIdentifier (S.mkQual qt) (Identifier "xmax"))) (S.SEQIdentifier (S.QIdentifier (S.mkQual qt) (Identifier "xmax")))
(S.SEUnsafe "0")) (S.SEUnsafe "0"))
(insertCheckExpr "insert check constraint failed" insCheck) (insertCheckConstraint insCheck)
(insertCheckExpr "update check constraint failed" updCheck) (insertCheckConstraint updCheck)
insertOrUpdateCheckExpr _ _ insCheck _ = insertOrUpdateCheckExpr _ _ insCheck _ =
-- If we won't generate an ON CONFLICT clause, there is no point -- If we won't generate an ON CONFLICT clause, there is no point
-- in testing xmax. In particular, views don't provide the xmax -- in testing xmax. In particular, views don't provide the xmax
@ -188,4 +166,4 @@ insertOrUpdateCheckExpr _ _ insCheck _ =
-- --
-- Alternatively, if there is no update check constraint, we should -- Alternatively, if there is no update check constraint, we should
-- use the insert check constraint, for backwards compatibility. -- use the insert check constraint, for backwards compatibility.
insertCheckExpr "insert check constraint failed" insCheck asCheckErrorExtractor $ insertCheckConstraint insCheck

View File

@ -1,5 +1,5 @@
module Hasura.Backends.Postgres.Translate.Mutation module Hasura.Backends.Postgres.Translate.Mutation
( mkSelCTEFromColVals ( mkSelectExpFromColumnValues
) )
where where
@ -22,19 +22,18 @@ import Hasura.SQL.Types
-- For example, let's consider the table, `CREATE TABLE test (id serial primary key, name text not null, age int)`. -- For example, let's consider the table, `CREATE TABLE test (id serial primary key, name text not null, age int)`.
-- The generated values expression should be in order of columns; -- The generated values expression should be in order of columns;
-- `SELECT ("row"::table).* VALUES (1, 'Robert', 23) AS "row"`. -- `SELECT ("row"::table).* VALUES (1, 'Robert', 23) AS "row"`.
mkSelCTEFromColVals mkSelectExpFromColumnValues
:: (MonadError QErr m) :: (MonadError QErr m)
=> QualifiedTable -> [ColumnInfo 'Postgres] -> [ColumnValues TxtEncodedPGVal] -> m S.CTE => QualifiedTable -> [ColumnInfo 'Postgres] -> [ColumnValues TxtEncodedPGVal] -> m S.Select
mkSelCTEFromColVals qt allCols colVals = mkSelectExpFromColumnValues qt allCols = \case
S.CTESelect <$> case colVals of [] -> return selNoRows
[] -> return selNoRows colVals -> do
_ -> do tuples <- mapM mkTupsFromColVal colVals
tuples <- mapM mkTupsFromColVal colVals let fromItem = S.FIValues (S.ValuesExp tuples) (S.Alias rowAlias) Nothing
let fromItem = S.FIValues (S.ValuesExp tuples) (S.Alias rowAlias) Nothing return S.mkSelect
return S.mkSelect { S.selExtr = [extractor]
{ S.selExtr = [extractor] , S.selFrom = Just $ S.FromExp [fromItem]
, S.selFrom = Just $ S.FromExp [fromItem] }
}
where where
rowAlias = Identifier "row" rowAlias = Identifier "row"
extractor = S.selectStar' $ S.QualifiedIdentifier rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt extractor = S.selectStar' $ S.QualifiedIdentifier rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt

View File

@ -1,7 +1,10 @@
module Hasura.Backends.Postgres.Translate.Returning module Hasura.Backends.Postgres.Translate.Returning
( mkMutFldExp ( mkMutFldExp
, mkDefaultMutFlds , mkDefaultMutFlds
, mkCheckErrorExp
, mkMutationOutputExp , mkMutationOutputExp
, checkConstraintIdentifier
, asCheckErrorExtractor
, checkRetCols , checkRetCols
) where ) where
@ -58,10 +61,7 @@ WITH "<table-name>__mutation_result_alias" AS (
(<insert-value-row>[..]) (<insert-value-row>[..])
ON CONFLICT ON CONSTRAINT "<table-constraint-name>" DO NOTHING RETURNING *, ON CONFLICT ON CONSTRAINT "<table-constraint-name>" DO NOTHING RETURNING *,
-- An extra column expression which performs the 'CHECK' validation -- An extra column expression which performs the 'CHECK' validation
CASE (<CHECK Condition>) AS "check__constraint"
WHEN (<CHECK Condition>) THEN NULL
ELSE "hdb_catalog"."check_violation"('insert check constraint failed')
END
), ),
"<table-name>__all_columns_alias" AS ( "<table-name>__all_columns_alias" AS (
-- Only extract columns from mutated rows. Columns sorted by ordinal position so that -- Only extract columns from mutated rows. Columns sorted by ordinal position so that
@ -70,7 +70,8 @@ WITH "<table-name>__mutation_result_alias" AS (
FROM FROM
"<table-name>__mutation_result_alias" "<table-name>__mutation_result_alias"
) )
<SELECT statement to generate mutation response using '<table-name>__all_columns_alias' as FROM> <SELECT statement to generate mutation response using '<table-name>__all_columns_alias' as FROM
and bool_and("check__constraint") from "<table-name>__mutation_result_alias">
-} -}
-- | Generate mutation output expression with given mutation CTE statement. -- | Generate mutation output expression with given mutation CTE statement.
@ -79,24 +80,27 @@ mkMutationOutputExp
:: QualifiedTable :: QualifiedTable
-> [ColumnInfo 'Postgres] -> [ColumnInfo 'Postgres]
-> Maybe Int -> Maybe Int
-> S.CTE -> MutationCTE
-> MutationOutput 'Postgres -> MutationOutput 'Postgres
-> Bool -> Bool
-> S.SelectWith -> S.SelectWith
mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum = mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum =
S.SelectWith [ (S.Alias mutationResultAlias, cte) S.SelectWith [ (S.Alias mutationResultAlias, getMutationCTE cte)
, (S.Alias allColumnsAlias, allColumnsSelect) , (S.Alias allColumnsAlias, allColumnsSelect)
] sel ] sel
where where
mutationResultAlias = Identifier $ snakeCaseQualifiedObject qt <> "__mutation_result_alias" mutationResultAlias = Identifier $ snakeCaseQualifiedObject qt <> "__mutation_result_alias"
allColumnsAlias = Identifier $ snakeCaseQualifiedObject qt <> "__all_columns_alias" allColumnsAlias = Identifier $ snakeCaseQualifiedObject qt <> "__all_columns_alias"
allColumnsSelect = S.CTESelect $ S.mkSelect allColumnsSelect = S.CTESelect $ S.mkSelect
{ S.selExtr = map (S.mkExtr . pgiColumn) $ sortCols allCols { S.selExtr = map (S.mkExtr . pgiColumn) (sortCols allCols)
, S.selFrom = Just $ S.mkIdenFromExp mutationResultAlias , S.selFrom = Just $ S.mkIdenFromExp mutationResultAlias
} }
sel = S.mkSelect { S.selExtr = [S.Extractor extrExp Nothing] } sel = S.mkSelect { S.selExtr = S.Extractor extrExp Nothing
: bool [] [S.Extractor checkErrorExp Nothing] (checkPermissionRequired cte)
}
where where
checkErrorExp = mkCheckErrorExp mutationResultAlias
extrExp = case mutOutput of extrExp = case mutOutput of
MOutMultirowFields mutFlds -> MOutMultirowFields mutFlds ->
let jsonBuildObjArgs = flip concatMap mutFlds $ let jsonBuildObjArgs = flip concatMap mutFlds $
@ -111,6 +115,22 @@ mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum =
in S.SESelect $ mkSQLSelect JASSingleObject $ in S.SESelect $ mkSQLSelect JASSingleObject $
AnnSelectG annFlds tabFrom tabPerm noSelectArgs strfyNum AnnSelectG annFlds tabFrom tabPerm noSelectArgs strfyNum
mkCheckErrorExp :: IsIdentifier a => a -> S.SQLExp
mkCheckErrorExp alias =
let boolAndCheckConstraint =
S.handleIfNull (S.SEBool $ S.BELit True) $
S.SEFnApp "bool_and" [S.SEIdentifier checkConstraintIdentifier] Nothing
in S.SESelect $
S.mkSelect { S.selExtr = [S.Extractor boolAndCheckConstraint Nothing]
, S.selFrom = Just $ S.mkIdenFromExp alias
}
checkConstraintIdentifier :: Identifier
checkConstraintIdentifier = Identifier "check__constraint"
asCheckErrorExtractor :: S.SQLExp -> S.Extractor
asCheckErrorExtractor s =
S.Extractor s $ Just $ S.Alias checkConstraintIdentifier
checkRetCols checkRetCols
:: (UserInfoM m, QErrM m) :: (UserInfoM m, QErrM m)

View File

@ -4,15 +4,16 @@ module Hasura.Backends.Postgres.Translate.Update
import Hasura.Prelude import Hasura.Prelude
import Instances.TH.Lift () import Instances.TH.Lift ()
import qualified Hasura.Backends.Postgres.SQL.DML as S import qualified Hasura.Backends.Postgres.SQL.DML as S
import Hasura.Backends.Postgres.SQL.Types import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.BoolExp import Hasura.Backends.Postgres.Translate.BoolExp
import Hasura.Backends.Postgres.Translate.Insert import Hasura.Backends.Postgres.Translate.Insert
import Hasura.Backends.Postgres.Translate.Returning
import Hasura.RQL.Instances ()
import Hasura.RQL.IR.Update import Hasura.RQL.IR.Update
import Hasura.RQL.Instances ()
import Hasura.RQL.Types import Hasura.RQL.Types
@ -26,7 +27,7 @@ mkUpdateCTE (AnnUpd tn opExps (permFltr, wc) chk _ columnsInfo) =
. Just . Just
. S.RetExp . S.RetExp
$ [ S.selectStar $ [ S.selectStar
, S.Extractor (insertCheckExpr "update check constraint failed" checkExpr) Nothing , asCheckErrorExtractor $ insertCheckConstraint checkExpr
] ]
setExp = S.SetExp $ map (expandOperator columnsInfo) opExps setExp = S.SetExp $ map (expandOperator columnsInfo) opExps
tableFltr = Just $ S.WhereFrag tableFltrExpr tableFltr = Just $ S.WhereFrag tableFltrExpr

View File

@ -5,25 +5,24 @@ module Hasura.GraphQL.Execute.Insert
import Hasura.Prelude import Hasura.Prelude
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.Environment as Env import qualified Data.Environment as Env
import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict as Map
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import qualified Data.Text as T import qualified Data.Text as T
import qualified Database.PG.Query as Q import qualified Database.PG.Query as Q
import Data.Text.Extended import Data.Text.Extended
import qualified Hasura.Backends.Postgres.Execute.Mutation as RQL import qualified Hasura.Backends.Postgres.Execute.Mutation as RQL
import qualified Hasura.Backends.Postgres.Execute.RemoteJoin as RQL import qualified Hasura.Backends.Postgres.Execute.RemoteJoin as RQL
import qualified Hasura.Backends.Postgres.SQL.DML as S import qualified Hasura.Backends.Postgres.SQL.DML as S
import qualified Hasura.Backends.Postgres.Translate.BoolExp as RQL import qualified Hasura.Backends.Postgres.Translate.BoolExp as RQL
import qualified Hasura.Backends.Postgres.Translate.Insert as RQL import qualified Hasura.Backends.Postgres.Translate.Insert as RQL
import qualified Hasura.Backends.Postgres.Translate.Mutation as RQL import qualified Hasura.Backends.Postgres.Translate.Mutation as RQL
import qualified Hasura.Backends.Postgres.Translate.Returning as RQL import qualified Hasura.RQL.IR.Insert as RQL
import qualified Hasura.RQL.IR.Insert as RQL import qualified Hasura.RQL.IR.Returning as RQL
import qualified Hasura.RQL.IR.Returning as RQL import qualified Hasura.Tracing as Tracing
import qualified Hasura.Tracing as Tracing
import Hasura.Backends.Postgres.Connection import Hasura.Backends.Postgres.Connection
import Hasura.Backends.Postgres.SQL.Types import Hasura.Backends.Postgres.SQL.Types
@ -31,8 +30,7 @@ import Hasura.Backends.Postgres.SQL.Value
import Hasura.EncJSON import Hasura.EncJSON
import Hasura.GraphQL.Schema.Insert import Hasura.GraphQL.Schema.Insert
import Hasura.RQL.Types import Hasura.RQL.Types
import Hasura.SQL.Types import Hasura.Server.Version (HasVersion)
import Hasura.Server.Version (HasVersion)
traverseAnnInsert traverseAnnInsert
@ -133,11 +131,10 @@ insertMultipleObjects env multiObjIns additionalColumns remoteJoinCtx mutationOu
insertObject env singleObj additionalColumns remoteJoinCtx planVars stringifyNum insertObject env singleObj additionalColumns remoteJoinCtx planVars stringifyNum
let affectedRows = sum $ map fst insertRequests let affectedRows = sum $ map fst insertRequests
columnValues = mapMaybe snd insertRequests columnValues = mapMaybe snd insertRequests
selectExpr <- RQL.mkSelCTEFromColVals table columnInfos columnValues selectExpr <- RQL.mkSelectExpFromColumnValues table columnInfos columnValues
let (mutOutputRJ, remoteJoins) = RQL.getRemoteJoinsMutationOutput mutationOutput let (mutOutputRJ, remoteJoins) = RQL.getRemoteJoinsMutationOutput mutationOutput
sqlQuery = Q.fromBuilder $ toSQL $ RQL.executeMutationOutputQuery env table columnInfos (Just affectedRows) (RQL.MCSelectValues selectExpr)
RQL.mkMutationOutputExp table columnInfos (Just affectedRows) selectExpr mutOutputRJ stringifyNum mutOutputRJ stringifyNum [] $ (, remoteJoinCtx) <$> remoteJoins
RQL.executeMutationOutputQuery env sqlQuery [] $ (,remoteJoinCtx) <$> remoteJoins
insertObject insertObject
:: (HasVersion, MonadTx m, MonadIO m, Tracing.MonadTrace m) :: (HasVersion, MonadTx m, MonadIO m, Tracing.MonadTrace m)
@ -161,7 +158,8 @@ insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringify
cte <- mkInsertQ table onConflict finalInsCols defaultValues checkCond cte <- mkInsertQ table onConflict finalInsCols defaultValues checkCond
MutateResp affRows colVals <- liftTx $ RQL.mutateAndFetchCols table allColumns (cte, planVars) stringifyNum MutateResp affRows colVals <- liftTx $
RQL.mutateAndFetchCols table allColumns (RQL.MCCheckConstraint cte, planVars) stringifyNum
colValM <- asSingleObject colVals colValM <- asSingleObject colVals
arrRelAffRows <- bool (withArrRels colValM) (return 0) $ null arrayRels arrRelAffRows <- bool (withArrRels colValM) (return 0) $ null arrayRels
@ -288,11 +286,9 @@ mkInsertQ table onConflictM insCols defVals (insCheck, updCheck) = do
. Just . Just
$ S.RetExp $ S.RetExp
[ S.selectStar [ S.selectStar
, S.Extractor , RQL.insertOrUpdateCheckExpr table onConflictM
(RQL.insertOrUpdateCheckExpr table onConflictM (RQL.toSQLBoolExp (S.QualTable table) insCheck)
(RQL.toSQLBoolExp (S.QualTable table) insCheck) (fmap (RQL.toSQLBoolExp (S.QualTable table)) updCheck)
(fmap (RQL.toSQLBoolExp (S.QualTable table)) updCheck))
Nothing
] ]
pure $ S.CTEInsert sqlInsert pure $ S.CTEInsert sqlInsert

View File

@ -2,14 +2,16 @@ module Hasura.RQL.IR.Returning where
import Hasura.Prelude import Hasura.Prelude
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.HashMap.Strict.InsOrd as OMap
import Hasura.EncJSON import Hasura.EncJSON
import Hasura.RQL.IR.Select import Hasura.RQL.IR.Select
import Hasura.RQL.Types.Common import Hasura.RQL.Types.Common
import Hasura.SQL.Backend import Hasura.SQL.Backend
import qualified Hasura.Backends.Postgres.SQL.DML as S
data MutFldG (b :: Backend) v data MutFldG (b :: Backend) v
= MCount = MCount
@ -78,3 +80,24 @@ hasNestedFld = \case
AFObjectRelation _ -> True AFObjectRelation _ -> True
AFArrayRelation _ -> True AFArrayRelation _ -> True
_ -> False _ -> False
-- | The postgres common table expression (CTE) for mutation queries.
-- This CTE expression is used to generate mutation field output expression,
-- see Note [Mutation output expression].
data MutationCTE
= MCCheckConstraint !S.CTE -- ^ A Mutation with check constraint validation (Insert or Update)
| MCSelectValues !S.Select -- ^ A Select statement which emits mutated table rows
| MCDelete !S.SQLDelete -- ^ A Delete statement
deriving (Show, Eq)
getMutationCTE :: MutationCTE -> S.CTE
getMutationCTE = \case
MCCheckConstraint cte -> cte
MCSelectValues select -> S.CTESelect select
MCDelete delete -> S.CTEDelete delete
checkPermissionRequired :: MutationCTE -> Bool
checkPermissionRequired = \case
MCCheckConstraint _ -> True
MCSelectValues _ -> False
MCDelete _ -> False

View File

@ -6,7 +6,7 @@ response:
- extensions: - extensions:
path: $.selectionSet.insert_computer.args.objects path: $.selectionSet.insert_computer.args.objects
code: permission-error code: permission-error
message: Check constraint violation. insert check constraint failed message: check constraint of an insert/update permission has failed
headers: headers:
X-Hasura-Role: developer X-Hasura-Role: developer
X-Hasura-Spec-Keys: |- X-Hasura-Spec-Keys: |-

View File

@ -9,7 +9,7 @@ response:
- extensions: - extensions:
path: $.selectionSet.insert_article.args.objects path: $.selectionSet.insert_article.args.objects
code: permission-error code: permission-error
message: Check constraint violation. insert check constraint failed message: check constraint of an insert/update permission has failed
query: query:
query: | query: |
mutation insert_article { mutation insert_article {

View File

@ -6,7 +6,7 @@ response:
- extensions: - extensions:
path: $.selectionSet.insert_computer.args.objects path: $.selectionSet.insert_computer.args.objects
code: permission-error code: permission-error
message: Check constraint violation. insert check constraint failed message: check constraint of an insert/update permission has failed
headers: headers:
X-Hasura-Role: seller X-Hasura-Role: seller
X-Hasura-Spec-Required-Keys: |- X-Hasura-Spec-Required-Keys: |-
@ -15,8 +15,8 @@ query:
variables: variables:
spec: spec:
processor: AMD processor: AMD
display: '16" FHD' display: 16" FHD
memory: '16 GB DDR4' memory: 16 GB DDR4
query: | query: |
mutation insert_computer($spec: jsonb) { mutation insert_computer($spec: jsonb) {
insert_computer ( insert_computer (

View File

@ -7,9 +7,9 @@ headers:
response: response:
errors: errors:
- extensions: - extensions:
path: "$.selectionSet.insert_account.args.objects" path: $.selectionSet.insert_account.args.objects
code: permission-error code: permission-error
message: Check constraint violation. insert check constraint failed message: check constraint of an insert/update permission has failed
query: query:
query: | query: |
mutation{ mutation{

View File

@ -5,7 +5,7 @@ response:
- extensions: - extensions:
path: $ path: $
code: permission-error code: permission-error
message: Check constraint violation. update check constraint failed message: check constraint of an insert/update permission has failed
headers: headers:
X-Hasura-Role: user X-Hasura-Role: user
X-Hasura-User-Id: '1' X-Hasura-User-Id: '1'

View File

@ -5,13 +5,13 @@ headers:
X-Hasura-Role: student X-Hasura-Role: student
response: response:
path: $.args path: $.args
error: Check constraint violation. insert check constraint failed error: check constraint of an insert/update permission has failed
code: permission-error code: permission-error
query: query:
type: insert type: insert
args: args:
table: author table: author
objects: objects:
- id: 5 - id: 5
name: Student 1 name: Student 1
is_registered: false is_registered: false

View File

@ -6,7 +6,7 @@ headers:
X-Hasura-User-Id: '5' X-Hasura-User-Id: '5'
response: response:
path: $.args path: $.args
error: Check constraint violation. insert check constraint failed error: check constraint of an insert/update permission has failed
code: permission-error code: permission-error
query: query:
type: insert type: insert
@ -19,4 +19,4 @@ query:
returning: returning:
- id - id
- name - name
- is_registered - is_registered

View File

@ -6,7 +6,7 @@ headers:
X-Hasura-User-Id: '5' X-Hasura-User-Id: '5'
response: response:
path: $.args path: $.args
error: Check constraint violation. insert check constraint failed error: check constraint of an insert/update permission has failed
code: permission-error code: permission-error
query: query:
type: insert type: insert
@ -19,4 +19,4 @@ query:
returning: returning:
- id - id
- name - name
- is_registered - is_registered