AST 的旧 visitor API 不太好用,我们希望换成一套新的 API。
单看工程实现,这件事并不算难。AST 节点虽然很多,但大部分迁移代码模式重复,适合批量生成,也适合由 AI 完成大量样板工作。
真正让人不放心的不是代码能不能写出来,而是写完以后怎样确认它没有静悄悄地改变语义。
在数据库内核里,AST Visitor 是一层基础设施。它可能服务于 parser、binder、optimizer、formatter、权限检查、表达式重写等多个下游环节。迁移一旦出现偏差,未必会马上表现为编译错误,更可能是某个节点不再被访问、某个字段多访问了一次,或者原本稳定的遍历顺序发生变化。代码表面上仍然合理,真正的影响却可能在更靠后的逻辑中暴露。
最初我们关心的风险主要有两类:
这类问题很难只靠 code review 兜住。节点数量太多,人工很难在脑子里展开整棵 AST;大量生成代码又高度相似,review 很容易退化成对样板代码的走读。
因此,我们开始尝试建立一套可执行的验证机制,让正确性更多地由程序化证据约束,而不是依赖人逐行确认。
最终形成的系统大致如下:

这个闭环不是一次搭好的。为了讲清各部分的边界,下面先完整说明由源码扫描、运行时观测、独立比较和失败定位组成的内核,再说明真实输入和覆盖率反馈如何接入。
要让后面的验证可信,不是写一套更复杂的逻辑来描述 AST,而是尽量直接地从源码中提取结构。类型、variant 和字段已经写在源码里,采集工具只需要机械地把它们展开,作为后续代码生成的输入。
这里最重要的约束是少写逻辑。如果为了确认 visitor 有没有漏项,又手工实现一套节点分类、递归关系和特殊规则,那么只是把原来的审计问题搬进了验证工具:要相信验证结果,就得先完整审计这套同样复杂的逻辑。
因此,源码分析应该保持短而直接:沿用语法分析得到的类型定义,枚举 struct、enum、variant 和字段,让产出能够回到对应的源码位置。一个声明对应一条结构记录,必要的排除项显式列出,不在采集阶段推断遍历语义,也不复制 visitor 的特殊顺序。
这种做法的可信度来自映射关系足够简单。需要审计的是少量通用的源码提取规则,而不是另一套与 AST 同规模的手写模型。它提供的是一份可追溯、可审计的结构输入;visitor 实际怎样遍历这些结构,则交给运行时观测和后续比较回答。
扫描源码以后,下一件事是把 visitor 的实际运行行为记录下来。这里最重要的设计,是把观测和比较分开,不要生成代码同时承担正确性判断。
这两个部分的关系是:

这样,大量生成代码可以保持机械,正确性规则则集中在少量手写代码中。同一份原始 trace 也能继续用于覆盖率统计和失败定位。
这部分的原则可以概括为:
这个分层很关键。AI 可以生成大量迁移代码,也可以生成一部分观测代码,但判断正确性的规则应该足够集中、明确、可审计。
观测层能够稳定地产生数据以后,下一个问题才真正具体起来:什么样的差异算 visitor 语义变化?
最初最容易想到的是比较“访问到了哪些项目”。这可以发现某个节点或 hook 在一边出现、另一边没有出现。
但单纯比较集合不够。集合会丢掉访问次数:一个节点访问一次和访问两次,在集合里看起来完全相同。因此,检查漏项和重复访问时,至少需要比较多重集,或者直接比较保留次数的事件记录。
顺序则是另一类约束。有些 visitor 逻辑只关心最终是否见过某些节点,顺序并不重要;另一些逻辑却依赖隐含的遍历顺序,例如:
所以比较器不能只有一种全局规则。它需要根据 hook 的语义分别检查:
这时,“等价”的定义才逐渐变得准确:不是要求新旧代码实现相同,而是要求它们在约定的可观察遍历行为上没有未解释差异。
旧 API 在这里是历史行为基线,但不必被假定为天然正确。如果新实现有意修复旧行为,或者新 API 明确改变了某项契约,这类差异应该显式记录,而不是为了让比较通过而被悄悄过滤。
比较器能够报出不一致以后,新的问题又出现了:只知道两份 trace 不相等,对修复迁移帮助有限。
如果失败信息只是一个布尔值,开发者仍然需要重新翻阅大量 visitor 代码,寻找究竟是哪一个节点或 hook 产生了偏差。这样虽然有了自动检查,定位成本却仍然很高。
因此,trace 和比较器继续增加了面向诊断的信息:
这项工作看起来发生在比较器完成之后,实际上会反过来影响观测层。要报告 AST 路径,记录事件时就必须携带路径;要定位第一个顺序差异,就不能提前把 trace 排序;要发现重复访问,就不能在观测阶段去重。
这也是为什么观测层应该尽量保留事实,而把规范化和判断留给比较器。失败定位不是最终输出上的装饰,而是 trace 协议需要从一开始支持的能力。
到这里,最初的可用内核才算完整:源码扫描提供 AST 结构,观测层把实际遍历行为记录成 trace,比较层根据遍历契约判断新旧行为是否一致,差异报告则让失败能够直接进入修复。
只要给定一棵 AST,这个内核就已经能够运行。

后面的工作不再是在补齐这个内核的基本组成,而是在扩大它能处理的输入,并让验证边界持续可见。
有了可用内核,并不意味着验证已经充分。它只能比较实际送进去的 AST。
如果依靠手工构造 AST,很快会遇到两个现实问题:
因此,我们开始复用 parser 已有的 SQL 语料:
parser golden 是一个自然的输入来源。一条 SQL 往往能同时覆盖多个节点和嵌套关系,这些语料也是项目已经认可的真实输入形状。相比专门维护一批庞大的手写 AST,它们更接近 visitor 实际会遇到的数据。
但这里需要把结论说准确:这种方法证明的是“在当前 SQL 语料实际覆盖到的 AST 上,新旧遍历行为一致”,而不是整个 AST 状态空间都已经被穷举。
运行时等价比较始终依赖一个前提:输入必须真正触达我们关心的 AST 分支。
最初复用 parser SQL 语料时,我们只能知道测试运行了,却不知道它究竟覆盖了多少手写 walk 分支。如果某个 enum variant、可选字段或特殊路径从未出现,那么新旧 visitor 即使在所有测试上完全一致,也不能说明那部分迁移得到了验证。
这时,覆盖率成为验证输入是否充分的反馈信号:
覆盖率在这里不负责证明代码正确。它回答的是另一个问题:我们用来比较新旧行为的输入,实际触达了多少待验证空间。
这样就形成了两个相互配合但不能互相替代的证据:
发现缺口后,能够由真实 SQL 表达的,优先补到 parser 上游,使语料同时服务 parser、AST 构造和 visitor;难以通过 SQL 稳定构造的边界状态,再用局部定向测试覆盖。
回头看,这套系统包含源码扫描、运行时 trace、独立比较、失败定位、真实 SQL 输入和覆盖率反馈,但它们承担的角色并不相同。前四项构成基本可用的验证内核:给定一棵 AST,它们能够运行比较并把差异直接交给修复。真实 SQL 输入扩大了内核的适用范围,覆盖率则建立了继续扩展验证范围的反馈机制。
它们分别解决不同的问题:
这些部分在后续迁移中会同时工作,并彼此反馈:
复制代码迁移代码变化 ─→ trace 差异 ─→ 修正迁移或记录有意变化AST 定义变化 ─→ 生成结果变化 ─→ 暴露未处理结构覆盖率缺口 ─→ 新增 SQL 语料 ─→ 扩大等价比较范围定位信息不足 ─→ 扩展 trace 协议 ─→ 改善后续差异报告
因此,更准确的说法不是“按照五个步骤完成 visitor 迁移”,而是 “围绕迁移不断补齐不同种类的证据,直到这些证据形成可以持续运行的闭环”。
最终,我们希望得到的结论包括:
它仍然不是对整个 AST 状态空间的数学证明。 有限 SQL 语料上的 trace 比较,只能约束实际触达的输入;覆盖率可以暴露没有运行的路径,却不能单独证明已经运行的代码正确;生成器和比较器本身也仍然需要审计。
因此,把它称为 “有明确覆盖边界的可执行等价性验证” 更加准确。它不承诺一次性证明一切,而是把原本依赖人工信心的迁移,变成一个能够持续发现缺口、补充证据并逐渐收敛的工程过程。
这套机制最初是为了降低一次 visitor 迁移的风险,但它也形成了一种更清晰的人、AI 和程序之间的分工。
因此,更合适的分工是:
这里的重点不是为 AI 单独设计一套较低的正确性标准。无论代码由 AI 还是人完成,大规模基础设施迁移都应该接受同一套独立验证。 AI 只是提高了代码生产速度,也让“不能把正确性寄托在逐行 review 上”这件事更加明显。
Databend 是一个面向现代数据工作负载的云原生数仓。SQL parser、AST、binder、optimizer、表达式系统和执行计划生成,都是支撑复杂查询能力的基础设施。
这类基础设施的长期演进,需要同时满足两个要求:
AST Visitor 迁移验证体现的是 Databend 工程体系中更底层的一类能力:不是只追求“代码生成得快”,而是把复杂变更放进可观测、可比较、可诊断、可持续扩展的验证闭环里。
这和 Databend 的用户心智是一致的。作为开放、Rust-native、面向云原生分析场景的数据仓库,Databend 不只是提供 SQL 功能本身,也需要在内核层面保证这些能力可以长期演进、可维护、可验证。
对于正在把 AI 引入工程流程的团队来说,这类实践也提供了一个更实际的判断标准:AI 是否真正提升工程效率,不取决于它能生成多少代码,而取决于系统是否能验证这些代码进入基础设施之后仍然保持正确。
旧 visitor API 换成新 API,真正困难的从来不是批量写出代码,而是建立足够可信的迁移证据。
这套方法也不是一开始就以最终形态出现的。围绕“怎样验证一棵给定 AST 上的新旧行为”,最终形成了四个相互配合的内核能力:源码扫描、运行时观测、独立比较和失败定位。在此之上,真实 SQL 语料扩大了输入范围,覆盖率反馈又让尚未验证的范围保持可见,最终形成完整闭环。
AI 可以加速迁移代码的生产,但它不负责为自己的输出提供可信度。真正让这种大规模基础设施迁移变得可控的,是独立观测、集中判断、真实输入、可诊断差异以及持续可见的覆盖边界。