mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
ae124e2ee8
commit
f00404e0f6
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 ->
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
10
server/src-lib/Hasura/Backends/MSSQL/Types.hs
Normal file
10
server/src-lib/Hasura/Backends/MSSQL/Types.hs
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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".'
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
);
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
)
|
||||||
|
;
|
@ -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);
|
@ -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
|
||||||
|
);
|
@ -0,0 +1,10 @@
|
|||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
|
||||||
|
- type: mssql_run_sql
|
||||||
|
args:
|
||||||
|
source: mssql
|
||||||
|
sql: |
|
||||||
|
DROP TABLE article;
|
||||||
|
DROP TABLE author;
|
||||||
|
cascade: true
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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: []
|
@ -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'''
|
@ -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: []
|
@ -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
|
@ -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: []
|
@ -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
|
||||||
|
)
|
||||||
|
;
|
@ -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);
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user