Skip to content

feat(planner): Stage 1 Pass 1 per-computation candidate generation - #561

Merged
zzylol merged 9 commits into
mainfrom
stack/528-05b-local-alternatives
Oct 13, 2026
Merged

zzylol merged 9 commits into
mainfrom
stack/528-05b-local-alternatives

Conversation

@zzylol

@zzylol zzylol commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Why

#509 Stage 1 starts with Pass 1, per-computation candidate generation. docs/design_docs/proposals/planner-layering.md says:

For each eligible sub-DAG, Pass 1 identifies its computation semantics, applies rewrite rules, and generates every candidate that is not provably unable to meet its accuracy requirement.

A sub-DAG is eligible when its root is an aggregation (an Aggregate operator) whose intent has at least one realization in the summary-family capabilities F: exact accumulator state or a summary family. Nested aggregations are separate targets: in sum by (job) (rate(x[1m])), the per-series rate and the per-job sum are each a target, and their options multiply.

  • Pruning removes an invalid candidate. Any stage may prune, but only when the candidate is provably invalid (for example, a summary family that cannot meet the query's accuracy target), and every pruned candidate carries a reason.
  • Selection discards valid candidates. Only stage 3 does this, when it picks the cheapest one.

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 of QueryRoots. It takes no cost model, accuracy model, capabilities, storage policy or materialization assignment, and does not rank, size or build replacement sub-DAGs.

  • Candidates (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: avg rewritten as exact sum and count accumulator states.
  • One exact option per target, listed first: the exact accumulator form (ExactAccumulator, or AvgAsSumCount for avg) when the intent has one and it can be built, otherwise Exact. 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.
  • Pruning with reasons (PruneReason): RequiresExact (an exact request does not get approximate summaries) and NullableAvgInput (count counts rows, so avg over a possibly-null input is not rewritten; plain Exact becomes the target's exact option). ComputationChoices { candidates, pruned } keeps both lists.
  • Targets (ComputationTarget { target, measure, choices, filter, having }): one per measure of every reachable Aggregate whose 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).
  • Filters and HAVING keep every candidate. The target records how they are applied; composition (feat(planner): compose candidate logical ASAP DAGs from Pass 1 per-computation candidates #575) applies them.
  • Input checks (PerComputationCandidateError): Structure (QueryRoot::validate_structure failed), 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)).
  • Docs: the module doc states the stage and pass and the rules above; there is no separate develop doc. planner-layering.md now 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 each Aggregate it computes the HAVING entries from the consumers, then calls computation_candidates_for_intent(intent) per measure, using the existing accuracy_target, accuracy_budget and summary_candidates from replacement.rs. The roots are returned unchanged with the targets.

Public API:

pub fn computation_candidates_for_intent(intent: &AggIntent) -> Result<ComputationChoices, PerComputationCandidateError>;
pub fn generate_per_computation_candidates<Id: Debug>(
    roots: Vec<(Id, QueryRoot)>,
) -> Result<PerComputationCandidates<Id>, PerComputationCandidateError>; // { roots, targets }

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's stage1_pass1_per_computation.

Examples (approximate = EpsilonDelta { 0.05, 0.01 }):

Measure Candidates Pruned
count, approximate ExactAccumulator{Count}, CMS, Count Sketch, UnivMon —
distinct count of one column, approximate (#509 Example 2) Exact, HLL, Theta, KMV, UnivMon —
distinct count of a column tuple, approximate Exact, HLL, Theta, KMV (UnivMon not declared) —
L2 / entropy, approximate Exact, UnivMon —
quantile, approximate Exact, KLL, DDSketch —
quantile, exact Exact KLL, DDSketch (RequiresExact)
TopK, approximate Exact, CMS with heap, Count Sketch with heap —
avg over a non-null input AvgAsSumCount —
avg over a nullable input Exact AvgAsSumCount (NullableAvgInput)
stddev not a target

#509 Example 1 with exact accuracy: Q1 sum by (job) (rate(http_requests_total[1m])) gives two targets, ExactAccumulator{Sum} and ExactAccumulator{Rate}; Q2 topk by (job) (10, sum_over_time(http_requests_total[1m])) gives Exact for the top-k with both heap summaries pruned RequiresExact, and ExactAccumulator{Sum} for sum_over_time.

Evidence

Execution examples: crates/asap-aware-mapping/tests/per_computation_candidates.rs and crates/integration-tests/tests/per_computation_candidates_sql.rs (SQL HAVING src = 'a' → BelowAggregate; HAVING count(*) > 10 → AboveOutput; global HAVING false → AboveOutput). Screenshots and measurements: not applicable.

Verification

  • Unit and integration tests: the unit test approximate_count_keeps_one_exact_accumulator_and_universal_choices; in tests/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).
  • End-to-end tests: example_1_exact_q1_keeps_exact_accumulators_only and example_1_exact_q2_prunes_topk_heaps lower 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.
  • Other checks on the PR head: cargo fmt --check; workspace all-target Clippy with -D warnings; cargo test --workspace (1,568 passed, 0 failed, 2 ignored); dag-viewer test_render (29 passed); tools/verify_metricsql_external_consumer.sh OK.

Architectural decisions

Limitations and follow-up

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 pipeline run_stage1_to_stage3 / Stage1ToStage3Pass.

Stack: tracker #528 · main → #542 → #665 → #666 → #543 → #667 → #561.

Human review — do not complete with an agent

  • The MVP boundary is correct.
  • New conceptual layers or public interfaces are necessary.
  • The before/after description matches the intended product behavior.
  • Human reviewer:
  • Decision and rationale:

🤖 Generated with Claude Code

https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW

@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch from 102ad03 to bca78d8 Compare October 3, 2026 16:07
@zzylol
zzylol force-pushed the stack/528-05-promql branch 2 times, most recently from fb30215 to d133a1d Compare October 3, 2026 16:08
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch from bca78d8 to ce188ad Compare October 3, 2026 16:08
@zzylol
zzylol force-pushed the stack/528-05-promql branch from d133a1d to d61bf7b Compare October 3, 2026 16:33
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch from ce188ad to ba07130 Compare October 3, 2026 16:33
@zzylol
zzylol force-pushed the stack/528-05-promql branch from d61bf7b to 130fc54 Compare October 3, 2026 17:02
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch 2 times, most recently from 681ba3a to 6f174e1 Compare October 3, 2026 17:11
@zzylol
zzylol force-pushed the stack/528-05-promql branch 2 times, most recently from 4cd558c to 7f65a80 Compare October 3, 2026 17:23
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch 2 times, most recently from aeb762b to bc4c4d5 Compare October 3, 2026 17:29
@zzylol
zzylol force-pushed the stack/528-05-promql branch from 7f65a80 to 569b845 Compare October 3, 2026 17:29
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch from bc4c4d5 to 043e1d5 Compare October 3, 2026 17:40
@zzylol
zzylol force-pushed the stack/528-05-promql branch from 569b845 to 00c2776 Compare October 3, 2026 17:40
@zzylol
zzylol marked this pull request as draft October 3, 2026 19:29
@zzylol

zzylol commented Oct 3, 2026

Copy link
Copy Markdown
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 logical-optimizer crate then.

🤖 Generated with Claude Code

@zzylol
zzylol force-pushed the stack/528-05-promql branch from 00c2776 to 2918b78 Compare October 3, 2026 19:32
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch 2 times, most recently from b044f4f to 7b4972b Compare October 3, 2026 19:45
@zzylol
zzylol force-pushed the stack/528-05-promql branch from 2918b78 to 21b014f Compare October 3, 2026 19:46
zzylol and others added 8 commits October 13, 2026 02:56
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
zzylol force-pushed the stack/528-05b-local-alternatives branch from 8817d84 to d3e39fe Compare October 13, 2026 02:57
@zzylol
zzylol changed the base branch from stack/528-08b-query-time-default to main October 13, 2026 02:57
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
@zzylol zzylol changed the title feat(planner): enumerate local logical alternatives without execution timing feat(planner): Stage 1 Pass 1 per-computation candidate generation 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
@zzylol
zzylol force-pushed the stack/528-05b-local-alternatives branch from d448394 to e683b11 Compare October 13, 2026 03:24
@zzylol
zzylol merged commit 15c190f into main Oct 13, 2026
3 of 4 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant