java.lang.Object
com.darkcollective.relix.ast.internal.RelNodeOperands

public final class RelNodeOperands extends Object
The operand-level counterpart of RelNode.children(): a single, exhaustive registration point for the Operand and Predicate expressions each RelNode carries directly.

children() answers "what relations feed this node"; this answers "what expressions does this node evaluate". The second question had no single answer before — every analysis that needed it either wrote its own partial switch or, more often, missed the expressions entirely. Two node kinds are the classic blind spot, because they carry expressions that are not relational children and therefore do not show up in any structural traversal at all: RelationFunctionCall.arguments() (a leaf, so children() is empty) and LateralJoinNode.arguments() (children() reports only the left input).

Like children(), the implementation is an exhaustive switch over the sealed hierarchy with no default arm, so a new RelNode does not compile until its expressions are registered here. That is the point: an analysis built on this walker cannot silently go stale when the AST grows.

The walk comes in two forms — forEach(com.darkcollective.relix.ast.RelNode, java.util.function.Consumer<com.darkcollective.relix.ast.Operand>, java.util.function.Consumer<com.darkcollective.relix.ast.Predicate>) reads the expressions, and map(com.darkcollective.relix.ast.RelNode, java.util.function.UnaryOperator<com.darkcollective.relix.ast.Operand>, java.util.function.UnaryOperator<com.darkcollective.relix.ast.Predicate>) rewrites them, returning the rebuilt node. Both are exhaustive, so the "which expressions does this node carry" and "how is this node rebuilt around new expressions" knowledge sits in one place rather than being re-derived per consumer.

What it does and does not recurse

This is a shallow walk: it reports the expressions of node itself and does not descend into its children. Compose it with RelNode.children() to walk a whole tree, and with OperandWalker to descend within a reported operand or predicate:


 static void walkTree(RelNode node, Consumer<FunctionCall> onCall) {
     RelNodeOperands.forEach(node,
             operand   -> OperandWalker.walk(operand,   attr -> { }, onCall),
             predicate -> OperandWalker.walk(predicate, attr -> { }, onCall));
     node.children().forEach(child -> walkTree(child, onCall));
 }
 

EmptyRelationNode.heading() is deliberately not reported. It is an inert carrier for a replaced sub-expression rather than something the node evaluates — the same reason children() omits it.

This module carries no dependencies beyond the JDK, so the walker is safe to reuse from any module that depends on the AST.