Repository navigation
feat(planner): Stage 1 Pass 1 per-computation candidate generation - #561
Merged
Merged
Conversation
This was referenced Oct 3, 2026
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 3, 2026 16:07
102ad03 to
bca78d8
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
2 times, most recently
from
October 3, 2026 16:08
fb30215 to
d133a1d
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 3, 2026 16:08
bca78d8 to
ce188ad
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
from
October 3, 2026 16:33
d133a1d to
d61bf7b
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 3, 2026 16:33
ce188ad to
ba07130
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
from
October 3, 2026 17:02
d61bf7b to
130fc54
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
2 times, most recently
from
October 3, 2026 17:11
681ba3a to
6f174e1
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
2 times, most recently
from
October 3, 2026 17:23
4cd558c to
7f65a80
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
2 times, most recently
from
October 3, 2026 17:29
aeb762b to
bc4c4d5
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
from
October 3, 2026 17:29
7f65a80 to
569b845
Compare
3 tasks
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 3, 2026 17:40
bc4c4d5 to
043e1d5
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
from
October 3, 2026 17:40
569b845 to
00c2776
Compare
zzylol
marked this pull request as draft
October 3, 2026 19:29
Contributor
Author
|
Parked as draft until Phase C (see #528). Stage 1 Pass 1 local alternatives belong to the #509 end-to-end work, which comes after finishing #511 (Phase A) and the #572 reorganization (Phase B). It will move into the 🤖 Generated with Claude Code |
zzylol
force-pushed
the
stack/528-05-promql
branch
from
October 3, 2026 19:32
00c2776 to
2918b78
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
2 times, most recently
from
October 3, 2026 19:45
b044f4f to
7b4972b
Compare
zzylol
force-pushed
the
stack/528-05-promql
branch
from
October 3, 2026 19:46
2918b78 to
21b014f
Compare
This was referenced Oct 12, 2026
zzylol
force-pushed
the
stack/528-08b-query-time-default
branch
from
October 13, 2026 01:43
6a79e94 to
4411a62
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 13, 2026 01:43
8eccd53 to
d18125b
Compare
zzylol
force-pushed
the
stack/528-08b-query-time-default
branch
from
October 13, 2026 01:54
4411a62 to
a7dc2c2
Compare
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 13, 2026 02:14
d18125b to
8817d84
Compare
zzylol
force-pushed
the
stack/528-08b-query-time-default
branch
from
October 13, 2026 02:36
88650a0 to
0b4173d
Compare
2 tasks done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PromQL frontend's unified module was promoted to the crate root later in the stack; asap_frontend_promql::unified no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the export removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Summary candidates are unsized (LocalCandidate::Summary carries only the algorithm); sizing happens after Pass 2 sharing. Every pruned candidate is recorded on its target with a PruneReason (RequiresExact, NullableAvgInput). Each measure of an aggregate with a realization besides exact execution is its own target; input containing ASAP operators is rejected. avg is rewritten as exact sum/count accumulator states. The Example 1 test runs Q1 as exact, and the dev doc places composition in Pass 1. - One exact option per target: the exact accumulator (ExactAccumulator, or AvgAsSumCount for avg) when it can be built, otherwise plain Exact (NullableAvgInput prune, or no accumulator form). The plain form would duplicate "accumulator, not materialized"; Stage 2/3 decide materialization and sharing. - Per-measure filters and HAVING keep every candidate. The target records the measure's filter (applied to the state as SummaryAgg.filter) and a HavingPlacement for the aggregate's having or a Filter directly above it (the SQL frontend's HAVING form): BelowAggregate when it reads only group keys (pushed below as a row predicate), else AboveOutput (a Filter above the finalize/estimate output; approximate candidates carry guarantee = None). Composition (#575) applies them; PruneReason::FilteredAggregate is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…ter Pass 1 input checks LocalLogicalTarget.having is now one HavingEntry (consumer Filter, predicate, placement) per HAVING. A consumer Filter's key-only predicate is pushed below only when every consumer of the aggregate applies it, and a global aggregate's HAVING always stays above the output. Only the adjacent form is recorded; value predicates elsewhere get guarantee None via #575, multi-level push-down is #664. Inputs with assigned materialization are rejected too (AssignedPhysicalAnnotation). Approximate accuracy needs epsilon and delta in (0, 1), and InvalidAccuracy names the root and measure. The avg nullability check runs only for avg. Tests cover SQL HAVING placement, a shared aggregate, a global constant HAVING and #509 Example 1 Q2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
The dev doc describes per-consumer HAVING entries, the adjacent-form scope and that TopK Hydra is added in #605. planner-layering says a filter or HAVING does not prevent accumulator or summary candidates, and Example 1 says Sum has no summary family rather than pruned ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
A Filter that reads an aggregate only from a scalar expression (PromQL scalar(), subqueries) stays a plain consumer instead of a HavingEntry; filters above are deduplicated by pointer. Print the InvalidAccuracy root once, add tests for Project/scalar consumers, mixed SQL HAVING, without/PerEntity and annotated non-root nodes, and rewrap the doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 13, 2026 02:57
8817d84 to
d3e39fe
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Replays #575's composition on the #561 Pass 1 API: choice[i] indexes targets[i].choices.alternatives, several targets (measures) of one aggregate are grouped, an exact accumulator becomes SummaryAgg → FinalizeExactAccumulator and a summary SummaryAgg → SummaryEstimate. Pass 1 summaries are unsized, so composition sizes each for its measure's accuracy requirement. Summary coverage is derived from the subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Replays #575's devtools commits on the #561 Pass 1 API: stage 0 is the frontends' workload DAG and stage 1 holds one composed whole-workload candidate per choice of a local candidate for every target, as flat DAGs, capped by --max-candidates. Example 1 has three candidates (one exact option per target), so the cap test cuts at two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
… targets Stage 1 composition realizes every #561 Pass 1 choice. A measure filter becomes SummaryAgg.filter (the realization rule #608 carried, with its filtered-output nullability in ASAPOp schemas); a key-only HAVING that every consumer applies becomes a Filter below the state (SummaryAgg.filter would keep the dropped groups); a value HAVING stays a Filter above the output. avg divides its finalized sum and count accumulators, and the measures of a multi-measure aggregate are realized one by one and joined back on the group keys. ScalarExpr::map_columns renumbers a predicate's columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…lanning-stages doc Follow the review on #561: use the terms of planner-layering.md (Stage 1, Pass 1: per-computation candidate generation) instead of "local". - Module logical_candidates -> per_computation_candidates; LocalCandidate -> ComputationCandidate, LocalLogicalTarget -> ComputationTarget, LocalChoices -> ComputationChoices (field alternatives -> candidates), LocalLogicalCandidates -> PerComputationCandidates, enumerate_local_logical_candidates -> generate_per_computation_candidates, local_candidates_for_intent -> computation_candidates_for_intent. - The module doc states the stage and pass and carries the rules that were in docs/develop_docs/local-logical-candidates.md, which is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…lanning-stages doc Follow the review on #561: use the terms of planner-layering.md (Stage 1, Pass 1: per-computation candidate generation) instead of "local". - Module logical_candidates -> per_computation_candidates; LocalCandidate -> ComputationCandidate, LocalLogicalTarget -> ComputationTarget, LocalChoices -> ComputationChoices (field alternatives -> candidates), LocalLogicalCandidates -> PerComputationCandidates, enumerate_local_logical_candidates -> generate_per_computation_candidates, local_candidates_for_intent -> computation_candidates_for_intent. - The module doc states the stage and pass and carries the rules that were in docs/develop_docs/local-logical-candidates.md, which is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
force-pushed
the
stack/528-05b-local-alternatives
branch
from
October 13, 2026 03:24
d448394 to
e683b11
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Replays #575's composition on the #561 Pass 1 API: choice[i] indexes targets[i].choices.candidates, several targets (measures) of one aggregate are grouped, an exact accumulator becomes SummaryAgg → FinalizeExactAccumulator and a summary SummaryAgg → SummaryEstimate. Pass 1 summaries are unsized, so composition sizes each for its measure's accuracy requirement. Summary coverage is derived from the subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…DAG documents Replays #575's devtools commits on the #561 Pass 1 API: the frontends section is the frontends' workload DAG and logical_asap_optimization holds one composed whole-workload candidate per choice of a per-computation candidate for every target, as flat DAGs, capped by --max-candidates. Example 1 has three candidates (one exact option per target), so the cap test cuts at two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
… targets Composition substitutes every #561 Pass 1 choice. A measure filter becomes SummaryAgg.filter (the realization rule #608 carried, with its filtered-output nullability in ASAPOp schemas); a key-only HAVING that every consumer applies becomes a Filter below the state (SummaryAgg.filter would keep the dropped groups); a value HAVING stays a Filter above the output. avg divides its finalized sum and count accumulators, and the measures of a multi-measure aggregate are substituted one by one and joined back on the group keys. ScalarExpr::map_columns renumbers a predicate's columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…mputation candidates (#575) * feat(planner): compose a whole-workload candidate from Pass 1 choices Replays #575's composition on the #561 Pass 1 API: choice[i] indexes targets[i].choices.candidates, several targets (measures) of one aggregate are grouped, an exact accumulator becomes SummaryAgg → FinalizeExactAccumulator and a summary SummaryAgg → SummaryEstimate. Pass 1 summaries are unsized, so composition sizes each for its measure's accuracy requirement. Summary coverage is derived from the subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): name a finalized exact accumulator after the aggregate it realizes FinalizeExactAccumulator kept the state column's name (`state`), so a consumer reading the sample value (e.g. sum by (job) over a finalized per-series rate) found no `value` column at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(planner): rank Top-K summary items by the PromQL series identity column Rows that carry the full series identity are closed; Pass 1 rejected them and the runtime needs explicit item columns for keyed summaries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(devtools): stage_pipeline writes real frontend and logical ASAP DAG documents Replays #575's devtools commits on the #561 Pass 1 API: the frontends section is the frontends' workload DAG and logical_asap_optimization holds one composed whole-workload candidate per choice of a per-computation candidate for every target, as flat DAGs, capped by --max-candidates. Example 1 has three candidates (one exact option per target), so the cap test cuts at two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * feat(planner): compose measure filters, HAVING, avg and multi-measure targets Composition substitutes every #561 Pass 1 choice. A measure filter becomes SummaryAgg.filter (the realization rule #608 carried, with its filtered-output nullability in ASAPOp schemas); a key-only HAVING that every consumer applies becomes a Filter below the state (SummaryAgg.filter would keep the dropped groups); a value HAVING stays a Filter above the output. avg divides its finalized sum and count accumulators, and the measures of a multi-measure aggregate are substituted one by one and joined back on the group keys. ScalarExpr::map_columns renumbers a predicate's columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * feat(planner): give composed logical ASAP candidates theoretical guarantees Every node of a composed candidate gets a guarantee: exact over exact inputs, a summary estimate's sized family bound over exact input, and None for anything computed from an approximate value, including a value HAVING or any other predicate at any depth (#669 defines composition; plan selection's accuracy model decides). The Example 1 fixture shows them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): keep summary candidates for combinations of approximate values A comparison of approximate operands (quantile(0.99, …) > quantile(0.5, …)) now gets the same BinaryOp candidate over summary operands as arithmetic does, so no combination loses its summary candidates. Automatic selection still needs evidence about the candidate's output: a root guarantee that meets the target, which composition (#669) supplies, or the deployment's empirical output-level accuracy model (plan selection, #576). Certified inputs are not enough, since a difference or ratio of them can be unbounded, so such candidates are kept but not selected under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(planner): composition of Pass 1 candidates in the module doc The composition of per-computation candidates into whole-workload candidates is Algorithm 1's SUBSTITUTE; its helpers are named so. The module doc describes it and states that summaries are sized here per measure because Pass 2 sharing is not implemented yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): no frequency bound on Top-K or signed Count-Min estimates A heap sketch's bound is per item frequency, not Top-K membership, and a Count-Min bound assumes non-negative weights. A composed Top-K estimate, and a Count-Min estimate over weights not proven non-negative, now get no guarantee; plan selection's accuracy model decides them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): rank Top-K over a closed PromQL child by its labels topk(3, sum by (job) (rate(m[5m]))) listed heap candidates that composition could not substitute: the closed child [job, value] has no series identity. Its item is now the tuple of labels outside the partition, as in the legacy current-series and counter rules. stage_pipeline records a candidate it cannot compose with its error instead of aborting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): substituted outputs keep the aggregate's column names An estimate or accumulator names its value after the operation (quantile_0_99 became quantile, avg sum, a per-series count value count). A single-measure substitution is now renamed to the aggregate's columns, as join_measures already did; a renaming projection keeps its child's guarantee. The logical_asap_optimization test compares column names too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): substitute subqueries inside measure filters and HAVING A subquery in a substituted aggregate's measure filter or own HAVING now reads its rewritten plan, and a summary state's guarantee requires every input, its filter's subqueries included, to be exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): a filtered quantile aggregate is nullable As its summary estimate is: no row passing the filter gives NULL. The filtered quantile queries join the SQL candidate composition test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): compose SQL avg as a keyed join of sum and count A PromQL BinaryOp matches Utf8 labels: it merges NULL and '' groups and rejects other key types. For SQL rows the finalized sum and count are joined on the group keys (NULL keys match) and divided as SQL, NULL where the count is 0 (a filtered-empty group, an empty global input). PromQL keeps the BinaryOp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): count states read a column non-null on every counted row The count state behind avg-as-sum/count and SQL count read the sample value, which for SQL rows is `value` or the only numeric column: avg(c) divided by count(value), count(*) counted a nullable column, and with several numeric columns the composed DAG was invalid. The runtime skips NULL updates, so: - avg's count reads the averaged column (non-null, or the candidate is pruned); - SQL count reads a column its filter proves non-null (count(col)), else a non-nullable column; with none it is Unsupported; - an unresolvable state input is Unsupported, and a composed DAG is validated before it is returned. PromQL counts still read the sample value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(planner): a subquery in an aggregate's own HAVING is substituted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
#509 Stage 1 starts with Pass 1, per-computation candidate generation.
docs/design_docs/proposals/planner-layering.mdsays:The planner at the time (the legacy search,
MajorPass, #542; retired in #674) built replacements and ranked them by cost in one search. There is no logical-only step that lists every valid per-computation candidate over the unified IR without a cost model.What
A new module,
asap_aware_mapping::per_computation_candidates, that implements Stage 1 (logical ASAP-aware optimization), Pass 1: per-computation candidate generation from planner-layering.md, for a workload ofQueryRoots. It takes no cost model, accuracy model, capabilities, storage policy or materialization assignment, and does not rank, size or build replacement sub-DAGs.ComputationCandidate):Exact: plain exact execution of the original sub-DAG.ExactAccumulator { kind, params }: an exact mergeable accumulator state (count,sum,min,max,rate,irate,increase).Summary(SketchAlgorithm): a summary family, unsized. Pass 2 may share a summary and tighten it to the strictest consumer, so sizing happens later.AvgAsSumCount:avgrewritten as exactsumandcountaccumulator states.ExactAccumulator, orAvgAsSumCountforavg) when the intent has one and it can be built, otherwiseExact. Offering both would duplicate "accumulator, not materialized"; Stage 2/3 decide materialization and sharing. Summary families follow in stable catalog order, which carries no preference.PruneReason):RequiresExact(an exact request does not get approximate summaries) andNullableAvgInput(countcounts rows, soavgover a possibly-null input is not rewritten; plainExactbecomes the target's exact option).ComputationChoices { candidates, pruned }keeps both lists.ComputationTarget { target, measure, choices, filter, having }): one per measure of every reachableAggregatewhose intent has a realization besides plain exact execution (even if all of them are pruned). Each measure of a multi-measure aggregate is its own target. Nested aggregates are separate targets. Producers read by scalar roots and scalar expressions are discovered, and a shared producer is one target (pointer identity).filter: the measure's own filter, applied to the state asSummaryAgg.filter.having: oneHavingEntry { filter, predicate, placement }per HAVING: the aggregate's ownhaving(no consumerFilter), then one per consumerFilterdirectly above the aggregate (the form the SQL frontend lowers HAVING to). Only this adjacentFilteris recorded.HavingPlacement::BelowAggregateonly when the predicate reads onlyby(...)group keys and, for a consumerFilter, every consumer of the aggregate applies the same predicate; it is then pushed below as a row predicate. Everything else isAboveOutput: aFilterabove the finalize/estimate output. A global aggregate's HAVING is alwaysAboveOutput, since its one output row exists even with no input rows.guarantee = Nonethrough feat(planner): compose candidate logical ASAP DAGs from Pass 1 per-computation candidates #575; multi-level push-down is Pass 1: push down filters and HAVING across levels to create more candidates (and reuse DataFusion's filter push-down) #664.PerComputationCandidateError):Structure(QueryRoot::validate_structurefailed),ContainsAsap(input already has ASAP operators),AssignedPhysicalAnnotation(a node already has an execution timing or a materialization),InvalidAccuracy { root, measure }(an approximate target needs epsilon and delta in (0, 1)).planner-layering.mdnow says a filter or HAVING does not prevent the accumulator or summary candidates, and Example 1's diagram says Sum has no summary family instead of showing pruned summaries.How
generate_per_computation_candidates(roots)validates each root, walks every operator node reachable from its operator roots (including those referenced by scalar expressions) once, rejects physical annotations and ASAP operators, and records each node's consumers. For eachAggregateit computes the HAVING entries from the consumers, then callscomputation_candidates_for_intent(intent)per measure, using the existingaccuracy_target,accuracy_budgetandsummary_candidatesfromreplacement.rs. The roots are returned unchanged with the targets.Public API:
Before this PR
No API lists Pass 1 candidates over
OperatorNode/QueryRoot. The only candidate generation is the legacy cost-ranked search.After this PR
Callers can get, for a lowered workload, every computation (target) with its unranked candidates, its pruned candidates and reasons, and how its filters and HAVING are applied. On main this module is Stage 1 Pass 1 of the planner pipeline (
run_stage1_to_stage3/Stage1ToStage3Pass, #674 and #675), and its output is the run'sstage1_pass1_per_computation.Examples (approximate =
EpsilonDelta { 0.05, 0.01 }):count, approximateExactAccumulator{Count}, CMS, Count Sketch, UnivMonExact, HLL, Theta, KMV, UnivMonExact, HLL, Theta, KMV (UnivMon not declared)Exact, UnivMonExact, KLL, DDSketchExactRequiresExact)Exact, CMS with heap, Count Sketch with heapavgover a non-null inputAvgAsSumCountavgover a nullable inputExactAvgAsSumCount(NullableAvgInput)stddev#509 Example 1 with exact accuracy: Q1
sum by (job) (rate(http_requests_total[1m]))gives two targets,ExactAccumulator{Sum}andExactAccumulator{Rate}; Q2topk by (job) (10, sum_over_time(http_requests_total[1m]))givesExactfor the top-k with both heap summaries prunedRequiresExact, andExactAccumulator{Sum}forsum_over_time.Evidence
Execution examples:
crates/asap-aware-mapping/tests/per_computation_candidates.rsandcrates/integration-tests/tests/per_computation_candidates_sql.rs(SQLHAVING src = 'a'→BelowAggregate;HAVING count(*) > 10→AboveOutput; globalHAVING false→AboveOutput). Screenshots and measurements: not applicable.Verification
approximate_count_keeps_one_exact_accumulator_and_universal_choices; intests/per_computation_candidates.rs: catalogs (cardinality_keeps_exact_specialized_and_universal_alternatives,frequency_statistics_keep_universal_choices,exact_quantile_stays_exact_and_approximate_keeps_both_families,topk_keeps_both_specialized_heap_choices,summary_candidates_are_unsized,avg_rewrites_to_sum_and_count_states), targets (scalar_root_producers_are_discovered_once,multi_measure_aggregate_has_one_target_per_measure,aggregate_without_realization_is_not_a_target), filters and HAVING (filtered_measure_keeps_candidates_and_records_its_filter,key_only_having_is_pushed_below_and_keeps_candidates,value_having_stays_above_output_and_keeps_candidates,having_on_one_consumer_of_a_shared_aggregate_stays_above,global_aggregate_having_stays_above), input checks (assigned_physical_annotation_asap_input_and_invalid_accuracy_are_rejected).example_1_exact_q1_keeps_exact_accumulators_onlyandexample_1_exact_q2_prunes_topk_heapslower docs: propose workload-wide planning, summary sharing, and materialization #509 Example 1 through the PromQL frontend; the three SQL HAVING tests lower through the SQL frontend.cargo fmt --check; workspace all-target Clippy with-D warnings;cargo test --workspace(1,568 passed, 0 failed, 2 ignored); dag-viewertest_render(29 passed);tools/verify_metricsql_external_consumer.shOK.Architectural decisions
Exact).Limitations and follow-up
guarantee = Noneunder value predicates).avg(for example recognizing entropy / L2 forms) and extra candidates for opaqueExtensionintents are not added.Later changes: #575 added composition (
compose_logical_candidate) next to Pass 1; #674 made Pass 1 the start of the planner pipeline, replacing the legacy search (MajorPass), and #675 named the pipelinerun_stage1_to_stage3/Stage1ToStage3Pass.Stack: tracker #528 · main → #542 → #665 → #666 → #543 → #667 → #561.
Human review — do not complete with an agent
🤖 Generated with Claude Code
https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW