pg_query_go: PostgreSQL's own parser exposed to Go
Go library to parse and normalize SQL queries using the PostgreSQL query parser
At a glance
- What is it?
- pg_query_go is a Go binding around the actual PostgreSQL server source, so a query can be parsed into the same internal parse tree the server itself builds, in JSON or as Go structs, and deparsed back into SQL. The cost is a cgo extension that compiles parts of PostgreSQL, so the first build takes minutes, and the reward is a parser that agrees with the server on every edge case.
- Who is it for?
- Adopt pg_query_go when you need to analyse, fingerprint, normalise or rewrite SQL with exactly the semantics the PostgreSQL server will apply, particularly in a database proxy, an audit tool or a query optimiser. Do not adopt it in a service where a cgo dependency and a multi-minute first build are unacceptable.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 22 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library does and why it uses the real server source
pg_query_go is the Go version of pg_query, and the README states its approach plainly: the Go library and its cgo extension use the actual PostgreSQL server source to parse SQL queries and return the internal PostgreSQL parse tree.
That is the whole design decision, and it is what separates this from a SQL parser written in Go. A parser reimplemented in another language will eventually disagree with the server about some construct, because the grammar is large and the specification leaves room for interpretation that the implementation settles. By compiling the server's own parser, this library cannot disagree with it, because it is the same code.
The tree shows the consequence. There is a parser/ directory holding the C sources, a pg_query.go file for the cgo bindings, and a pg_query.pb.go Protobuf definition. The go.mod is small, with google.golang.org/protobuf, go-cmp for test comparison, and hujson from Tailscale for JSON with comments.
The other structural element is the licence split, which is unusual and worth noticing. There is a LICENSE file for the Go code and a separate LICENSE.POSTGRESQL file, and the README records the copyright as Lukas Fittl for 2015 and then Duboce Labs, Inc. (pganalyze) for 2016-2025, with the note that the project includes code derived from PostgreSQL itself. The 3-clause BSD licence covers the Go binding, and the PostgreSQL licence covers the derived parser code. Anyone redistributing this in a product needs to keep both notices, which is a normal consequence of linking a permissive-licensed binding to a copyleft-licensed core, and the split is documented rather than left to be discovered.
The library is under the v6 module path, and the README links to a pganalyze blog post on why a query's parse tree is useful, which is the background reading for the design.
Installing it, and the three-minute first build
Installation is a single go get:
go get github.com/pganalyze/pg_query_go/v6@latestThe README then warns about what happens next, and this is the first thing anyone using the library will notice. Due to compiling parts of PostgreSQL, the first time you build against this library it will take a bit longer. Expect up to 3 minutes, and you can use go build -x to see the progress.
That is not a build system quirk, it is the direct consequence of the design decision. The parser directory contains the PostgreSQL C sources, which are compiled as part of your program through cgo. The Makefile shows the machinery behind it, with a target that downloads a tarball of the libpg_query sources at a pinned tag, extracts it, copies the sources down into a single directory, moves the PostgreSQL headers into place, and then generates the Go protobuf bindings by installing protoc-gen-go and running protoc against the bundled pg_query.proto definition.
The pinned upstream tag is 17-6.2.5, which tells you which PostgreSQL branch the bundled parser corresponds to. The Makefile marks everything from update_source onwards as only needed for releasing new versions, which separates the normal user build from the maintainer workflow.
There is a practical consequence for CI. A build cache that survives between runs makes this a non-issue, and a cold cache in every pipeline means every job pays three minutes. The go.mod declares Go 1.20, and the module path includes the major version, so this is the v6 line of a library that has changed its major version before.
The benchmarks live in benchmark_test.go at the root, and the Makefile has a benchmark target that builds with -a and runs the suite with a ten second benchtime, memory reporting and four CPUs.
Parsing to JSON and parsing to Go structs
There are two ways to consume a parse tree, and the README is explicit that the Go structs are the recommended one when you are working with the query information inside Go.
The JSON route is the shortest introduction:
package main
import (
"fmt"
pg_query "github.com/pganalyze/pg_query_go/v6"
)
func main() {
tree, err := pg_query.ParseToJSON("SELECT 1")
if err != nil {
panic(err)
}
fmt.Printf("%s\n", tree)
}The output is the raw PostgreSQL node structure, with a version number and a statement list. For SELECT 1 the JSON is a SelectStmt with a target list holding a single ResTarget whose value is an A_Const with the integer value 1. The node names are PostgreSQL's own, which is the point.
The Go struct route uses Parse:
package main
import (
"fmt"
pg_query "github.com/pganalyze/pg_query_go/v6"
)
func main() {
result, err := pg_query.Parse("SELECT 42")
if err != nil {
panic(err)
}
// This will output "42"
fmt.Printf("%d\n", result.Stmts[0].Stmt.GetSelectStmt().GetTargetList()[0].GetResTarget().GetVal().GetAConst().GetIval().Ival)
}The chain of getters is the Protobuf-generated accessors, and the README points at the pg_query.pb.go Protobuf definition for the full set of node types. Every getter returns a pointer and a nil check is folded in, so the chain does not panic on a missing node, which is the behaviour you want when walking a tree that came from arbitrary SQL.
That accessibility is the reason the Protobuf representation was chosen rather than hand-written Go structs. The same tree can be serialised, compared with go-cmp in tests, and produced in a language-neutral form for other tools to consume.
Deparsing a tree back into SQL, and the warning attached to it
The reverse direction is deparsing, which turns a parse tree back into a SQL statement.
package main
import (
"fmt"
pg_query "github.com/pganalyze/pg_query_go/v6"
)
func main() {
result, err := pg_query.Parse("SELECT 42")
if err != nil {
panic(err)
}
result.Stmts[0].Stmt.GetSelectStmt().GetTargetList()[0].GetResTarget().Val = pg_query.MakeAConstStrNode("Hello World", -1)
stmt, err := pg_query.Deparse(result)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", stmt)
}That program parses SELECT 42, replaces the constant with a string node, deparses it, and prints SELECT 'Hello World'. The node is built with MakeAConstStrNode and the location is passed as -1, which means no source location, and that is worth understanding because the location fields are how a tree is mapped back to positions in the original text.
Then there is the warning, and it is the most important line in the README for anyone doing query rewriting: it is currently not recommended to pass unsanitised input to the deparser, as it may lead to crashes.
That is a real constraint on the deparse path, and it is a different kind of statement from a bug report. The deparser takes a tree you may have constructed from untrusted input, and PostgreSQL's deparser is not hardened for that. Any use of Deparse in a service that handles third-party SQL has to treat the deparser as a component that can crash the process, which in Go means a recoverable panic only if you are careful, or a hard failure of the goroutine if you are not.
The parse path is unaffected by this. Parsing untrusted SQL is safe, because the parser is the same one the server runs against hostile input already.
PL/pgSQL parsing, fingerprints and normalisation
Beyond plain SQL, the library can parse a PL/pgSQL function body, which is marked experimental:
tree, err := pg_query.ParsePlPgSqlToJSON(
`CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name varchar, v_version varchar)
RETURNS varchar AS $$
BEGIN
IF v_version IS NULL THEN
RETURN v_name;
END IF;
RETURN v_name || '/' || v_version;
END;
$$ LANGUAGE plpgsql;`)The output is a different node family, PLpgSQL_function with datums and an action block of statements, each carrying its own lineno so you can map a construct back to a line in the function source.
The benchmark names reveal the two other capabilities the library provides beyond parsing and deparsing: fingerprinting and normalisation. Fingerprinting produces a stable identifier for a query shape so that two queries that differ only in their literal values can be grouped, which is what you want for a query statistics view. Normalisation produces a canonical form of the query with the varying parts factored out, which is what you want before comparing or storing queries.
The benchmark table shows how the three operations compare. On the same hardware, BenchmarkNormalizeSelect1 runs at 1133 ns/op with 32 B/op and 2 allocs/op, and BenchmarkFingerprintSelect1 at 1875 ns/op with 48 B/op, while the plain parse of the same query is 4186 ns/op with 1040 B/op and 18 allocs. The raw parse variants are faster still, at 3183 ns/op with 128 B/op and 3 allocs.
The parallel variants show what concurrency buys. BenchmarkParseSelect1Parallel runs at 1320 ns/op against 4186 ns/op for the serial version, so parsing scales across cores, which is what you need if you are parsing queries on the request path.
One note on the numbers, and it applies to all of them: the README states that allocation counts exclude the cgo portion, so they are higher than shown. That is the cost of moving a tree across the cgo boundary, and the figures do not include it.
Limits: a cgo dependency, a pinned parser version, and a deparser that can crash
The limits are visible in the README and in the repository layout.
The first is cgo. The library compiles PostgreSQL's C sources into your binary. That means a C toolchain is required, that cross-compilation is more involved than for a pure Go dependency, that the binary is much larger, and that the library cannot be used in environments where cgo is unavailable, such as a WebAssembly build or a serverless function with a pure Go runtime.
The second is the pinned parser version. The Makefile pins LIB_PG_QUERY_TAG to 17-6.2.5, so the grammar you parse with is the grammar of that PostgreSQL branch. Newer syntax is not understood until the pin is bumped and the vendored sources are regenerated, which is a maintainer step involving a tarball download, a source reshuffle and a protoc run. That is a real dependency on the upstream release cycle, and it means a bug fix or a new syntax feature in the server appears in this library only when someone updates the pin.
The third is the deparser warning. Not recommended for unsanitised input, because it may lead to crashes. Any service that rewrites SQL it did not write itself has to treat that path carefully.
The fourth is the experimental status of PL/pgSQL parsing. The README marks it experimental, and the PL/pgSQL node set is a different shape from the SQL one, so the tooling you would build around the SQL parse tree does not transfer directly.
The fifth is the licence mix. The Go code is 3-clause BSD, the derived PostgreSQL parts carry the PostgreSQL licence, and the repository carries both LICENSE files. That is manageable and documented, but it is more paperwork than a single-permission dependency.
The sixth is the release situation. The repository has no GitHub releases, so the module proxy and the tags are the version reference, and the CHANGELOG.md at the root is the record of what changed.
pg_query_go against a Go SQL parser and against asking the server
Two comparisons make the choice clearer.
The first is against a pure Go SQL parser, of which there are several good ones. The difference in approach is fidelity. A Go parser has its own interpretation of the grammar and its own set of supported constructs, and it will disagree with PostgreSQL on some inputs, usually at the edges. This library cannot disagree, because it is the server's parser.
That fidelity is worth real money in the applications where it matters. If you are writing a database proxy that has to normalise queries before forwarding them, or a migration tool that has to compare an application's queries against the schema, or an audit system that has to answer whether two logged statements are the same query, a parser that occasionally differs from the server produces wrong answers silently. The cost of this library is the cgo dependency and the build time, which for a tool that runs occasionally is nothing.
The second comparison is against asking the server to do the parsing. You could send the query to PostgreSQL with EXPLAIN or a plan-only statement and read back the parse tree, or use the server's own statistics views. The difference in approach is that the server will not give you the tree. It gives you a plan, which is a different thing: a plan is the result of planning, including the choices the planner made, and it is not the same shape as the parse tree. A query that fails to parse gives you an error rather than a tree.
There is also the round-trip question. If you want to rewrite a query, you need to parse it, modify the tree and deparse it, and the server will not do that for you at all. That three-step cycle, which this library supports directly, is what makes it a tool rather than a parser.
So the choice is between a convenient pure Go parser that is nearly right and a heavier binding that is exactly right. For a linter, a formatter or a query builder, the pure Go parser is usually enough. For anything that has to agree with a running server, this one is.
Editorial conclusion
Adopt pg_query_go when you need to analyse, fingerprint, normalise or rewrite SQL with exactly the semantics the PostgreSQL server will apply, particularly in a database proxy, an audit tool or a query optimiser. Do not adopt it in a service where a cgo dependency and a multi-minute first build are unacceptable. Verify first that your PostgreSQL version is covered by the bundled parser version, that the build toolchain can compile the vendored C, and that you never pass unsanitised input to the deparser, which the README warns can crash.
Frequently asked questions
What does pg_query_go use to parse SQL?
The README states that the Go library and its cgo extension use the actual PostgreSQL server source to parse SQL queries and return the internal PostgreSQL parse tree. The parser/ directory in the repository holds the vendored C sources, and the Makefile pins the upstream libpg_query tag it was generated from.
How do you parse a query into a Go struct with pg_query_go?
Use the Parse method, which the README recommends when working with the query information inside Go. The result exposes Stmts, and the Protobuf-generated getters chain down to the node you need, for example result.Stmts[0].Stmt.GetSelectStmt().GetTargetList()[0].GetResTarget().GetVal().GetAConst().GetIval().Ival.
Is it safe to deparse untrusted SQL with pg_query_go?
No. The README states that it is currently not recommended to pass unsanitised input to the deparser, as it may lead to crashes. Parsing untrusted SQL is the safe direction, since it uses the same parser the server runs.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pganalyze-pg-query-go)Was this useful?