mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
server: avoid raising mutation check constraint violation in pg procedure (#6123)
https://github.com/hasura/graphql-engine/pull/6123
This commit is contained in:
parent
01c4cd2ff3
commit
ca47c92f50
@ -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
|
||||
|
||||
.. tab:: CLI
|
||||
|
||||
|
||||
You can add roles and permissions in the ``tables.yaml`` file inside the ``metadata`` directory:
|
||||
|
||||
.. code-block:: yaml
|
||||
@ -345,7 +345,7 @@ If we try to insert an article with ``title = ""``, we will get a ``permission-e
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"message": "Check constraint violation. insert check constraint failed",
|
||||
"message": "check constraint of an insert/update permission has failed",
|
||||
"extensions": {
|
||||
"path": "$.selectionSet.insert_article.args.objects",
|
||||
"code": "permission-error"
|
||||
@ -449,7 +449,7 @@ will receive a ``permission-error`` :
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"message": "Check constraint violation. insert check constraint failed",
|
||||
"message": "check constraint of an insert/update permission has failed",
|
||||
"extensions": {
|
||||
"path": "$.selectionSet.insert_article.args.objects",
|
||||
"code": "permission-error"
|
||||
@ -545,7 +545,7 @@ we get the following error message:
|
||||
InsertAuthor(author: { name: "Thanos" }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
:response:
|
||||
{
|
||||
"errors": [
|
||||
|
@ -35,16 +35,16 @@ import Hasura.Backends.Postgres.Translate.Select
|
||||
import Hasura.Backends.Postgres.Translate.Update
|
||||
import Hasura.EncJSON
|
||||
import Hasura.RQL.DML.Internal
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.IR.Delete
|
||||
import Hasura.RQL.IR.Insert
|
||||
import Hasura.RQL.IR.Returning
|
||||
import Hasura.RQL.IR.Select
|
||||
import Hasura.RQL.IR.Update
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Session
|
||||
import Hasura.SQL.Types
|
||||
|
||||
|
||||
type MutationRemoteJoinCtx = (HTTP.Manager, [N.Header], UserInfo)
|
||||
@ -52,7 +52,7 @@ type MutationRemoteJoinCtx = (HTTP.Manager, [N.Header], UserInfo)
|
||||
data Mutation (b :: Backend)
|
||||
= Mutation
|
||||
{ _mTable :: !QualifiedTable
|
||||
, _mQuery :: !(S.CTE, DS.Seq Q.PrepArg)
|
||||
, _mQuery :: !(MutationCTE, DS.Seq Q.PrepArg)
|
||||
, _mOutput :: !(MutationOutput b)
|
||||
, _mCols :: ![ColumnInfo b]
|
||||
, _mRemoteJoins :: !(Maybe (RemoteJoins b, MutationRemoteJoinCtx))
|
||||
@ -62,7 +62,7 @@ data Mutation (b :: Backend)
|
||||
mkMutation
|
||||
:: Maybe MutationRemoteJoinCtx
|
||||
-> QualifiedTable
|
||||
-> (S.CTE, DS.Seq Q.PrepArg)
|
||||
-> (MutationCTE, DS.Seq Q.PrepArg)
|
||||
-> MutationOutput 'Postgres
|
||||
-> [ColumnInfo 'Postgres]
|
||||
-> Bool
|
||||
@ -97,10 +97,7 @@ mutateAndReturn
|
||||
-> Mutation 'Postgres
|
||||
-> m EncJSON
|
||||
mutateAndReturn env (Mutation qt (cte, p) mutationOutput allCols remoteJoins strfyNum) =
|
||||
executeMutationOutputQuery env sqlQuery (toList p) remoteJoins
|
||||
where
|
||||
sqlQuery = Q.fromBuilder $ toSQL $
|
||||
mkMutationOutputExp qt allCols Nothing cte mutationOutput strfyNum
|
||||
executeMutationOutputQuery env qt allCols Nothing cte mutationOutput strfyNum (toList p) remoteJoins
|
||||
|
||||
|
||||
execUpdateQuery
|
||||
@ -116,7 +113,7 @@ execUpdateQuery
|
||||
-> (AnnUpd 'Postgres, DS.Seq Q.PrepArg)
|
||||
-> m EncJSON
|
||||
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
|
||||
where
|
||||
updateCTE = mkUpdateCTE u
|
||||
@ -134,10 +131,10 @@ execDeleteQuery
|
||||
-> (AnnDel 'Postgres, DS.Seq Q.PrepArg)
|
||||
-> m EncJSON
|
||||
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
|
||||
where
|
||||
deleteCTE = mkDeleteCTE u
|
||||
delete = mkDelete u
|
||||
|
||||
execInsertQuery
|
||||
:: ( HasVersion
|
||||
@ -152,7 +149,7 @@ execInsertQuery
|
||||
-> m EncJSON
|
||||
execInsertQuery env strfyNum remoteJoinCtx (u, p) =
|
||||
runMutation env
|
||||
$ mkMutation remoteJoinCtx (iqp1Table u) (insertCTE, p)
|
||||
$ mkMutation remoteJoinCtx (iqp1Table u) (MCCheckConstraint insertCTE, p)
|
||||
(iqp1Output u) (iqp1AllCols u) strfyNum
|
||||
where
|
||||
insertCTE = mkInsertCTE u
|
||||
@ -186,41 +183,67 @@ mutateAndSel
|
||||
mutateAndSel env (Mutation qt q mutationOutput allCols remoteJoins strfyNum) = do
|
||||
-- Perform mutation and fetch unique columns
|
||||
MutateResp _ columnVals <- liftTx $ mutateAndFetchCols qt allCols q strfyNum
|
||||
selCTE <- mkSelCTEFromColVals qt allCols columnVals
|
||||
let selWith = mkMutationOutputExp qt allCols Nothing selCTE mutationOutput strfyNum
|
||||
select <- mkSelectExpFromColumnValues qt allCols columnVals
|
||||
-- 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
|
||||
::
|
||||
:: forall m.
|
||||
( HasVersion
|
||||
, MonadTx m
|
||||
, MonadIO m
|
||||
, Tracing.MonadTrace m
|
||||
)
|
||||
=> Env.Environment
|
||||
-> Q.Query -- ^ SQL query
|
||||
-> QualifiedTable
|
||||
-> [ColumnInfo 'Postgres]
|
||||
-> Maybe Int
|
||||
-> MutationCTE
|
||||
-> MutationOutput 'Postgres
|
||||
-> Bool
|
||||
-> [Q.PrepArg] -- ^ Prepared params
|
||||
-> Maybe (RemoteJoins 'Postgres, MutationRemoteJoinCtx) -- ^ Remote joins context
|
||||
-> m EncJSON
|
||||
executeMutationOutputQuery env query prepArgs = \case
|
||||
Nothing ->
|
||||
runIdentity . Q.getRow
|
||||
-- See Note [Prepared statements in Mutations]
|
||||
<$> liftTx (Q.rawQE dmlTxErrorHandler query prepArgs False)
|
||||
Just (remoteJoins, (httpManager, reqHeaders, userInfo)) ->
|
||||
executeQueryWithRemoteJoins env httpManager reqHeaders userInfo query prepArgs remoteJoins
|
||||
executeMutationOutputQuery env qt allCols preCalAffRows cte mutOutput strfyNum prepArgs maybeRJ = do
|
||||
let queryTx :: Q.FromRes a => m a
|
||||
queryTx = do
|
||||
let selectWith = mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum
|
||||
query = Q.fromBuilder $ toSQL selectWith
|
||||
-- See Note [Prepared statements in Mutations]
|
||||
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
|
||||
:: QualifiedTable
|
||||
-> [ColumnInfo 'Postgres]
|
||||
-> (S.CTE, DS.Seq Q.PrepArg)
|
||||
-> (MutationCTE, DS.Seq Q.PrepArg)
|
||||
-> Bool
|
||||
-> Q.TxE QErr (MutateResp TxtEncodedPGVal)
|
||||
mutateAndFetchCols qt cols (cte, p) strfyNum =
|
||||
Q.getAltJ . runIdentity . Q.getRow
|
||||
-- See Note [Prepared statements in Mutations]
|
||||
<$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) (toList p) False
|
||||
mutateAndFetchCols qt cols (cte, p) strfyNum = do
|
||||
let mutationTx :: Q.FromRes a => Q.TxE QErr a
|
||||
mutationTx =
|
||||
-- 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
|
||||
aliasIdentifier = Identifier $ qualifiedObjectToText qt <> "__mutation_result"
|
||||
tabFrom = FromIdentifier aliasIdentifier
|
||||
@ -228,9 +251,12 @@ mutateAndFetchCols qt cols (cte, p) strfyNum =
|
||||
selFlds = flip map cols $
|
||||
\ci -> (fromPGCol $ pgiColumn ci, mkAnnColumnFieldAsText ci)
|
||||
|
||||
sql = toSQL selectWith
|
||||
selectWith = S.SelectWith [(S.Alias aliasIdentifier, cte)] select
|
||||
select = S.mkSelect {S.selExtr = [S.Extractor extrExp Nothing]}
|
||||
sqlText = Q.fromBuilder $ toSQL selectWith
|
||||
selectWith = S.SelectWith [(S.Alias aliasIdentifier, getMutationCTE cte)] select
|
||||
select = S.mkSelect { S.selExtr = S.Extractor extrExp Nothing
|
||||
: bool [] [S.Extractor checkErrExp Nothing] (checkPermissionRequired cte)
|
||||
}
|
||||
checkErrExp = mkCheckErrorExp aliasIdentifier
|
||||
extrExp = S.applyJsonBuildObj
|
||||
[ S.SELit "affected_rows", affRowsSel
|
||||
, S.SELit "returning_columns", colSel
|
||||
|
@ -8,12 +8,14 @@ module Hasura.Backends.Postgres.Execute.RemoteJoin
|
||||
, FieldPath(..)
|
||||
, RemoteJoin(..)
|
||||
, executeQueryWithRemoteJoins
|
||||
, processRemoteJoins
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Ordered as AO
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.Environment as Env
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.HashMap.Strict.Extended as Map
|
||||
@ -64,8 +66,26 @@ executeQueryWithRemoteJoins
|
||||
-> RemoteJoins 'Postgres
|
||||
-> m EncJSON
|
||||
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))
|
||||
-- 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)
|
||||
-- Step 2: Traverse through the JSON obtained in above step and generate composite JSON value with remote joins
|
||||
compositeJson <- traverseQueryResponseJSON rjMap jsonRes
|
||||
|
@ -2,17 +2,17 @@ module Hasura.Backends.Postgres.SQL.DML where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text as T
|
||||
import qualified Text.Builder as TB
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text as T
|
||||
import qualified Text.Builder as TB
|
||||
|
||||
import Data.String (fromString)
|
||||
import Data.String (fromString)
|
||||
import Data.Text.Extended
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
|
||||
import Hasura.Backends.Postgres.SQL.Types
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.Incremental (Cacheable)
|
||||
import Hasura.SQL.Types
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
module Hasura.Backends.Postgres.Translate.Delete
|
||||
( mkDeleteCTE
|
||||
( mkDelete
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
@ -12,12 +12,9 @@ import Hasura.Backends.Postgres.Translate.BoolExp
|
||||
import Hasura.RQL.IR.Delete
|
||||
import Hasura.RQL.Types
|
||||
|
||||
|
||||
mkDeleteCTE
|
||||
:: AnnDel 'Postgres -> S.CTE
|
||||
mkDeleteCTE (AnnDel tn (fltr, wc) _ _) =
|
||||
S.CTEDelete delete
|
||||
mkDelete :: AnnDel 'Postgres -> S.SQLDelete
|
||||
mkDelete (AnnDel tn (fltr, wc) _ _) =
|
||||
S.SQLDelete tn Nothing tableFltr $ Just S.returningStar
|
||||
where
|
||||
delete = S.SQLDelete tn Nothing tableFltr $ Just S.returningStar
|
||||
tableFltr = Just $ S.WhereFrag $
|
||||
toSQLBoolExp (S.QualTable tn) $ andAnnBoolExps fltr wc
|
||||
|
@ -1,22 +1,23 @@
|
||||
module Hasura.Backends.Postgres.Translate.Insert
|
||||
( mkInsertCTE
|
||||
, insertCheckExpr
|
||||
, buildConflictClause
|
||||
, toSQLConflict
|
||||
, insertCheckConstraint
|
||||
, insertOrUpdateCheckExpr
|
||||
) where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.HashSet as HS
|
||||
import qualified Data.HashSet as HS
|
||||
|
||||
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.Translate.BoolExp
|
||||
import Hasura.Backends.Postgres.Translate.Returning
|
||||
import Hasura.RQL.DML.Internal
|
||||
import Hasura.RQL.IR.Insert
|
||||
import Hasura.RQL.Types
|
||||
@ -32,11 +33,9 @@ mkInsertCTE (InsertQueryP1 tn cols vals conflict (insCheck, updCheck) _ _) =
|
||||
. Just
|
||||
. S.RetExp
|
||||
$ [ S.selectStar
|
||||
, S.Extractor
|
||||
(insertOrUpdateCheckExpr tn conflict
|
||||
(toSQLBool insCheck)
|
||||
(fmap toSQLBool updCheck))
|
||||
Nothing
|
||||
, insertOrUpdateCheckExpr tn conflict
|
||||
(toSQLBool insCheck)
|
||||
(fmap toSQLBool updCheck)
|
||||
]
|
||||
toSQLBool = toSQLBoolExp $ S.QualTable tn
|
||||
|
||||
@ -117,28 +116,11 @@ buildConflictClause sessVarBldr tableInfo inpCols (OnConflict mTCol mTCons act)
|
||||
validateInpCols inpCols updCols
|
||||
return (updFiltr, preSet)
|
||||
|
||||
-- | Create an expression which will fail with a check constraint violation error
|
||||
-- if the condition is not met on any of the inserted rows.
|
||||
--
|
||||
-- The resulting SQL will look something like this:
|
||||
--
|
||||
-- > 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)
|
||||
)
|
||||
-- | Annotates the check constraint expression with @boolean@
|
||||
-- (<check-condition>)::boolean
|
||||
insertCheckConstraint :: S.BoolExp -> S.SQLExp
|
||||
insertCheckConstraint boolExp =
|
||||
S.SETyAnn (S.SEBool boolExp) S.boolTypeAnn
|
||||
|
||||
-- | When inserting data, we might need to also enforce the update
|
||||
-- check condition, because we might fall back to an update via an
|
||||
@ -153,15 +135,10 @@ insertCheckExpr errorMessage condExpr =
|
||||
-- > RETURNING
|
||||
-- > *,
|
||||
-- > CASE WHEN xmax = 0
|
||||
-- > THEN CASE WHEN {insert_cond}
|
||||
-- > THEN NULL
|
||||
-- > 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
|
||||
-- > THEN {insert_cond}
|
||||
-- > ELSE {update_cond}
|
||||
-- > END
|
||||
-- > AS "check__constraint"
|
||||
--
|
||||
-- See @https://stackoverflow.com/q/34762732@ for more information on the use of
|
||||
-- the @xmax@ system column.
|
||||
@ -170,15 +147,16 @@ insertOrUpdateCheckExpr
|
||||
-> Maybe (ConflictClauseP1 'Postgres S.SQLExp)
|
||||
-> S.BoolExp
|
||||
-> Maybe S.BoolExp
|
||||
-> S.SQLExp
|
||||
-> S.Extractor
|
||||
insertOrUpdateCheckExpr qt (Just _conflict) insCheck (Just updCheck) =
|
||||
asCheckErrorExtractor $
|
||||
S.SECond
|
||||
(S.BECompare
|
||||
S.SEQ
|
||||
(S.SEQIdentifier (S.QIdentifier (S.mkQual qt) (Identifier "xmax")))
|
||||
(S.SEUnsafe "0"))
|
||||
(insertCheckExpr "insert check constraint failed" insCheck)
|
||||
(insertCheckExpr "update check constraint failed" updCheck)
|
||||
(insertCheckConstraint insCheck)
|
||||
(insertCheckConstraint updCheck)
|
||||
insertOrUpdateCheckExpr _ _ insCheck _ =
|
||||
-- If we won't generate an ON CONFLICT clause, there is no point
|
||||
-- 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
|
||||
-- use the insert check constraint, for backwards compatibility.
|
||||
insertCheckExpr "insert check constraint failed" insCheck
|
||||
asCheckErrorExtractor $ insertCheckConstraint insCheck
|
||||
|
@ -1,5 +1,5 @@
|
||||
module Hasura.Backends.Postgres.Translate.Mutation
|
||||
( mkSelCTEFromColVals
|
||||
( mkSelectExpFromColumnValues
|
||||
)
|
||||
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)`.
|
||||
-- The generated values expression should be in order of columns;
|
||||
-- `SELECT ("row"::table).* VALUES (1, 'Robert', 23) AS "row"`.
|
||||
mkSelCTEFromColVals
|
||||
mkSelectExpFromColumnValues
|
||||
:: (MonadError QErr m)
|
||||
=> QualifiedTable -> [ColumnInfo 'Postgres] -> [ColumnValues TxtEncodedPGVal] -> m S.CTE
|
||||
mkSelCTEFromColVals qt allCols colVals =
|
||||
S.CTESelect <$> case colVals of
|
||||
[] -> return selNoRows
|
||||
_ -> do
|
||||
tuples <- mapM mkTupsFromColVal colVals
|
||||
let fromItem = S.FIValues (S.ValuesExp tuples) (S.Alias rowAlias) Nothing
|
||||
return S.mkSelect
|
||||
{ S.selExtr = [extractor]
|
||||
, S.selFrom = Just $ S.FromExp [fromItem]
|
||||
}
|
||||
=> QualifiedTable -> [ColumnInfo 'Postgres] -> [ColumnValues TxtEncodedPGVal] -> m S.Select
|
||||
mkSelectExpFromColumnValues qt allCols = \case
|
||||
[] -> return selNoRows
|
||||
colVals -> do
|
||||
tuples <- mapM mkTupsFromColVal colVals
|
||||
let fromItem = S.FIValues (S.ValuesExp tuples) (S.Alias rowAlias) Nothing
|
||||
return S.mkSelect
|
||||
{ S.selExtr = [extractor]
|
||||
, S.selFrom = Just $ S.FromExp [fromItem]
|
||||
}
|
||||
where
|
||||
rowAlias = Identifier "row"
|
||||
extractor = S.selectStar' $ S.QualifiedIdentifier rowAlias $ Just $ S.TypeAnn $ toSQLTxt qt
|
||||
|
@ -1,7 +1,10 @@
|
||||
module Hasura.Backends.Postgres.Translate.Returning
|
||||
( mkMutFldExp
|
||||
, mkDefaultMutFlds
|
||||
, mkCheckErrorExp
|
||||
, mkMutationOutputExp
|
||||
, checkConstraintIdentifier
|
||||
, asCheckErrorExtractor
|
||||
, checkRetCols
|
||||
) where
|
||||
|
||||
@ -58,10 +61,7 @@ WITH "<table-name>__mutation_result_alias" AS (
|
||||
(<insert-value-row>[..])
|
||||
ON CONFLICT ON CONSTRAINT "<table-constraint-name>" DO NOTHING RETURNING *,
|
||||
-- An extra column expression which performs the 'CHECK' validation
|
||||
CASE
|
||||
WHEN (<CHECK Condition>) THEN NULL
|
||||
ELSE "hdb_catalog"."check_violation"('insert check constraint failed')
|
||||
END
|
||||
(<CHECK Condition>) AS "check__constraint"
|
||||
),
|
||||
"<table-name>__all_columns_alias" AS (
|
||||
-- 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
|
||||
"<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.
|
||||
@ -79,24 +80,27 @@ mkMutationOutputExp
|
||||
:: QualifiedTable
|
||||
-> [ColumnInfo 'Postgres]
|
||||
-> Maybe Int
|
||||
-> S.CTE
|
||||
-> MutationCTE
|
||||
-> MutationOutput 'Postgres
|
||||
-> Bool
|
||||
-> S.SelectWith
|
||||
mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum =
|
||||
S.SelectWith [ (S.Alias mutationResultAlias, cte)
|
||||
S.SelectWith [ (S.Alias mutationResultAlias, getMutationCTE cte)
|
||||
, (S.Alias allColumnsAlias, allColumnsSelect)
|
||||
] sel
|
||||
where
|
||||
mutationResultAlias = Identifier $ snakeCaseQualifiedObject qt <> "__mutation_result_alias"
|
||||
allColumnsAlias = Identifier $ snakeCaseQualifiedObject qt <> "__all_columns_alias"
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
checkErrorExp = mkCheckErrorExp mutationResultAlias
|
||||
extrExp = case mutOutput of
|
||||
MOutMultirowFields mutFlds ->
|
||||
let jsonBuildObjArgs = flip concatMap mutFlds $
|
||||
@ -111,6 +115,22 @@ mkMutationOutputExp qt allCols preCalAffRows cte mutOutput strfyNum =
|
||||
in S.SESelect $ mkSQLSelect JASSingleObject $
|
||||
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
|
||||
:: (UserInfoM m, QErrM m)
|
||||
|
@ -4,15 +4,16 @@ module Hasura.Backends.Postgres.Translate.Update
|
||||
|
||||
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.Translate.BoolExp
|
||||
import Hasura.Backends.Postgres.Translate.Insert
|
||||
import Hasura.Backends.Postgres.Translate.Returning
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.IR.Update
|
||||
import Hasura.RQL.Instances ()
|
||||
import Hasura.RQL.Types
|
||||
|
||||
|
||||
@ -26,7 +27,7 @@ mkUpdateCTE (AnnUpd tn opExps (permFltr, wc) chk _ columnsInfo) =
|
||||
. Just
|
||||
. S.RetExp
|
||||
$ [ S.selectStar
|
||||
, S.Extractor (insertCheckExpr "update check constraint failed" checkExpr) Nothing
|
||||
, asCheckErrorExtractor $ insertCheckConstraint checkExpr
|
||||
]
|
||||
setExp = S.SetExp $ map (expandOperator columnsInfo) opExps
|
||||
tableFltr = Just $ S.WhereFrag tableFltrExpr
|
||||
|
@ -5,25 +5,24 @@ module Hasura.GraphQL.Execute.Insert
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Environment as Env
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Sequence as Seq
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Environment as Env
|
||||
import qualified Data.HashMap.Strict as Map
|
||||
import qualified Data.Sequence as Seq
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PG.Query as Q
|
||||
|
||||
import Data.Text.Extended
|
||||
|
||||
import qualified Hasura.Backends.Postgres.Execute.Mutation 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.Translate.BoolExp 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.Returning as RQL
|
||||
import qualified Hasura.RQL.IR.Insert as RQL
|
||||
import qualified Hasura.RQL.IR.Returning as RQL
|
||||
import qualified Hasura.Tracing as Tracing
|
||||
import qualified Hasura.Backends.Postgres.Execute.Mutation 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.Translate.BoolExp as RQL
|
||||
import qualified Hasura.Backends.Postgres.Translate.Insert as RQL
|
||||
import qualified Hasura.Backends.Postgres.Translate.Mutation as RQL
|
||||
import qualified Hasura.RQL.IR.Insert as RQL
|
||||
import qualified Hasura.RQL.IR.Returning as RQL
|
||||
import qualified Hasura.Tracing as Tracing
|
||||
|
||||
import Hasura.Backends.Postgres.Connection
|
||||
import Hasura.Backends.Postgres.SQL.Types
|
||||
@ -31,8 +30,7 @@ import Hasura.Backends.Postgres.SQL.Value
|
||||
import Hasura.EncJSON
|
||||
import Hasura.GraphQL.Schema.Insert
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.SQL.Types
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
|
||||
|
||||
traverseAnnInsert
|
||||
@ -133,11 +131,10 @@ insertMultipleObjects env multiObjIns additionalColumns remoteJoinCtx mutationOu
|
||||
insertObject env singleObj additionalColumns remoteJoinCtx planVars stringifyNum
|
||||
let affectedRows = sum $ map fst insertRequests
|
||||
columnValues = mapMaybe snd insertRequests
|
||||
selectExpr <- RQL.mkSelCTEFromColVals table columnInfos columnValues
|
||||
selectExpr <- RQL.mkSelectExpFromColumnValues table columnInfos columnValues
|
||||
let (mutOutputRJ, remoteJoins) = RQL.getRemoteJoinsMutationOutput mutationOutput
|
||||
sqlQuery = Q.fromBuilder $ toSQL $
|
||||
RQL.mkMutationOutputExp table columnInfos (Just affectedRows) selectExpr mutOutputRJ stringifyNum
|
||||
RQL.executeMutationOutputQuery env sqlQuery [] $ (,remoteJoinCtx) <$> remoteJoins
|
||||
RQL.executeMutationOutputQuery env table columnInfos (Just affectedRows) (RQL.MCSelectValues selectExpr)
|
||||
mutOutputRJ stringifyNum [] $ (, remoteJoinCtx) <$> remoteJoins
|
||||
|
||||
insertObject
|
||||
:: (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
|
||||
|
||||
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
|
||||
|
||||
arrRelAffRows <- bool (withArrRels colValM) (return 0) $ null arrayRels
|
||||
@ -288,11 +286,9 @@ mkInsertQ table onConflictM insCols defVals (insCheck, updCheck) = do
|
||||
. Just
|
||||
$ S.RetExp
|
||||
[ S.selectStar
|
||||
, S.Extractor
|
||||
(RQL.insertOrUpdateCheckExpr table onConflictM
|
||||
(RQL.toSQLBoolExp (S.QualTable table) insCheck)
|
||||
(fmap (RQL.toSQLBoolExp (S.QualTable table)) updCheck))
|
||||
Nothing
|
||||
, RQL.insertOrUpdateCheckExpr table onConflictM
|
||||
(RQL.toSQLBoolExp (S.QualTable table) insCheck)
|
||||
(fmap (RQL.toSQLBoolExp (S.QualTable table)) updCheck)
|
||||
]
|
||||
pure $ S.CTEInsert sqlInsert
|
||||
|
||||
|
@ -2,14 +2,16 @@ module Hasura.RQL.IR.Returning where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.HashMap.Strict.InsOrd as OMap
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.HashMap.Strict.InsOrd as OMap
|
||||
|
||||
import Hasura.EncJSON
|
||||
import Hasura.RQL.IR.Select
|
||||
import Hasura.RQL.Types.Common
|
||||
import Hasura.SQL.Backend
|
||||
|
||||
import qualified Hasura.Backends.Postgres.SQL.DML as S
|
||||
|
||||
|
||||
data MutFldG (b :: Backend) v
|
||||
= MCount
|
||||
@ -78,3 +80,24 @@ hasNestedFld = \case
|
||||
AFObjectRelation _ -> True
|
||||
AFArrayRelation _ -> True
|
||||
_ -> 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
|
||||
|
@ -6,7 +6,7 @@ response:
|
||||
- extensions:
|
||||
path: $.selectionSet.insert_computer.args.objects
|
||||
code: permission-error
|
||||
message: Check constraint violation. insert check constraint failed
|
||||
message: check constraint of an insert/update permission has failed
|
||||
headers:
|
||||
X-Hasura-Role: developer
|
||||
X-Hasura-Spec-Keys: |-
|
||||
|
@ -9,7 +9,7 @@ response:
|
||||
- extensions:
|
||||
path: $.selectionSet.insert_article.args.objects
|
||||
code: permission-error
|
||||
message: Check constraint violation. insert check constraint failed
|
||||
message: check constraint of an insert/update permission has failed
|
||||
query:
|
||||
query: |
|
||||
mutation insert_article {
|
||||
|
@ -6,7 +6,7 @@ response:
|
||||
- extensions:
|
||||
path: $.selectionSet.insert_computer.args.objects
|
||||
code: permission-error
|
||||
message: Check constraint violation. insert check constraint failed
|
||||
message: check constraint of an insert/update permission has failed
|
||||
headers:
|
||||
X-Hasura-Role: seller
|
||||
X-Hasura-Spec-Required-Keys: |-
|
||||
@ -15,8 +15,8 @@ query:
|
||||
variables:
|
||||
spec:
|
||||
processor: AMD
|
||||
display: '16" FHD'
|
||||
memory: '16 GB DDR4'
|
||||
display: 16" FHD
|
||||
memory: 16 GB DDR4
|
||||
query: |
|
||||
mutation insert_computer($spec: jsonb) {
|
||||
insert_computer (
|
||||
|
@ -7,9 +7,9 @@ headers:
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: "$.selectionSet.insert_account.args.objects"
|
||||
path: $.selectionSet.insert_account.args.objects
|
||||
code: permission-error
|
||||
message: Check constraint violation. insert check constraint failed
|
||||
message: check constraint of an insert/update permission has failed
|
||||
query:
|
||||
query: |
|
||||
mutation{
|
||||
|
@ -5,7 +5,7 @@ response:
|
||||
- extensions:
|
||||
path: $
|
||||
code: permission-error
|
||||
message: Check constraint violation. update check constraint failed
|
||||
message: check constraint of an insert/update permission has failed
|
||||
headers:
|
||||
X-Hasura-Role: user
|
||||
X-Hasura-User-Id: '1'
|
||||
|
@ -5,13 +5,13 @@ headers:
|
||||
X-Hasura-Role: student
|
||||
response:
|
||||
path: $.args
|
||||
error: Check constraint violation. insert check constraint failed
|
||||
error: check constraint of an insert/update permission has failed
|
||||
code: permission-error
|
||||
query:
|
||||
type: insert
|
||||
args:
|
||||
table: author
|
||||
objects:
|
||||
- id: 5
|
||||
name: Student 1
|
||||
is_registered: false
|
||||
- id: 5
|
||||
name: Student 1
|
||||
is_registered: false
|
||||
|
@ -6,7 +6,7 @@ headers:
|
||||
X-Hasura-User-Id: '5'
|
||||
response:
|
||||
path: $.args
|
||||
error: Check constraint violation. insert check constraint failed
|
||||
error: check constraint of an insert/update permission has failed
|
||||
code: permission-error
|
||||
query:
|
||||
type: insert
|
||||
@ -19,4 +19,4 @@ query:
|
||||
returning:
|
||||
- id
|
||||
- name
|
||||
- is_registered
|
||||
- is_registered
|
||||
|
@ -6,7 +6,7 @@ headers:
|
||||
X-Hasura-User-Id: '5'
|
||||
response:
|
||||
path: $.args
|
||||
error: Check constraint violation. insert check constraint failed
|
||||
error: check constraint of an insert/update permission has failed
|
||||
code: permission-error
|
||||
query:
|
||||
type: insert
|
||||
@ -19,4 +19,4 @@ query:
|
||||
returning:
|
||||
- id
|
||||
- name
|
||||
- is_registered
|
||||
- is_registered
|
||||
|
Loading…
Reference in New Issue
Block a user