拖拽式离线建模有一种欺骗性:页面看起来很直观,于是后端似乎只需要“按顺序把节点翻译成 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,并为每种节点建立“输入字段—输出字段”的契约测试。可视化编排本质上已经接近编译器问题,越早承认这一点,后面付出的字符串维护成本越少。