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
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