Skip to content

Commit abb57e5

Browse files
Naveenaiduimperfect-fourth
authored andcommitted
server/MSSQL: Event Delivery System (Incremental PR - 3)
</details> PR-URL: hasura/graphql-engine-mono#3392 Co-authored-by: Divi <[email protected]> GitOrigin-RevId: 9df6b0aa7d91f22571b72d3e467da23b916c9140
1 parent e43a5e4 commit abb57e5

File tree

110 files changed

+3455
-292
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+3455
-292
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Bug fixes and improvements
66

7+
- server: add support for MSSQL event triggers (#7228)
78
- server: update pg_dump to be compatible with postgres 14 (#7676)
89
- server: fix parsing remote relationship json definition from 1.x server catalog on migration (fix #7906)
910
- server: Don't drop nested typed null fields in actions (fix #8237)

docs/docs/graphql/core/api-reference/metadata-api/event-triggers.mdx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,154 @@ X-Hasura-Role: admin
169169
| name | true | [TriggerName](/graphql/core/api-reference/syntax-defs.mdx#triggername) | Name of the event trigger |
170170
| payload | true | JSON | Some JSON payload to send to trigger |
171171
| source | false | [SourceName](/graphql/core/api-reference/syntax-defs.mdx#sourcename) | Name of the source database of the trigger (default: `default`) |
172+
173+
174+
--------------------
175+
176+
177+
178+
## mssql_create_event_trigger {#metadata-mssql-create-event-trigger}
179+
180+
`mssql_create_event_trigger` is used to create a new event trigger or
181+
replace an existing event trigger.
182+
183+
```http
184+
POST /v1/metadata HTTP/1.1
185+
Content-Type: application/json
186+
X-Hasura-Role: admin
187+
188+
{
189+
"type" : "mssql_create_event_trigger",
190+
"args" : {
191+
"name": "sample_trigger",
192+
"table": {
193+
"name": "users",
194+
"schema": "public"
195+
},
196+
"source": "default",
197+
"webhook": "https://httpbin.org/post",
198+
"insert": {
199+
"columns": "*",
200+
"payload": ["username"]
201+
},
202+
"update": {
203+
"columns": ["username", "real_name"],
204+
"payload": "*"
205+
},
206+
"delete": {
207+
"columns": "*"
208+
},
209+
"headers":[
210+
{
211+
"name": "X-Hasura-From-Val",
212+
"value": "myvalue"
213+
},
214+
{
215+
"name": "X-Hasura-From-Env",
216+
"value_from_env": "EVENT_WEBHOOK_HEADER"
217+
}
218+
],
219+
"replace": false
220+
}
221+
}
222+
```
223+
224+
### Args syntax {#metadata-mssql-create-event-trigger-syntax}
225+
226+
| Key | Required | Schema | Description |
227+
|---------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
228+
| name | true | [TriggerName](/graphql/core/api-reference/syntax-defs.mdx#triggername) | Name of the event trigger |
229+
| table | true | [QualifiedTable](/graphql/core/api-reference/syntax-defs.mdx#qualifiedtable) | Object with table name and schema |
230+
| source | false | [SourceName](/graphql/core/api-reference/syntax-defs.mdx#sourcename) | Name of the source database of the table (default: `default`) |
231+
| webhook | false | String | Full url of webhook (\*) |
232+
| webhook_from_env | false | String | Environment variable name of webhook (must exist at boot time) (\*) |
233+
| insert | false | [OperationSpec](/graphql/core/api-reference/syntax-defs.mdx#operationspec) | Specification for insert operation |
234+
| update | false | [OperationSpec](/graphql/core/api-reference/syntax-defs.mdx#operationspec) | Specification for update operation |
235+
| delete | false | [OperationSpec](/graphql/core/api-reference/syntax-defs.mdx#operationspec) | Specification for delete operation |
236+
| headers | false | [ [HeaderFromValue](/graphql/core/api-reference/syntax-defs.mdx#headerfromvalue) \| [HeaderFromEnv](/graphql/core/api-reference/syntax-defs.mdx#headerfromenv) ] | List of headers to be sent with the webhook |
237+
| retry_conf | false | [RetryConf](/graphql/core/api-reference/syntax-defs.mdx#retryconf) | Retry configuration if event delivery fails |
238+
| replace | false | Boolean | If set to true, the event trigger is replaced with the new definition |
239+
| enable_manual | false | Boolean | If set to true, the event trigger can be invoked manually |
240+
| request_transform | false | [RequestTransformation](/graphql/core/api-reference/syntax-defs.mdx#requesttransformation) | Attaches a Request Transformation to the Event Trigger. |
241+
| response_transform | false | [ResponseTransformation](/graphql/core/api-reference/syntax-defs.mdx#responsetransformation) | Attaches a Request Transformation to the Event Trigger. |
242+
243+
(\*) Either `webhook` or `webhook_from_env` are required.
244+
245+
## mssql_delete_event_trigger {#metadata-mssql-delete-event-trigger}
246+
247+
`mssql_delete_event_trigger` is used to delete an event trigger.
248+
249+
```http
250+
POST /v1/metadata HTTP/1.1
251+
Content-Type: application/json
252+
X-Hasura-Role: admin
253+
254+
{
255+
"type" : "pg_delete_event_trigger",
256+
"args" : {
257+
"name": "sample_trigger",
258+
"source": "default"
259+
}
260+
}
261+
```
262+
263+
### Args syntax {#metadata-mssql-delete-event-trigger-syntax}
264+
265+
| Key | Required | Schema | Description |
266+
|--------|----------|------------------------------------------------------------------------|-----------------------------------------------------------------|
267+
| name | true | [TriggerName](/graphql/core/api-reference/syntax-defs.mdx#triggername) | Name of the event trigger |
268+
| source | false | [SourceName](/graphql/core/api-reference/syntax-defs.mdx#sourcename) | Name of the source database of the trigger (default: `default`) |
269+
270+
## mssql_redeliver_event {#metadata-mssql-redeliver-event}
271+
272+
`redeliver_event` is used to redeliver an existing event. For example,
273+
if an event is marked as error ( say it did not succeed after retries),
274+
you can redeliver it using this API. Note that this will reset the count
275+
of retries so far. If the event fails to deliver, it will be retried
276+
automatically according to its `retry_conf`.
277+
278+
```http
279+
POST /v1/metadata HTTP/1.1
280+
Content-Type: application/json
281+
X-Hasura-Role: admin
282+
283+
{
284+
"type" : "mssql_redeliver_event",
285+
"args" : {
286+
"event_id": "ad4f698f-a14e-4a6d-a01b-38cd252dd8bf"
287+
}
288+
}
289+
```
290+
291+
### Args syntax {#metadata-mssql-redeliver-event-syntax}
292+
293+
| Key | Required | Schema | Description |
294+
|----------|----------|--------|-------------------|
295+
| event_id | true | String | UUID of the event |
296+
297+
## mssql_invoke_event_trigger {#metadata-mssql-invoke-event-trigger}
298+
299+
`invoke_event_trigger` is used to invoke an event trigger with custom payload.
300+
301+
```http
302+
POST /v1/metadata HTTP/1.1
303+
Content-Type: application/json
304+
X-Hasura-Role: admin
305+
306+
{
307+
"type" : "mssql_invoke_event_trigger",
308+
"args" : {
309+
"name": "sample_trigger",
310+
"source": "default",
311+
"payload": {}
312+
}
313+
}
314+
```
315+
316+
### Args syntax {#metadata-pg-invoke-event-trigger-syntax}
317+
318+
| Key | Required | Schema | Description |
319+
|---------|----------|------------------------------------------------------------------------|-----------------------------------------------------------------|
320+
| name | true | [TriggerName](/graphql/core/api-reference/syntax-defs.mdx#triggername) | Name of the event trigger |
321+
| payload | true | JSON | Some JSON payload to send to trigger |
322+
| source | false | [SourceName](/graphql/core/api-reference/syntax-defs.mdx#sourcename) | Name of the source database of the trigger (default: `default`) |

rfcs/mssql-event-triggers-research.md

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,109 @@ which can be divided in two parts as the following:
115115
BEGIN
116116
DECLARE @json NVARCHAR(MAX)
117117
SET @json = (
118-
select d.id as [data.old.id], d.name as [data.old.name], i.id as [data.new.id], i.name as [data.new.name]
119-
from deleted d
120-
JOIN inserted i on i.id = d.id
121-
where (i.id != d.id OR i.name != d.name)
118+
select deleted.id as [data.old.id], deleted.name as [data.old.name], inserted.id as [data.new.id], inserted.name as [data.new.name]
119+
from deleted
120+
JOIN inserted
121+
ON inserted.id = deleted.id
122+
where (inserted.id != deleted.id OR inserted.name != deleted.name)
122123
FOR JSON PATH
123124
)
124125
insert into hdb_catalog.event_log (schema_name,table_name,trigger_name, payload)
125126
select 'dbo','authors','authors_update', value from OPENJSON (@json)
126127
END
127128
```
128129

129-
NOTE: the above will work only when a table has a primary key, which is used
130+
**NOTE**: The above will work only when a table has a primary key, which is used
130131
to join the `deleted` and the `inserted` tables.
131132

133+
**NOTE**: Since we use the primary keys to co-relate DELETED and INSERTED table,
134+
no trigger will fire when the primary key is updated. To fix this problem, we
135+
update the UPDATE Trigger Spec as following.
136+
137+
1. When PK is not updated, then we send both `data.new` and `data.old`
138+
2. When PK is updated, there are two cases:
139+
* The updated PK value is already present in the table, then this case is
140+
similar to CASE 1, where a single row is being updated. In such cases
141+
send both `data.new` and `data.old`
142+
* The updated PK value is not present in the table, then the updated value
143+
will be sent as `data.new` and `data.old` will be made NULL
144+
145+
Thus, the `UPDATE` trigger will now look like following:
146+
147+
```sql
148+
CREATE TRIGGER hasuraAuthorsAfterUpdate
149+
ON books
150+
AFTER UPDATE
151+
AS
152+
BEGIN
153+
DECLARE @json_pk_not_updated NVARCHAR(MAX)
154+
DECLARE @json_pk_updated NVARCHAR(MAX)
155+
156+
-- When primary key is not updated during a UPDATE transaction then construct both
157+
-- 'data.old' and 'data.new'.
158+
SET @json_pk_not_updated =
159+
(SELECT
160+
DELETED.name as [payload.data.old.name], DELETED.id as [payload.data.old.id], INSERTED.name as [payload.data.new.name], INSERTED.id as [payload.data.new.id],
161+
'UPDATE' as [payload.op],
162+
'dbo' as [schema_name],
163+
'books' as [table_name],
164+
'insert_test_books' as [trigger_name]
165+
FROM DELETED
166+
JOIN INSERTED
167+
ON INSERTED.id = DELETED.id
168+
where INSERTED.name != DELETED.name OR INSERTED.id != DELETED.id
169+
FOR JSON PATH
170+
)
171+
172+
insert into hdb_catalog.event_log (schema_name,table_name,trigger_name,payload)
173+
select * from OPENJSON (@json_pk_not_updated)
174+
WITH(
175+
schema_name NVARCHAR(MAX) '$.schema_name',
176+
table_name NVARCHAR(MAX) '$.table_name',
177+
trigger_name NVARCHAR(MAX) '$.trigger_name',
178+
[payload] NVARCHAR(MAX) AS JSON
179+
)
180+
181+
-- When primary key is updated during a UPDATE transaction then construct only 'data.new'
182+
-- since according to the UPDATE Event trigger spec for MSSQL, the 'data.old' would be NULL
183+
IF (1 = 1)
184+
BEGIN
185+
SET @json_pk_updated =
186+
-- The following SQL statement checks, if there are any rows in INSERTED
187+
-- table whose primary key does not match to any rows present in DELETED
188+
-- table. When such an situation occurs during a UPDATE transaction, then
189+
-- this means that the primary key of the row was updated.
190+
(SELECT
191+
NULL as [payload.data.old], INSERTED.name as [payload.data.new.name], INSERTED.id as [payload.data.new.id],
192+
'UPDATE' as [payload.op],
193+
'dbo' as [schema_name],
194+
'books' as [table_name],
195+
'insert_test_books' as [trigger_name]
196+
FROM INSERTED
197+
WHERE NOT EXISTS (SELECT * FROM DELETED WHERE INSERTED.id = DELETED.id )
198+
FOR JSON PATH, INCLUDE_NULL_VALUES
199+
)
200+
201+
insert into hdb_catalog.event_log (schema_name,table_name,trigger_name,payload)
202+
select * from OPENJSON (@json_pk_updated)
203+
WITH(
204+
schema_name NVARCHAR(MAX) '$.schema_name',
205+
table_name NVARCHAR(MAX) '$.table_name',
206+
trigger_name NVARCHAR(MAX) '$.trigger_name',
207+
[payload] NVARCHAR(MAX) AS JSON
208+
)
209+
END
210+
211+
END;
212+
```
213+
132214
The triggers will be created with template string values where the values of
133215
the tables or row expressions will be substitutedcbefore creating the
134216
trigger, as it is done for postgres [here](https://github.com/hasura/graphql-engine-mono/blob/main/server/src-rsr/trigger.sql.shakespeare).
135217

136218
5. MS-SQL doesn't allow for the trigger to be created in a different schema from
137219
the target table's schema. For example, if a table is created in the `dbo`
138-
schema, then the trigger should also be in the `dbo` schema.
220+
schema, then the trigger should also be in the `dbo` schema. Ref: [MSSQL Docs](https://docs.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?redirectedfrom=MSDN&view=sql-server-ver15)
139221

140222
6. In postgres, the session variables and trace context were set in runtime
141223
configurations, `hasura.user` and `hasura.tracecontext` respectively, it's

server/src-lib/Hasura/Backends/MSSQL/Connection.hs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ module Hasura.Backends.MSSQL.Connection
1515
getEnv,
1616
odbcValueToJValue,
1717
mkMSSQLExecCtx,
18+
runMSSQLSourceReadTx,
19+
runMSSQLSourceWriteTx,
1820
)
1921
where
2022

@@ -234,3 +236,19 @@ odbcValueToJValue = \case
234236
ODBC.TimeOfDayValue td -> J.toJSON td
235237
ODBC.LocalTimeValue l -> J.toJSON l
236238
ODBC.NullValue -> J.Null
239+
240+
runMSSQLSourceReadTx ::
241+
(MonadIO m, MonadBaseControl IO m) =>
242+
MSSQLSourceConfig ->
243+
MSTx.TxET QErr m a ->
244+
m (Either QErr a)
245+
runMSSQLSourceReadTx msc =
246+
runExceptT . mssqlRunReadOnly (_mscExecCtx msc)
247+
248+
runMSSQLSourceWriteTx ::
249+
(MonadIO m, MonadBaseControl IO m) =>
250+
MSSQLSourceConfig ->
251+
MSTx.TxET QErr m a ->
252+
m (Either QErr a)
253+
runMSSQLSourceWriteTx msc =
254+
runExceptT . mssqlRunReadWrite (_mscExecCtx msc)

0 commit comments

Comments
 (0)