server/mssql: update mutation, SQL generation and execution

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3059
GitOrigin-RevId: 4ed0cbf54ac2a7103cb2b7adc97b2dfdf9994c4f
This commit is contained in:
Rakesh Emmadi 2021-12-15 19:25:41 +05:30 committed by hasura-bot
parent ae124e2ee8
commit f00404e0f6
36 changed files with 1112 additions and 33 deletions

View File

@ -6,6 +6,8 @@
### Bug fixes and improvements ### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others) (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 ## v2.1.0
- server: fix issue interpreting urls from environment in the `TestWebhookTransform` endpoint. - server: fix issue interpreting urls from environment in the `TestWebhookTransform` endpoint.

View File

@ -384,6 +384,7 @@ library
, Hasura.Backends.MSSQL.Plan , Hasura.Backends.MSSQL.Plan
, Hasura.Backends.MSSQL.SQL.Value , Hasura.Backends.MSSQL.SQL.Value
, Hasura.Backends.MSSQL.ToQuery , Hasura.Backends.MSSQL.ToQuery
, Hasura.Backends.MSSQL.Types
, Hasura.Backends.MSSQL.Types.Insert , Hasura.Backends.MSSQL.Types.Insert
, Hasura.Backends.MSSQL.Types.Instances , Hasura.Backends.MSSQL.Types.Instances
, Hasura.Backends.MSSQL.Types.Internal , Hasura.Backends.MSSQL.Types.Internal

View File

@ -29,6 +29,7 @@ module Hasura.Backends.MSSQL.FromIr
jsonFieldName, jsonFieldName,
fromInsert, fromInsert,
fromDelete, fromDelete,
fromUpdate,
toSelectIntoTempTable, toSelectIntoTempTable,
) )
where where
@ -41,8 +42,7 @@ import Data.Proxy
import Data.Text qualified as T import Data.Text qualified as T
import Database.ODBC.SQLServer qualified as ODBC import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.Instances.Types () import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Insert as TSQL (MSSQLExtraInsertData (..)) import Hasura.Backends.MSSQL.Types as TSQL
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude import Hasura.Prelude
import Hasura.RQL.IR qualified as IR import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types.Column qualified as IR import Hasura.RQL.Types.Column qualified as IR
@ -1132,6 +1132,30 @@ fromDelete (IR.AnnDel tableName (permFilter, whereClause) _ allColumns) = do
) )
tableAlias 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. -- | Create a temporary table with the same schema as the given table.
toSelectIntoTempTable :: TempTableName -> TableName -> [IR.ColumnInfo 'MSSQL] -> SelectIntoTempTable toSelectIntoTempTable :: TempTableName -> TableName -> [IR.ColumnInfo 'MSSQL] -> SelectIntoTempTable
toSelectIntoTempTable tempTableName fromTable allColumns = do toSelectIntoTempTable tempTableName fromTable allColumns = do

View File

@ -24,6 +24,7 @@ import Hasura.Backends.MSSQL.SQL.Value (txtEncodedColVal)
import Hasura.Backends.MSSQL.ToQuery as TQ import Hasura.Backends.MSSQL.ToQuery as TQ
import Hasura.Backends.MSSQL.Types.Insert (MSSQLExtraInsertData (..)) import Hasura.Backends.MSSQL.Types.Insert (MSSQLExtraInsertData (..))
import Hasura.Backends.MSSQL.Types.Internal as TSQL import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Backends.MSSQL.Types.Update
import Hasura.Base.Error import Hasura.Base.Error
import Hasura.EncJSON import Hasura.EncJSON
import Hasura.GraphQL.Execute.Backend import Hasura.GraphQL.Execute.Backend
@ -228,7 +229,7 @@ msDBMutationPlan userInfo stringifyNum sourceName sourceConfig mrf = do
go <$> case mrf of go <$> case mrf of
MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert
MDBDelete annDelete -> executeDelete userInfo stringifyNum sourceConfig annDelete 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" MDBFunction {} -> throw400 NotSupported "function mutations are not supported in MSSQL"
where where
go v = DBStepInfo @'MSSQL sourceName sourceConfig Nothing v go v = DBStepInfo @'MSSQL sourceName sourceConfig Nothing v
@ -337,7 +338,7 @@ executeInsert userInfo stringifyNum sourceConfig annInsert = do
`onLeft` (throw500 . tshow) `onLeft` (throw500 . tshow)
-- SELECT (<mutation_output_select>) AS [mutation_response], (<check_constraint_select>) AS [check_constraint_select] -- SELECT (<mutation_output_select>) AS [mutation_response], (<check_constraint_select>) AS [check_constraint_select]
let mutationOutputCheckConstraintSelect = selectMutationOutputAndCheckCondition mutationOutputSelect checkBoolExp let mutationOutputCheckConstraintSelect = selectMutationOutputAndCheckCondition withAlias mutationOutputSelect checkBoolExp
-- WITH "with_alias" AS (<table_select>) -- WITH "with_alias" AS (<table_select>)
-- SELECT (<mutation_output_select>) AS [mutation_response], (<check_constraint_select>) AS [check_constraint_select] -- SELECT (<mutation_output_select>) AS [mutation_response], (<check_constraint_select>) AS [check_constraint_select]
@ -366,25 +367,6 @@ executeInsert userInfo stringifyNum sourceConfig annInsert = do
OpExpression EQ' (columnFieldExpression column) (ValueExpression value) OpExpression EQ' (columnFieldExpression column) (ValueExpression value)
in Where $ pure $ OrExpression $ map (AndExpression . map mkColumnEqExpression) pkeyValues 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 -- Delete
-- | Executes a Delete IR AST and return results as JSON. -- | 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 -- Execute SELECT query and fetch mutation response
encJFromText <$> Tx.singleRowQueryE fromMSSQLTxError mutationOutputSelectQuery 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 <temp_table> WHERE <false> - 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 -- | Generate a SQL SELECT statement which outputs the mutation response
-- --
-- For multi row inserts: -- For multi row inserts:
@ -467,6 +520,40 @@ mkMutationOutputSelect stringifyNum withAlias = \case
pure emptySelect {selectFor = forJson, selectProjections = projections} pure emptySelect {selectFor = forJson, selectProjections = projections}
IR.MOutSinglerowObject singleRowField -> mkSelect stringifyNum withAlias JASSingleObject singleRowField 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).
--
-- <check_constraint_select> :=
-- SELECT SUM(CASE WHEN <check_boolean_expression> THEN 0 ELSE 1 END) FROM [with_alias]
--
-- <mutation_output_select> :=
-- SELECT (SELECT COUNT(*) FROM [with_alias]) AS [affected_rows], (select_from_returning) AS [returning] FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER
--
-- SELECT (<mutation_output_select>) AS [mutation_response], (<check_constraint_select>) 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 :: mkSelect ::
MonadError QErr m => MonadError QErr m =>
Bool -> Bool ->

View File

@ -38,7 +38,7 @@ instance BackendSchema 'MSSQL where
buildTableRelayQueryFields = msBuildTableRelayQueryFields buildTableRelayQueryFields = msBuildTableRelayQueryFields
buildTableInsertMutationFields = msBuildTableInsertMutationFields buildTableInsertMutationFields = msBuildTableInsertMutationFields
buildTableDeleteMutationFields = GSB.buildTableDeleteMutationFields buildTableDeleteMutationFields = GSB.buildTableDeleteMutationFields
buildTableUpdateMutationFields = \_ _ _ _ _ _ -> return [] -- see _msBuildTableUpdateMutationFields. buildTableUpdateMutationFields = msBuildTableUpdateMutationFields
buildFunctionQueryFields = msBuildFunctionQueryFields buildFunctionQueryFields = msBuildFunctionQueryFields
buildFunctionRelayQueryFields = msBuildFunctionRelayQueryFields buildFunctionRelayQueryFields = msBuildFunctionRelayQueryFields
@ -121,10 +121,7 @@ getExtraInsertData tableInfo =
identityColumns = _tciExtraTableMetadata $ _tiCoreInfo tableInfo identityColumns = _tciExtraTableMetadata $ _tiCoreInfo tableInfo
in MSSQLExtraInsertData (fromMaybe [] pkeyColumns) identityColumns in MSSQLExtraInsertData (fromMaybe [] pkeyColumns) identityColumns
-- Replace the instance implementation of 'buildTableUpdateMutationFields' with msBuildTableUpdateMutationFields ::
-- the below when we have an executable implementation of updates, in order to
-- enable the update schema.
_msBuildTableUpdateMutationFields ::
MonadBuildSchema 'MSSQL r m n => MonadBuildSchema 'MSSQL r m n =>
SourceName -> SourceName ->
TableName 'MSSQL -> TableName 'MSSQL ->
@ -133,7 +130,7 @@ _msBuildTableUpdateMutationFields ::
UpdPermInfo 'MSSQL -> UpdPermInfo 'MSSQL ->
Maybe (SelPermInfo 'MSSQL) -> Maybe (SelPermInfo 'MSSQL) ->
m [FieldParser n (AnnotatedUpdateG 'MSSQL (RemoteRelationshipField UnpreparedValue) (UnpreparedValue 'MSSQL))] m [FieldParser n (AnnotatedUpdateG 'MSSQL (RemoteRelationshipField UnpreparedValue) (UnpreparedValue 'MSSQL))]
_msBuildTableUpdateMutationFields = msBuildTableUpdateMutationFields =
GSB.buildTableUpdateMutationFields GSB.buildTableUpdateMutationFields
( \ti updPerms -> ( \ti updPerms ->
fmap BackendUpdate fmap BackendUpdate

View File

@ -10,12 +10,15 @@ module Hasura.Backends.MSSQL.ToQuery
fromInsert, fromInsert,
fromSetIdentityInsert, fromSetIdentityInsert,
fromDelete, fromDelete,
fromUpdate,
fromSelectIntoTempTable, fromSelectIntoTempTable,
dropTempTableQuery,
Printer (..), Printer (..),
) )
where where
import Data.Aeson (ToJSON (..)) import Data.Aeson (ToJSON (..))
import Data.HashMap.Strict qualified as HM
import Data.List (intersperse) import Data.List (intersperse)
import Data.String import Data.String
import Data.Text qualified as T 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 qualified as L
import Data.Text.Lazy.Builder qualified as L import Data.Text.Lazy.Builder qualified as L
import Database.ODBC.SQLServer import Database.ODBC.SQLServer
import Hasura.Backends.MSSQL.Types.Instances () import Hasura.Backends.MSSQL.Types
import Hasura.Backends.MSSQL.Types.Internal
import Hasura.Prelude hiding (GT, LT) import Hasura.Prelude hiding (GT, LT)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -112,6 +114,11 @@ fromExpression =
<+> " (" <+> " ("
<+> fromExpression y <+> fromExpression y
<+> ")" <+> ")"
FunctionExpression function args ->
fromString (T.unpack function)
<+> "("
<+> SepByPrinter ", " (map fromExpression args)
<+> ")"
ListExpression xs -> SepByPrinter ", " $ fromExpression <$> xs ListExpression xs -> SepByPrinter ", " $ fromExpression <$> xs
STOpExpression op e str -> STOpExpression op e str ->
"(" <+> fromExpression e <+> ")." "(" <+> fromExpression e <+> ")."
@ -184,6 +191,9 @@ fromInsertOutput = fromOutput fromInserted
fromDeleteOutput :: DeleteOutput -> Printer fromDeleteOutput :: DeleteOutput -> Printer
fromDeleteOutput = fromOutput fromDeleted fromDeleteOutput = fromOutput fromDeleted
fromUpdateOutput :: UpdateOutput -> Printer
fromUpdateOutput = fromOutput fromInserted
fromValues :: Values -> Printer fromValues :: Values -> Printer
fromValues (Values values) = fromValues (Values values) =
"( " <+> SepByPrinter ", " (map fromExpression values) <+> " )" "( " <+> SepByPrinter ", " (map fromExpression values) <+> " )"
@ -233,6 +243,42 @@ fromDelete Delete {deleteTable, deleteOutput, deleteTempTable, deleteWhere} =
fromWhere 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`. -- | Converts `SelectIntoTempTable`.
-- --
-- > SelectIntoTempTable (TempTableName "deleted") [UnifiedColumn "id" IntegerType, UnifiedColumn "name" TextType] (TableName "table" "schema") -- > SelectIntoTempTable (TempTableName "deleted") [UnifiedColumn "id" IntegerType, UnifiedColumn "name" TextType] (TableName "table" "schema")
@ -281,6 +327,11 @@ fromTempTable :: TempTable -> Printer
fromTempTable (TempTable table columns) = fromTempTable (TempTable table columns) =
fromTempTableName table <+> parens (SepByPrinter ", " (map fromColumnName 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 -> Printer
fromSelect Select {..} = fmap fromWith selectWith ?<+> wrapFor selectFor result fromSelect Select {..} = fmap fromWith selectWith ?<+> wrapFor selectFor result
where where

View File

@ -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

View File

@ -71,6 +71,7 @@ module Hasura.Backends.MSSQL.Types.Internal
snakeCaseTableName, snakeCaseTableName,
stringTypes, stringTypes,
tempTableNameDeleted, tempTableNameDeleted,
tempTableNameUpdated,
) )
where where
@ -217,6 +218,9 @@ newtype TempTableName = TempTableName Text
tempTableNameDeleted :: TempTableName tempTableNameDeleted :: TempTableName
tempTableNameDeleted = TempTableName "deleted" tempTableNameDeleted = TempTableName "deleted"
tempTableNameUpdated :: TempTableName
tempTableNameUpdated = TempTableName "updated"
data TempTable = TempTable data TempTable = TempTable
{ ttName :: !TempTableName, { ttName :: !TempTableName,
ttColumns :: ![ColumnName] ttColumns :: ![ColumnName]
@ -311,6 +315,8 @@ data Expression
| -- | This is for getting actual atomic values out of a JSON | -- | This is for getting actual atomic values out of a JSON
-- string. -- string.
JsonValueExpression Expression JsonPath JsonValueExpression Expression JsonPath
| -- | This is for evaluating SQL functions, text(e1, e2, ..).
FunctionExpression Text [Expression]
| OpExpression Op Expression Expression | OpExpression Op Expression Expression
| ListExpression [Expression] | ListExpression [Expression]
| STOpExpression SpatialOp Expression Expression | STOpExpression SpatialOp Expression Expression

View File

@ -2,11 +2,14 @@
module Hasura.Backends.MSSQL.Types.Update module Hasura.Backends.MSSQL.Types.Update
( BackendUpdate (..), ( BackendUpdate (..),
UpdateOperator (..), UpdateOperator (..),
Update (..),
UpdateSet,
UpdateOutput,
) )
where where
import Hasura.Backends.MSSQL.Types.Instances () import Hasura.Backends.MSSQL.Types.Instances ()
import Hasura.Backends.MSSQL.Types.Internal qualified as MSSQL import Hasura.Backends.MSSQL.Types.Internal
import Hasura.Prelude import Hasura.Prelude
-- | The MSSQL-specific data of an Update expression. -- | The MSSQL-specific data of an Update expression.
@ -16,7 +19,7 @@ import Hasura.Prelude
-- the data at the leaves. -- the data at the leaves.
data BackendUpdate v = BackendUpdate data BackendUpdate v = BackendUpdate
{ -- | The update operations to perform on each column. { -- | 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) deriving (Functor, Foldable, Traversable, Generic, Data)
@ -29,3 +32,17 @@ data UpdateOperator v
= UpdateSet v = UpdateSet v
| UpdateInc v | UpdateInc v
deriving (Functor, Foldable, Traversable, Generic, Data) 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 <filter-expression>
data Update = Update
{ updateTable :: Aliased TableName,
updateSet :: UpdateSet,
updateOutput :: UpdateOutput,
updateTempTable :: TempTable,
updateWhere :: Where
}

View File

@ -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".'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)
;

View File

@ -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);

View File

@ -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
);

View File

@ -0,0 +1,10 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
sql: |
DROP TABLE article;
DROP TABLE author;
cascade: true

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: []

View File

@ -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'''

View File

@ -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: []

View File

@ -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

View File

@ -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: []

View File

@ -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
)
;

View File

@ -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);

View File

@ -512,6 +512,63 @@ class TestGraphqlUpdatePermissions:
def dir(cls): def dir(cls):
return "queries/graphql_mutation/update/permissions" 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']) @pytest.mark.parametrize("transport", ['http', 'websocket'])
@use_mutation_fixtures @use_mutation_fixtures