Class AstBuilders

java.lang.Object
com.darkcollective.relix.ast.AstBuilders
Direct Known Subclasses:
Expr, ScriptBuilders

public abstract class AstBuilders extends Object
The AST authoring surface — one factory per node kind, for tests and embedders that build a tree directly rather than parsing one.

The AST is the engine's public contract, so constructing it must not require the grammar. These factories are that alternative: every concrete RelNode, Predicate and Operand kind has one, and AstBuilderCoverageTest fails the build if a newly permitted kind arrives without it.

The one rule

A factory takes the record's own components minus SourceLocation — which defaults to SourceLocation.UNKNOWN — arranged so that the relation inputs sit where the operator's own notation puts them. Where a node offers a narrower convenience constructor, the factory is overloaded to match it.

That arrangement is decided by the operator, not by the record, because the records themselves are not consistent about it — most declare the input last, seven declare it first, and UnnestNode declares it fourth. Reproducing each record's own order would publish that inconsistency as the thing a caller has to learn. So:

  1. A leaf takes no relation. rel("Orders"), unitRel().
  2. A unary operator's input goes last, after the operator's own arguments, so the call reads as the operator does: select(pred, rel("R")) is σpred(R), and closure("src", "dst", rel("Edges")) is exactly how CLOSURE src, dst (Edges) is written.
  3. A join or set operation's inputs lead, before any modifier, so the call reads as the infix operator does: join(left, right, cond) is L ⋈cond R, union(a, b) is A ∪ B. This is what puts lateral(left, "f", args) here rather than under (2) — its right input is a table-function call rather than a relation, but it is a join.
  4. A binder precedes what it scopes. fixpoint(name, base, step) and iterate(name, base, step, stop) are the nodes where a name scopes an input: step cannot be read without knowing what name binds, so it leads its two relations.

AstBuilderOrderTest holds every factory to this, so the rule is checked rather than remembered — the four graph operators (closure, cluster, path, trace) each took their input first until it was, which put them in silent disagreement with their own surface syntax.

List-valued components are taken as List; the element helpers (attrs(java.lang.String...), cols(java.lang.String...), asc(java.lang.String), desc(java.lang.String), projected(com.darkcollective.relix.ast.Operand), agg(com.darkcollective.relix.ast.AggregateOperator, java.lang.String)) are what keep the call sites short:

RelNode plan = project(attrs("dept", "total"),
                       groupBy(cols("dept"), List.of(agg(AggregateOperator.SUM, "amount")),
                               select(cmp(attr("status"), ComparisonOperator.EQUAL, str("OPEN")),
                                      rel("Orders"))));

Extend this class to reach the factories unqualified, or import them statically.

This is main source, not a test fixture: building a tree without the grammar is what the AST being the engine's public contract means, and a surface published from testFixtures is one embedders were told to use but could not depend on. The consequence is that these factories carry the same compatibility promise as the records they build — a renamed record component is a breaking change here, not a test edit.

For a shorter spelling of the same nodes — eq(attr("id"), num(42)) rather than cmp(attr("id"), ComparisonOperator.EQUAL, num("42")) — and for building literals from Java values instead of their source spelling, see Expr, which extends this class.

See Also: