mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
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:
parent
a7195155ab
commit
0e65932355
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
184
docs/graphql/core/databases/ms-sql-server/mutations/delete.rst
Normal file
184
docs/graphql/core/databases/ms-sql-server/mutations/delete.rst
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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} =
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 <> "]"))
|
||||
|
||||
|
@ -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|
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
@ -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;
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
query: |
|
||||
mutation {
|
||||
delete_author(where: {id: {_eq: 2}}){
|
||||
affected_rows
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
@ -0,0 +1,9 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
drop table article;
|
||||
drop table author;
|
@ -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
|
@ -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
|
@ -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) ;
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user