
Hasura GraphQL Engine 中 SQL Server 的 DELETE 突变实现原理从 RFC 到三条 SQL 事务【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine在 Hasura GraphQL Enginegraphql-engine 仓库中通过/v1/graphql端点向关系数据库发送 GraphQL 突变mutation是其核心能力之一。本文基于仓库中的 RFC 文档 rfcs/mssql-delete-mutations.md系统讲解 SQL ServerMSSQL数据源的DELETE突变是如何定义的、为什么要面对与 Postgres 不同的实现难题以及最终落地的「三条 SQL 语句事务」执行方案。读完后你将能理解 MSSQL 删除突变从 GraphQL 请求到 T-SQL 生成的完整链路并掌握returning结果捕获这一核心设计问题在源码中的解法。需求背景让 MSSQL 表拥有与 Postgres 一致的删除语义RFC 开篇的用户故事User story定义了目标As a user, I would like to be able to delete rows from a certain mssql table using a predicate, similarly to how Im able to do so for a postgres table.即用户希望像操作 Postgres 表一样通过谓词predicate从 MSSQL 表中删除行。删除操作统一通过向/v1/graphql端点发送 GraphQL 突变完成并且删除突变必须遵守行级权限row-level permissions规则。这与仓库整体的多数据源架构一致MSSQL 后端与 Postgres 后端分别位于server/src-lib/Hasura/Backends/MSSQL/和server/src-lib/Hasura/Backends/Postgres/两者共享Hasura.RQL.IR查询中间表示这一层各自负责「IR → SQL」的翻译。DELETE 突变在 IR 中被表示为AnnDel MSSQL类型携带表名、权限过滤器、where 子句、全部列、删除输出returning 模型等元数据。接口设计delete 与 delete_by_pk 两种语法RFC 的 Interface 章节规定了用户可用的两种删除语法与 Postgres 侧保持同构。delete 语法按谓词删除支持三种谓词形态基于根表字段root table fields的谓词基于嵌套对象字段nested object fields的谓词使用{}作为where参数删除表中所有对象。通用语法骨架为mutation [mutation-name] { mutation-field-name ( [where-argument!] ) { [mutation-response!] } }delete_by_pk 语法按主键删除对具备主键或自然键的表可额外提供table_delete_by_pk根字段直接以主键值删除单行。这一字段由通用的突变字段构建器生成buildTableDeleteMutationFields 会同时构建按 where 的delete和按主键的deleteByPk两个字段见 Build.hs#L304-L305二者均为Maybe最终经catMaybes拼合——表若无可用主键则deleteByPk不存在。响应结构RFC 指出突变响应遵循 GraphQL 规范成功时为{ data { affected_rows returning { # the returning statement can include nested objects response-field1 response-field2 .. } } }失败时返回{ errors { extensions message } }其中returning可以包含嵌套对象——这是与 Postgres 最大的兼容性约束之一也是后面执行层设计复杂度的来源。核心难题MSSQL 没有 DELETE ... RETURNINGPostgres 原生支持DELETE ... RETURNING删除的同时把被删行带回来。T-SQL 则没有这个特性只有OUTPUT子句可以捕获删除行的数据但OUTPUT捕获结果需要一个目标表变量或另一张表且不能直接作为响应 JSON 的查询来源。RFC 的 Checkpoints 章节给出了两种候选方案方案一表变量 OUTPUT 子句使用 T-SQL 表变量 结合OUTPUT子句捕获 DELETE 语句的输出DELETE FROM table_name OUTPUT deleted.* INTO table_var WHERE predicate方案二先 SELECT 后 DELETE先跑一条包含 DELETE 的 WHERE 与权限过滤器的 SELECT 查询取回returning与affected_rows所需数据再执行 DELETEWITH with_alias AS ( SELECT * FROM table_name WHERE where-from-delete-query ) select statement generated by mkMutationOutputSelect functionRFC 明确指出开发者在实现时应考虑两种方案与事务隔离级别的交互影响内部讨论后倾向方案一但若不确定更好的做法应与数据源团队重新讨论或做一个简短技术验证spike。同时约定嵌套 return 语句可参考既有的mkMutationOutputSelect函数实现删除权限与 WHERE 表达式在 IR 中经由mkDeleteObject打包生成的 SQL 应把权限过滤器与用户 where 放在同一个WHERE子句里对于INSTEAD OF DELETE触发器不追求比OUTPUT语句本身更好的行为。源码实现三条 SQL 语句的事务仓库中的最终实现见 server/src-lib/Hasura/Backends/MSSQL/Execute/Delete.hs其模块注释把方案写得很直白——一次 GraphQL 删除突变被翻译为三条 SQL 语句外加一次临时表清理在同一读写事务mssqlRunReadWrite内执行SELECT INTO temp_table WHERE false—— 创建与源表 schema 相同含约束处理的临时表用于暂存被删行DELETE FROM ... OUTPUT—— 真正删除行并通过OUTPUT子句把被删行写入第一步创建的临时表SELECT—— 从临时表构建returning响应包括与其他表的关系nested objects。关键函数buildDeleteTx的执行链如下Delete.hs#L78-L115buildDeleteTx deleteOperation stringifyNum queryTags do let createInsertedTempTableQuery toQueryFlat $ TQ.fromSelectIntoTempTable $ TSQL.toSelectIntoTempTable tempTableNameDeleted (_adTable deleteOperation) (_adAllCols deleteOperation) RemoveConstraints -- 1. Create a temp table Tx.unitQueryE defaultMSSQLTxErrorHandler (createInsertedTempTableQuery withQueryTags queryTags) let deleteQuery TQ.fromDelete $ TSQL.fromDelete deleteOperation deleteQueryValidated - toQueryFlat . qwdQuery $ runFromIrErrorOnCTEs deleteQuery -- 2. Execute DELETE statement Tx.unitQueryE mutationMSSQLTxErrorHandler (deleteQueryValidated withQueryTags queryTags) mutationOutputSelect - qwdQuery $ runFromIrUseCTEs (mkMutationOutputSelect stringifyNum withAlias $ _adOutput deleteOperation) -- 3. SELECT from the temp table, wrapped in a CTE alias with_alias let withSelect emptySelect { selectProjections [StarProjection], selectFrom Just $ FromTempTable $ Aliased tempTableNameDeleted deleted_alias } finalMutationOutputSelect mutationOutputSelect {selectWith Just $ With $ pure $ Aliased (CTESelect withSelect) withAlias} -- ... -- 4. DROP the temporary table, then return results几个值得注意的实现细节临时表名常量临时表统一命名为deleted定义于 server/src-lib/Hasura/Backends/MSSQL/FromIr/Constants.hstempTableNameDeleted TempTableName deleted。同一文件还定义了插入/更新路径共用的inserted、values、updated等临时表名说明 INSERT、UPDATE、DELETE 突变共享同一套「临时表 OUTPUT」的基础设施。SELECT 阶段复用 CTE 别名机制第三步的 SELECT 通过WITH with_alias AS (SELECT * FROM deleted)包装临时表见withAlias with_alias与deleted_alias这与 RFC 方案二中提到的with_alias命名一致并让mkMutationOutputSelect生成的关系查询嵌套 returning无需感知数据来自临时表而非原表。权限与 where 同在一个 WHERE 子句IR 到 T-SQL 的翻译在 server/src-lib/Hasura/Backends/MSSQL/FromIr/Delete.hs 的fromDelete中完成它把权限过滤器permFilter与用户 where 子句whereClause分别经fromGBoolExp翻译后放入同一个Where列表fromDelete (IR.AnnDel table (permFilter, whereClause) _ allColumns _ _validateInput _isDeleteByPrimaryKey) do tableAlias - generateAlias (TableTemplate (tableName table)) ... pure Delete { deleteTable Aliased { aliasedAlias tableAlias, aliasedThing table }, deleteOutput Output Deleted (map OutputColumn columnNames), deleteTempTable TempTable tempTableNameDeleted columnNames, deleteWhere Where [permissionsFilter, whereExpression] }deleteOutput中的Output Deleted正是驱动OUTPUT deleted.*语义的标记deleteTempTable则把输出定向到deleted临时表。这落实了 RFC 的约定「generated SQL should contain the filter expression and permissions in the same WHERE clause」。查询标签query tags每条 SQL 都经withQueryTags附加注释标签便于在数据库侧的查询日志/监控中识别该语句属于哪个 GraphQL 操作这是仓库跨数据源的统一可观测性机制。端到端调用链把上述模块串起来一次 MSSQL DELETE 突变的完整链路为Schema 生成buildTableDeleteMutationFields 依据表元数据生成table_delete与table_delete_by_pk根字段IR 构建请求解析后权限与 where 被打包进 IRAnnDel事务构建executeDelete 接收 IR调用buildDeleteTx生成「建临时表 → DELETE OUTPUT → SELECT」的事务计划并由mssqlRunReadWrite在读写连接上执行响应生成mkMutationOutputSelect生成的 SELECT 从deleted临时表取回affected_rows与嵌套的returning数据最终以 JSON 编码返回。该执行器经由 server/src-lib/Hasura/Backends/MSSQL/Instances/Execute.hs 挂接到 MSSQL 后端的Execute类型类实例与 Postgres 侧的 Hasura.Backends.Postgres.Execute.Mutation 形成对照——后者利用原生RETURNING单条语句完成而 MSSQL 侧用事务内多条语句达到等价语义。验收标准与测试覆盖RFC 的 Success 章节列出了明确的验收清单仓库中均有对应落点Python 集成测试server/tests-py/test_graphql_mutations.py 需在 MSSQL 数据库上通过覆盖 RFC 列出的每个查询形态对应测试类包括基本删除TestGraphqlDeleteBasic表权限TestGraphqlDeletePermissions约束与错误路径TestGraphqlDeleteConstraints自定义 schema自定义表名TestGraphqlMutationCustomGraphQLTableName、自定义根字段TestGraphqlMutationCustomSchema控制台与 CLI删除突变可通过 Hasura Console 和 CLI 执行Console 前端位于 frontend/ 目录CLI 位于 cli/ 目录文档SQL Server 的删除突变文档以 Postgres 的 delete 文档为参照模板编写对应仓库文档目录 docs/docs/ 下的 mutations 相关 mdx 内容。适用前提与设计边界结合 RFC 与源码MSSQL DELETE 突变有几个明确的边界使用与排查问题时需要注意事务性三条 SQL 语句含临时表清理在同一mssqlRunReadWrite事务内执行returning数据与实际删除是原子的临时表方案RFC 方案一的变体用真实临时表替代表变量避免了「先 SELECT 后 DELETE」在并发下可能读到的数据与最终删除集合不一致的隔离级别风险。INSTEAD OF DELETE 触发器RFC 明确不追求超越OUTPUT语句本身的能力——若表上定义了INSTEAD OF DELETE触发器其拦截行为决定OUTPUT能否捕获到行MSSQL 后端不做额外补偿。affected_rows与嵌套 returning嵌套对象通过mkMutationOutputSelect生成跨表 SELECT 完成与 Postgres 的returning语义对齐。小结rfcs/mssql-delete-mutations.md 描述了 Hasura 为 SQL Server 补齐 DELETE 突变的设计过程接口上完全对齐 Postgres 的delete/delete_by_pk语法与affected_rowsreturning响应实现上由于 T-SQL 缺乏RETURNING最终采用「SELECT INTO建临时表 →DELETE ... OUTPUT写入deleted临时表 →WITH with_alias包装临时表执行 returning SELECT → 清理临时表」的四步事务方案核心代码集中在 Hasura.Backends.MSSQL.Execute.Delete、Hasura.Backends.MSSQL.FromIr.Delete 与 FromIr.Constants并由 test_graphql_mutations.py 中的删除测试类在 MSSQL 上持续验证。【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考