TL;DR
- PostgreSQL 已经是 dominate 的存在。很多系统即使不是 PostgreSQL,也会优先考虑 PostgreSQL SQL 方言的兼容性。
libpg_query提供了一个可用的 PostgreSQL parser 封装。它直接使用 PostgreSQL server source,可以在 server 外部解析 SQL,并返回 PostgreSQL 内部 parse tree。- 接入
libpg_query以后,不应该把 PostgreSQL parse tree 直接扩散到整个系统。更好的做法是定义一层自己喜欢的 parser tree,作为中间表示。这样后续可以隔离 PostgreSQL 端的变化,也更容易接入其他 parser,比如 Oracle 兼容性。
细节补充
如果今天要给一个数据库系统、分析系统、SQL lint 工具或者查询改写工具选 parser,我会先问一个很实际的问题:用户最可能拿什么 SQL 来试?
答案通常不是某个教科书里的 SQL 标准子集,而是 PostgreSQL 风格的 SQL。PostgreSQL 的生态、文档、扩展和云服务都太强了。即使一个系统底层不是 PostgreSQL,前端 SQL 兼容 PostgreSQL 也会让用户更容易迁移和理解。
这就是为什么现在写 parser,不应该从“我要不要自己写一个完整 SQL grammar”开始。更实际的起点是:先把 PostgreSQL parser 接进来。
libpg_query 解决了什么
libpg_query 是 pganalyze 维护的 C library。它的目标很直接:在 PostgreSQL server 外部访问 PostgreSQL parser。
它不是一个重新实现的 PostgreSQL-like parser。它使用 actual PostgreSQL server source 来 parse SQL query,并返回 PostgreSQL internal parse tree。这个区别很重要:如果只是手写一个“长得像 PostgreSQL”的 parser,边角行为很快会偏;如果复用 PostgreSQL 源码,至少 parser 行为的基础会更接近真实 PostgreSQL。
典型调用形式也很直接:
PgQueryParseResult result;
result = pg_query_parse("SELECT 1");
printf("%s\n", result.parse_tree);
pg_query_free_parse_result(result);
返回结果里会带 parser version。例如当前 libpg_query README 的例子里,parse tree 顶层有:
{
"version": 180004,
"stmts": [
...
]
}
这个 version 很有用,因为 parse tree 不是稳定的跨版本 API。你不能假设 PostgreSQL 16、17、18 的 parse tree 字段永远一致。
PG parser 的版本号应该怎么处理
现实里很多项目会把 PostgreSQL parser 版本号模糊化处理。不是因为版本号不重要,而是因为用户真正关心的是“这个 SQL 能不能 parse、parse 出来的语义是不是我支持的”。
但在工程上,版本号不能完全忽略。至少需要记录三层版本:
libpg_query的 release/tag,例如18.0.0或17-6.x。- 它对应的 PostgreSQL major version,例如 18、17、16。
- parse result 里的
version字段,例如180004。
我不建议业务逻辑到处判断这些版本。更好的方式是:在 parser adapter 边界处理版本差异,然后输出系统自己的 AST。
SQL text
-> libpg_query / PostgreSQL parse tree
-> parser adapter
-> system AST
-> binder / analyzer / optimizer
这样 PostgreSQL parser 升级时,主要改 adapter,而不是把整个系统都翻一遍。
为什么还需要自己的 parser tree
直接使用 PostgreSQL parse tree 的诱惑很大,因为它信息完整,而且已经能工作。但长期看,这会把系统绑死在 PostgreSQL 的内部结构上。
PostgreSQL parse tree 是 PostgreSQL 自己用的结构,不是为你的系统设计的稳定 IR。字段命名、节点形状、默认值、语法扩展方式,都服务于 PostgreSQL 的 parser/analyzer/executor 体系。
你的系统可能需要不同的重点:
- 更简单的 expression tree。
- 更明确的 table reference / join / projection 结构。
- 更适合 rule rewrite 的节点。
- 更适合权限、血缘、lint、catalog binding 的 metadata。
- 更容易兼容 Oracle、MySQL 或 Spark SQL 的扩展点。
所以 libpg_query 应该是入口,不应该是系统内部的最终 AST。
libpg_query 的实现细节
libpg_query 的关键工程价值,是把 PostgreSQL parser 从 server 里抽出来,让它能作为普通 C library 使用。
早期版本里,一个重要实现点是使用 LLVM 相关的抽取流程,以 parser 为 root,从 PostgreSQL 源码里抽出 parser 需要的函数和 header。这样做的目的不是用 LLVM 来“编译 SQL”,而是解决一个更基础的问题:PostgreSQL parser 原本长在 PostgreSQL server 代码库里,直接复用会牵出大量 server 依赖。
所以这里的 LLVM 更像源码依赖抽取工具链的一部分,不是 query compiler,也不是 optimizer framework。
从使用者角度看,最重要的不是这个抽取过程本身,而是它带来的边界:
PostgreSQL source
-> extracted parser library
-> C API
-> JSON / protobuf parse tree
-> your adapter
这个边界让应用系统可以复用 PostgreSQL parser,而不用把整个 PostgreSQL server 嵌进去。
结论
现在做 SQL parser,最实际的默认选择是 PostgreSQL parser。
但正确架构不是:
SQL text -> PostgreSQL parse tree -> everywhere
而是:
SQL text -> PostgreSQL parse tree -> your parser tree -> rest of system
libpg_query 解决的是“如何可靠接入 PostgreSQL parser”。你自己的 parser tree 解决的是“如何让系统长期可维护、可升级、可兼容其他 SQL 方言”。
这两层都需要。只接 libpg_query,会太贴 PostgreSQL 内部;只写自己的 parser,又容易在 PostgreSQL 兼容性上消耗太多时间。比较稳的做法,是把 PostgreSQL 当成现实基准,把自己的 AST 当成系统边界。