Skip to content

Safe casting of PgNode values with bindgen tag discovery#2347

Merged
eeeebbbbrrrr merged 2 commits into
pgcentralfoundation:developfrom
cbandy:node-casting
Jul 13, 2026
Merged

Safe casting of PgNode values with bindgen tag discovery#2347
eeeebbbbrrrr merged 2 commits into
pgcentralfoundation:developfrom
cbandy:node-casting

Conversation

@cbandy

@cbandy cbandy commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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 PR 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.

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.

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.

See: #1048

@cbandy

This comment was marked as resolved.

Comment thread pgrx-pg-sys/src/include/pg13.rs
Comment thread pgrx-pg-sys/src/node.rs
@cbandy

This comment was marked as resolved.

@cbandy
cbandy marked this pull request as draft July 10, 2026 12:36
@cbandy
cbandy force-pushed the node-casting branch 4 times, most recently from 55ee800 to 673f4ad Compare July 11, 2026 03:24
@cbandy
cbandy marked this pull request as ready for review July 11, 2026 03:24
@cbandy

cbandy commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

This is ready! The diff --stat is huge because I included the changes to the generated files to aide in review:

git diff --stat -- pgrx-pg-sys/src/include
 7 files changed, 11323 insertions(+), 3191 deletions(-)

The code changes are small-ish:

git diff --stat -- pgrx-bindgen pgrx-pg-sys/src/node.rs
 2 files changed, 404 insertions(+), 153 deletions(-)

Comment thread pgrx-pg-sys/src/node.rs
Comment on lines +15 to +20
if Self::CAST_TAGS.contains(&node.node_tag()) {
Some(unsafe { &*core::ptr::from_ref(node).cast() })
} else {
None
}
}

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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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())
}

@cbandy cbandy Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

cbandy added 2 commits July 12, 2026 03:50
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
eeeebbbbrrrr merged commit 368c036 into pgcentralfoundation:develop Jul 13, 2026
16 checks passed
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