8000 perf(postgres): read libpg_query's tree from its c nodes by colinhacks · Pull Request #56 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

perf(postgres): read libpg_query's tree from its c nodes - #56

Merged
colinhacks merged 2 commits into
mainfrom
postgres-direct-tree
Oct 23, 2026
Merged

colinhacks merged 2 commits into
mainfrom
postgres-direct-tree

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

The Postgres engine no longer goes through protobuf to get libpg_query's parse tree, or to deparse one. pg_query::parse had libpg_query encode the C tree as protobuf, then prost decode it into pg_query::protobuf's structs; pg_query::deparse encoded those again for libpg_query to decode back into C nodes. The engine now reads the C nodes straight into the same structs, and builds C nodes from them for the deparser.

Before After
packages/postgres/engine.wasm 3,848,749 bytes 3,166,146 bytes (−683 KB)
Parsing Postgres 18's 49,975 regression statements, native 355 ms 175 ms

How

engine/postgres/build.rs generates both converters from the files libpg_query generates its own protobuf encoder and decoder from: pg_query_outfuncs_defs.c, pg_query_readfuncs_defs.c, their _conds.c dispatch files and pg_query_enum_defs.c, matched against pg_query's prost structs. Every node and field libpg_query encodes is read, the same way; a field kind the generator does not know stops the build. bindgen gives the C structs for each target. The few nodes libpg_query handles by hand (lists, the value nodes, A_Const) are written by hand in src/raw.rs, as there.

  • Reading (raw::parse): pg_query_raw_parse in a memory context, then the tree into pg_query::protobuf::ParseResult. The depth bound is counted exactly as before: past half of PG_QUERY_PROTOBUF_MAX_DEPTH generic nodes, as _outNode counts them, or a message that deep, as upb's encoder counts them, so every statement that parsed before parses and every one refused is refused.
  • Deparsing (raw::deparse): the tree built into C nodes in memory the engine owns, then csrc/deparse.c, which runs libpg_query's deparser with default options under PG_TRY, so an error never unwinds through Rust frames.
  • stack::parse returns a Parsed holding the tree: pg_query::ParseResult also walks the whole tree to list its tables and functions, which only the analysis's read and write sets use (Parsed::listed).

Verified

  • Every statement of Postgres 18's regression suite (src/test/regress/sql of REL_18_STABLE, 49,975 statements) gives the same tree or the same refusal as pg_query::parse, and each tree deparses to the same text as pg_query::deparse. The test that checks it runs on demand over a corpus (REYLO_PG_CORPUS); one that always runs does the same over every Postgres statement of the oracle's cases.
  • The Postgres engine's tests, the oracle on every case, the loader's tests (the depth tests at each bound included) and the Postgres labs pass.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 03:22
@vercel
vercel Bot commented Oct 23, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 5:20am UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues. One gap in the fingerprint and one avoidable clone, both inline.

Reviewed changes

I reviewed the whole of the single commit, which replaces the protobuf round trip through pg_query::parse and pg_query::deparse with a reader and builder that work directly on libpg_query's C nodes.

  • Generated converters: engine/postgres/build.rs runs bindgen over libpg_query's node headers. It also scrapes libpg_query's out/read defs, their conds and its enum defs, plus prost's protobuf.rs, and from those it emits raw_convert.rs (C → pb) and raw_build.rs (pb → C). A field kind it doesn't know stops the build.
  • Reading (raw::parse): calls pg_query_raw_parse inside a memory context. A Depth counter copies the patched _outNode bound and upb's message depth. Too-deep trees are now refused with the new text "parse tree is nested too deeply to read", and stack::too_deep still matches it.
  • Deparsing (raw::deparse and csrc/deparse.c): the C nodes live in a zeroed arena owned by Rust, and libpg_query's deparser runs under PG_TRY with default options, the same way pg_query_deparse_protobuf_opts does. I checked the pinned postgres_deparse.c: every lappend, pfree and list_free call works on its own DeparseState and never on the input nodes. So nodes allocated by Rust are safe to hand it.
  • Parsed: stack::parse now returns the bare tree. The table listing through pg_query::ParseResult only happens on demand, through Parsed::listed().
  • Tests: every Postgres statement in the oracle is compared against pg_query's tree and deparse output on every run. The same comparison over a whole corpus can be run on demand (REYLO_PG_CORPUS).
  • Artifacts: all three engine.wasm files are rebuilt.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/postgres/build.rs Resolved
Comment thread engine/postgres/src/raw.rs Resolved
@colinhacks
colinhacks force-pushed the postgres-direct-tree branch 2 times, most recently from daecc34 to e137804 Compare October 23, 2026 03:50
The engine had libpg_query encode each tree as protobuf for prost to
decode, and the reverse to deparse. Converters generated from
libpg_query's own outfuncs and readfuncs now read and build the C
nodes directly: the same trees and deparsed text on all of Postgres
18's regression statements, 682 KB less engine, parsing twice as fast.
@colinhacks
colinhacks force-pushed the postgres-direct-tree branch from e137804 to 61fb567 Compare October 23, 2026 05:19
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. Both earlier findings are fixed.

Reviewed changes

I reviewed 61fb567, which addresses the two findings from the previous review.

  • Fingerprint covers csrc/: engine/postgres/build.rs now hashes csrc along with build.rs, src and vendor. The extra rerun-if-changed=csrc line is gone because emit already prints one for each entry, and packages/postgres/engine.wasm is rebuilt. tests/fingerprint.test.ts passes for all three artifacts.
  • Guard test: the new test the_fingerprint_hashes_every_source_of_the_crate reads the emit list from build.rs and fails if any top-level entry of the crate is missing from it. Dot-entries, Cargo.toml and examples/ are skipped. The crate's top level today is Cargo.toml, build.rs, csrc, examples, src and vendor, so the test passes.
  • No clone in Parsed::listed: it now takes self and moves the tree into ParseResult::new. Its only caller, analyze.rs, doesn't use parsed after that call.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit 671a70f into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the postgres-direct-tree branch October 23, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0