RelNode tree into an executable
PhysicalNode plan, fixing every physical decision up front:
- Join algorithm —
PhysicalNode.JoinAlgorithm.MERGEwhen at least one input already delivers an ordering satisfying the join keys (the other gets aPhysicalNode.Sortenforcer inserted);PhysicalNode.JoinAlgorithm.HASHwhen an equi-join key exists but neither input is sorted; otherwisePhysicalNode.JoinAlgorithm.NESTED_LOOP. MERGE is eligible for INNER, NATURAL, SEMI, and ANTI joins. - Build side — for HASH/nested-loop: the cheaper input is built for
symmetric joins (inner, natural, full outer, product); the non-preserved
side is built for asymmetric joins. For MERGE joins the field is
PhysicalNode.BuildSide.RIGHTby convention (not used by the merge executor). - Schemas — each physical node's output schema is resolved from the
supplied
SchemaAnnotations(or the relation's declared schema for a scan), so the executor never re-infers or looks schemas up by identity.
Named views (QueryRelationSymbol) are inlined: a reference to
a view is planned by planning the view's body, so views never appear as plan
nodes.
The supplied SchemaAnnotations must cover every node the planner
visits — both the input tree and the bodies of any inlined views. A model's
SemanticModel.nodeSchemas() satisfies
this for an un-rewritten tree; a rewritten tree should be annotated via
SchemaInference first.
Thread safety
A Planner is bound to one symbol table and annotation set; it is
stateless beyond those and may be reused across plan(com.darkcollective.relix.ast.RelNode) calls.
-
Constructor Summary
ConstructorsConstructorDescriptionPlanner(SymbolTable symbols, SchemaAnnotations schemas) Creates a planner with no relation statistics; the cost model falls back to its tier-based heuristics for every build-side decision.Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics) Creates a planner backed by per-relation statistics, used to choose hash-join build sides by estimated row count (smaller side built) when both inputs' cardinalities are known, falling back to the tier model otherwise.Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections) Creates a planner with statistics and SQL pushdown enabled.Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections, QueryEventListener listener) AsPlanner(SymbolTable, SchemaAnnotations, Map, Map, Map), but also emits aQueryEventtolistenerfor each physical decision — a join's algorithm/build side and each sub-tree pushed down as SQL.Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections, QueryEventListener listener, BoundednessSource boundedness) AsPlanner(SymbolTable, SchemaAnnotations, Map, Map, Map, QueryEventListener), plus a per-leafBoundednessSourcethat drives the join build-side rule: an unbounded input must be the probe side, never the hash build side; both sides unbounded is a plan-timeBoundednessException.Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections, QueryEventListener listener, BoundednessSource boundedness, FunctionCatalog functions) AsPlanner(SymbolTable, SchemaAnnotations, Map, Map, Map, QueryEventListener, BoundednessSource), plus the function catalogue the tree was analysed against. -
Method Summary
Modifier and TypeMethodDescriptionThe estimated row counts recorded while planning, keyed by plan node.static OrderingmergeOrdering(List<Integer> keyIndices, Schema schema) Produces the physical plan fornode.withFunctionContext(FunctionContext context) Lets this planner evaluate a call that is constant for the run —NOW()— and push the resulting value, rather than declining the fold because the backend would answer from its own clock.Lets this planner prefer a row count a previous run actually produced over the one the cost model would compute for the same expression.withSolvers(SolverCatalog catalog)
-
Constructor Details
-
Planner
Creates a planner with no relation statistics; the cost model falls back to its tier-based heuristics for every build-side decision.- Parameters:
symbols- the symbol table used to resolve relations (and inline views); must not be nullschemas- per-node schemas covering the tree(s) to be planned; must not be null
-
Planner
public Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics) Creates a planner backed by per-relation statistics, used to choose hash-join build sides by estimated row count (smaller side built) when both inputs' cardinalities are known, falling back to the tier model otherwise.- Parameters:
symbols- the symbol table used to resolve relations (and inline views); must not be nullschemas- per-node schemas covering the tree(s) to be planned; must not be nullstatistics- canonical relation name → statistics (as inSemanticModel.statistics()); must not be null
-
Planner
public Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections) Creates a planner with statistics and SQL pushdown enabled. Sub-trees over a single connection's tables are translated to aPhysicalNode.PushedScanand run in the database; everything else is planned in-engine as usual.- Parameters:
symbols- the symbol table used to resolve relations (and inline views); must not be nullschemas- per-node schemas covering the tree(s) to be planned; must not be nullstatistics- canonical relation name → statistics; must not be nullsources- canonical name → source declaration (as inSemanticModel.sources()); must not be nullconnections- canonical name → connection declaration (as inSemanticModel.connections()); must not be null
-
Planner
public Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections, QueryEventListener listener) AsPlanner(SymbolTable, SchemaAnnotations, Map, Map, Map), but also emits aQueryEventtolistenerfor each physical decision — a join's algorithm/build side and each sub-tree pushed down as SQL.- Parameters:
symbols- the symbol table; must not be nullschemas- per-node schemas covering the tree(s); must not be nullstatistics- canonical relation name → statistics; must not be nullsources- canonical name → source declaration; must not be nullconnections- canonical name → connection declaration; must not be nulllistener- notified on each physical decision; must not be null (useQueryEventListener.NONEfor no observation)
-
Planner
public Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections, QueryEventListener listener, BoundednessSource boundedness) AsPlanner(SymbolTable, SchemaAnnotations, Map, Map, Map, QueryEventListener), plus a per-leafBoundednessSourcethat drives the join build-side rule: an unbounded input must be the probe side, never the hash build side; both sides unbounded is a plan-timeBoundednessException.- Parameters:
boundedness- the per-leaf boundedness lookup; must not be null (useBoundednessSource.ALL_BOUNDEDwhen every leaf is finite)
-
Planner
public Planner(SymbolTable symbols, SchemaAnnotations schemas, Map<String, RelationStatistics> statistics, Map<String, SourceDeclaration> sources, Map<String, ConnectionDeclaration> connections, QueryEventListener listener, BoundednessSource boundedness, FunctionCatalog functions) AsPlanner(SymbolTable, SchemaAnnotations, Map, Map, Map, QueryEventListener, BoundednessSource), plus the function catalogue the tree was analysed against.It is read in two places. Inlining a table-valued function call substitutes the call's arguments into the function's body, and that freshly-built body has to be re-annotated before it can be planned; passing the analysis catalogue (
SemanticModel.functions()) is what makes a call inside such a body type exactly as it did during analysis. Pushdown reads it too: a function's backend spelling comes from the function itself, so a call folds into a pushed query only when the catalogue holds it.The other constructors supply an empty catalogue. Under one, a call in an inlined body that the symbol table does not already hold types as
ANY, and no function folds into a pushed query — a slower plan, never a wrong one. Supply the catalogue the tree was analysed against and both follow.- Parameters:
functions- the catalogue the tree was analysed against; must not be null
-
-
Method Details
-
withObservedCardinalities
Lets this planner prefer a row count a previous run actually produced over the one the cost model would compute for the same expression.A wither rather than a constructor parameter for the reason
withSolvers(com.darkcollective.relix.solver.SolverCatalog)is one: this class already has six constructors, and the seventh argument nobody passes is how a constructor list becomes unreadable.- Parameters:
observed- the recorded counts; must not be null- Returns:
- this planner, for chaining
-
withFunctionContext
Lets this planner evaluate a call that is constant for the run —NOW()— and push the resulting value, rather than declining the fold because the backend would answer from its own clock.The context must be the one the execution will use, because the substituted value has to equal the one the unfolded half of the same query computes. That is why it is passed rather than made here:
ExecutionContextpins the run's instant, and this is that instant travelling to the renderers.- Parameters:
context- the run's ambient state; must not be null- Returns:
- this planner, for chaining
-
withSolvers
-
estimates
The estimated row counts recorded while planning, keyed by plan node.Populated by
plan(RelNode)and empty before it runs. A node the planner built inside another arm rather than throughplan— an order-enforcingSortwrapper, say — carries no estimate, which reads as unknown; seePlanEstimatesfor why unknown is deliberately not zero.- Returns:
- the estimates for the plan this instance produced; never null
-
plan
Produces the physical plan fornode.Each node is costed as it is built, and the estimate is filed in
estimates()rather than carried on the node — seePlanEstimatesfor the reasoning. The estimate is taken from the logical node, which is where the cost model's statistics apply; a pushed-down scan is therefore costed as the sub-tree it replaced, which is the number a reader wants (how many rows come back), not the number of tables it folded.- Parameters:
node- the logical root to plan; must not be null- Returns:
- the executable physical plan
-
mergeOrdering
Builds the requiredOrderingfor a merge join onkeyIndices(column positions inschema). UsesSortDirection.ASCfor all keys — the planner only promotes MERGE when the delivered ordering already satisfies this (or inserts an ASC enforcer), so direction is consistent on both sides.Also the ordering a merge join delivers: shared with
PhysicalNode.Join.deliveredOrdering()so the required and delivered forms are one definition rather than two that can drift.
-