diff --git a/docs/graphql/core/schema/data-validations.rst b/docs/graphql/core/schema/data-validations.rst index 79223db2fe9..1fc8c49c818 100644 --- a/docs/graphql/core/schema/data-validations.rst +++ b/docs/graphql/core/schema/data-validations.rst @@ -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": [ diff --git a/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs b/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs index 2c1cd6aafe2..ec208862a45 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Execute/Mutation.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/Postgres/Execute/RemoteJoin.hs b/server/src-lib/Hasura/Backends/Postgres/Execute/RemoteJoin.hs index bf73cc2fde5..4319ee2e127 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Execute/RemoteJoin.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Execute/RemoteJoin.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs b/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs index 49c9d988cd7..b3c9cc99af3 100644 --- a/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs +++ b/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Delete.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Delete.hs index d486e657338..792908cfea1 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Delete.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Delete.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Insert.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Insert.hs index 06955402ca1..3ee227a06e0 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Insert.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Insert.hs @@ -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@ +-- ()::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 diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Mutation.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Mutation.hs index 78c54b51fc4..f702ee5677a 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Mutation.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Mutation.hs @@ -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 diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs index 5965d3b0dee..58e68f7fe62 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Returning.hs @@ -1,7 +1,10 @@ module Hasura.Backends.Postgres.Translate.Returning ( mkMutFldExp , mkDefaultMutFlds + , mkCheckErrorExp , mkMutationOutputExp + , checkConstraintIdentifier + , asCheckErrorExtractor , checkRetCols ) where @@ -58,10 +61,7 @@ WITH "__mutation_result_alias" AS ( ([..]) ON CONFLICT ON CONSTRAINT "" DO NOTHING RETURNING *, -- An extra column expression which performs the 'CHECK' validation - CASE - WHEN () THEN NULL - ELSE "hdb_catalog"."check_violation"('insert check constraint failed') - END + () AS "check__constraint" ), "__all_columns_alias" AS ( -- Only extract columns from mutated rows. Columns sorted by ordinal position so that @@ -70,7 +70,8 @@ WITH "__mutation_result_alias" AS ( FROM "__mutation_result_alias" ) -__all_columns_alias' as FROM + and bool_and("check__constraint") from "__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) diff --git a/server/src-lib/Hasura/Backends/Postgres/Translate/Update.hs b/server/src-lib/Hasura/Backends/Postgres/Translate/Update.hs index ab53b720f6b..023d9576549 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Translate/Update.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Translate/Update.hs @@ -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 diff --git a/server/src-lib/Hasura/GraphQL/Execute/Insert.hs b/server/src-lib/Hasura/GraphQL/Execute/Insert.hs index f7e6ac54b36..4509a88130b 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Insert.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Insert.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/IR/Returning.hs b/server/src-lib/Hasura/RQL/IR/Returning.hs index 0fc134a0df6..8d91214695f 100644 --- a/server/src-lib/Hasura/RQL/IR/Returning.hs +++ b/server/src-lib/Hasura/RQL/IR/Returning.hs @@ -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 diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml index 675276cdd24..ffb753b7cb0 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml @@ -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: |- diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml index dd6750697ca..30c04a5f0ef 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml @@ -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 { diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml index 671676b8c24..7d5adc0874f 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml @@ -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 ( diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/user_insert_account_fail.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/user_insert_account_fail.yaml index e2b8c8545e0..cffcea76cef 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/user_insert_account_fail.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/user_insert_account_fail.yaml @@ -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{ diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish.yaml index 7e1e26640ee..85857e09b20 100644 --- a/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish.yaml +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish.yaml @@ -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' diff --git a/server/tests-py/queries/v1/insert/permissions/author_student_role_insert_check_bio_fail.yaml b/server/tests-py/queries/v1/insert/permissions/author_student_role_insert_check_bio_fail.yaml index c71149269e5..f4dccbc03ab 100644 --- a/server/tests-py/queries/v1/insert/permissions/author_student_role_insert_check_bio_fail.yaml +++ b/server/tests-py/queries/v1/insert/permissions/author_student_role_insert_check_bio_fail.yaml @@ -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 diff --git a/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_is_registered_fail.yaml b/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_is_registered_fail.yaml index 0984282d90b..e6307765a3a 100644 --- a/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_is_registered_fail.yaml +++ b/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_is_registered_fail.yaml @@ -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 diff --git a/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_user_id_fail.yaml b/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_user_id_fail.yaml index 39061d32d72..48e072f67a7 100644 --- a/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_user_id_fail.yaml +++ b/server/tests-py/queries/v1/insert/permissions/author_user_role_insert_check_user_id_fail.yaml @@ -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