java.lang.Object
com.darkcollective.relix.optimizer.internal.OptimizationPipeline

public final class OptimizationPipeline extends Object
The optimizer's rule registry and its bounded fixpoint driver.

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 π) and PROJ-003 (π below σ);
  • SEL-001 (split a conjunction) and SEL-002 (merge adjacent selections);
  • SEL-009 (distribute a σ over a set operation) and SEL-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.

  • Field Details

    • DEFAULT_MAX_ITERATIONS

      public static final int DEFAULT_MAX_ITERATIONS
      Sweeps 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_PASS
      A 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

      public static OptimizationPipeline 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 SymbolTable and no cost model. It did until JOIN-003 was 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; see Planner.buildSide.

      Returns:
      the pipeline; never null
    • phases

      public List<OptimizationPipeline.Phase> phases()
      Returns the phases, in application order.
      Returns:
      unmodifiable list; never null or empty
    • rules

      public List<OptimizationRule> 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 over node, each iterated to a fixpoint within its cap.
      Parameters:
      node - the root of the tree to optimize; must not be null
      queryName - display name used in transformation records; must not be blank
      schemas - schema annotations from semantic analysis; must not be null
      ctx - context that accumulates transformation records; must not be null
      Returns:
      the optimized tree; the original node when no rule fired