Class OptimizationPipeline
A pipeline is an ordered list of OptimizationPipeline.Phases; a phase is an ordered list of
OptimizationRules plus the number of times that group may be re-run.
QueryOptimizer builds the default pipeline
once per query and calls run(com.darkcollective.relix.ast.RelNode, java.lang.String, com.darkcollective.relix.semantic.SchemaAnnotations, com.darkcollective.relix.optimizer.internal.OptimizationContext).
Why phases, and not one global loop
Some pairs of rules are mutual inverses and would oscillate forever if iterated together:
SEL-003(σ below π) andPROJ-003(π below σ);SEL-001(split a conjunction) andSEL-002(merge adjacent selections);SEL-009(distribute a σ over a set operation) andSEL-010(merge a set operation over two selections of one input).
Each such pair is split across two phases, and phases run once each in order —
only the rules within a phase are iterated. That is why the pushdown
phase deliberately excludes ProjectionPass and SelectionMergePass,
which live in the cleanup phase that follows it. Any new rule must be
placed in a phase whose other members it cannot undo.
What iteration buys
Within a phase, one rule's rewrite can expose the pattern a rule earlier
in the same phase matches on, and that earlier rule has already had its turn. The
demonstrable case is DIST-001 removing a δ from between two selections: the
σ pair is now adjacent, which is exactly what SEL-002 merges — and
SEL-002 runs before DIST-001 in the cleanup phase, so without a
second sweep the merge never happens (PipelineConvergenceTest pins this at
cap 1 versus cap 8).
What iteration does not buy is a cascade within one rule: every
pass here recurses bottom-up, so it already reaches its own fixpoint in a single
application. AGG-001 is the case to be clear about — a three-deep γ stack
collapses fully inside one apply, because each input is rewritten
before the rule is tested at the node above it. With today's rule set the default
pipeline settles in one sweep per phase on every shape tested; the loop's value is
the guarantee it gives the next rule, at a cost of one confirming sweep per
phase that fired.
Termination
A sweep counts as progress only when the phase's rules both recorded at
least one firing in the OptimizationContext and returned a
different tree — reference inequality, relying on RelNode.mapChildren(java.util.function.UnaryOperator<com.darkcollective.relix.ast.RelNode>)'s
documented contract that it returns this when no child changed. The first
sweep that makes no progress ends the phase. Iteration is additionally capped at
OptimizationPipeline.Phase.maxIterations() sweeps, so a rule pair that oscillates despite the
phase split degrades to a bounded amount of wasted work rather than a hang.
Instances are immutable and safe to share.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final recordOne group of rules, applied in order and re-applied while they keep making progress. -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intSweeps allowed for an iterated phase.static final intA phase run exactly once, for a rule that must not be re-applied. -
Method Summary
Modifier and TypeMethodDescriptionstatic OptimizationPipelineThe default pipeline: every rule the optimizer ships, in phase order.static OptimizationPipelineof(List<OptimizationPipeline.Phase> phases) Creates a pipeline from an explicit phase list — the seam for a caller that wants a subset of the rules — a test pinning one phase, for instance.phases()Returns the phases, in application order.rules()Returns every rule in the pipeline, flattened in application order.run(RelNode node, String queryName, SchemaAnnotations schemas, OptimizationContext ctx) Runs every phase overnode, each iterated to a fixpoint within its cap.
-
Field Details
-
DEFAULT_MAX_ITERATIONS
public static final int DEFAULT_MAX_ITERATIONSSweeps allowed for an iterated phase. Chosen to comfortably exceed the deepest cascade the current rule set can produce — which is one confirming sweep beyond the first, since every pass is internally recursive — while keeping the worst case bounded for a rule that genuinely needs several.- See Also:
-
SINGLE_PASS
public static final int SINGLE_PASSA phase run exactly once, for a rule that must not be re-applied.- See Also:
-
-
Method Details
-
of
Creates a pipeline from an explicit phase list — the seam for a caller that wants a subset of the rules — a test pinning one phase, for instance.- Parameters:
phases- the phases, in application order; must not be null or empty- Returns:
- the pipeline; never null
-
defaultPipeline
The default pipeline: every rule the optimizer ships, in phase order.Every rule here is a pure syntactic rewrite, so the pipeline needs no
SymbolTableand no cost model. It did untilJOIN-003was removed — that rule swapped a commutative join's inputs on a cost estimate, and was the sole reason this factory took a symbol table. A cost-driven rewrite belongs in the planner, where the decision can be made without permuting the logical schema; seePlanner.buildSide.- Returns:
- the pipeline; never null
-
phases
Returns the phases, in application order.- Returns:
- unmodifiable list; never null or empty
-
rules
Returns every rule in the pipeline, flattened in application order.- Returns:
- unmodifiable list; never null or empty
-
run
public RelNode run(RelNode node, String queryName, SchemaAnnotations schemas, OptimizationContext ctx) Runs every phase overnode, each iterated to a fixpoint within its cap.- Parameters:
node- the root of the tree to optimize; must not be nullqueryName- display name used in transformation records; must not be blankschemas- schema annotations from semantic analysis; must not be nullctx- context that accumulates transformation records; must not be null- Returns:
- the optimized tree; the original
nodewhen no rule fired
-