server/mssql support delete mutations (close hasura/graphql-engine#7626)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2691
Co-authored-by: Abby Sassel <3883855+sassela@users.noreply.github.com>
Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com>
GitOrigin-RevId: 8b66cc30e8e036ee56a5267c1d2f308155951ae9
This commit is contained in:
Gil Mizrahi 2021-11-19 19:05:01 +02:00 committed by hasura-bot
parent a7195155ab
commit 0e65932355
34 changed files with 1346 additions and 60 deletions

View File

@ -3,6 +3,7 @@
## Next release
(Add highlights/major features below)
- server: implement delete mutations for MS SQL Server (closes #7626)
- server: fix JSON path in error when parsing sources in metadata (fix #7769)
- server: log locking DB queries during source catalog migration
- server: fix to allow remote schema response to contain an "extensions" field (#7143)

View File

@ -87,10 +87,10 @@ source-repository-package
location: https://github.com/hasura/ekg-json.git
tag: 098e3a5951c4991c823815706f1f58f608bb6ec3
-- This is v1.2.3.2 with https://github.com/haskell/text/pull/348
-- This is v1.2.3.2 with https://github.com/haskell/text/pull/348
-- cherry-picked. When 1.3 is released we can move from this fork.
source-repository-package
type: git
location: https://github.com/hasura/text.git
tag: 874c3164fadf39a83382359d2b6ce941a3e134da
tag: 874c3164fadf39a83382359d2b6ce941a3e134da

View File

@ -40,7 +40,7 @@ Hasura currently supports queries, subscriptions, relationships and permissions
Next up on our roadmap for Hasura + SQL Server:
- Support for stored procedures & functions (`#7073 <https://github.com/hasura/graphql-engine/issues/7073>`__)
- Mutations: Run inserts, updates, deletes, stored procedures and transactions securely on SQL Server over a GraphQL API (`#7074 <https://github.com/hasura/graphql-engine/issues/7074>`__)
- Mutations: Run inserts, updates, stored procedures and transactions securely on SQL Server over a GraphQL API (`#7074 <https://github.com/hasura/graphql-engine/issues/7074>`__)
- Event triggers: Trigger HTTP webhooks with atomic capture and atleast once guarantee whenever data changes inside the database (`#7075 <https://github.com/hasura/graphql-engine/issues/7075>`__)
- Remote Joins: Join models in SQL Server to models from other API services (GraphQL or REST) (`#7076 <https://github.com/hasura/graphql-engine/issues/7076>`__)
@ -66,6 +66,7 @@ Know more
Getting Started <getting-started/index>
Schema <schema/index>
Queries <queries/index>
Mutations <mutations/index>
Subscriptions <subscriptions/index>
.. TODO: DB-COMPATIBILITY

View File

@ -0,0 +1,184 @@
.. meta::
:description: Delete an object from MS SQL Server using a mutation
:keywords: hasura, docs, ms sql server, mutation, delete
.. _ms_sql_server_delete:
MS SQL Server: Delete mutation
==============================
.. contents:: Table of contents
:backlinks: none
:depth: 1
:local:
Auto-generated delete mutation schema
-------------------------------------
**For example**, the auto-generated schema for the delete mutation field for a table ``article`` looks like the following:
.. code-block:: graphql
delete_article (
where: article_bool_exp!
): article_mutation_response
# response of any mutation on the table "article"
type article_mutation_response {
# number of affected rows by the mutation
affected_rows: Int!
# data of the affected rows by the mutation
returning: [article!]!
}
# single object delete
delete_article_by_pk (
# all primary key columns args
id: Int
): article
As you can see from the schema:
- The ``where`` argument is compulsory to filter rows to be deleted. See :ref:`Filter queries <filter_queries>`
for filtering options. Objects can be deleted based on filters on their own fields or those in their nested objects.
The ``{}`` expression can be used to delete all rows.
- You can return the number of affected rows and the affected objects (with nested objects) in the response.
See the :ref:`delete mutation API reference <delete_syntax>` for the full specifications.
.. note::
If a table is not in the default ``dbo`` MS SQL Server schema, the delete mutation field will be of the format
``delete_<schema_name>_<table_name>``.
Delete an object by its primary key
-----------------------------------
You can delete a single object in a table using the primary key.
The output type is the nullable table object. The mutation returns the deleted
row object or ``null`` if the row does not exist.
**Example:** Delete an article where ``id`` is ``1``:
.. graphiql::
:view_only:
:query:
mutation delete_an_object {
delete_article_by_pk (
id: 1
) {
id
title
user_id
}
}
:response:
{
"data": {
"delete_article_by_pk": {
"id": 1,
"title": "Article 1",
"user_id": 1
}
}
}
**Example:** Delete a non-existent article:
.. graphiql::
:view_only:
:query:
mutation delete_an_object {
delete_article_by_pk (
id: 100
) {
id
title
user_id
}
}
:response:
{
"data": {
"delete_article_by_pk": null
}
}
.. note::
``delete_<table>_by_pk`` will **only** be available if you have select permissions on the table, as it returns the deleted row.
Delete objects based on their fields
------------------------------------
**Example:** Delete all articles rated less than 3:
.. graphiql::
:view_only:
:query:
mutation delete_low_rated_articles {
delete_article(
where: {rating: {_lt: 3}}
) {
affected_rows
}
}
:response:
{
"data": {
"delete_low_rated_articles": {
"affected_rows": 8
}
}
}
Delete objects based on nested objects' fields
----------------------------------------------
**Example:** Delete all articles written by a particular author:
.. graphiql::
:view_only:
:query:
mutation delete_authors_articles {
delete_article(
where: {author: {name: {_eq: "Corny"}}}
) {
affected_rows
}
}
:response:
{
"data": {
"delete_authors_articles": {
"affected_rows": 2
}
}
}
Delete all objects
------------------
You can delete all objects in a table using the ``{}`` expression as the ``where`` argument. ``{}`` basically
evaluates to ``true`` for all objects.
**Example:** Delete all articles:
.. graphiql::
:view_only:
:query:
mutation delete_all_articles {
delete_article (
where: {}
) {
affected_rows
}
}
:response:
{
"data": {
"delete_article": {
"affected_rows": 20
}
}
}

View File

@ -0,0 +1,44 @@
.. meta::
:description: Manage mutations on MS SQL Server with Hasura
:keywords: hasura, docs, ms sql server, mutation
.. _ms_sql_server_mutations:
MS SQL Server: Mutations
========================
.. contents:: Table of contents
:backlinks: none
:depth: 1
:local:
Introduction
------------
GraphQL mutations are used to modify data on the server (i.e. write, update or delete data).
Hasura GraphQL engine auto-generates mutations as part of the GraphQL schema from your MS SQL Server schema model.
Data of all tables in the database tracked by the GraphQL engine can be modified over the GraphQL endpoint.
If you have a tracked table in your database, its insert/update/delete mutation fields are added as nested
fields under the ``mutation_root`` root level type.
Types of mutation requests
--------------------------
The following types of mutation requests are possible:
.. toctree::
:maxdepth: 1
Delete <delete>
.. TODO: DBCOMPATIBILITY
.. toctree::
:maxdepth: 1
Insert <insert>
Upsert <upsert>
Update <update>
multiple-mutations

View File

@ -29,6 +29,7 @@ module Hasura.Backends.MSSQL.FromIr
jsonFieldName,
fromInsert,
fromDelete,
toSelectIntoTempTable,
)
where
@ -47,6 +48,7 @@ import Hasura.RQL.Types.Column qualified as IR
import Hasura.RQL.Types.Common qualified as IR
import Hasura.RQL.Types.Relationship qualified as IR
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax (unName)
--------------------------------------------------------------------------------
-- Types
@ -1113,8 +1115,9 @@ normalizeInsertRows IR.AnnIns {..} insertRows =
--------------------------------------------------------------------------------
-- Delete
-- | Convert IR AST representing delete into MSSQL AST representing a delete statement
fromDelete :: IR.AnnDel 'MSSQL -> FromIr Delete
fromDelete (IR.AnnDel tableName (permFilter, whereClause) _ _) = do
fromDelete (IR.AnnDel tableName (permFilter, whereClause) _ allColumns) = do
tableAlias <- fromTableName tableName
runReaderT
( do
@ -1127,11 +1130,38 @@ fromDelete (IR.AnnDel tableName (permFilter, whereClause) _ _) = do
{ aliasedAlias = entityAliasText tableAlias,
aliasedThing = tableName
},
deleteColumns = map (ColumnName . unName . IR.pgiName) allColumns,
deleteWhere = Where [permissionsFilter, whereExpression]
}
)
tableAlias
-- | Convert IR AST representing delete into MSSQL AST representing a creation of a temporary table
-- with the same schema as the deleted from table.
toSelectIntoTempTable :: TempTableName -> IR.AnnDel 'MSSQL -> SelectIntoTempTable
toSelectIntoTempTable tempTableName (IR.AnnDel {IR.dqp1Table = fromTable, IR.dqp1AllCols = allColumns}) = do
SelectIntoTempTable
{ sittTempTableName = tempTableName,
sittColumns = map columnInfoToUnifiedColumn allColumns,
sittFromTableName = fromTable
}
-- | Extracts the type and column name of a ColumnInfo
columnInfoToUnifiedColumn :: IR.ColumnInfo 'MSSQL -> UnifiedColumn
columnInfoToUnifiedColumn colInfo =
case IR.pgiType colInfo of
IR.ColumnScalar t ->
UnifiedColumn
{ name = unName $ IR.pgiName colInfo,
type' = t
}
-- Enum values are represented as text value so they will always be of type text
IR.ColumnEnumReference {} ->
UnifiedColumn
{ name = unName $ IR.pgiName colInfo,
type' = TextType
}
--------------------------------------------------------------------------------
-- Misc combinators
@ -1187,6 +1217,7 @@ fromAlias (FromQualifiedTable Aliased {aliasedAlias}) = EntityAlias aliasedAlias
fromAlias (FromOpenJson Aliased {aliasedAlias}) = EntityAlias aliasedAlias
fromAlias (FromSelect Aliased {aliasedAlias}) = EntityAlias aliasedAlias
fromAlias (FromIdentifier identifier) = EntityAlias identifier
fromAlias (FromTempTable Aliased {aliasedAlias}) = EntityAlias aliasedAlias
columnNameToFieldName :: ColumnName -> EntityAlias -> FieldName
columnNameToFieldName (ColumnName fieldName) EntityAlias {entityAliasText = fieldNameEntity} =

View File

@ -226,7 +226,7 @@ msDBMutationPlan ::
msDBMutationPlan userInfo stringifyNum sourceName sourceConfig mrf = do
go <$> case mrf of
MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert
MDBDelete _annDelete -> throw400 NotSupported "delete mutations are not supported in MSSQL"
MDBDelete annDelete -> executeDelete userInfo stringifyNum sourceConfig annDelete
MDBFunction {} -> throw400 NotSupported "function mutations are not supported in MSSQL"
where
go v = DBStepInfo @'MSSQL sourceName sourceConfig Nothing v
@ -313,7 +313,7 @@ executeInsert userInfo stringifyNum sourceConfig annInsert = do
Tx.unitQueryE fromMSSQLTxError $
toQueryFlat $
TQ.fromSetIdentityInsert $
SetIdenityInsert (_aiTableName $ _aiData insert) SetON
SetIdentityInsert (_aiTableName $ _aiData insert) SetON
-- Generate the INSERT query
let insertQuery = toQueryFlat $ TQ.fromInsert $ TSQL.fromInsert insert
@ -383,6 +383,62 @@ executeInsert userInfo stringifyNum sourceConfig annInsert = do
ExpressionProjection $ Aliased (SelectExpression (generateCheckConstraintSelect checkBoolExp)) "check_constraint_select"
in emptySelect {selectProjections = [mutationOutputProjection, checkConstraintProjection]}
-- Delete
-- | Executes a Delete IR AST and return results as JSON.
executeDelete ::
MonadError QErr m =>
UserInfo ->
Bool ->
SourceConfig 'MSSQL ->
AnnDelG 'MSSQL (Const Void) (UnpreparedValue 'MSSQL) ->
m (ExceptT QErr IO EncJSON)
executeDelete userInfo stringifyNum sourceConfig deleteOperation = do
preparedDelete <- traverse (prepareValueQuery $ _uiSession userInfo) deleteOperation
let pool = _mscConnectionPool sourceConfig
pure $ withMSSQLPool pool $ Tx.runTxE fromMSSQLTxError (buildDeleteTx preparedDelete stringifyNum)
-- | Converts a Delete IR AST to a transaction of three delete sql statements.
--
-- A GraphQL delete mutation does two things:
--
-- 1. Deletes rows in a table according to some predicate
-- 2. (Potentially) returns the deleted 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 deleted rows
-- from the table we are deleting
-- 2. DELETE FROM with OUTPUT - deletes the rows from the table and inserts the
-- deleted rows to the temporary table from (1)
-- 3. SELECT - constructs the @returning@ query from the temporary table, including
-- relationships with other tables.
buildDeleteTx ::
AnnDel 'MSSQL ->
Bool ->
Tx.TxET QErr IO EncJSON
buildDeleteTx deleteOperation stringifyNum = do
let tempTableName = TSQL.TempTableName "deleted"
withAlias = "with_alias"
createTempTableQuery = toQueryFlat $ TQ.fromSelectIntoTempTable $ TSQL.toSelectIntoTempTable tempTableName deleteOperation
-- Create a temp table
Tx.unitQueryE fromMSSQLTxError createTempTableQuery
let deleteQuery = TQ.fromDelete tempTableName <$> TSQL.fromDelete deleteOperation
deleteQueryValidated <- toQueryFlat <$> V.runValidate (runFromIr deleteQuery) `onLeft` (throw500 . tshow)
-- Execute DELETE statement
Tx.unitQueryE fromMSSQLTxError deleteQueryValidated
mutationOutputSelect <- mkMutationOutputSelect stringifyNum withAlias $ dqp1Output deleteOperation
let withSelect =
emptySelect
{ selectProjections = [StarProjection],
selectFrom = Just $ FromTempTable $ Aliased tempTableName "deleted_alias"
}
finalMutationOutputSelect = mutationOutputSelect {selectWith = Just $ With $ pure $ Aliased withSelect withAlias}
mutationOutputSelectQuery = toQueryFlat $ TQ.fromSelect finalMutationOutputSelect
-- Execute SELECT query and fetch mutation response
encJFromText <$> Tx.singleRowQueryE fromMSSQLTxError mutationOutputSelectQuery
-- | Generate a SQL SELECT statement which outputs the mutation response
--
-- For multi row inserts:
@ -400,28 +456,38 @@ mkMutationOutputSelect stringifyNum withAlias = \case
projections <- forM multiRowFields $ \(fieldName, field') -> do
let mkProjection = ExpressionProjection . flip Aliased (getFieldNameTxt fieldName) . SelectExpression
mkProjection <$> case field' of
IR.MCount -> pure countSelect
IR.MCount -> pure $ countSelect withAlias
IR.MExp t -> pure $ textSelect t
IR.MRet returningFields -> mkSelect JASMultipleRows returningFields
IR.MRet returningFields -> mkSelect stringifyNum withAlias JASMultipleRows returningFields
let forJson = JsonFor $ ForJson JsonSingleton NoRoot
pure emptySelect {selectFor = forJson, selectProjections = projections}
IR.MOutSinglerowObject singleRowField -> mkSelect JASSingleObject singleRowField
where
mkSelect jsonAggSelect annFields = do
let annSelect = IR.AnnSelectG annFields (IR.FromIdentifier withAlias) IR.noTablePermissions IR.noSelectArgs stringifyNum
V.runValidate (runFromIr $ mkSQLSelect jsonAggSelect annSelect) `onLeft` (throw500 . tshow)
IR.MOutSinglerowObject singleRowField -> mkSelect stringifyNum withAlias JASSingleObject singleRowField
-- SELECT COUNT(*) FROM [with_alias]
countSelect =
let countProjection = AggregateProjection $ Aliased (CountAggregate StarCountable) "count"
in emptySelect
{ selectProjections = [countProjection],
selectFrom = Just $ TSQL.FromIdentifier withAlias
}
mkSelect ::
MonadError QErr m =>
Bool ->
Text ->
JsonAggSelect ->
Fields (AnnFieldG 'MSSQL (Const Void) Expression) ->
m Select
mkSelect stringifyNum withAlias jsonAggSelect annFields = do
let annSelect = IR.AnnSelectG annFields (IR.FromIdentifier withAlias) IR.noTablePermissions IR.noSelectArgs stringifyNum
V.runValidate (runFromIr $ mkSQLSelect jsonAggSelect annSelect) `onLeft` (throw500 . tshow)
textSelect t =
let textProjection = ExpressionProjection $ Aliased (ValueExpression (ODBC.TextValue t)) "exp"
in emptySelect {selectProjections = [textProjection]}
-- SELECT COUNT(*) AS "count" FROM [with_alias]
countSelect :: Text -> Select
countSelect withAlias =
let countProjection = AggregateProjection $ Aliased (CountAggregate StarCountable) "count"
in emptySelect
{ selectProjections = [countProjection],
selectFrom = Just $ TSQL.FromIdentifier withAlias
}
-- SELECT '<text-value>' AS "exp"
textSelect :: Text -> Select
textSelect t =
let textProjection = ExpressionProjection $ Aliased (ValueExpression (ODBC.TextValue t)) "exp"
in emptySelect {selectProjections = [textProjection]}
-- subscription

View File

@ -35,7 +35,7 @@ instance BackendSchema 'MSSQL where
buildTableRelayQueryFields = msBuildTableRelayQueryFields
buildTableInsertMutationFields = msBuildTableInsertMutationFields
buildTableUpdateMutationFields = msBuildTableUpdateMutationFields
buildTableDeleteMutationFields = msBuildTableDeleteMutationFields
buildTableDeleteMutationFields = GSB.buildTableDeleteMutationFields
buildFunctionQueryFields = msBuildFunctionQueryFields
buildFunctionRelayQueryFields = msBuildFunctionRelayQueryFields
buildFunctionMutationFields = msBuildFunctionMutationFields

View File

@ -10,6 +10,7 @@ module Hasura.Backends.MSSQL.ToQuery
fromInsert,
fromSetIdentityInsert,
fromDelete,
fromSelectIntoTempTable,
Printer (..),
)
where
@ -189,8 +190,8 @@ fromSetValue = \case
SetON -> "ON"
SetOFF -> "OFF"
fromSetIdentityInsert :: SetIdenityInsert -> Printer
fromSetIdentityInsert SetIdenityInsert {..} =
fromSetIdentityInsert :: SetIdentityInsert -> Printer
fromSetIdentityInsert SetIdentityInsert {..} =
SepByPrinter
" "
[ "SET IDENTITY_INSERT",
@ -198,15 +199,71 @@ fromSetIdentityInsert SetIdenityInsert {..} =
fromSetValue setValue
]
fromDelete :: Delete -> Printer
fromDelete Delete {deleteTable, deleteWhere} =
-- | Generate a delete statement
--
-- > Delete
-- > (Aliased (TableName "table" "schema") "alias")
-- > [ColumnName "id", ColumnName "name"]
-- > (Where [OpExpression EQ' (ValueExpression (IntValue 1)) (ValueExpression (IntValue 1))])
--
-- Becomes:
--
-- > DELETE [alias] OUTPUT DELETED.[id], DELETED.[name] INTO #deleted([id], [name]) FROM [schema].[table] AS [alias] WHERE ((1) = (1))
fromDelete :: TempTableName -> Delete -> Printer
fromDelete tempTableName Delete {deleteTable, deleteColumns, deleteWhere} =
SepByPrinter
NewlinePrinter
[ "DELETE " <+> fromNameText (aliasedAlias deleteTable),
"OUTPUT " <+> SepByPrinter ", " (map ((<+>) "DELETED." . fromColumnName) deleteColumns),
"INTO " <+> fromTempTableName tempTableName <+> parens (SepByPrinter ", " (map fromColumnName deleteColumns)),
"FROM " <+> fromAliased (fmap fromTableName deleteTable),
fromWhere deleteWhere
]
-- | Converts `SelectIntoTempTable`.
--
-- > SelectIntoTempTable (TempTableName "deleted") [UnifiedColumn "id" IntegerType, UnifiedColumn "name" TextType] (TableName "table" "schema")
--
-- Becomes:
--
-- > SELECT [id], [name] INTO #deleted([id], [name]) FROM [schema].[table] WHERE (1<>1) UNION ALL SELECT [id], [name] FROM [schema].[table];
--
-- We add the `UNION ALL` part to avoid copying identity constraints, and we cast columns with types such as `timestamp`
-- which are non-insertable to a different type.
fromSelectIntoTempTable :: SelectIntoTempTable -> Printer
fromSelectIntoTempTable SelectIntoTempTable {sittTempTableName, sittColumns, sittFromTableName} =
SepByPrinter
NewlinePrinter
[ "SELECT "
<+> columns,
"INTO " <+> fromTempTableName sittTempTableName,
"FROM " <+> fromTableName sittFromTableName,
"WHERE " <+> falsePrinter,
"UNION ALL SELECT " <+> columns,
"FROM " <+> fromTableName sittFromTableName,
"WHERE " <+> falsePrinter
]
where
-- column names separated by commas
columns =
SepByPrinter
("," <+> NewlinePrinter)
(map columnNameFromUnifiedColumn sittColumns)
-- column name with potential modifications of types
columnNameFromUnifiedColumn (UnifiedColumn columnName columnType) =
case columnType of
-- The "timestamp" is type synonym for "rowversion" and it is just an incrementing number and does not preserve a date or a time.
-- So, the "timestamp" type is neither insertable nor explicitly updatable. Its values are unique binary numbers within a database.
-- We're using "binary" type instead so that we can copy a timestamp row value safely into the temporary table.
-- See https://docs.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql for more details.
TimestampType -> "CAST(" <+> fromNameText columnName <+> " AS binary(8)) AS " <+> fromNameText columnName
_ -> fromNameText columnName
-- | @TempTableName "deleted"@ becomes @\#deleted@
fromTempTableName :: TempTableName -> Printer
fromTempTableName (TempTableName v) = QueryPrinter (fromString . T.unpack $ "#" <> v)
fromSelect :: Select -> Printer
fromSelect Select {..} = fmap fromWith selectWith ?<+> wrapFor selectFor result
where
@ -438,6 +495,7 @@ fromFrom =
FromOpenJson openJson -> fromAliased (fmap fromOpenJson openJson)
FromSelect select -> fromAliased (fmap (parens . fromSelect) select)
FromIdentifier identifier -> fromNameText identifier
FromTempTable aliasedTempTable -> fromAliased (fmap fromTempTableName aliasedTempTable)
fromOpenJson :: OpenJson -> Printer
fromOpenJson OpenJson {openJsonExpression, openJsonWith} =
@ -482,6 +540,9 @@ fromAliased Aliased {..} =
aliasedThing
<+> ((" AS " <+>) . fromNameText) aliasedAlias
fromColumnName :: ColumnName -> Printer
fromColumnName (ColumnName colname) = fromNameText colname
fromNameText :: Text -> Printer
fromNameText t = QueryPrinter (rawUnescapedText ("[" <> t <> "]"))

View File

@ -41,7 +41,8 @@ $( fmap concat $ for
''UnifiedArrayRelationship,
''UnifiedUsing,
''UnifiedOn,
''UnifiedColumn
''UnifiedColumn,
''TempTableName
]
\name ->
[d|
@ -57,6 +58,8 @@ $( fmap concat $ for
deriving instance Data $(conT name)
instance NFData $(conT name)
instance FromJSON $(conT name)
deriving instance Ord $(conT name)
@ -92,7 +95,8 @@ $( fmap concat $ for
''OpenJson,
''JsonFieldSpec,
''Join,
''JoinSource
''JoinSource,
''SelectIntoTempTable
]
\name ->
[d|

View File

@ -38,8 +38,10 @@ module Hasura.Backends.MSSQL.Types.Internal
ScalarType (..),
SchemaName (..),
Select (..),
SetIdenityInsert (..),
SetIdentityInsert (..),
TempTableName (..),
SetValue (..),
SelectIntoTempTable (..),
SpatialOp (..),
TableName (..),
Top (..),
@ -172,16 +174,28 @@ data SetValue
= SetON
| SetOFF
data SetIdenityInsert = SetIdenityInsert
data SetIdentityInsert = SetIdentityInsert
{ setTable :: !TableName,
setValue :: !SetValue
}
data Delete = Delete
{ deleteTable :: !(Aliased TableName),
deleteColumns :: [ColumnName],
deleteWhere :: !Where
}
-- | SELECT INTO temporary table statement without values.
-- Used to create a temporary table with the same schema as an existing table.
data SelectIntoTempTable = SelectIntoTempTable
{ sittTempTableName :: TempTableName,
sittColumns :: [UnifiedColumn],
sittFromTableName :: TableName
}
-- | A temporary table name is prepended by a hash-sign
newtype TempTableName = TempTableName Text
data Reselect = Reselect
{ reselectProjections :: ![Projection],
reselectFor :: !For,
@ -302,6 +316,7 @@ data From
| FromOpenJson (Aliased OpenJson)
| FromSelect (Aliased Select)
| FromIdentifier Text
| FromTempTable (Aliased TempTableName)
data OpenJson = OpenJson
{ openJsonExpression :: Expression,

View File

@ -1,3 +1,5 @@
{-# LANGUAGE UndecidableInstances #-}
module Hasura.RQL.IR.Delete
( AnnDel,
AnnDelG (..),
@ -21,3 +23,5 @@ data AnnDelG (b :: BackendType) (r :: BackendType -> Type) v = AnnDel
deriving (Functor, Foldable, Traversable)
type AnnDel b = AnnDelG b (Const Void) (SQLExpression b)
deriving instance (Show (MutationOutputG b r a), Backend b, Show (BooleanOperators b a), Show a) => Show (AnnDelG b r a)

View File

@ -1,3 +1,5 @@
{-# LANGUAGE UndecidableInstances #-}
module Hasura.RQL.IR.Returning
( MutFld,
MutFldG (..),
@ -25,6 +27,8 @@ data MutFldG (b :: BackendType) (r :: BackendType -> Type) v
| MRet !(AnnFieldsG b r v)
deriving (Functor, Foldable, Traversable)
deriving instance (Show (r b), Backend b, Show (BooleanOperators b a), Show a) => Show (MutFldG b r a)
type MutFld b = MutFldG b (Const Void) (SQLExpression b)
type MutFldsG b r v = Fields (MutFldG b r v)
@ -34,6 +38,8 @@ data MutationOutputG (b :: BackendType) (r :: BackendType -> Type) v
| MOutSinglerowObject !(AnnFieldsG b r v)
deriving (Functor, Foldable, Traversable)
deriving instance (Show (MutFldsG b r a), Show (r b), Backend b, Show (BooleanOperators b a), Show a) => Show (MutationOutputG b r a)
type MutationOutput b = MutationOutputG b (Const Void) (SQLExpression b)
type MutFlds b = MutFldsG b (Const Void) (SQLExpression b)

View File

@ -786,6 +786,15 @@ class HGECtx:
yml = yaml.YAML()
return self.v1metadataq(yml.load(f))
def v1graphqlq(self, q, headers = {}):
return self.execute_query(q, "/v1/graphql", headers)
def v1graphql_f(self, fn):
with open(fn) as f:
# NOTE: preserve ordering with ruamel
yml = yaml.YAML()
return self.v1graphqlq(yml.load(f))
def teardown(self):
self.http.close()
self.engine.dispose()

View File

@ -0,0 +1,16 @@
description: Delete mutation on article
url: /v1/graphql
status: 200
response:
data:
delete_article:
affected_rows: 1
query:
query: |
mutation delete_article {
delete_article (
where: {id: {_eq: 2}}
) {
affected_rows
}
}

View File

@ -0,0 +1,27 @@
description: Delete mutation on author with returning articles
url: /v1/graphql
status: 200
response:
data:
delete_author:
affected_rows: 1
returning:
- id: 3
name: Author 3
articles: []
query:
query: |
mutation DeleteAuthor3 {
delete_author(where: {id: {_eq: 3}}){
affected_rows
returning{
id
name
articles{
id
title
content
}
}
}
}

View File

@ -0,0 +1,159 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
sql: |
CREATE TABLE test_types
(
c1_smallint smallint,
c2_integer integer,
c3_bigint bigint,
c4_decimal decimal(5, 2),
c5_numeric numeric (4, 3),
c6_real real,
c7_double_precision double precision,
c11_varchar_3 varchar(3),
c12_char_4 char(4),
c13_text text,
c16_date date,
c17_time time,
c44_xml xml,
c45_money money,
c47_smallmoney smallmoney,
c48_bit bit,
c49_tinyint tinyint,
c50_float float,
c51_real real,
c52_datetime datetime,
c53_datetime2 datetime2,
c54_datetimeoffset datetimeoffset,
c55_smalldatetime smalldatetime,
c56_binary binary(4),
c57_varbinary varbinary(4),
c58_hierarchyid hierarchyid,
c59_uniqueidentifier uniqueidentifier
);
CREATE TABLE author
(
id int identity NOT NULL PRIMARY KEY,
name nvarchar(450) UNIQUE,
createdAt datetime
);
CREATE TABLE article
(
id int identity NOT NULL PRIMARY KEY,
title TEXT,
content TEXT,
is_published BIT,
published_on timestamp,
author_id int,
co_author_id int,
FOREIGN KEY (author_id) REFERENCES author(id),
FOREIGN KEY (co_author_id) REFERENCES author(id),
);
INSERT INTO test_types
(
c1_smallint,
c2_integer,
c3_bigint,
c4_decimal,
c5_numeric,
c6_real,
c7_double_precision,
c11_varchar_3,
c12_char_4,
c13_text,
c16_date,
c17_time,
c44_xml,
c45_money,
c47_smallmoney,
c48_bit,
c49_tinyint,
c50_float,
c51_real,
c52_datetime,
c53_datetime2,
c54_datetimeoffset,
c55_smalldatetime,
c56_binary,
c57_varbinary,
c58_hierarchyid,
c59_uniqueidentifier
)
VALUES
( 3277 -- c1_smallint
, 2147483647 -- c2_integer
, 9223372036854775807 -- c3_bigint
, 123.45 -- c4_decimal
, 1.234 -- c5_numeric
, 0.00390625 -- c6_real
, 16.0001220703125 -- c7_double_precision
, 'abc' -- c11_varchar_3
, 'baaz' -- c12_char_4
, 'foo bar baz' -- c13_text
, '2014-09-14' -- c16_date
, '11:09:23' -- c17_time
, '<foo>bar</foo>' -- c44_xml
, 123.45 -- c45_money
, -123.45 -- c47_smallmoney
, 0 -- c48_bit
, 254 -- c49_tinyint
, -305.77 -- c50_float
, -36.82 -- c51_real
, '04-15-96 4am' -- c52_datetime
, '04-15-9999 23:59:59.9999999' -- c53_datetime2
, '2007-05-08 12:35:29.1234567 +12:15' -- c54_datetimeoffset
, '1955-12-13 12:43:10' -- c55_smalldatetime
, 0x0001e240 -- c56_binary
, 0x0001e240 -- c57_varbinary
, '/0.1/0.2/' -- c58_hierarchyid
, '0E984725-C51C-4BF4-9960-E1C80E27ABA0' -- c59_uniqueidentifier
);
INSERT INTO author
(name, createdAt)
VALUES
('Author 1', '2017-09-21T09:39:44Z'),
('Author 2', '2017-09-21T09:50:44Z'),
('Author 3', '2017-09-21T09:55:44Z');
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',
1,
1
),
(
'Article 4',
'Sample article content 4',
2,
1
),
(
'Article 5',
'Sample article content 5',
2,
0
);

View File

@ -0,0 +1,11 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
cascade: true
sql: |
drop table test_types;
drop table article;
drop table author;

View File

@ -0,0 +1,40 @@
type: bulk
args:
# track tables
- type: mssql_track_table
args:
source: mssql
table:
name: test_types
- type: mssql_track_table
args:
source: mssql
table:
name: author
- type: mssql_track_table
args:
source: mssql
table:
name: article
# create relationships
- type: mssql_create_object_relationship
args:
source: mssql
table: article
name: author
using:
foreign_key_constraint_on: author_id
- 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,24 @@
type: bulk
args:
# untrack tables
- type: mssql_untrack_table
args:
source: mssql
table:
name: test_types
cascade: true
- type: mssql_untrack_table
args:
source: mssql
table:
name: article
cascade: true
- type: mssql_untrack_table
args:
source: mssql
table:
name: author
cascade: true

View File

@ -0,0 +1,75 @@
description: Delete mutation on test_types
url: /v1/graphql
status: 200
response:
data:
delete_test_types:
affected_rows: 1
returning:
- c1_smallint: 3277
c2_integer: 2147483647
c3_bigint: 9223372036854775807
c4_decimal: 123.45
c5_numeric: 1.234
c6_real: 0.00390625
c7_double_precision: 16.0001220703125
c11_varchar_3: 'abc'
c12_char_4: 'baaz'
c13_text: 'foo bar baz'
c16_date: '2014-09-14'
c17_time: '11:09:23'
c44_xml: '<foo>bar</foo>'
c45_money: 123.45
c47_smallmoney: -123.45
c48_bit: 0
c49_tinyint: 254
c50_float: -305.77
c51_real: -36.82
c52_datetime: '1996-04-15T04:00:00'
c53_datetime2: '9999-04-15T23:59:59.9999999'
c54_datetimeoffset: '2007-05-08T12:35:29.1234567+12:15'
c55_smalldatetime: '1955-12-13T12:43:00'
c56_binary: AAHiQA==
c57_varbinary: AAHiQA==
c58_hierarchyid: '/0.1/0.2/'
c59_uniqueidentifier: '0E984725-C51C-4BF4-9960-E1C80E27ABA0'
query:
query: |
mutation delete_test_types {
delete_test_types (
where: {c1_smallint: {_eq: 3277}}
) {
affected_rows
returning {
c1_smallint
c2_integer
c3_bigint
c4_decimal
c5_numeric
c6_real
c7_double_precision
c11_varchar_3
c12_char_4
c13_text
c16_date
c17_time
c44_xml
c45_money
c47_smallmoney
c48_bit
c49_tinyint
c50_float
c51_real
c52_datetime
c53_datetime2
c54_datetimeoffset
c55_smalldatetime
c56_binary
c57_varbinary
c58_hierarchyid
c59_uniqueidentifier
}
}
}

View File

@ -0,0 +1,6 @@
query: |
mutation {
delete_author(where: {id: {_eq: 2}}){
affected_rows
}
}

View File

@ -0,0 +1,58 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
sql: |
create table author(
id INT IDENTITY NOT NULL PRIMARY KEY,
name NVARCHAR(450) UNIQUE
);
CREATE TABLE article (
id INT IDENTITY NOT NULL PRIMARY KEY,
title TEXT,
content TEXT,
is_published BIT,
author_id INTEGER NOT NULL REFERENCES author(id),
published_on TIMESTAMP
);
INSERT INTO author(name)
VALUES
('Author 1'),
('Author 2'),
('Author 3');
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',
1,
1
),
(
'Article 4',
'Sample article content 4',
2,
1
),
(
'Article 5',
'Sample article content 5',
2,
0
);

View File

@ -0,0 +1,9 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
sql: |
drop table article;
drop table author;

View File

@ -0,0 +1,36 @@
type: bulk
args:
# Author table
- type: mssql_track_table
args:
source: mssql
table:
name: author
# Article table
- type: mssql_track_table
args:
source: mssql
table:
name: article
# Object relationship
- type: mssql_create_object_relationship
args:
source: mssql
table: article
name: author
using:
foreign_key_constraint_on: author_id
# Array relationship
- 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,24 @@
type: bulk
args:
# Drop relationship first
- type: mssql_drop_relationship
args:
source: mssql
relationship: articles
table:
name: author
- type: mssql_untrack_table
args:
source: mssql
table:
name: article
cascade: true
- type: mssql_untrack_table
args:
source: mssql
table:
name: author
cascade: true

View File

@ -0,0 +1,78 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
sql: |
create table author(
id INT IDENTITY NOT NULL PRIMARY KEY,
name NVARCHAR(450) UNIQUE,
payments_done BIT NOT NULL DEFAULT 0,
user_id INT
);
CREATE TABLE article (
id INT IDENTITY NOT NULL PRIMARY KEY,
title TEXT,
content TEXT,
author_id INTEGER NOT NULL REFERENCES author(id),
is_published BIT,
published_on TIMESTAMP
);
CREATE TABLE resident (
id INT IDENTITY NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
);
create table "user" (
id INT IDENTITY NOT NULL PRIMARY KEY,
name NVARCHAR(450) UNIQUE,
is_admin BIT DEFAULT 0
);
create table account (
id INT IDENTITY NOT NULL PRIMARY KEY,
account_no INTEGER NOT NULL
);
insert into resident (name, age)
values
('Griffin', 25),
('Clarke', 26);
insert into author (name, payments_done)
values
('Author 1', 0),
('Author 2', 0),
('Author 3', 1),
('Author 4', 1),
('Author 5', 1);
insert into article (content, title, author_id)
values
( 'Sample article content 1',
'Article 1',
1
),
( 'Sample article content 2',
'Article 2',
1
),
( 'Sample article content 3',
'Article 3',
1
),
( 'Sample article content 4',
'Article 4',
2
),
( 'Sample article content 5',
'Article 5',
2
);
insert into "user" (name, is_admin)
values ('user_1', 0), ('user_2', 1)
;
insert into account (account_no) values (1), (2) ;

View File

@ -0,0 +1,13 @@
type: bulk
args:
- type: mssql_run_sql
args:
source: mssql
sql: |
drop table article;
drop table author;
drop table resident;
drop table "user";
drop table account;
cascade: true

View File

@ -0,0 +1,172 @@
type: bulk
args:
# Author table
- type: mssql_track_table
args:
source: mssql
table:
name: author
# Article table
- type: mssql_track_table
args:
source: mssql
table:
name: article
# Object relationship
- type: mssql_create_object_relationship
args:
source: mssql
table: article
name: author
using:
foreign_key_constraint_on: author_id
# Array relationship
- type: mssql_create_array_relationship
args:
source: mssql
table: author
name: articles
using:
foreign_key_constraint_on:
table: article
column: author_id
# Prevent deletion if payments to the author is not yet done
- type: mssql_create_delete_permission
args:
source: mssql
table: author
role: user
permission:
filter:
$and:
- id: X-HASURA-USER-ID
- payments_done:
_eq: 1
# Author select permission for user
- type: mssql_create_select_permission
args:
source: mssql
table: author
role: user
permission:
columns: [id, name, payments_done]
filter:
id: X-HASURA-USER-ID
# A user can delete only his articles
- type: mssql_create_select_permission
args:
source: mssql
table: article
role: user
permission:
columns:
- id
- title
- content
- author_id
filter:
$and:
- author_id: X-HASURA-USER-ID
# A user can delete only his articles
- type: mssql_create_delete_permission
args:
source: mssql
table: article
role: user
permission:
filter:
$and:
- author_id: X-HASURA-USER-ID
# Create resident table
- type: mssql_track_table
args:
source: mssql
table:
name: resident
- type: mssql_create_delete_permission
args:
source: mssql
table: resident
role: resident
permission:
filter:
id: X-Hasura-Resident-Id
- type: mssql_create_delete_permission
args:
source: mssql
table: resident
role: agent
permission:
filter:
id:
$in: X-Hasura-Allowed-Resident-Ids
- type: mssql_create_select_permission
args:
source: mssql
table: resident
role: agent
permission:
columns:
- id
- name
- age
filter:
id:
$in: X-Hasura-Allowed-Resident-Ids
# Tables to test '_exist' field
- type: mssql_track_table
args:
source: mssql
table:
name: user
- type: mssql_track_table
args:
source: mssql
table:
name: account
- type: mssql_create_delete_permission
args:
source: mssql
table: account
role: user
permission:
filter:
_exists:
_table: user
_where:
id: X-Hasura-User-Id
is_admin:
_eq: 1
- type: mssql_create_select_permission
args:
source: mssql
table: account
role: user
permission:
columns:
- id
- account_no
filter:
_exists:
_table: user
_where:
id: X-Hasura-User-Id
is_admin:
_eq: 1

View File

@ -0,0 +1,16 @@
type: bulk
args:
- type: mssql_drop_relationship
args:
source: mssql
relationship: author
table:
name: article
- type: mssql_drop_relationship
args:
source: mssql
relationship: articles
table:
name: author

View File

@ -30,31 +30,3 @@
}
}
}
- description: Delete an author by pk
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: user
X-Hasura-User-Id: '5'
response:
data:
delete_author_by_pk:
id: 5
name: Author 5
articles: []
query:
query: |
mutation {
delete_author_by_pk(
id: 5
){
id
name
articles {
id
title
content
}
}
}

View File

@ -0,0 +1,27 @@
- description: Delete an author by pk
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: user
X-Hasura-User-Id: '5'
response:
data:
delete_author_by_pk:
id: 5
name: Author 5
articles: []
query:
query: |
mutation {
delete_author_by_pk(
id: 5
){
id
name
articles {
id
title
content
}
}
}

View File

@ -539,6 +539,37 @@ class TestGraphqlDeleteBasic:
def dir(cls):
return "queries/graphql_mutation/delete/basic"
@pytest.mark.parametrize("backend", ['mssql'])
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@usefixtures('per_method_tests_db_state')
class TestGraphqlDeleteBasicMSSQL:
def test_article_delete(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/article.yaml", transport)
def test_article_delete_returning(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/article_returning.yaml", transport)
def test_article_delete_returning_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/article_returning_author.yaml", transport)
def test_author_returning_empty_articles(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/author_returning_empty_articles_mssql.yaml", transport)
def test_article_by_pk(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/article_by_pk.yaml", transport)
def test_article_by_pk_null(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/article_by_pk_null.yaml", transport)
def test_test_types_delete(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/test_types_mssql.yaml", transport)
@classmethod
def dir(cls):
return "queries/graphql_mutation/delete/basic"
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@use_mutation_fixtures
class TestGraphqlDeleteConstraints:
@ -550,6 +581,24 @@ class TestGraphqlDeleteConstraints:
def dir(cls):
return "queries/graphql_mutation/delete/constraints"
@pytest.mark.parametrize("backend", ['mssql'])
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@usefixtures('per_method_tests_db_state')
class TestGraphqlDeleteConstraintsMSSQL:
# The response from this query is non-determinstic. It looks something like:
# `conflicted with the REFERENCE constraint "FK__article__author___1B29035F"`
# where 1B29035F changes with each call.
# This makes it hard to write an equality-based test for it, so we just check the error code.
def test_author_delete_foreign_key_violation(self, hge_ctx, transport):
st_code, resp = hge_ctx.v1graphql_f(self.dir() + '/author_foreign_key_violation_mssql.yaml')
assert st_code == 200, resp
assert len(resp['errors']) == 1, resp
@classmethod
def dir(cls):
return "queries/graphql_mutation/delete/constraints"
@use_mutation_fixtures
class TestGraphqlDeletePermissions:
@ -572,15 +621,57 @@ class TestGraphqlDeletePermissions:
def test_user_delete_author(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_author.yaml")
def test_user_delete_author_by_pk(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_author_by_pk.yaml")
def test_user_delete_account_success(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_account_success.yaml")
def test_user_delete_account_no_rows(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_account_no_rows.yaml")
@classmethod
def dir(cls):
return "queries/graphql_mutation/delete/permissions"
@pytest.mark.parametrize("backend", ['mssql'])
@usefixtures('per_method_tests_db_state')
class TestGraphqlDeletePermissionsMSSQL:
def test_author_can_delete_his_articles(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/author_can_delete_his_articles.yaml")
def test_author_cannot_delete_other_users_articles(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/author_cannot_delete_other_users_articles.yaml")
def test_resident_delete_without_select_perm_fail(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/resident_delete_without_select_perm_fail.yaml")
# array types for session values not supported
# def test_agent_delete_perm_arr_sess_var(self, hge_ctx):
# check_query_f(hge_ctx, self.dir() + "/agent_delete_perm_arr_sess_var.yaml")
# def test_agent_delete_perm_arr_sess_var_fail(self, hge_ctx):
# check_query_f(hge_ctx, self.dir() + "/agent_delete_perm_arr_sess_var_fail.yaml")
def test_user_delete_author(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_author.yaml")
def test_user_delete_author_by_pk(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_author_by_pk.yaml")
def test_user_delete_account_success(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_account_success.yaml")
def test_user_delete_account_no_rows(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/user_delete_account_no_rows.yaml")
@classmethod
def dir(cls):
return "queries/graphql_mutation/delete/permissions"
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@use_mutation_fixtures
class TestGraphqlMutationCustomSchema:

View File

@ -306,6 +306,12 @@ class TestGraphQLQueryBasicCommon:
def test_select_query_multiple_columns_obj_fkey(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/select_multiple_columns_obj_fkey.yaml", transport)
def test_select_query_author_pk(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_query_author_by_pkey.yaml', transport)
def test_select_query_author_pk_null(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_query_author_by_pkey_null.yaml', transport)
@classmethod
def dir(cls):
return 'queries/graphql_query/basic'