Reylo的Rust分析引擎是怎么工作的:从libpg_query、sqlparser到WASM制品的完整链路
Reylo的Rust分析引擎是怎么工作的:从libpg_query、sqlparser到WASM制品的完整链路
Reylo 是一款能把 .sql 文件直接 import 进 TypeScript 并获得完整类型推断的开源工具,全程零代码生成。在编辑器里悬浮到一行 run() 上,类型检查器立刻知道这行数据的形状——这背后的功臣是一套用 Rust 写的 SQL 分析引擎:它先用 libpg_query / sqlparser 解析 SQL,再推断类型与可空性,最后被编译成一个 WASM 制品,由 Node 侧直接加载调用。本文带你拆解它的完整工作链路。
🔌 最关键的设计:引擎从不打开数据库连接
Reylo 引擎的核心承诺是纯函数(pure):analyze 是一次"重放"(replay)。宿主把"目前从数据库里知道的一切"传给它,引擎要么返回最终分析结果,要么返回一份"我还缺什么"的清单——只有两类:
Describe:让数据库准备一条语句,读出参数与结果列的类型;Query:让宿主跑一条目录查询,读回 schema 事实。
引擎自己从不建立任何连接,所有"要问数据库的问题"都以 Need 的形式抛回给宿主(见 engine/core/src/api.rs)。这个设计让同一份引擎代码可以出现在三个地方:Node 里的 WASM 模块、Postgres 服务器内部的 pgreylo 扩展、以及用于对拍测试的 oracle。
🧩 三套数据库,三套解析器
整个引擎是一个 Cargo 工作区(见 Cargo.toml),共享一个 reylo-engine-core,每套数据库一个独立 crate,且互相不依赖(总览见 engine/README.md):
| 数据库 | 解析器 | 说明 |
|---|---|---|
| Postgres | libpg_query(打补丁的 pg_query 18.0 crate) |
Postgres 18 官方解析器,parse tree 直接从 C 节点读入 |
| MySQL | 自维护、裁剪过的 sqlparser 0.63 |
只保留它要读的语句;类型信息来自服务端 prepare 元数据 |
| SQLite | 打了引擎钩子的 SQLite amalgamation | SQLite 解析到什么,分析就读到什么,不链接任何额外解析器 |
Postgres:libpg_query
pg_query 18.0 在 crates.io 上还没有,所以 engine/postgres/Cargo.toml 通过 <a href="https://link.gitcode.com/i/f9181f365284a09f1c54826808491b25" target="_blank">patch] 指向本地副本:从上游 pg-18 分支拉取固定提交,再打上两个补丁——支持编译到 wasm32-wasip1、放开语句嵌套深度限制。parse tree 由 build.rs 生成的转换器直接从 C 节点读进 Rust 结构体([engine/postgres/src/raw.rs),其上再叠加表达式推断(infer)、行数证明(cardinality)和 JSON 形状分析(jsonshape)。
MySQL:sqlparser 补位
MySQL 没有 Postgres 那样"官方 parser + 丰富元数据"的组合,Reylo 就双管齐下:参数与列的类型、可空性来自服务端的 prepare 元数据;语句树则由 vendored 的 sqlparser 提供(engine/mysql/src/lib.rs),用于行数证明。这份 sqlparser 被裁剪到只读它需要的语句,避免无用语法带来噪音。
SQLite:在引擎内部准备语句
SQLite 走的是最"自给自足"的路子:amalgamation 源码被打了引擎钩子(C 代码在 engine/sqlite/csrc/,由 build.rs 编译),prepare 直接在引擎内部完成——宿主连 Describe 的答案都不用给(见 engine/sqlite/src/lib.rs)。
占位符词法分析:core 共享
三套方言统一支持 :name 命名占位符,外加各自的定位形式(Postgres 的 $1、MySQL 的 ? 等)。engine/core/src/lex.rs 把占位符改写成数据库实际绑定的形式,并保留一份映射回原文的偏移表——这样服务器报的语法错误能精准落回 .sql 文件里对应的那几个字符。
📦 WASM 制品是怎么做出来的
WASM 产物由一层极薄的 entry crate 生成:engine/wasm/postgres/src/lib.rs 整文件只有一行——展开 export_wasm_abi! 宏。它对外的 ABI 只有三个函数(engine/core/src/abi.rs):
| 导出函数 | 职责 |
|---|---|
reylo_alloc(len) |
在 WASM 侧分配一块缓冲区 |
reylo_call(ptr, len) |
读入 JSON 请求,处理完返回带 4 字节长度前缀的 JSON 响应 |
reylo_free(ptr, len) |
释放缓冲区 |
构建由 engine/build.sh 完成:wasi-sdk 34 提供 C 工具链,编译目标是 wasm32-wasip1。制品直接提交在 packages/postgres/ 等 npm 包里——没有 Rust 工具链也能开箱工作,每个 crate 的 build.rs 还会哈希自身源码,保证制品与源码一一对应。
🔏 指纹:让"答案可信"有据可查
engine/core/src/fingerprint.rs 用 FNV-1a 对 core 与对应数据库 crate 的源码哈希组合出 16 位十六进制指纹。两个指纹相同的引擎必然给出相同答案——客户端正是靠它判断:数据库里的 pgreylo 扩展(见 engine/postgres/src/extension.rs)是否和自己同源,可否让它代答;它同时也是分析缓存的 key。
🔗 TypeScript 侧如何驱动引擎
src/engine.ts 用 node:wasi 加载对应数据库包的 engine.wasm(@reylosql/postgres 等),然后进入循环:引擎报出 Need → 宿主用项目自己的 driver 去问数据库 → 带着答案再次调用,直到拿到 Analysis(参数、列、基数、诊断信息)交给类型检查器。在 Jest 这类必须同步应答的场景,src/bridge.ts 用 worker 线程做分析、当前线程阻塞等结果。
🧪 正确性如何保证
工作区自带一个"真机对拍器":oracle/src/main.rs 连接真实的 Postgres / MySQL / SQLite,把 oracle/cases/ 里成百上千条语句按宿主在生产的同一套 replay 流程跑一遍,逐条比对 expected.txt。引擎的任何改动都要先过这一关,这也是它敢在编辑器里直接断言"列名拼错了就是类型错误"的底气。
✅ 小结
Reylo 的 Rust 分析引擎做对了一件关键的事:把"问数据库"与"分析 SQL"彻底解耦。libpg_query、sqlparser 和打过钩子的 SQLite 各自负责产出语句树,纯函数的 replay 协议负责推断类型,三个 ABI 函数把一切封装进 engine.wasm——于是 .sql 的类型既不需要手写,也不需要生成文件,永远来自你的 SQL 本身。

