Design status (updated)
This issue defines a required planning capability, not a settled post-ASAP IR design. The implementation on the feat/exact-composition-planner family of branches is a prototype for evaluation and has not been merged into main. Its ExactTransform / ExactPostProcess vocabulary must not be treated as the accepted architecture.
The semantic requirement is:
ASAPPlanner must be able to represent, validate, cost, and select plans that compose exact computation with summary computation without hiding an independently realizable summary subtree inside an opaque fallback.
In particular, rate, irate, and increase are exact functions. They should not be collectively named deriv, because PromQL already has a distinct deriv() function, and they should not be classified as “transforms” as a semantic identity. Function semantics and execution placement are separate concerns: an exact function may have update-time, query-time, or multiple legal physical implementations, and the planner should compare those alternatives rather than encode one phase into the function name.
Any proposed design must satisfy these implementation requirements:
- Extensible: adding another exact function, aggregate, or relational operation must not require another pair of phase-specific IR variants or a new one-off planner path.
- General: the representation and selection mechanism must cover the general exact/summary composition problem in either nesting direction, rather than only the example queries that exposed this issue.
- No issue-specific special nodes: do not introduce a special node solely to handle
rate, max over quantile, or any other single reproducer. Nodes must represent stable semantic or physical concepts with invariants that apply across operators.
- Open to downstream extension: deployments must be able to register capabilities, costs, and legal execution modes without changing the core IR for every new exact function.
- Conservative and compositional: unsupported combinations must retain an explicit fallback without swallowing independently selectable child candidates.
Any proposed design should therefore evaluate at least these options before choosing an IR:
- an
ExactFunction / ExactAggregate semantic vocabulary with execution phase represented separately;
- a general exact-operation node with independently selected physical execution modes;
- expressing the composition only in a lower physical/stage DAG rather than adding specialized post-ASAP logical nodes; and
- retaining multiple legal placements for cost-based selection instead of assigning functions such as
rate permanently to the update path.
The existing branch is useful as a prototype and source of tests, especially for phase legality, shared-child selection, conservative fallback, and cost accounting. It is not evidence that its node names or architectural boundary are final. Acceptance of this issue requires an explicit design decision followed by an implementation merged into main.
Context
Same migration as issues #150/#151 (ASAPQuery-backend's control_plane adopting asap_plan::bind::implement_tree_in_with for L3→L4 binding, replacing its own locally-defined L4 IR).
We hit a real coverage gap that shows up in both nesting orders of "mix an exact computation with a summary/sketch computation in the same query": an outer exact op wrapping an inner realized summary, and an outer summary op wrapping an inner exact computation. Neither direction has an L4 representation today.
Direction 1 — outer exact fold over an inner realized summary
max by (zone) (quantile_over_time(0.99, http_latency_ms[5m]))
avg by (zone) (quantile_over_time(0.99, http_latency_ms[5m]))
Structurally this is two nested Aggregate nodes: an outer AggIntent::Max/Avg whose child is itself an Aggregate carrying an inner, independently-realizable AggIntent::Quantile. Semantically, the query wants: read the inner quantile's per-zone answer (from whatever summary/accumulator actually answers it), then fold same-zone rows by max/avg. It is not asking for a real, independently-materialized "max" or "avg" accumulator over raw samples — in our deployment this shape is typically the identity fold (the inner summary already emits one row per zone), but it can be a real multi-row fold too (e.g. the inner summary is keyed more granularly than the outer by).
crates/plan/src/boundary.rs's implementation_for_with has no representation for this composition:
AggIntent::Min { .. } | AggIntent::Max { .. } => {
accumulator(intent, SummaryKind::MinMax, SummaryParams::MinMax)
}
// ...
AggIntent::Avg { .. } | AggIntent::StdDev { .. } | AggIntent::Variance { .. } => {
Implementation::PassThrough
}
Max/Min unconditionally commit to SummaryKind::MinMax — this requires a real, independently-registered MinMax-family sid to exist for the metric. One never does for this shape (there's no reason to plan/register a MinMax accumulator alongside a Quantile sketch just so an outer max by (...) can wrap it) — so find_candidates (our SummaryExecutor impl) always returns no candidates for the outer node, and the whole tree fails to execute even though the inner Quantile would answer fine on its own.
Avg/StdDev/Variance go straight to PassThrough — per implement_tree_in_with's conservative-fallback behavior (a logical parent above a bindable aggregate subsumes it unbound), this wraps the entire tree, including the otherwise-realizable inner Quantile, as one opaque Logical blob.
Either way, the outer wrapper poisons an inner sub-tree that would otherwise realize correctly on its own.
Direction 2 — outer summary op over an inner exact computation
The mirror image: rate(...)/irate(...) are themselves exact, per-sample computations over raw counter data (counter-reset-aware delta ÷ time). A query can wrap one in an operator that would otherwise be summary-realizable, e.g.
sum_over_time(rate(metric[5m])[10m:])
or stack a top-k summary on top of an exact rate, e.g. our own already-excluded
topk(5, sum by (zone) (rate(metric[5m])))
Here the inner rate(...) is the exact computation, and the outer sum_over_time/topk is the one that could in principle be answered from a summary (CMS-with-heap for the top-k case) — but there's no way to express "realize the outer as a summary over the OUTPUT of an inner exact per-sample transform" either. This deployment doesn't currently attempt this composition at all: any query containing rate(...)/irate(...) anywhere in its tree is unconditionally excluded from our SummaryExecutor binding before we ever try (see "What we did instead" below) — direction 2 is why.
Both directions are the same underlying gap, just which side of the composition is exact vs. summary-realized.
The composition should consider when a query DAG, part of the DAG computes from raw data, part of the DAG computes from summaries as well.
Request
Is there room in the L4 model for a third answer shape, alongside "realized as a summary/accumulator" and "opaque Logical, compute from raw data": an L4 node that composes an exact computation with a nested realized summary, in either nesting order — outer-exact-over-inner-summary (direction 1) or outer-summary-over-inner-exact (direction 2)? Concretely, something like the L4Node DAG carrying an explicit "apply this function to the child's per-group output, independent of whether the child bound as a summary or stayed exact" node, so implement_tree_in_with can commit whichever sub-tree can realize independently, and let the caller (or a CostModel-style hook) apply the outer/inner composition at readout — instead of one side either requiring its own unrelated accumulator/summary sid or poisoning the other side's binding.
We don't have a strong opinion on the right shape for this (a new L4Node/SummaryExpr variant vs. a CostModel hook similar to realize_extension/readout_extension vs. something else) — flagging the gap for discussion rather than proposing a specific API.
What we did instead (for now)
Nothing routed around locally for direction 1 yet. We're treating it the same way we treat #151's TopK { accuracy: Exact } gap: a known, accepted "falls over to the exact/archive tier" limitation for now, rather than forking implement_tree_in_with's tree walk to special-case it in control_plane.
For direction 2, we already have a standing local exclusion: any candidate whose PromQL contains rate(...)/irate(...) anywhere in its tree is excluded from our SummaryExecutor binding before it's ever attempted (a RateShape skip in our serving-time lowering), and topk(K, sum by (...) (rate(...))) is excluded the same way. We hadn't connected these to direction 1 until writing this up — they're the same gap.
If a CostModel-reachable hook (or a new IR shape) lands upstream for either direction, we'd rather adopt that than build a parallel local mechanism for either one.
References
crates/plan/src/boundary.rs — implementation_for_with's AggIntent::{Min,Max} / AggIntent::{Avg,StdDev,Variance} arms (direction 1), and the Rate/Increase accumulator arms (direction 2)
- ASAPController#151 — same "deployment wants a different answer than core's default realization for one intent shape" pattern, one layer over (ranking within an already-offered candidate set, vs. no candidate set being offered at all here)
- ASAPController#150 — the
CostModel-style extension-point precedent (realize_extension/readout_extension) this request is loosely modeled after
Design status (updated)
This issue defines a required planning capability, not a settled post-ASAP IR design. The implementation on the
feat/exact-composition-plannerfamily of branches is a prototype for evaluation and has not been merged intomain. ItsExactTransform/ExactPostProcessvocabulary must not be treated as the accepted architecture.The semantic requirement is:
In particular,
rate,irate, andincreaseare exact functions. They should not be collectively namedderiv, because PromQL already has a distinctderiv()function, and they should not be classified as “transforms” as a semantic identity. Function semantics and execution placement are separate concerns: an exact function may have update-time, query-time, or multiple legal physical implementations, and the planner should compare those alternatives rather than encode one phase into the function name.Any proposed design must satisfy these implementation requirements:
rate,max over quantile, or any other single reproducer. Nodes must represent stable semantic or physical concepts with invariants that apply across operators.Any proposed design should therefore evaluate at least these options before choosing an IR:
ExactFunction/ExactAggregatesemantic vocabulary with execution phase represented separately;ratepermanently to the update path.The existing branch is useful as a prototype and source of tests, especially for phase legality, shared-child selection, conservative fallback, and cost accounting. It is not evidence that its node names or architectural boundary are final. Acceptance of this issue requires an explicit design decision followed by an implementation merged into
main.Context
Same migration as issues #150/#151 (ASAPQuery-backend's
control_planeadoptingasap_plan::bind::implement_tree_in_withfor L3→L4 binding, replacing its own locally-defined L4 IR).We hit a real coverage gap that shows up in both nesting orders of "mix an exact computation with a summary/sketch computation in the same query": an outer exact op wrapping an inner realized summary, and an outer summary op wrapping an inner exact computation. Neither direction has an L4 representation today.
Direction 1 — outer exact fold over an inner realized summary
Structurally this is two nested
Aggregatenodes: an outerAggIntent::Max/Avgwhose child is itself anAggregatecarrying an inner, independently-realizableAggIntent::Quantile. Semantically, the query wants: read the inner quantile's per-zoneanswer (from whatever summary/accumulator actually answers it), then fold same-zonerows by max/avg. It is not asking for a real, independently-materialized "max" or "avg" accumulator over raw samples — in our deployment this shape is typically the identity fold (the inner summary already emits one row perzone), but it can be a real multi-row fold too (e.g. the inner summary is keyed more granularly than the outerby).crates/plan/src/boundary.rs'simplementation_for_withhas no representation for this composition:Max/Minunconditionally commit toSummaryKind::MinMax— this requires a real, independently-registeredMinMax-family sid to exist for the metric. One never does for this shape (there's no reason to plan/register aMinMaxaccumulator alongside aQuantilesketch just so an outermax by (...)can wrap it) — sofind_candidates(ourSummaryExecutorimpl) always returns no candidates for the outer node, and the whole tree fails to execute even though the innerQuantilewould answer fine on its own.Avg/StdDev/Variancego straight toPassThrough— perimplement_tree_in_with's conservative-fallback behavior (a logical parent above a bindable aggregate subsumes it unbound), this wraps the entire tree, including the otherwise-realizable innerQuantile, as one opaqueLogicalblob.Either way, the outer wrapper poisons an inner sub-tree that would otherwise realize correctly on its own.
Direction 2 — outer summary op over an inner exact computation
The mirror image:
rate(...)/irate(...)are themselves exact, per-sample computations over raw counter data (counter-reset-aware delta ÷ time). A query can wrap one in an operator that would otherwise be summary-realizable, e.g.or stack a top-k summary on top of an exact rate, e.g. our own already-excluded
Here the inner
rate(...)is the exact computation, and the outersum_over_time/topkis the one that could in principle be answered from a summary (CMS-with-heap for the top-k case) — but there's no way to express "realize the outer as a summary over the OUTPUT of an inner exact per-sample transform" either. This deployment doesn't currently attempt this composition at all: any query containingrate(...)/irate(...)anywhere in its tree is unconditionally excluded from ourSummaryExecutorbinding before we ever try (see "What we did instead" below) — direction 2 is why.Both directions are the same underlying gap, just which side of the composition is exact vs. summary-realized.
The composition should consider when a query DAG, part of the DAG computes from raw data, part of the DAG computes from summaries as well.
Request
Is there room in the L4 model for a third answer shape, alongside "realized as a summary/accumulator" and "opaque
Logical, compute from raw data": an L4 node that composes an exact computation with a nested realized summary, in either nesting order — outer-exact-over-inner-summary (direction 1) or outer-summary-over-inner-exact (direction 2)? Concretely, something like theL4NodeDAG carrying an explicit "apply this function to the child's per-group output, independent of whether the child bound as a summary or stayed exact" node, soimplement_tree_in_withcan commit whichever sub-tree can realize independently, and let the caller (or aCostModel-style hook) apply the outer/inner composition at readout — instead of one side either requiring its own unrelated accumulator/summary sid or poisoning the other side's binding.We don't have a strong opinion on the right shape for this (a new
L4Node/SummaryExprvariant vs. aCostModelhook similar torealize_extension/readout_extensionvs. something else) — flagging the gap for discussion rather than proposing a specific API.What we did instead (for now)
Nothing routed around locally for direction 1 yet. We're treating it the same way we treat #151's
TopK { accuracy: Exact }gap: a known, accepted "falls over to the exact/archive tier" limitation for now, rather than forkingimplement_tree_in_with's tree walk to special-case it incontrol_plane.For direction 2, we already have a standing local exclusion: any candidate whose PromQL contains
rate(...)/irate(...)anywhere in its tree is excluded from ourSummaryExecutorbinding before it's ever attempted (aRateShapeskip in our serving-time lowering), andtopk(K, sum by (...) (rate(...)))is excluded the same way. We hadn't connected these to direction 1 until writing this up — they're the same gap.If a
CostModel-reachable hook (or a new IR shape) lands upstream for either direction, we'd rather adopt that than build a parallel local mechanism for either one.References
crates/plan/src/boundary.rs—implementation_for_with'sAggIntent::{Min,Max}/AggIntent::{Avg,StdDev,Variance}arms (direction 1), and theRate/Increaseaccumulator arms (direction 2)CostModel-style extension-point precedent (realize_extension/readout_extension) this request is loosely modeled after