From f00404e0f607b761c339cac6d51be850767fcd4e Mon Sep 17 00:00:00 2001 From: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com> Date: Wed, 15 Dec 2021 19:25:41 +0530 Subject: [PATCH] server/mssql: update mutation, SQL generation and execution PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3059 GitOrigin-RevId: 4ed0cbf54ac2a7103cb2b7adc97b2dfdf9994c4f --- CHANGELOG.md | 2 + server/graphql-engine.cabal | 1 + .../src-lib/Hasura/Backends/MSSQL/FromIr.hs | 28 +++- .../Backends/MSSQL/Instances/Execute.hs | 129 +++++++++++++++--- .../Hasura/Backends/MSSQL/Instances/Schema.hs | 9 +- .../src-lib/Hasura/Backends/MSSQL/ToQuery.hs | 55 +++++++- server/src-lib/Hasura/Backends/MSSQL/Types.hs | 10 ++ .../Hasura/Backends/MSSQL/Types/Internal.hs | 6 + .../Hasura/Backends/MSSQL/Types/Update.hs | 21 ++- ...ticle_column_multiple_operators_mssql.yaml | 20 +++ .../update/basic/article_inc_mssql.yaml | 30 ++++ .../update/basic/author_by_pk_mssql.yaml | 36 +++++ .../update/basic/author_by_pk_null_mssql.yaml | 17 +++ .../basic/author_no_operator_mssql.yaml | 14 ++ .../update/basic/author_set_name_mssql.yaml | 36 +++++ .../update/basic/numerics_inc_mssql.yaml | 45 ++++++ .../update/basic/schema_setup_mssql.yaml | 34 +++++ .../update/basic/schema_teardown_mssql.yaml | 11 ++ .../update/basic/setup_mssql.yaml | 40 ++++++ .../update/basic/teardown_mssql.yaml | 39 ++++++ .../update/basic/values_setup_mssql.yaml | 54 ++++++++ .../update/basic/values_teardown_mssql.yaml | 16 +++ .../permissions/schema_setup_mssql.yaml | 22 +++ .../permissions/schema_teardown_mssql.yaml | 10 ++ .../update/permissions/setup_mssql.yaml | 90 ++++++++++++ .../update/permissions/teardown_mssql.yaml | 32 +++++ ..._can_update_unpublished_article_mssql.yaml | 38 ++++++ .../user_cannot_publish_mssql.yaml | 32 +++++ ...ot_update_another_users_article_mssql.yaml | 31 +++++ ...er_cannot_update_id_col_article_mssql.yaml | 32 +++++ ...cannot_update_published_article_mssql.yaml | 31 +++++ .../permissions/user_update_author_mssql.yaml | 39 ++++++ ...user_update_author_other_userid_mssql.yaml | 30 ++++ .../permissions/values_setup_mssql.yaml | 34 +++++ .../permissions/values_teardown_mssql.yaml | 14 ++ server/tests-py/test_graphql_mutations.py | 57 ++++++++ 36 files changed, 1112 insertions(+), 33 deletions(-) create mode 100644 server/src-lib/Hasura/Backends/MSSQL/Types.hs create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/article_column_multiple_operators_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/article_inc_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_null_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/author_no_operator_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/author_set_name_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/numerics_inc_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/schema_setup_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/schema_teardown_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/setup_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/teardown_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/values_setup_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/basic/values_teardown_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/schema_setup_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/schema_teardown_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/setup_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/teardown_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_can_update_unpublished_article_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_another_users_article_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_id_col_article_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_published_article_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_other_userid_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/values_setup_mssql.yaml create mode 100644 server/tests-py/queries/graphql_mutation/update/permissions/values_teardown_mssql.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 984b4dc73a1..c7077a21a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Bug fixes and improvements (Add entries below in the order of server, console, cli, docs, others) +- server: implement update mutations for MS SQL Server (closes #7834) + ## v2.1.0 - server: fix issue interpreting urls from environment in the `TestWebhookTransform` endpoint. diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 486a5ae9089..bcbab3b78ae 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -384,6 +384,7 @@ library , Hasura.Backends.MSSQL.Plan , Hasura.Backends.MSSQL.SQL.Value , Hasura.Backends.MSSQL.ToQuery + , Hasura.Backends.MSSQL.Types , Hasura.Backends.MSSQL.Types.Insert , Hasura.Backends.MSSQL.Types.Instances , Hasura.Backends.MSSQL.Types.Internal diff --git a/server/src-lib/Hasura/Backends/MSSQL/FromIr.hs b/server/src-lib/Hasura/Backends/MSSQL/FromIr.hs index 0fddd9b8151..8b89c271ac8 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/FromIr.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/FromIr.hs @@ -29,6 +29,7 @@ module Hasura.Backends.MSSQL.FromIr jsonFieldName, fromInsert, fromDelete, + fromUpdate, toSelectIntoTempTable, ) where @@ -41,8 +42,7 @@ import Data.Proxy import Data.Text qualified as T import Database.ODBC.SQLServer qualified as ODBC import Hasura.Backends.MSSQL.Instances.Types () -import Hasura.Backends.MSSQL.Types.Insert as TSQL (MSSQLExtraInsertData (..)) -import Hasura.Backends.MSSQL.Types.Internal as TSQL +import Hasura.Backends.MSSQL.Types as TSQL import Hasura.Prelude import Hasura.RQL.IR qualified as IR import Hasura.RQL.Types.Column qualified as IR @@ -1132,6 +1132,30 @@ fromDelete (IR.AnnDel tableName (permFilter, whereClause) _ allColumns) = do ) tableAlias +-- | Convert IR AST representing update into MSSQL AST representing an update statement +fromUpdate :: IR.AnnotatedUpdate 'MSSQL -> FromIr Update +fromUpdate (IR.AnnotatedUpdateG tableName (permFilter, whereClause) _ backendUpdate _ allColumns) = do + tableAlias <- fromTableName tableName + runReaderT + ( do + permissionsFilter <- fromGBoolExp permFilter + whereExpression <- fromGBoolExp whereClause + let columnNames = map (ColumnName . unName . IR.pgiName) allColumns + pure + Update + { updateTable = + Aliased + { aliasedAlias = entityAliasText tableAlias, + aliasedThing = tableName + }, + updateSet = updateOperations backendUpdate, + updateOutput = Output Inserted (map OutputColumn columnNames), + updateTempTable = TempTable tempTableNameUpdated columnNames, + updateWhere = Where [permissionsFilter, whereExpression] + } + ) + tableAlias + -- | Create a temporary table with the same schema as the given table. toSelectIntoTempTable :: TempTableName -> TableName -> [IR.ColumnInfo 'MSSQL] -> SelectIntoTempTable toSelectIntoTempTable tempTableName fromTable allColumns = do diff --git a/server/src-lib/Hasura/Backends/MSSQL/Instances/Execute.hs b/server/src-lib/Hasura/Backends/MSSQL/Instances/Execute.hs index b4553f0cdb8..9814cd4cfcf 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Instances/Execute.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Instances/Execute.hs @@ -24,6 +24,7 @@ import Hasura.Backends.MSSQL.SQL.Value (txtEncodedColVal) import Hasura.Backends.MSSQL.ToQuery as TQ import Hasura.Backends.MSSQL.Types.Insert (MSSQLExtraInsertData (..)) import Hasura.Backends.MSSQL.Types.Internal as TSQL +import Hasura.Backends.MSSQL.Types.Update import Hasura.Base.Error import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend @@ -228,7 +229,7 @@ msDBMutationPlan userInfo stringifyNum sourceName sourceConfig mrf = do go <$> case mrf of MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert MDBDelete annDelete -> executeDelete userInfo stringifyNum sourceConfig annDelete - MDBUpdate _annUpdate -> throw400 NotSupported "update mutations are not supported in MS SQL Server" + MDBUpdate annUpdate -> executeUpdate userInfo stringifyNum sourceConfig annUpdate MDBFunction {} -> throw400 NotSupported "function mutations are not supported in MSSQL" where go v = DBStepInfo @'MSSQL sourceName sourceConfig Nothing v @@ -337,7 +338,7 @@ executeInsert userInfo stringifyNum sourceConfig annInsert = do `onLeft` (throw500 . tshow) -- SELECT () AS [mutation_response], () AS [check_constraint_select] - let mutationOutputCheckConstraintSelect = selectMutationOutputAndCheckCondition mutationOutputSelect checkBoolExp + let mutationOutputCheckConstraintSelect = selectMutationOutputAndCheckCondition withAlias mutationOutputSelect checkBoolExp -- WITH "with_alias" AS () -- SELECT () AS [mutation_response], () AS [check_constraint_select] @@ -366,25 +367,6 @@ executeInsert userInfo stringifyNum sourceConfig annInsert = do OpExpression EQ' (columnFieldExpression column) (ValueExpression value) in Where $ pure $ OrExpression $ map (AndExpression . map mkColumnEqExpression) pkeyValues - generateCheckConstraintSelect :: Expression -> Select - generateCheckConstraintSelect checkBoolExp = - let zeroValue = ValueExpression $ ODBC.IntValue 0 - oneValue = ValueExpression $ ODBC.IntValue 1 - caseExpression = ConditionalExpression checkBoolExp zeroValue oneValue - sumAggregate = OpAggregate "SUM" [caseExpression] - in emptySelect - { selectProjections = [AggregateProjection (Aliased sumAggregate "check")], - selectFrom = Just $ TSQL.FromIdentifier withAlias - } - - selectMutationOutputAndCheckCondition :: Select -> Expression -> Select - selectMutationOutputAndCheckCondition mutationOutputSelect checkBoolExp = - let mutationOutputProjection = - ExpressionProjection $ Aliased (SelectExpression mutationOutputSelect) "mutation_response" - checkConstraintProjection = - ExpressionProjection $ Aliased (SelectExpression (generateCheckConstraintSelect checkBoolExp)) "check_constraint_select" - in emptySelect {selectProjections = [mutationOutputProjection, checkConstraintProjection]} - -- Delete -- | Executes a Delete IR AST and return results as JSON. @@ -443,6 +425,77 @@ buildDeleteTx deleteOperation stringifyNum = do -- Execute SELECT query and fetch mutation response encJFromText <$> Tx.singleRowQueryE fromMSSQLTxError mutationOutputSelectQuery +-- | Executes an Update IR AST and return results as JSON. +executeUpdate :: + MonadError QErr m => + UserInfo -> + Bool -> + SourceConfig 'MSSQL -> + AnnotatedUpdateG 'MSSQL Void (UnpreparedValue 'MSSQL) -> + m (ExceptT QErr IO EncJSON) +executeUpdate userInfo stringifyNum sourceConfig updateOperation = do + preparedUpdate <- traverse (prepareValueQuery $ _uiSession userInfo) updateOperation + let pool = _mscConnectionPool sourceConfig + if null $ updateOperations . _auBackend $ updateOperation + then pure $ pure $ IR.buildEmptyMutResp $ _auOutput preparedUpdate + else pure $ withMSSQLPool pool $ Tx.runTxE fromMSSQLTxError (buildUpdateTx preparedUpdate stringifyNum) + +-- | Converts an Update IR AST to a transaction of three update sql statements. +-- +-- A GraphQL update mutation does two things: +-- +-- 1. Update rows in a table according to some predicate +-- 2. (Potentially) returns the updated rows (including relationships) as JSON +-- +-- In order to complete these 2 things we need 3 SQL statements: +-- +-- 1. SELECT INTO WHERE - creates a temporary table +-- with the same schema as the original table in which we'll store the updated rows +-- from the table we are deleting +-- 2. UPDATE SET FROM with OUTPUT - updates the rows from the table and inserts the +-- updated rows to the temporary table from (1) +-- 3. SELECT - constructs the @returning@ query from the temporary table, including +-- relationships with other tables. +buildUpdateTx :: + AnnotatedUpdate 'MSSQL -> + Bool -> + Tx.TxET QErr IO EncJSON +buildUpdateTx updateOperation stringifyNum = do + let withAlias = "with_alias" + createTempTableQuery = + toQueryFlat $ + TQ.fromSelectIntoTempTable $ + TSQL.toSelectIntoTempTable tempTableNameUpdated (_auTable updateOperation) (_auAllCols updateOperation) + -- Create a temp table + Tx.unitQueryE fromMSSQLTxError createTempTableQuery + let updateQuery = TQ.fromUpdate <$> TSQL.fromUpdate updateOperation + updateQueryValidated <- toQueryFlat <$> V.runValidate (runFromIr updateQuery) `onLeft` (throw500 . tshow) + -- Execute UPDATE statement + Tx.unitQueryE fromMSSQLTxError updateQueryValidated + mutationOutputSelect <- mkMutationOutputSelect stringifyNum withAlias $ _auOutput updateOperation + let checkCondition = _auCheck updateOperation + -- The check constraint is translated to boolean expression + checkBoolExp <- + V.runValidate (runFromIr $ runReaderT (fromGBoolExp checkCondition) (EntityAlias withAlias)) + `onLeft` (throw500 . tshow) + + let withSelect = + emptySelect + { selectProjections = [StarProjection], + selectFrom = Just $ FromTempTable $ Aliased tempTableNameUpdated "updated_alias" + } + mutationOutputCheckConstraintSelect = selectMutationOutputAndCheckCondition withAlias mutationOutputSelect checkBoolExp + finalSelect = mutationOutputCheckConstraintSelect {selectWith = Just $ With $ pure $ Aliased withSelect withAlias} + + -- Execute SELECT query to fetch mutation response and check constraint result + (responseText, checkConditionInt) <- Tx.singleRowQueryE fromMSSQLTxError (toQueryFlat $ TQ.fromSelect finalSelect) + -- Drop the temp table + Tx.unitQueryE fromMSSQLTxError $ toQueryFlat $ dropTempTableQuery tempTableNameUpdated + -- Raise an exception if the check condition is not met + unless (checkConditionInt == (0 :: Int)) $ + throw400 PermissionError "check constraint of an update permission has failed" + pure $ encJFromText responseText + -- | Generate a SQL SELECT statement which outputs the mutation response -- -- For multi row inserts: @@ -467,6 +520,40 @@ mkMutationOutputSelect stringifyNum withAlias = \case pure emptySelect {selectFor = forJson, selectProjections = projections} IR.MOutSinglerowObject singleRowField -> mkSelect stringifyNum withAlias JASSingleObject singleRowField +-- | Generate a SQL SELECT statement which outputs the mutation response and check constraint result +-- +-- The check constraint boolean expression is evaluated on mutated rows in a CASE expression so that +-- the int value "0" is returned when check constraint is true otherwise the int value "1" is returned. +-- We use "SUM" aggregation on the returned value and if check constraint on any row is not met, the summed +-- value will not equal to "0" (always > 1). +-- +-- := +-- SELECT SUM(CASE WHEN THEN 0 ELSE 1 END) FROM [with_alias] +-- +-- := +-- SELECT (SELECT COUNT(*) FROM [with_alias]) AS [affected_rows], (select_from_returning) AS [returning] FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER +-- +-- SELECT () AS [mutation_response], () AS [check_constraint_select] +selectMutationOutputAndCheckCondition :: Text -> Select -> Expression -> Select +selectMutationOutputAndCheckCondition alias mutationOutputSelect checkBoolExp = + let mutationOutputProjection = + ExpressionProjection $ Aliased (SelectExpression mutationOutputSelect) "mutation_response" + checkConstraintProjection = + -- apply ISNULL() to avoid check constraint select statement yielding empty rows + ExpressionProjection $ + Aliased (FunctionExpression "ISNULL" [SelectExpression checkConstraintSelect, ValueExpression (ODBC.IntValue 0)]) "check_constraint_select" + in emptySelect {selectProjections = [mutationOutputProjection, checkConstraintProjection]} + where + checkConstraintSelect = + let zeroValue = ValueExpression $ ODBC.IntValue 0 + oneValue = ValueExpression $ ODBC.IntValue 1 + caseExpression = ConditionalExpression checkBoolExp zeroValue oneValue + sumAggregate = OpAggregate "SUM" [caseExpression] + in emptySelect + { selectProjections = [AggregateProjection (Aliased sumAggregate "check")], + selectFrom = Just $ TSQL.FromIdentifier alias + } + mkSelect :: MonadError QErr m => Bool -> diff --git a/server/src-lib/Hasura/Backends/MSSQL/Instances/Schema.hs b/server/src-lib/Hasura/Backends/MSSQL/Instances/Schema.hs index 60131bc337e..be29e0562fa 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Instances/Schema.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Instances/Schema.hs @@ -38,7 +38,7 @@ instance BackendSchema 'MSSQL where buildTableRelayQueryFields = msBuildTableRelayQueryFields buildTableInsertMutationFields = msBuildTableInsertMutationFields buildTableDeleteMutationFields = GSB.buildTableDeleteMutationFields - buildTableUpdateMutationFields = \_ _ _ _ _ _ -> return [] -- see _msBuildTableUpdateMutationFields. + buildTableUpdateMutationFields = msBuildTableUpdateMutationFields buildFunctionQueryFields = msBuildFunctionQueryFields buildFunctionRelayQueryFields = msBuildFunctionRelayQueryFields @@ -121,10 +121,7 @@ getExtraInsertData tableInfo = identityColumns = _tciExtraTableMetadata $ _tiCoreInfo tableInfo in MSSQLExtraInsertData (fromMaybe [] pkeyColumns) identityColumns --- Replace the instance implementation of 'buildTableUpdateMutationFields' with --- the below when we have an executable implementation of updates, in order to --- enable the update schema. -_msBuildTableUpdateMutationFields :: +msBuildTableUpdateMutationFields :: MonadBuildSchema 'MSSQL r m n => SourceName -> TableName 'MSSQL -> @@ -133,7 +130,7 @@ _msBuildTableUpdateMutationFields :: UpdPermInfo 'MSSQL -> Maybe (SelPermInfo 'MSSQL) -> m [FieldParser n (AnnotatedUpdateG 'MSSQL (RemoteRelationshipField UnpreparedValue) (UnpreparedValue 'MSSQL))] -_msBuildTableUpdateMutationFields = +msBuildTableUpdateMutationFields = GSB.buildTableUpdateMutationFields ( \ti updPerms -> fmap BackendUpdate diff --git a/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs b/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs index 4da38f7fc1e..168daf21664 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs @@ -10,12 +10,15 @@ module Hasura.Backends.MSSQL.ToQuery fromInsert, fromSetIdentityInsert, fromDelete, + fromUpdate, fromSelectIntoTempTable, + dropTempTableQuery, Printer (..), ) where import Data.Aeson (ToJSON (..)) +import Data.HashMap.Strict qualified as HM import Data.List (intersperse) import Data.String import Data.Text qualified as T @@ -23,8 +26,7 @@ import Data.Text.Extended qualified as T import Data.Text.Lazy qualified as L import Data.Text.Lazy.Builder qualified as L import Database.ODBC.SQLServer -import Hasura.Backends.MSSQL.Types.Instances () -import Hasura.Backends.MSSQL.Types.Internal +import Hasura.Backends.MSSQL.Types import Hasura.Prelude hiding (GT, LT) -------------------------------------------------------------------------------- @@ -112,6 +114,11 @@ fromExpression = <+> " (" <+> fromExpression y <+> ")" + FunctionExpression function args -> + fromString (T.unpack function) + <+> "(" + <+> SepByPrinter ", " (map fromExpression args) + <+> ")" ListExpression xs -> SepByPrinter ", " $ fromExpression <$> xs STOpExpression op e str -> "(" <+> fromExpression e <+> ")." @@ -184,6 +191,9 @@ fromInsertOutput = fromOutput fromInserted fromDeleteOutput :: DeleteOutput -> Printer fromDeleteOutput = fromOutput fromDeleted +fromUpdateOutput :: UpdateOutput -> Printer +fromUpdateOutput = fromOutput fromInserted + fromValues :: Values -> Printer fromValues (Values values) = "( " <+> SepByPrinter ", " (map fromExpression values) <+> " )" @@ -233,6 +243,42 @@ fromDelete Delete {deleteTable, deleteOutput, deleteTempTable, deleteWhere} = fromWhere deleteWhere ] +-- | Generate an update statement +-- +-- > Update +-- > (Aliased (TableName "table" "schema") "alias") +-- > (fromList [(ColumnName "name", ValueExpression (TextValue "updated_name"))]) +-- > (Output Inserted) +-- > (TempTable (TempTableName "updated") [ColumnName "id", ColumnName "name"]) +-- > (Where [OpExpression EQ' (ColumnName "id") (ValueExpression (IntValue 1))]) +-- +-- Becomes: +-- +-- > UPDATE [alias] SET [alias].[name] = 'updated_name' OUTPUT INSERTED.[id], INSERTED.[name] INTO +-- > #updated([id], [name]) FROM [schema].[table] AS [alias] WHERE (id = 1) +fromUpdate :: Update -> Printer +fromUpdate Update {..} = + SepByPrinter + NewlinePrinter + [ "UPDATE " <+> fromNameText (aliasedAlias updateTable), + fromUpdateSet updateSet, + fromUpdateOutput updateOutput, + "INTO " <+> fromTempTable updateTempTable, + "FROM " <+> fromAliased (fmap fromTableName updateTable), + fromWhere updateWhere + ] + where + fromUpdateSet :: UpdateSet -> Printer + fromUpdateSet setColumns = + let updateColumnValue (column, updateOp) = + fromColumnName column <+> fromUpdateOperator (fromExpression <$> updateOp) + in "SET " <+> SepByPrinter ", " (map updateColumnValue (HM.toList setColumns)) + + fromUpdateOperator :: UpdateOperator Printer -> Printer + fromUpdateOperator = \case + UpdateSet p -> " = " <+> p + UpdateInc p -> " += " <+> p + -- | Converts `SelectIntoTempTable`. -- -- > SelectIntoTempTable (TempTableName "deleted") [UnifiedColumn "id" IntegerType, UnifiedColumn "name" TextType] (TableName "table" "schema") @@ -281,6 +327,11 @@ fromTempTable :: TempTable -> Printer fromTempTable (TempTable table columns) = fromTempTableName table <+> parens (SepByPrinter ", " (map fromColumnName columns)) +-- | @TempTableName "temp_table" is converted to "DROP TABLE #temp_table" +dropTempTableQuery :: TempTableName -> Printer +dropTempTableQuery tempTableName = + QueryPrinter "DROP TABLE " <+> fromTempTableName tempTableName + fromSelect :: Select -> Printer fromSelect Select {..} = fmap fromWith selectWith ?<+> wrapFor selectFor result where diff --git a/server/src-lib/Hasura/Backends/MSSQL/Types.hs b/server/src-lib/Hasura/Backends/MSSQL/Types.hs new file mode 100644 index 00000000000..0b9d919d463 --- /dev/null +++ b/server/src-lib/Hasura/Backends/MSSQL/Types.hs @@ -0,0 +1,10 @@ +-- | This module exports modules @Hasura.Backends.MSSQL.Types.*@ +module Hasura.Backends.MSSQL.Types + ( module M, + ) +where + +import Hasura.Backends.MSSQL.Types.Insert as M +import Hasura.Backends.MSSQL.Types.Instances () +import Hasura.Backends.MSSQL.Types.Internal as M +import Hasura.Backends.MSSQL.Types.Update as M diff --git a/server/src-lib/Hasura/Backends/MSSQL/Types/Internal.hs b/server/src-lib/Hasura/Backends/MSSQL/Types/Internal.hs index fd74c1b23af..37802c2301d 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Types/Internal.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Types/Internal.hs @@ -71,6 +71,7 @@ module Hasura.Backends.MSSQL.Types.Internal snakeCaseTableName, stringTypes, tempTableNameDeleted, + tempTableNameUpdated, ) where @@ -217,6 +218,9 @@ newtype TempTableName = TempTableName Text tempTableNameDeleted :: TempTableName tempTableNameDeleted = TempTableName "deleted" +tempTableNameUpdated :: TempTableName +tempTableNameUpdated = TempTableName "updated" + data TempTable = TempTable { ttName :: !TempTableName, ttColumns :: ![ColumnName] @@ -311,6 +315,8 @@ data Expression | -- | This is for getting actual atomic values out of a JSON -- string. JsonValueExpression Expression JsonPath + | -- | This is for evaluating SQL functions, text(e1, e2, ..). + FunctionExpression Text [Expression] | OpExpression Op Expression Expression | ListExpression [Expression] | STOpExpression SpatialOp Expression Expression diff --git a/server/src-lib/Hasura/Backends/MSSQL/Types/Update.hs b/server/src-lib/Hasura/Backends/MSSQL/Types/Update.hs index 7e53f20a641..c5d01147124 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Types/Update.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Types/Update.hs @@ -2,11 +2,14 @@ module Hasura.Backends.MSSQL.Types.Update ( BackendUpdate (..), UpdateOperator (..), + Update (..), + UpdateSet, + UpdateOutput, ) where import Hasura.Backends.MSSQL.Types.Instances () -import Hasura.Backends.MSSQL.Types.Internal qualified as MSSQL +import Hasura.Backends.MSSQL.Types.Internal import Hasura.Prelude -- | The MSSQL-specific data of an Update expression. @@ -16,7 +19,7 @@ import Hasura.Prelude -- the data at the leaves. data BackendUpdate v = BackendUpdate { -- | The update operations to perform on each column. - updateOperations :: HashMap MSSQL.ColumnName (UpdateOperator v) + updateOperations :: HashMap ColumnName (UpdateOperator v) } deriving (Functor, Foldable, Traversable, Generic, Data) @@ -29,3 +32,17 @@ data UpdateOperator v = UpdateSet v | UpdateInc v deriving (Functor, Foldable, Traversable, Generic, Data) + +type UpdateSet = HashMap ColumnName (UpdateOperator Expression) + +type UpdateOutput = Output Inserted + +-- | UPDATE [table_alias] SET [table_alias].column = 'value' OUTPUT INSERTED.column INTO #updated +-- FROM [table_name] AS [table_alias] WHERE +data Update = Update + { updateTable :: Aliased TableName, + updateSet :: UpdateSet, + updateOutput :: UpdateOutput, + updateTempTable :: TempTable, + updateWhere :: Where + } diff --git a/server/tests-py/queries/graphql_mutation/update/basic/article_column_multiple_operators_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/article_column_multiple_operators_mssql.yaml new file mode 100644 index 00000000000..ae5ca411f00 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/article_column_multiple_operators_mssql.yaml @@ -0,0 +1,20 @@ +description: Update an article with a column used in multiple operators +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_article( + _set: {author_id: 2} + _inc: {author_id: 1} + where: {id: {_eq: 1}} + ){ + affected_rows + } + } +response: + errors: + - extensions: + path: "$.selectionSet.update_article.args" + code: validation-failed + message: 'Column found in multiple operators: "author_id".' diff --git a/server/tests-py/queries/graphql_mutation/update/basic/article_inc_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/article_inc_mssql.yaml new file mode 100644 index 00000000000..0a3126e1a18 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/article_inc_mssql.yaml @@ -0,0 +1,30 @@ +description: Increment author_id for the article which will re-assign the author +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_article(_inc: {author_id: 1}, where: {id: {_eq: 1}}){ + affected_rows + returning{ + id + title + content + author{ + id + name + } + } + } + } +response: + data: + update_article: + affected_rows: 1 + returning: + - id: 1 + title: Article 1 + content: Sample article content 1 + author: + id: 2 + name: Author 2 diff --git a/server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_mssql.yaml new file mode 100644 index 00000000000..cd434b5a16f --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_mssql.yaml @@ -0,0 +1,36 @@ +description: Update a row of author by primary key +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_author_by_pk( + _set: {name: "Author 2 updated"} + pk_columns: {id: 2} + ){ + id + name + articles_aggregate{ + aggregate{ + count + } + nodes{ + id + title + content + } + } + } + } +response: + data: + update_author_by_pk: + id: 2 + name: Author 2 updated + articles_aggregate: + aggregate: + count: 1 + nodes: + - id: 3 + title: Article 3 + content: Sample article content 3 diff --git a/server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_null_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_null_mssql.yaml new file mode 100644 index 00000000000..b76800bfa26 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/author_by_pk_null_mssql.yaml @@ -0,0 +1,17 @@ +description: Update a row of author by primary key +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_author_by_pk( + _set: {name: "Author 2 updated"} + pk_columns: {id: 123} + ){ + id + name + } + } +response: +data: + update_author_by_pk: null diff --git a/server/tests-py/queries/graphql_mutation/update/basic/author_no_operator_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/author_no_operator_mssql.yaml new file mode 100644 index 00000000000..5df002d9acc --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/author_no_operator_mssql.yaml @@ -0,0 +1,14 @@ +description: Update author mutation without any update operators should result in empty mutation response +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_author(where: {id: {_eq: 1}}){ + affected_rows + } + } +response: + data: + update_author: + affected_rows: 0 diff --git a/server/tests-py/queries/graphql_mutation/update/basic/author_set_name_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/author_set_name_mssql.yaml new file mode 100644 index 00000000000..741c1de0e67 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/author_set_name_mssql.yaml @@ -0,0 +1,36 @@ +description: Update mutation on author +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_author( + where: {id: {_eq: 1}}, + _set: {name: "Author 1 updated"} + ){ + affected_rows + returning{ + id + name + articles{ + id + title + content + } + } + } + } +response: + data: + update_author: + affected_rows: 1 + returning: + - id: 1 + name: Author 1 updated + articles: + - id: 1 + title: Article 1 + content: Sample article content 1 + - id: 2 + title: Article 2 + content: Sample article content 2 diff --git a/server/tests-py/queries/graphql_mutation/update/basic/numerics_inc_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/numerics_inc_mssql.yaml new file mode 100644 index 00000000000..6758ee433ff --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/numerics_inc_mssql.yaml @@ -0,0 +1,45 @@ +description: Updated numerics data using _inc operator +url: /v1/graphql +status: 200 +query: + query: | + mutation { + update_numerics( + where: {id: {_eq: 1}} + _inc: { + numeric_col: -1.1 + decimal_col: -1.1 + int_col: -1 + smallint_col: -1 + float_col: -1.1 + real_col: -1.1 + bigint_col: -1 + tinyint_col: -1 + } + ){ + affected_rows + returning{ + numeric_col + decimal_col + int_col + smallint_col + float_col + real_col + bigint_col + tinyint_col + } + } + } +response: + data: + update_numerics: + affected_rows: 1 + returning: + - numeric_col: 0 + decimal_col: 122 + int_col: 2147483646 + smallint_col: 3276 + float_col: -306.87 + real_col: -37.919998 + bigint_col: 9223372036854775806 + tinyint_col: 253 diff --git a/server/tests-py/queries/graphql_mutation/update/basic/schema_setup_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/schema_setup_mssql.yaml new file mode 100644 index 00000000000..1a9902faa2b --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/schema_setup_mssql.yaml @@ -0,0 +1,34 @@ +type: bulk +args: + +#Create tables +- type: mssql_run_sql + args: + source: mssql + sql: | + -- author table + create table author( + id int identity not null primary key, + name text + ); + -- article table + CREATE TABLE article ( + id INT IDENTITY NOT NULL PRIMARY KEY, + title TEXT, + content TEXT, + author_id INTEGER REFERENCES author(id), + is_published BIT, + published_on TIMESTAMP + ); + -- table with numeric columns + CREATE TABLE numerics ( + id INT IDENTITY NOT NULL PRIMARY KEY, + numeric_col numeric, + decimal_col decimal, + int_col int, + smallint_col smallint, + float_col float, + real_col real, + bigint_col bigint, + tinyint_col tinyint + ); diff --git a/server/tests-py/queries/graphql_mutation/update/basic/schema_teardown_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/schema_teardown_mssql.yaml new file mode 100644 index 00000000000..82ec85cf7e8 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/schema_teardown_mssql.yaml @@ -0,0 +1,11 @@ +type: bulk +args: + +- type: mssql_run_sql + args: + source: mssql + sql: | + DROP TABLE article; + DROP TABLE author; + DROP TABLE numerics; + cascade: true diff --git a/server/tests-py/queries/graphql_mutation/update/basic/setup_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/setup_mssql.yaml new file mode 100644 index 00000000000..e77531e9504 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/setup_mssql.yaml @@ -0,0 +1,40 @@ +type: bulk +args: + +- type: mssql_track_table + args: + source: mssql + table: + name: author + +- type: mssql_track_table + args: + source: mssql + table: + name: article + +- type: mssql_track_table + args: + source: mssql + table: + name: numerics + +#Object relationship article <-> author +- type: mssql_create_object_relationship + args: + source: mssql + table: article + name: author + using: + foreign_key_constraint_on: author_id + +#Array relationship author <-> article +- type: mssql_create_array_relationship + args: + source: mssql + table: author + name: articles + using: + foreign_key_constraint_on: + table: article + column: author_id diff --git a/server/tests-py/queries/graphql_mutation/update/basic/teardown_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/teardown_mssql.yaml new file mode 100644 index 00000000000..e03ef03a1ec --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/teardown_mssql.yaml @@ -0,0 +1,39 @@ +type: bulk +args: + +#Drop relationships +- type: mssql_drop_relationship + args: + source: mssql + table: + name: author + relationship: articles + +- type: mssql_drop_relationship + args: + source: mssql + table: + name: article + relationship: author + +#Untrack tables +- type: mssql_untrack_table + args: + source: mssql + table: + name: author + cascade: true + +- type: mssql_untrack_table + args: + source: mssql + table: + name: article + cascade: true + +- type: mssql_untrack_table + args: + source: mssql + table: + name: numerics + cascade: true diff --git a/server/tests-py/queries/graphql_mutation/update/basic/values_setup_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/values_setup_mssql.yaml new file mode 100644 index 00000000000..9516380ca93 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/values_setup_mssql.yaml @@ -0,0 +1,54 @@ +type: bulk +args: + +- type: mssql_run_sql + args: + source: mssql + sql: | + -- insert data + INSERT INTO author (name) + VALUES + ('Author 1'), + ('Author 2') + ; + INSERT INTO article (title,content,author_id,is_published) + VALUES + ( + 'Article 1', + 'Sample article content 1', + 1, + 0 + ), + ( + 'Article 2', + 'Sample article content 2', + 1, + 1 + ), + ( + 'Article 3', + 'Sample article content 3', + 2, + 1 + ) + ; + + INSERT INTO numerics ( + numeric_col, + decimal_col, + int_col, + smallint_col, + float_col, + real_col, + bigint_col, + tinyint_col + ) VALUES ( 1.234 + , 123.45 + , 2147483647 + , 3277 + , 2.23E -308 + , 1.18E - 38 + , 9223372036854775807 + , 254 + ) + ; diff --git a/server/tests-py/queries/graphql_mutation/update/basic/values_teardown_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/basic/values_teardown_mssql.yaml new file mode 100644 index 00000000000..4a36ea1b7a3 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/basic/values_teardown_mssql.yaml @@ -0,0 +1,16 @@ +type: bulk +args: + +#truncate data +- type: mssql_run_sql + args: + source: mssql + sql: | + -- delete rows from table + DELETE FROM article; + DELETE FROM author; + DELETE FROM numerics; + -- reset identity columns + DBCC CHECKIDENT ('article', RESEED, 0); + DBCC CHECKIDENT ('author', RESEED, 0); + DBCC CHECKIDENT ('numerics', RESEED, 0); diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/schema_setup_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/schema_setup_mssql.yaml new file mode 100644 index 00000000000..9ce719a71c5 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/schema_setup_mssql.yaml @@ -0,0 +1,22 @@ +type: bulk +args: + +#Create tables +- type: mssql_run_sql + args: + source: mssql + sql: | + -- author table + create table author( + id int identity not null primary key, + name text + ); + -- article table + CREATE TABLE article ( + id INT IDENTITY NOT NULL PRIMARY KEY, + title TEXT, + content TEXT, + author_id INTEGER REFERENCES author(id), + is_published BIT, + published_on TIMESTAMP + ); diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/schema_teardown_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/schema_teardown_mssql.yaml new file mode 100644 index 00000000000..adb76b472ba --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/schema_teardown_mssql.yaml @@ -0,0 +1,10 @@ +type: bulk +args: + +- type: mssql_run_sql + args: + source: mssql + sql: | + DROP TABLE article; + DROP TABLE author; + cascade: true diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/setup_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/setup_mssql.yaml new file mode 100644 index 00000000000..135026a0ac6 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/setup_mssql.yaml @@ -0,0 +1,90 @@ +type: bulk +args: + +- type: mssql_track_table + args: + source: mssql + table: + name: author + +- type: mssql_track_table + args: + source: mssql + table: + name: article + +#Object relationship article <-> author +- type: mssql_create_object_relationship + args: + source: mssql + table: article + name: author + using: + foreign_key_constraint_on: author_id + +#Array relationship author <-> article +- type: mssql_create_array_relationship + args: + source: mssql + table: author + name: articles + using: + foreign_key_constraint_on: + table: article + column: author_id + +#Author select permission for user +- type: mssql_create_select_permission + args: + source: mssql + table: author + role: user + permission: + columns: [id, name] + filter: + id: X-HASURA-USER-ID + +#Author update permission for user +- type: mssql_create_update_permission + args: + source: mssql + table: author + role: user + permission: + columns: + - name + filter: + id: X-Hasura-User-Id + +#Article select permission for user +- type: mssql_create_select_permission + args: + source: mssql + table: article + role: user + permission: + columns: '*' + filter: + $or: + - author_id: X-HASURA-USER-ID + - is_published: 1 + +#Article update permission for user +#Allow modifications only on unpublished articles +- type: mssql_create_update_permission + args: + source: mssql + table: article + role: user + permission: + columns: + - title + - content + - is_published + - published_on + filter: + $and: + - author_id: X-HASURA-USER-ID + - is_published: 0 + check: + is_published: 0 diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/teardown_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/teardown_mssql.yaml new file mode 100644 index 00000000000..3e825d4edc7 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/teardown_mssql.yaml @@ -0,0 +1,32 @@ +type: bulk +args: + +#Drop relationships +- type: mssql_drop_relationship + args: + source: mssql + table: + name: author + relationship: articles + +- type: mssql_drop_relationship + args: + source: mssql + table: + name: article + relationship: author + +#Untrack tables +- type: mssql_untrack_table + args: + source: mssql + table: + name: author + cascade: true + +- type: mssql_untrack_table + args: + source: mssql + table: + name: article + cascade: true diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_can_update_unpublished_article_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_can_update_unpublished_article_mssql.yaml new file mode 100644 index 00000000000..3f5d50e7e03 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_can_update_unpublished_article_mssql.yaml @@ -0,0 +1,38 @@ +description: Update mutation on article +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +query: + query: | + mutation { + update_article( + where: {id: {_eq: 1}} + _set: {content: "Author 1 article content updated"} + ){ + affected_rows + returning{ + id + title + content + is_published + author{ + id + name + } + } + } + } +response: + data: + update_article: + affected_rows: 1 + returning: + - id: 1 + title: Article 1 + content: Author 1 article content updated + is_published: false + author: + id: 1 + name: Author 1 diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish_mssql.yaml new file mode 100644 index 00000000000..1523d0dcd2e --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_publish_mssql.yaml @@ -0,0 +1,32 @@ +description: Update mutation on article whose check constraint is not met. Trying to publish the article with user role. +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +query: + query: | + mutation { + update_article( + where: {id: {_eq: 1}} + _set: {is_published: true} + ){ + affected_rows + returning{ + id + title + content + is_published + author{ + id + name + } + } + } + } +response: + errors: + - extensions: + path: "$" + code: permission-error + message: check constraint of an update permission has failed diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_another_users_article_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_another_users_article_mssql.yaml new file mode 100644 index 00000000000..d7d81b30dd7 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_another_users_article_mssql.yaml @@ -0,0 +1,31 @@ +description: Update mutation on article where filter condition is not met. Article with id = 1 belongs to author id = 1 and it is published. Trying to update the article as author id = 2. +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '2' +query: + query: | + mutation { + update_article( + where: {id: {_eq: 1}} + _set: {content: "Author 1 article content updated"} + ){ + affected_rows + returning{ + id + title + content + is_published + author{ + id + name + } + } + } + } +response: + data: + update_article: + affected_rows: 0 + returning: [] diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_id_col_article_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_id_col_article_mssql.yaml new file mode 100644 index 00000000000..722ef154250 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_id_col_article_mssql.yaml @@ -0,0 +1,32 @@ +description: Update mutation on article where updating a column has been restricted. +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +query: + query: | + mutation { + update_article( + where: {id: {_eq: 1}} + _set: {id: 2} + ){ + affected_rows + returning{ + id + title + content + is_published + author{ + id + name + } + } + } + } +response: + errors: + - extensions: + path: "$.selectionSet.update_article.args._set.id" + code: validation-failed + message: 'field "id" not found in type: ''article_set_input''' diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_published_article_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_published_article_mssql.yaml new file mode 100644 index 00000000000..b7796b9f536 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_cannot_update_published_article_mssql.yaml @@ -0,0 +1,31 @@ +description: Update mutation on article where filter condition is not met. Article with id = 2 belongs to author id = 2 and it is unpublished. +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '2' +query: + query: | + mutation { + update_article( + where: {id: {_eq: 2}} + _set: {content: "Author 2 article content updated"} + ){ + affected_rows + returning{ + id + title + content + is_published + author{ + id + name + } + } + } + } +response: + data: + update_article: + affected_rows: 0 + returning: [] diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_mssql.yaml new file mode 100644 index 00000000000..64ab439b4f6 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_mssql.yaml @@ -0,0 +1,39 @@ +description: Update name of an author +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +query: + query: | + mutation { + update_author( + where: {id: {_eq: 1}} + _set: {name: "Author 1 updated"} + ){ + affected_rows + returning{ + id + name + articles{ + id + title + content + } + } + } + } +response: + data: + update_author: + affected_rows: 1 + returning: + - id: 1 + name: Author 1 updated + articles: + - id: 1 + title: Article 1 + content: Sample article content 1 + - id: 2 + title: Article 2 + content: Sample article content 2 diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_other_userid_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_other_userid_mssql.yaml new file mode 100644 index 00000000000..cafbf738e2f --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/user_update_author_other_userid_mssql.yaml @@ -0,0 +1,30 @@ +description: Trying to update name of author whose id /= X-Hasura-User-Id. This shouldn't mutate any data as permission filter condition is not met. +url: /v1/graphql +status: 200 +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +query: + query: | + mutation { + update_author( + where: {id: {_eq: 2}} + _set: {name: "Author 1 updated"} + ){ + affected_rows + returning{ + id + name + articles{ + id + title + content + } + } + } + } +response: + data: + update_author: + affected_rows: 0 + returning: [] diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/values_setup_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/values_setup_mssql.yaml new file mode 100644 index 00000000000..3a1f53c97b3 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/values_setup_mssql.yaml @@ -0,0 +1,34 @@ +type: bulk +args: + +- type: mssql_run_sql + args: + source: mssql + sql: | + -- insert data + INSERT INTO author (name) + VALUES + ('Author 1'), + ('Author 2') + ; + INSERT INTO article (title,content,author_id,is_published) + VALUES + ( + 'Article 1', + 'Sample article content 1', + 1, + 0 + ), + ( + 'Article 2', + 'Sample article content 2', + 1, + 1 + ), + ( + 'Article 3', + 'Sample article content 3', + 2, + 1 + ) + ; diff --git a/server/tests-py/queries/graphql_mutation/update/permissions/values_teardown_mssql.yaml b/server/tests-py/queries/graphql_mutation/update/permissions/values_teardown_mssql.yaml new file mode 100644 index 00000000000..e949b61283f --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/update/permissions/values_teardown_mssql.yaml @@ -0,0 +1,14 @@ +type: bulk +args: + +#truncate data +- type: mssql_run_sql + args: + source: mssql + sql: | + -- delete rows from table + DELETE FROM article; + DELETE FROM author; + -- reset identity columns + DBCC CHECKIDENT ('article', RESEED, 0); + DBCC CHECKIDENT ('author', RESEED, 0); diff --git a/server/tests-py/test_graphql_mutations.py b/server/tests-py/test_graphql_mutations.py index b3f9f875d95..84b30dd1db8 100644 --- a/server/tests-py/test_graphql_mutations.py +++ b/server/tests-py/test_graphql_mutations.py @@ -512,6 +512,63 @@ class TestGraphqlUpdatePermissions: def dir(cls): return "queries/graphql_mutation/update/permissions" +@pytest.mark.parametrize("backend", ['mssql']) +@use_mutation_fixtures +class TestGraphqlUpdateBasicMssql: + + def test_set_author_name(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/author_set_name_mssql.yaml") + + def test_article_inc(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/article_inc_mssql.yaml") + + def test_author_no_operator(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/author_no_operator_mssql.yaml") + + def test_article_column_multiple_operators(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/article_column_multiple_operators_mssql.yaml") + + def test_author_by_pk(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/author_by_pk_mssql.yaml") + + def test_author_by_pk_null(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/author_by_pk_null_mssql.yaml") + + def test_numerics_inc(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/numerics_inc_mssql.yaml") + + @classmethod + def dir(cls): + return "queries/graphql_mutation/update/basic" + +@pytest.mark.parametrize("backend", ['mssql']) +@use_mutation_fixtures +class TestGraphqlUpdatePermissionsMssql: + + def test_user_update_author(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_update_author_mssql.yaml") + + def test_user_update_author_other_userid(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_update_author_other_userid_mssql.yaml") + + def test_user_can_update_unpublished_article(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_can_update_unpublished_article_mssql.yaml") + + def test_user_cannot_update_published_article(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_cannot_update_published_article_mssql.yaml") + + def test_user_cannot_update_id_col_article(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_cannot_update_id_col_article_mssql.yaml") + + def test_user_cannot_update_another_users_article(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_cannot_update_another_users_article_mssql.yaml") + + def test_user_cannot_publish(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/user_cannot_publish_mssql.yaml") + + @classmethod + def dir(cls): + return "queries/graphql_mutation/update/permissions" @pytest.mark.parametrize("transport", ['http', 'websocket']) @use_mutation_fixtures