TL;DR

  1. PostgreSQL 已经是 dominate 的存在。很多系统即使不是 PostgreSQL,也会优先考虑 PostgreSQL SQL 方言的兼容性。
  2. libpg_query 提供了一个可用的 PostgreSQL parser 封装。它直接使用 PostgreSQL server source,可以在 server 外部解析 SQL,并返回 PostgreSQL 内部 parse tree。
  3. 接入 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 出来的语义是不是我支持的”。

但在工程上,版本号不能完全忽略。至少需要记录三层版本:

  1. libpg_query 的 release/tag,例如 18.0.0 或 17-6.x。
  2. 它对应的 PostgreSQL major version,例如 18、17、16。
  3. 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 当成系统边界。