Open-source project
pganalyze/pg_query avatar
pganalyze/pg_query

pg_query parses SQL with the real PostgreSQL parser

Ruby extension to parse, deparse and normalize SQL queries using the PostgreSQL query parser

888 stars98 forksCBSD-3-Clause

At a glance

What is it?
pg_query is a Ruby extension that uses the actual PostgreSQL server source to parse SQL and return the internal PostgreSQL parsetree, and it can normalise queries and fingerprint them the way pg_stat_statements does. The price of that fidelity is a native extension that compiles parts of PostgreSQL, which means a slow install and a CRuby only support matrix.
Who is it for?
Adopt pg_query if you need to analyse SQL in Ruby with exactly the same grammar the server uses, for a query linter, an access control layer, a slow query reporter or a migration checker, and if you are prepared to compile a native extension. Do not adopt it if you need a pure Ruby parser, since a different grammar will give different answers, and do not adopt it on JRuby or TruffleRuby, because the README states both are unsupported.
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 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The same grammar the server uses, at the cost of a compile

pg_query is a Ruby extension that uses the actual PostgreSQL server source to parse SQL queries and return the internal PostgreSQL parsetree. It also allows you to normalise queries, replacing constant values with $n, and parse those normalised queries into a parsetree again.

The README is upfront about the cost. When you build the extension it builds parts of the PostgreSQL server source, through the libpg_query project, and statically links it into the extension. The README's own words are that this may seem like a lot of complexity, but it is the only reliable way of parsing all valid PostgreSQL queries.

That is the trade in one sentence. A hand written or third party SQL parser will be lighter, and it will also disagree with the server on the queries you care about, because PostgreSQL's grammar has enough corners that the corners are where your production queries live. If your application blocks a query because a parser rejected it, and the server would have run it, that is a bug in your application caused by the parser.

The audience is therefore specific: a Ruby developer who must reason about SQL the way the database does, rather than approximately. A query linter, a row level security layer that has to know which tables a statement touches, a tool that reports slow queries, a migration checker, a data pipeline that rewrites queries. For all of them, using the server's own parser removes an entire category of disagreement.

The project is a Ruby gem with a C extension, primary language C, BSD-3-Clause licensed, with the last push on 2026-10-01 and no GitHub releases.

Normalising and fingerprinting, the pg_stat_statements way

The two features that make pg_query more than a parser are normalisation and fingerprinting, and they come from the same place.

Normalisation replaces constant values with positional parameters, exactly as pg_stat_statements does in Postgres 10 and later:

ruby
# Normalizing a query (like pg_stat_statements in Postgres 10+)
PgQuery.normalize("SELECT 1 FROM x WHERE y = 'foo'")

=> "SELECT $1 FROM x WHERE y = $2"

The point is grouping. Two queries that differ only in their literal values are the same query as far as a workload is concerned, and normalising them before you count or compare is what makes that comparison possible. A monitoring tool that counts raw query text reports a thousand queries where there is one.

Fingerprinting is the same idea taken further, producing a stable identifier for the shape of a query:

ruby
PgQuery.parse("SELECT 1").fingerprint

=> "50fde20626009aba"

PgQuery.parse("SELECT 2; --- comment").fingerprint

=> "50fde20626009aba"

Note that the second example changes both the literal and adds a comment, and the fingerprint is identical. There is also a faster form that skips the full parse tree:

ruby
PgQuery.fingerprint("SELECT $1")

=> "50fde20626009aba"

That last one is implemented inside the native C library, and it is the version to reach for in a hot path.

Tables, columns and a summary path that avoids protobuf

The extraction helpers are the everyday use of the library, and they come in two flavours.

The first is the parse tree route. Tables come out of the parsed query:

ruby
PgQuery.parse("SELECT $1 FROM x JOIN y USING (id) WHERE z = $2").tables

=> ["x", "y"]

and filter columns come out as pairs of table and column, with a nil table when the column is unqualified:

ruby
PgQuery.parse("SELECT $1 FROM x WHERE x.y = $2 AND z = $3").filter_columns

=> [["x", "y"], [nil, "z"]]

The second is the summary route, and the README explains the difference in performance terms. PgQuery.summary returns the same kind of information as PgQuery.parse, which is to say tables, functions, CTE names, aliases, filter columns and statement types, but gathers it inside the native C library instead of sending the full parse tree over protobuf, which is significantly faster for large queries.

ruby
summary = PgQuery.summary("SELECT $1 FROM x JOIN y USING (id) WHERE z = $2")

summary.tables
=> ["x", "y"]

summary.statement_types
=> ["SelectStmt"]

The summary object also has a truncation facility, with the limit passed in up front, which is useful when you are storing query text in a monitoring table and the text is long:

ruby
PgQuery.summary("SELECT a, b, c, d, e, f FROM xyz WHERE a = b", truncate_limit: 40).truncated_query

=> "SELECT ... FROM xyz WHERE a = b"

The README points at the comments on the SummaryParserResult class for how it differs from the ParserResult, which is where the details live.

Deparsing and walking, with two caveats worth reading

Because you have the tree, you can change it and turn it back into SQL. The README gives a simple deparse example:

ruby
parsed_query = PgQuery.parse("SELECT * FROM users")

# Modify the parse tree in some way
parsed_query.tree.stmts[0].stmt.select_stmt.from_clause[0].range_var.relname = 'other_users'

# Turn it into SQL again
parsed_query.deparse
=> "SELECT * FROM other_users"

And for anything more general, walk! recursively visits the tree, which the README says can be used to create a bespoke pretty printer:

ruby
parsed_query = PgQuery.parse "SELECT * FROM tbl"
parsed_query.walk! { |node, k, v, location| puts k }

The same mechanism rewrites queries, and the README's example prefixes table names:

ruby
parsed_query.walk! do |node, k, v, location|
  next unless k.eql?(:range_var) || k.eql?(:relation)
  next if v.relname.nil?
  v.relname = "X_" + v.relname
end

parsed_query.deparse

The README then states the caveats plainly, and they are the part to internalise before you rewrite anything in production. First, some of the tree nodes are frozen, so you can replace them but you cannot modify them in place. Second, table rewriting is more nuanced than the example, because while it will rewrite the table names, it will not correctly handle all CTEs, and will not rewrite columns with explicit table names.

That second caveat is the kind of thing that turns a demo into a bug report. A rewrite that silently misses a CTE or a qualified column reference is worse than no rewrite, because the query still runs and returns the wrong rows.

Scanning gives you tokens with positions and keyword kinds

The lowest level API is a scanner, and it returns the token stream with the offsets and the keyword classification that the lexer produced:

ruby
PgQuery.scan('SELECT 1 --comment')

=> [<PgQuery::ScanResult: version: 160001, tokens: [
<PgQuery::ScanToken: start: 0, end: 6, token: :SELECT, keyword_kind: :RESERVED_KEYWORD>,
<PgQuery::ScanToken: start: 7, end: 8, token: :ICONST, keyword_kind: :NO_KEYWORD>,
<PgQuery::ScanToken: start: 9, end: 18, token: :SQL_COMMENT, keyword_kind: :NO_KEYWORD>]>,
 []]

Each token carries its start and end offsets, the token type, and whether the lexer considered it a reserved keyword. The offsets are what make a scanner useful for a tool that wants to point at a position in the original text rather than in a normalised form, and the keyword classification is what lets you distinguish a keyword from an identifier that happens to have the same spelling.

The README also mentions that scanning works on normalised queries, and the result carries a version number, 160001, which is the Postgres parser version the tree came from. That number is worth noting, because it tells you which grammar you are working with, and it is the same version marker that appears on the parse results.

Installation compiles PostgreSQL, and JRuby is out

The installation is one command, with a warning attached:

bash
gem install pg_query

Because it compiles parts of PostgreSQL, installation might take a while on slower systems, and the README says to expect up to 5 minutes. That is a real operational detail for CI pipelines, where a native extension that takes five minutes to build is a cache miss every time the cache is invalidated.

The Ruby support matrix is the other thing to plan around. The currently tested and officially supported versions are CRuby 3.2, CRuby 3.3, CRuby 3.4 and CRuby 4.0. Two implementations are explicitly not supported. JRuby, because pg_query relies on a C extension, which is discouraged and not properly supported for JRuby. And TruffleRuby, because GraalVM does not support sigjmp, which is used by the Postgres error handling code, and pg_query uses a copy of the Postgres parser and error handling code.

That second reason is a nice illustration of how deep the dependency goes. The extension carries Postgres's own error handling, and that code needs a setjmp style mechanism that one of the alternative Ruby runtimes does not provide. The limitation is not about Ruby compatibility in general, it is about one specific piece of C that came along with the parser.

The developer task section explains how the parser is kept current. To update to a newer Postgres parser, you first update libpg_query to the new Postgres version and tag a release, then update LIB_PG_QUERY_TAG and LIB_PG_QUERY_SHA256SUM in the Rakefile, then run rake update_source, and then commit the Rakefile and the modified files in ext/pg_query and make a pull request. The repository has a Rakefile, a Gemfile, a gemspec, an ext/ directory for the extension, a lib/ directory for the Ruby code and a spec/ directory for the tests, with rubocop and rspec configuration.

Editorial conclusion

Adopt pg_query if you need to analyse SQL in Ruby with exactly the same grammar the server uses, for a query linter, an access control layer, a slow query reporter or a migration checker, and if you are prepared to compile a native extension. Do not adopt it if you need a pure Ruby parser, since a different grammar will give different answers, and do not adopt it on JRuby or TruffleRuby, because the README states both are unsupported. Before you start, check which Ruby versions you can use against the supported list, and remember that the parser is version specific, so a query that the server accepts today may not be the same shape as one an older server produces.

Frequently asked questions

What does the pg_query Ruby gem do?

It uses the actual PostgreSQL server source to parse SQL queries and return the internal PostgreSQL parsetree. It can also normalise queries, replacing constant values with $n, and parse those normalised queries back into a parsetree.

How do I install pg_query and how long does it take?

Run gem install pg_query. Because the extension compiles parts of the PostgreSQL server source, the README warns that installation might take a while on slower systems, and says to expect up to 5 minutes.

Which Ruby versions support pg_query?

The tested and officially supported versions are CRuby 3.2, CRuby 3.3, CRuby 3.4 and CRuby 4.0. JRuby is not supported because the gem relies on a C extension, and TruffleRuby is not supported because GraalVM does not support sigjmp, which the Postgres error handling code uses.

What is the difference between PgQuery.parse and PgQuery.summary?

Summary returns the same information as parse, namely tables, functions, CTE names, aliases, filter columns and statement types, but gathers it inside the native C library instead of sending the full parse tree over protobuf, which the README says is significantly faster for large queries.

Can I rewrite a query with pg_query?

Yes, by modifying the parse tree and calling deparse, or by using walk! to visit and change nodes. The README warns that some tree nodes are frozen and can only be replaced, and that rewriting table names will not correctly handle all CTEs or rewrite columns with explicit table names.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. pganalyze/pg_query on GitHub
  4. README
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pganalyze-pg-query.svg)](https://hysenlabs.com/projects/pganalyze-pg-query)

Was this useful?