Safe casting of PgNode values with bindgen tag discovery#2347
Merged
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
cbandy
commented
Jul 3, 2026
cbandy
commented
Jul 3, 2026
This comment was marked as resolved.
This comment was marked as resolved.
cbandy
marked this pull request as draft
July 10, 2026 12:36
cbandy
force-pushed
the
node-casting
branch
4 times, most recently
from
July 11, 2026 03:24
55ee800 to
673f4ad
Compare
cbandy
marked this pull request as ready for review
July 11, 2026 03:24
Contributor
Author
|
This is ready! The The code changes are small-ish: |
Comment on lines
+15
to
+20
| if Self::CAST_TAGS.contains(&node.node_tag()) { | ||
| Some(unsafe { &*core::ptr::from_ref(node).cast() }) | ||
| } else { | ||
| None | ||
| } | ||
| } |
Contributor
There was a problem hiding this comment.
I feel like this ought to allow casing to pg_sys::Node too, yeah? I don't see a need to include that in every CONST_TAGS definition, but hardcoding it here would be alright as an else if condition
Contributor
Author
There was a problem hiding this comment.
Yep, done. Rather than include every tag on Node, the code generation overrides this function for Node so every node can cast to it: build.rs#L600-L610
#[inline]
fn try_cast<T: pg_sys::PgNode>(node: &T) -> Option<&Self> {
Some(node.as_node())
}
Contributor
Author
There was a problem hiding this comment.
This function and the try_as method kinda swap arguments and types:
pg_sys::Expr::try_cast(&some_pg_node) -> Option<&pg_sys::Expr>
some_pg_node.try_as::<pg_sys::Expr>() -> Option<&pg_sys::Expr>pg_sys::Node::try_cast(&some_pg_node) -> Option<&pg_sys::Node>
some_pg_node.try_as::<pg_sys::Node>() -> Option<&pg_sys::Node>
some_pg_node.as_node() -> &pg_sys::Node
eeeebbbbrrrr
requested changes
Jul 11, 2026
Postgres implements various node types to represent parse, plan, and executor trees. It uses tagged polymorphism to emulate object-oriented inheritance in C: every node struct begins with a `NodeTag` field that identifies its type at runtime. Pointers to these nodes are accessed generically as `Node *` and queried using the `IsA` macro. The hierarchy includes intermediate base types, such as `Expr`, which are inherited by concrete types like `Const` and `Var`. This commit introduces type-safe node casting (both upcasting and downcasting) via the `PgNode` trait. Safe casts are statically generated with the trait implementations emitted by `pgrx-bindgen`. Each node type may be cast to any of its "parent" types, all the way up to `Node`, because they inherently occupy less space in memory and on the stack. The `PgNode` trait is extended with: - A constant `CAST_TAGS` slice containing the valid tags for the type and all of its descendants. This is efficient because the hierarchy is shallow and wide. The vast majority of types have four or fewer tags in `CAST_TAGS`. - A `try_cast` associated function and a `try_as` method check `CAST_TAGS` before performing an unsafe pointer cast. The lookup is a simple linear scan and relies on compiler and CPU optimizations. - A `node_tag` method that returns the tag and an `is_a` method that behaves like the `IsA` macro. See: pgcentralfoundation#1048
Postgres 13 and 14 use the `Value` struct to represent multiple possible value types, including integer, float, and string. Each type has its own `NodeTag`, but they can't be automatically associated with the struct using the information in the bindgen bindings. This hard-codes the associations just before emitting all the `PgNode` implementations. Postgres 15 introduced separate structs for each value type. These fit nicely into the automatic struct-to-tag scheme, but we weren't handling the `ValUnion` union Postgres uses to represent these multiple types in a single `Node`. This extends the detection of parent-child types to implement `PgNode` for union nodes.
eeeebbbbrrrr
approved these changes
Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Postgres implements various node types to represent parse, plan, and executor trees. It uses tagged polymorphism to emulate object-oriented inheritance in C: every node struct begins with a
NodeTagfield that identifies its type at runtime. Pointers to these nodes are accessed generically asNode *and queried using theIsAmacro. The hierarchy includes intermediate base types, such asExpr, which are inherited by concrete types likeConstandVar.This PR introduces type-safe node casting (both upcasting and downcasting) via the
PgNodetrait. Safe casts are statically generated with the trait implementations emitted bypgrx-bindgen. Each node type may be cast to any of its "parent" types, all the way up toNode.The
PgNodetrait is extended with:CAST_TAGSslice containing the valid tags for the type and all of its descendants. This is efficient because the hierarchy is shallow and wide. The vast majority of types have four or fewer tags inCAST_TAGS.try_castassociated function and atry_asmethod checkCAST_TAGSbefore performing an unsafe pointer cast. The lookup is a simple linear scan and relies on compiler and CPU optimizations.node_tagmethod that returns the tag and anis_amethod that behaves like theIsAmacro.Postgres 13 and 14 use the
Valuestruct to represent multiple possible value types, including integer, float, and string. Each type has its ownNodeTag, but they can't be automatically associated with the struct using the information in the bindgen bindings. This hard-codes the associations just before emitting all thePgNodeimplementations.Postgres 15 introduced separate structs for each value type. These fit nicely into the automatic struct-to-tag scheme, but we weren't handling the
ValUnionunion Postgres uses to represent these multiple types in a singleNode. This extends the detection of parent-child types to implementPgNodefor union nodes.See: #1048