Quellcode durchsuchen

docs: record M3A rule execution acceptance

马小龙 vor 4 Wochen
Ursprung
Commit
6effc389ff

+ 117 - 0
docs/validation/DATA_RULE_M3A_ACCEPTANCE_20260723.md

@@ -0,0 +1,117 @@
+# 数据规则 M3A 阶段验收总结
+
+日期:2026-07-23  
+分支:`codex/data-rule-execution-m3a-m5`  
+实施范围:数据规则可信契约、服务端 Schema/数据集绑定、类型化表达式、SQLGlot 真实执行垂直链路。
+
+## 1. 阶段结论
+
+M3A 通过验收。平台已经具备“受限但真实可执行”的 SQL 规则能力:
+
+1. 自然语言生成的 RuleSpec 必须规范化为 V2,旧 V1 只允许读取迁移,不能作为新规则写入。
+2. 表达式使用平台自有闭合 AST,具备函数白名单、字段类型检查、复杂度限制和后端能力判断。
+3. Schema Snapshot 由服务端可信元数据目录生成并计算 Hash,发布接口不再接受客户端自报 Schema Hash。
+4. DataFlow 发布固定规则和 Schema 语义,但物理表、数据源和方言仍在 Data Factory 部署阶段绑定。
+5. PostgreSQL/MySQL SQL 由固定版本 SQLGlot 通过 AST 编译,参数值不通过字符串拼接进入 SQL。
+6. 绑定计划在 M3A 只保存为 `compiled`;生产仓储不存在通过调用方自报证据提升为 `published` 的接口。
+7. Runner 只执行 `published` 计划,并校验 RuleVersion、Schema Snapshot、DatasetBinding、编译器版本和计划 Hash。
+8. SQL 首期只开放经过证明的同源、精确唯一键 `upsert`;其它写入模式、整数除法、不可排序字段去重和无安全契约的质量读取均失败关闭。
+
+## 2. 已落地能力
+
+### 2.1 可信运行契约
+
+- `SchemaSnapshot`、`DatasetBinding`、`ExecutionPlanV2` 均为闭合 JSON。
+- 拒绝凭据、连接串、内联查询、Python/序列化对象和未知后端操作。
+- Schema Hash 必须与规范化字段内容一致。
+- PostgreSQL/MySQL 标识符按 UTF-8 字节长度提前校验。
+
+### 2.2 服务端 Schema 与物理绑定
+
+- 默认 Schema 目录复用现有 Neo4j `BusinessDomain -> DataMeta` 治理图。
+- 稳定引用采用 `bd:<stable-domain-key>:<revision>`,缺失、歧义或版本不匹配时失败关闭。
+- 物理 DatasetBinding 只保存数据源 UID、对象引用、方言、访问/写入模式和 Schema Snapshot ID,不保存凭据。
+
+### 2.3 类型化规则表达式
+
+- 支持受限字面量、字段、括号、白名单函数、比较、逻辑和有限算术。
+- 新建规则只接受 RuleSpec V2;旧 V1 使用独立只读迁移入口。
+- 非整数小数以精确十进制字面量保存,避免二进制浮点提前损失精度。
+- 发布前依据规则自己的 `input_schema_ref` 对服务端字段执行类型检查。
+- 编译器目标必须属于表达式能力交集;没有安全后端时拒绝发布。
+
+### 2.4 SQL 编译与执行
+
+- 固定依赖 `sqlglot==30.13.0`,MIT License。
+- 支持 PostgreSQL/MySQL 严格方言编译和闭合 AST 校验。
+- 编译、持久化和 Runner 三处共同验证规范 RuleVersion/Schema/Binding/Compiler Hash。
+- MySQL upsert 要求配置键恰好对应单列主键/唯一键,并拒绝其它唯一索引可能触发的错误合并。
+- 事务结果区分 `committed`、`not_committed` 和 `unknown`。
+- 去重补充稳定字段排序;JSON、binary 等未证明可排序类型直接拒绝。
+
+## 3. 验收证据
+
+### 3.1 数据库迁移
+
+本地 Docker PostgreSQL 从 `20260723_120` 升级至 `20260723_130` 成功。`130` 仅允许执行计划状态:
+
+- `compiled`
+- `published`
+- `revoked`
+
+`tested` 和正式发布证据状态由 M4 Task 7 实现。
+
+### 3.2 M3A 聚焦验收
+
+覆盖执行契约、SchemaResolver、表达式、RuleSpec、发布服务、SQL 编译器、Runner、真实控制面 PostgreSQL 和 PostgreSQL/MySQL 规则执行:
+
+```text
+148 passed in 1.41s
+```
+
+其中真实数据库验收包括:
+
+- PostgreSQL 规则编译、写入、重复 upsert、篡改拒绝;
+- MySQL 规则编译、写入、重复 upsert;
+- MySQL 非唯一键和额外唯一索引目标拒绝,目标表零写入;
+- PostgreSQL 控制面原子发布及 `compiled` 逻辑计划落库。
+
+### 3.3 全量回归
+
+```text
+501 passed, 26 skipped, 59 subtests passed in 4.19s
+```
+
+### 3.4 本地运行环境
+
+验收时健康:
+
+- backend
+- frontend
+- runner
+- PostgreSQL
+- source PostgreSQL
+- source MySQL
+- MinIO
+- Neo4j
+- n8n
+
+Kestra 仍为 `Exited (1)`,属于 M5 数据工厂投产阶段的明确待解决项,不影响 M3A 编译器/Runner 直接实库验收。
+
+## 4. 阶段边界与遗留风险
+
+- M3A 不提供生产计划发布接口;正式 `compiled -> tested -> published` 必须使用 M4 的服务器运行证据。
+- SQL 质量检查仍失败关闭,待 M3B 证据模型和 M4 发布门禁完成后开放。
+- SQL 当前只支持同源精确唯一键 upsert;跨源、多节点和 Parquet 制品由 M3B 实现。
+- Neo4j 业务域必须具备稳定英文键和明确 revision/version;不满足时 Schema 解析拒绝。
+- SQL 方言的 collation、timezone、regex、rounding 能力必须在部署绑定中由服务器证明。
+
+## 5. 下一阶段
+
+进入 M3B:
+
+1. 增加 Polars 闭合 Lazy Plan 编译与执行。
+2. 建立 MinIO/Parquet 摘要绑定制品。
+3. 完成跨 PostgreSQL/MySQL 的多节点数据交接。
+4. 写入规则运行、违规样本、隔离行和制品血缘证据。
+

+ 12 - 2
tests/integration/test_data_rule_control_plane.py

@@ -26,7 +26,17 @@ class FakeMetadataCatalog:
                     "name": "customer_id",
                     "type": "string",
                     "nullable": False,
-                }
+                },
+                {
+                    "name": "name",
+                    "type": "string",
+                    "nullable": True,
+                },
+                {
+                    "name": "mobile",
+                    "type": "string",
+                    "nullable": True,
+                },
             ],
         }
 
@@ -119,7 +129,7 @@ def test_postgres_rule_standard_and_production_line_release_is_atomic(
                     " JOIN public.dataflow_component_bindings b "
                     " ON b.id = p.component_binding_id "
                     " WHERE b.dataflow_version_id = CAST(:id AS uuid) "
-                    " AND p.status = 'published') AS plans"
+                    " AND p.status = 'compiled') AS plans"
                 ),
                 {"id": released["id"]},
             ).one()