拖拽式离线建模有一种欺骗性:页面看起来很直观,于是后端似乎只需要“按顺序把节点翻译成 SQL”。

但画布不是顺序结构。它允许分叉、合并、多个输入,也允许用户画出环和断开的节点。SQL 又不是简单的节点文本相加,它有字段作用域、别名、连接条件和嵌套层级。

这篇记录一下我对这类转换器的理解。

先不要生成 SQL

收到画布 JSON 后,第一步应该构造真正的图模型,而不是立刻遍历节点拼接字符串。

至少需要保存:

type Node struct {
ID string
Type string
Config json.RawMessage
Inputs []string
Outputs []string
}

随后做静态检查:

  • 连线引用的节点是否存在;
  • 必须有输入的节点是否悬空;
  • 输出节点是否唯一或符合业务约束;
  • 图中是否存在环;
  • Join 是否拥有足够输入;
  • 聚合节点引用的字段能否从上游得到。

如果这些问题拖到数据库执行阶段才暴露,用户只能得到一条很难理解的 SQL 错误。

中间结果比最终字符串重要

我更倾向于让每个节点产生结构化中间结果:

Source
columns: [id, name, amount]
from: orders

Filter
condition: amount > 100

Project
columns: [id, amount]

节点转换器只关心自己的配置。Source 提供字段集合,Filter 增加条件,Join 合并两个输入的作用域,Aggregate 改变可用字段。到最后才由渲染器决定使用子查询还是公共表表达式。

这样做比直接拼 SQL 多了一层,但它让错误更早、更靠近用户输入。

一个 WHERE 节点带来的教训

增加 WHERE 节点时,单条件测试一直正常,多条件组合却缺少 AND。那次修复只有两行,却说明当时的表达仍然过于依赖字符串。

条件本身也应该是结构:

AND
├── amount > 100
└── OR
├── city = '北京'
└── city = '天津'

拥有这棵树以后,括号和连接符由渲染器统一负责,节点代码不再猜测自己前后是否还有条件。

为什么还保留手写 SQL

可视化并不天然优于 SQL。对于熟悉 SQL 的开发者,拖拽可能更慢;某些窗口函数和数据库特性也很难及时做成节点。

因此平台同时保留 SQL 模式和 DAG 模式,但两者后续进入同一套任务定义、执行实例、Cron 调度和状态反馈。入口可以不同,运行时最好不要分裂。

如果重新做一次,我会更早引入 AST 或成熟 SQL Builder,并为每种节点建立“输入字段—输出字段”的契约测试。可视化编排本质上已经接近编译器问题,越早承认这一点,后面付出的字符串维护成本越少。