TL;DR

  1. PostgreSQL is already dominant. Even systems that are not PostgreSQL often want PostgreSQL-style SQL compatibility first.
  2. libpg_query gives you a usable wrapper around the PostgreSQL parser. It uses PostgreSQL server source and lets code outside the server parse SQL into PostgreSQL's internal parse tree.
  3. After adopting libpg_query, do not let PostgreSQL parse trees spread through the whole system. Define your own parser tree as an intermediate representation. That isolates PostgreSQL-side changes and makes it easier to support other parsers, such as Oracle compatibility.

Details

If I had to choose a parser today for a database system, analytics system, SQL lint tool, or query rewrite tool, I would start with a practical question: what SQL will users try first?

Usually the answer is not a textbook SQL subset. It is PostgreSQL-style SQL. PostgreSQL has a strong ecosystem, documentation base, extension model, and cloud footprint. Even when a system is not built on PostgreSQL, supporting PostgreSQL-compatible SQL lowers migration and learning cost.

So parser work should not start with "should I write a full SQL grammar myself?" A more practical starting point is: connect to the PostgreSQL parser first.

What libpg_query solves

libpg_query is a C library maintained by pganalyze. Its goal is direct: make the PostgreSQL parser usable outside the PostgreSQL server.

It is not a handwritten PostgreSQL-like parser. It uses actual PostgreSQL server source to parse SQL and returns PostgreSQL's internal parse tree. That distinction matters. A parser that merely looks like PostgreSQL will drift on edge cases. Reusing PostgreSQL source gives behavior much closer to real PostgreSQL.

A typical call is simple:

PgQueryParseResult result;

result = pg_query_parse("SELECT 1");
printf("%s\n", result.parse_tree);
pg_query_free_parse_result(result);

The returned parse tree also includes a parser version. In the current libpg_query README example, the top-level parse tree includes:

{
  "version": 180004,
  "stmts": [
    ...
  ]
}

That version field matters because the parse tree is not a stable cross-version API. You should not assume PostgreSQL 16, 17, and 18 always return the same fields and node shapes.

Which parser version should matter

In practice, many projects blur the PostgreSQL parser version. Not because the version is irrelevant, but because users mostly care about whether the SQL parses and whether the supported semantics are correct.

Engineering still needs version boundaries. At minimum, record three versions:

  1. The libpg_query release or tag, such as 18.0.0 or 17-6.x.
  2. The corresponding PostgreSQL major version, such as 18, 17, or 16.
  3. The version field in the parse result, such as 180004.

I would not put version checks everywhere in business logic. Put those differences at the parser adapter boundary, then emit your own AST.

SQL text
  -> libpg_query / PostgreSQL parse tree
  -> parser adapter
  -> system AST
  -> binder / analyzer / optimizer

When PostgreSQL parser behavior changes, you mostly update the adapter instead of rewriting the rest of the system.

Why you still need your own parser tree

It is tempting to use the PostgreSQL parse tree directly. It is rich, detailed, and already works. Long term, that couples your system to PostgreSQL internals.

The PostgreSQL parse tree is PostgreSQL's own internal structure. It is not a stable IR designed for your system. Field names, node shapes, defaults, and extension points serve PostgreSQL's parser, analyzer, and executor.

Your system may need different priorities:

  • a simpler expression tree
  • clearer table reference, join, and projection structures
  • nodes better suited for rule rewriting
  • metadata for permissions, lineage, linting, and catalog binding
  • extension points for Oracle, MySQL, or Spark SQL compatibility

So libpg_query should be an entry point, not the final AST used everywhere.

How libpg_query works

The important engineering value of libpg_query is that it extracts the PostgreSQL parser from the server codebase and packages it as a normal C library.

In earlier versions, one key implementation detail was an LLVM-based extraction flow. Starting from the parser as the root, it pulled the needed functions and headers from PostgreSQL source. The purpose was not to use LLVM to compile SQL. It solved a dependency problem: the PostgreSQL parser originally lives inside the PostgreSQL server tree, and using it directly pulls in many server dependencies.

For users, the important boundary is:

PostgreSQL source
  -> extracted parser library
  -> C API
  -> JSON / protobuf parse tree
  -> your adapter

That boundary lets applications reuse the PostgreSQL parser without embedding the whole PostgreSQL server.

Conclusion

For a SQL parser today, PostgreSQL is the most practical default.

But the right architecture is not:

SQL text -> PostgreSQL parse tree -> everywhere

It is:

SQL text -> PostgreSQL parse tree -> your parser tree -> rest of system

libpg_query solves the problem of reliably connecting to the PostgreSQL parser. Your own parser tree solves the problem of keeping the system maintainable, upgradeable, and open to other SQL dialects.

Both layers are needed. Using only libpg_query ties you too closely to PostgreSQL internals. Writing only your own parser burns too much time on PostgreSQL compatibility. The stable path is to treat PostgreSQL as the real-world baseline and your own AST as the system boundary.