Repository navigation
refactor(planner): plan the workload on the unified operator IR - #542
Merged
Merged
Conversation
This was referenced Oct 2, 2026
zzylol
force-pushed
the
stack/528-06-native
branch
from
October 2, 2026 18:32
75903b5 to
b22fa53
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
2 times, most recently
from
October 2, 2026 19:40
2a3bcd0 to
a03efc2
Compare
zzylol
force-pushed
the
stack/528-06-native
branch
from
October 2, 2026 19:40
b22fa53 to
d82bcde
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 2, 2026 21:14
a03efc2 to
2c708f3
Compare
zzylol
force-pushed
the
stack/528-06-native
branch
2 times, most recently
from
October 2, 2026 21:22
337a0ff to
a94bd59
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 2, 2026 21:22
2c708f3 to
03166e7
Compare
zzylol
force-pushed
the
stack/528-06-native
branch
from
October 2, 2026 21:25
a94bd59 to
d84830e
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
2 times, most recently
from
October 2, 2026 21:56
4b84314 to
d9da0f9
Compare
zzylol
force-pushed
the
stack/528-06-native
branch
from
October 2, 2026 21:56
d84830e to
b631137
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 3, 2026 02:31
d9da0f9 to
ec9f8cb
Compare
zzylol
force-pushed
the
stack/528-06-native
branch
2 times, most recently
from
October 3, 2026 02:39
075d8e2 to
d9dc49c
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 3, 2026 02:39
ec9f8cb to
95eef55
Compare
3 tasks
zzylol
force-pushed
the
stack/528-06-native
branch
from
October 3, 2026 19:55
d9dc49c to
04f9db2
Compare
zzylol
marked this pull request as draft
October 3, 2026 20:02
zzylol
force-pushed
the
stack/528-06-native
branch
from
October 3, 2026 20:17
04f9db2 to
0ebce24
Compare
zzylol
added a commit
that referenced
this pull request
Oct 3, 2026
Cut today's planner over to the unified OperatorNode IR. Frontends return OperatorNode / QueryRoot, ParsedWorkload keeps scalar roots, Pass 1 (ASAPStrategies), the existing identical-sub-DAG sharing, selection, DAG assembly and lifecycle costing all run on OperatorNode, and PlanOutput exposes the whole workload DAG. The native compiler from #541 becomes the canonical physical_planner and consumes the PhysicalASAPDAG export. Ported from the earlier #542 (95eef55) without new #509 stage logic. Legacy QueryExpr/SummaryNode modules stay compiled for their own tests but are no longer re-exported from post_asap; the cleanup PR removes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 3, 2026 20:31
95eef55 to
74b4884
Compare
zzylol
added a commit
that referenced
this pull request
Oct 3, 2026
Cut today's planner over to the unified OperatorNode IR. Frontends return OperatorNode / QueryRoot, ParsedWorkload keeps scalar roots, Pass 1 (ASAPStrategies), the existing identical-sub-DAG sharing, selection, DAG assembly and lifecycle costing all run on OperatorNode, and PlanOutput exposes the whole workload DAG. The native compiler from #541 becomes the canonical physical_planner and consumes the PhysicalASAPDAG export. Ported from the earlier #542 (95eef55) without new #509 stage logic. Legacy QueryExpr/SummaryNode modules stay compiled for their own tests but are no longer re-exported from post_asap; the cleanup PR removes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 3, 2026 20:48
74b4884 to
e629370
Compare
zzylol
force-pushed
the
stack/528-06-native
branch
2 times, most recently
from
October 5, 2026 06:21
aaa8467 to
672a187
Compare
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 5, 2026 06:21
e629370 to
0c62c4a
Compare
A workload entry whose root is a standalone scalar (`scalar(quantile(0.99, rate(x[5m])))`, `scalar(sum(up)) + 1`) was copied through unplanned. Per #511 §2.3 it has no query node and reads operator sub-DAGs by reference; per operator-sharing §3 those sub-DAGs get the same validation and accuracy assessment as operator roots. MajorPass now searches, selects, assembles and lifecycle-plans each distinct operator sub-DAG a scalar root reads, against the scalar's entry and accuracy target, alongside the operator roots, and rebuilds the scalar over the selected sub-DAGs. PlanOutput::scalar_plans holds their lifecycle plans. PhysicalASAPDAG gains `scalar_roots`: each standalone scalar query as a `ScalarExpr<PhysicalASAPNodeId>` keyed by its position in the batch, so query i is the scalar root keyed i, or else the next of `roots`. Validation checks positions and treats scalar references as roots for existence, cycles and reachability. Wire version 9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
force-pushed
the
stack/528-07-planner
branch
from
October 12, 2026 20:30
19e77b0 to
f0afd89
Compare
3 tasks
A standalone scalar hands its entry's accuracy target to its operator reference only when it passes that one reference through (`scalar(v)`, or arithmetic/comparisons with literals). Otherwise each reference is planned exact: independently approximate references leave a combination such as a difference of two quantiles unbounded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
check_contract now verifies that `scalar_plans` holds one plan per distinct reference of each scalar root, in order and keyed by its entry. The scalar_roots doc says keys are batch positions, equal to entry indices because the contract requires dense indices. A validation test covers duplicate and descending scalar-root keys. The never-constructed VectorScalar/ScalarVector operand modes and the unused ParsedWorkload::new/entries are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
… inputs `a / scalar(b)` and `a > scalar(b)` failed costing with UnsupportedQueryOperator. A Project or Filter now takes one PromqlVectorToScalar input per distinct operator reference its expressions read, after its row input; its statistics carry one `scalar_inputs` edge per such input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
TopKLimitReuseStrategy offered a global top-10 as the source of a per-job top-5. It now requires equal `partition_by`, and the maintained population recognizer requires the limit to partition like its sort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
`is_logical_rewrite` inferred a logical rewrite from the replacement's shape (no guarantee, no ASAP operator). It is now `ReplacementSubDAG::is_logical_rewrite`, read from the candidate's provenance (LogicalRewrite, CseShare, CseRecompute, AccuracyReconciliation); the free function is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…erators The search entry points asserted that no workload root contains an ASAP operator. They now return `SearchError::AsapInput`; MajorPass reports it as `OptimizeError::Search`, and the explanation entry points pass it on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
The explanation report located only relational children, so a target read through `scalar(...)` in a predicate or projection had no breadcrumb; it now visits every operator input, as target discovery does. `frontier_from_timing` also puts an ingestion-time node a standalone scalar root reads on the frontier, like an operator root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
The IR canonicalize pass ignored measure filters, so `COUNT(*) FILTER (WHERE ...)` or `COUNT(col)` over a nullable column ranked by ORDER BY ... LIMIT became a heap sketch over every row. It now keeps Sort + Limit for a filtered measure (the legacy #466 guard), and the keyed heap-sketch input rule refuses a filtered inner aggregate. The legacy does_not_promote_a_filtered_count_ranking test is ported to the IR and through the SQL front end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Combinations of approximate values may read summaries: each scalar reference is planned against the entry's accuracy target again. Composing the guarantee of such a combination is left to #575 and Stage 3's accuracy model. This reverts commit c44ecda. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
A global Limit over a partitioned Sort keeps k rows overall, not k per group, so neither the heavy-hitter promotion nor the current-series heap candidates apply to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Cost lowering reads a node referenced several times from one operator's expressions once; the export now matches, deduping by node id (one id per pointer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…tract, provenance Project/Filter with two scalar references and one read twice lower and cost; the maintained population requires the Limit to partition like its Sort; the keyed heap input refuses a filtered count; check_contract rejects scalar plans out of order, under another entry, or extra; a guarantee-less summary realization is not a logical rewrite. 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> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Front ends build identical references as separate nodes and planning merges them, so pointer-distinct references left the rebuilt scalar with one reference but two scalar plans, failing the contract. References are now distinct by structure. Document that each reference keeps its summary candidates and that the combined guarantee is composed later (#669, #575). 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> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
MajorPass assembled roots with assemble_selected_dag, so a root whose selected candidate is an exact state came out as a bare SummaryAgg. The lifecycle plan now finalizes its root after costing (the finalize wraps the shared state, so sharing and cost evidence stay), scalar references included since scalar(v) reads values. Query-root callers and tests use assemble_selected_query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
This was referenced Oct 13, 2026
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…#667 materialization Stage 2 takes query roots, scalar ones included (#542), times every sub-DAG with Materialization::NotMaterialized (#667) and keeps the guarantee Stage 1 composed on every node it implements; a MaintainPopulation is no longer rejected, since it now runs at query time when not materialized. Stage 3 accepts a query's output when its root guarantee meets the target, or when the deployment's accuracy model shows this candidate's output does (AccuracyModel::output_guarantee, none by default). A certified estimate inside the plan is not enough: a difference of two quantile estimates stays listed as invalid, with a reason, under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
3 tasks
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…#667 materialization Stage 2 takes query roots, scalar ones included (#542), times every sub-DAG with Materialization::NotMaterialized (#667) and keeps the guarantee Stage 1 composed on every node it implements; a MaintainPopulation is no longer rejected, since it now runs at query time when not materialized. Stage 3 accepts a query's output when its root guarantee meets the target, or when the deployment's accuracy model shows this candidate's output does (AccuracyModel::output_guarantee, none by default). A certified estimate inside the plan is not enough: a difference of two quantile estimates stays listed as invalid, with a reason, under the built-in models. 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
…#667 materialization Stage 2 takes query roots, scalar ones included (#542), times every sub-DAG with Materialization::NotMaterialized (#667) and keeps the guarantee Stage 1 composed on every node it implements; a MaintainPopulation is no longer rejected, since it now runs at query time when not materialized. Stage 3 accepts a query's output when its root guarantee meets the target, or when the deployment's accuracy model shows this candidate's output does (AccuracyModel::output_guarantee, none by default). A certified estimate inside the plan is not enough: a difference of two quantile estimates stays listed as invalid, with a reason, under the built-in models. 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
…r candidate logical ASAP DAGs (#576) * feat(planner): #509 Stage 2 physical candidates and Stage 3 plan selection (MVP) Stage 2 implements one logical candidate physically: exact Aggregate{[TopK]} becomes Limit(Sort) per group, summaries stay build -> estimate, every node runs at query time, and the timed roots export as one PhysicalASAPDAG. Stage 3 is the only stage that computes cost. It rejects candidates whose summary misses its query's accuracy target (accuracy model, analytical guarantee) or uses Count-Min over weights not proven non-negative, prices every valid candidate node by node with analytical_cost (illustrative statistics), selects the cheapest and reports the rest as costlier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(devtools): stage_pipeline emits all four stages for Example 1 Add stage2_physical_asap (no cost) and stage3_selection (costs, selected, every other candidate rejected as invalid or costlier) per the v2 contract. PromQL rows carry the series identity from Stage 0, as runtime state needs. Regenerate tools/dag-viewer/examples/planner-layering-example1.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(planner): read Operator<NodeId> payloads in Stage 2/3 after the export removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): Stage 2/3 on #575's composed candidates, query roots and #667 materialization Stage 2 takes query roots, scalar ones included (#542), times every sub-DAG with Materialization::NotMaterialized (#667) and keeps the guarantee Stage 1 composed on every node it implements; a MaintainPopulation is no longer rejected, since it now runs at query time when not materialized. Stage 3 accepts a query's output when its root guarantee meets the target, or when the deployment's accuracy model shows this candidate's output does (AccuracyModel::output_guarantee, none by default). A certified estimate inside the plan is not enough: a difference of two quantile estimates stays listed as invalid, with a reason, under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(devtools): regenerate the four-stage Example 1 fixture on the new #575 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(planner): name Stage 2/3 by the planner-layering stage terms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(devtools): prune a logical candidate Stage 2 cannot implement, with its reason A Stage 2 error no longer fails the whole candidate set: stage_pipeline lists that candidate under stage2_physical_asap.pruned and plans the rest. Full materialization enumeration is left to #604. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): read empirical output accuracy from deployment evidence in Stage 3 The output-level hook moves from AccuracyModel to AccuracyEvidenceProvider: AccuracyModel stays theoretical, Stage 1 composes root guarantees with the built-in model, and deployment evidence enters only in Stage 3. A scalar root that is exactly scalar(v) inherits v's guarantee; any other scalar expression over an approximate operand has none until #669. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * refactor(planner): name Stage 2/3 code and pipeline keys after the stage names physical_candidates -> physical_asap_optimization (stage2_physical -> optimize_physical_asap, Stage2Error -> PhysicalASAPOptimizationError), stage3_select -> select_plan, and the stage_pipeline keys stage2_physical_asap -> physical_asap_optimization and stage3_selection -> plan_selection. The fixture is regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(planner): plan selection tests after Top-K estimates lost their bound #575 gives Top-K and signed Count-Min estimates guarantee None, so the CountSketch+heap candidate of the top-k fixture no longer carries an analytical guarantee: plan selection rejects it for missing evidence rather than for missing the target. The "misses target" case now uses a quantile summary planned at a looser target, and the per-node cost test admits the CountSketch+heap candidate through deployment evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): exact top-k sorts by the ranked measure in physical optimization Rank a one-measure grouped child by its measure (the SQL heavy-hitter and PromQL `topk(k, sum by ..)` shapes) and per-series rows by the sample value, instead of looking up a `value` column. A node reading the top-k gets a Project restoring the logical [keys.., topk_k] columns over the rows. An Aggregate{[TopK]} with other measures, a filter or HAVING now fails, so its candidate is pruned with the reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(devtools): stage_pipeline emits plan_selection when selection fails With no valid candidate the document lists the rejections with a null `selected` (and the Stage 2 pruned list as before); any other selection error is reported under `error`, instead of failing the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(planner): plan selection checks Count-Min only under a target; note MVP costing limits Reword the Count-Min test to the decided behavior and test that a query without a target admits the Count-Min candidate. Document that a cost error aborts selection (only from bad declared rates) and that binary, join and concat nodes are priced from their first input only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): exact top-k ranks by the measure of a composed Stage 1 child After Pass 1 composition a top-k reads its one-measure aggregate as Project(FinalizeExactAccumulator(SummaryAgg)) or Project(SummaryEstimate(..)), not as an Aggregate, so ranking fell back to the sample value: a key named `value` was sorted on, and an integer or multi-column key pruned the candidate. Look through renaming Projects, Filters, finalize and estimate to the SummaryAgg or Aggregate; a grouped one ranks by its last column. The SQL test now composes every Pass 1 choice under exact and approximate targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): exact top-k roots output the logical [keys.., topk_k] columns An exact top-k root returned its child's raw rows (Example 1's P1 leaked `ts`, `value` and `$promql_series_identity`), while summary candidates and the frontend give `[keys.., topk_k]`. The restoring Project now sits over the Limit at a root too, so one implementation serves roots and consumers. `topk_k` carries the ranked value's type and nullability, not the frontend's Utf8 placeholder. Regenerate the Example 1 fixture: P1 gains the root Project (node 12). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): charge a NotMaterialized shared node once per consuming query Plan selection prices each node once per computation: a materialized node once, a NotMaterialized node once per query (or materialized node) that reaches it, not counting through a materialized ancestor. Per-node costs carry the multiplier. Also list the per-entity case among the exact top-k shapes Stage 2 cannot rewrite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(planner): nodes below a materialized node are charged once for it 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
#539–#541 (merged) add the unified frontends, the timing pass, the physical export and a runtime that compiles it, but the planner still lowers queries to the legacy
QueryExprgraph and runs them through the legacyPostAsapDAGruntime. Until the planner switches, nothing in the product uses the #511 IR.What
The planner runs end to end on the unified
OperatorNodeDAG. No new #509 stage logic is added here: this PR ports the legacy cost-ranked candidate search (search_workloadwith theASAPStrategies), which was then the planner. #561 onward added the #509 stages, and #674 replaced the legacy search with them: on main the planner is the planner pipeline,run_stage1_to_stage3/Stage1ToStage3Pass(Stage 1 logical ASAP-aware optimization → Stage 2 physical ASAP-aware optimization → Stage 3 plan selection).asap_frontend_{sql,promql,metricsql}, refactor: move the unified frontends and physical planner to the crate roots #665) and getsQueryRoots;ParsedWorkloadkeeps scalar roots. The MetricsQL frontend input carries the planning clock and lowers throughlower_metricsql_query_workload, so bare selectors get the ingestion-interval lookback (feat(promql): lower queries to unified operator and scalar IR #540).OperatorNode. Candidate generation, selection and DAG assembly run onOperatorNode.PlanOutputexposes the whole workload DAG. Planned summary states derive their coverage from the node (feat(ir): derive summary coverage as definition + selection #646).scalar(quantile(0.99, rate(x[5m]))),scalar(sum(up)) + 1) used to be copied through unplanned. Per docs: define unified operators and SQL/PromQL scalar boundaries #511 §2.3 a scalar query references operator sub-DAGs; those sub-DAGs now go through the same planning as operator roots, so they can get summary candidates, and the scalar is rebuilt over the selected sub-DAGs (PlanOutput.scalar_plans).scalar(quantile(0.99, …)) - scalar(quantile(0.5, …))keep summary candidates. The guarantee of such a combination is None until Compose accuracy guarantees through arithmetic and comparisons over approximate values #669; Stage 3 plan selection rejects it without output-level evidence. References inside Project/Filter expressions follow the same rule.PlanOutput.scalar_plansholds one plan per distinct referenced node, and the export emits oneScalarRefedge per distinct referenced node. Structurally identical references (scalar(rate(x[5m])) - scalar(rate(x[5m]))) are separate nodes and are planned separately until Pass 2 (ASAP-aware CSE, Pass 2 sharing and Stage 1 coverage parity with the retired MajorPass #580).vector op scalar(v)in a Project or Filter (a / scalar(b),a > scalar(b)) is lowered with onePromqlVectorToScalarinput per distinct scalar reference, after the row input; its statistics carry onescalar_inputsedge per such input. These queries previously failed costing withUnsupportedQueryOperator.PhysicalASAPDAGgainsscalar_roots: Vec<(usize, ScalarExpr<PhysicalASAPNodeId>)>: entry i of the batch is the scalar root keyed i if there is one, otherwise the next entry ofroots. Validation checks the keys and treats scalar references as roots for existence, cycles and reachability (PHYSICAL_ASAP_DAG_WIRE_VERSION9 here; 10 on main since refactor(planner): replace the summary lifecycle with #509 materialization options #667). The workload export that emits them is added in feat(planner): export a planned workload as one physical ASAP DAG #666.check_contractchecks thatscalar_plansholds one plan per distinct reference of each scalar root, in order and keyed by its entry.frontier_from_timingand the explanation report walk scalar references like operator inputs.asap-aware-mappingfor the explanation entry points anddag_export):TopKLimitReuseStrategy) requires equalpartition_by; a global top-10 is no longer offered as the source of a per-job top-5. Heavy-hitter promotion and the maintained-population (current-series heap) candidates require the Limit to partition like its Sort.ReplacementSubDAG::is_logical_rewrite: LogicalRewrite, CseShare, CseRecompute, AccuracyReconciliation) instead of by the replacement's shape.COUNT(*) FILTER (WHERE ...),COUNT(col)over a nullable column, ordered and limited) are no longer promoted to a heavy-hitterTopK, either in IR canonicalize or inrealize_keyed_additive_summary_input. This bug was on main since feat(ir): add canonicalization, sub-DAG sharing and flat DAGs on the unified IR #645.sum_over_time(m[1m]),sum(rate(x[5m]))) is read throughFinalizeExactAccumulator, so the exported root yields values, not a bareSummaryAgg. Scalar references are finalized the same way, sincescalar(v)reads values. On main, Stage 1 composition (feat(planner): compose candidate logical ASAP DAGs from Pass 1 per-computation candidates #575) builds the finalize into every exact-accumulator candidate.SearchError::AsapInput, and the explanation entry points pass it on. On main the planner pipeline rejects such input in Stage 1 Pass 1 (PerComputationCandidateError::ContainsAsap, reported asOptimizeError::PerComputationCandidates).QueryExproutput, so it is deleted with its expression compiler, its PromQL fallback path and its raw sources. Every caller uses feat(runtime): export timed plans as PhysicalASAPDAG and compile them #541's physical planner and sources (physical_planner/sourcessince refactor: move the unified frontends and physical planner to the crate roots #665).readoutnames becomeevaluation: thereadoutmodule becomesevaluation,ReadoutQuery→SummaryEvaluation,ExactReadout→ExactEvaluation,GuaranteeSource::SketchReadout→SketchEvaluation,EstimatorContract::ClassicHll { max_distinct_per_evaluation }.VectorScalar/ScalarVectoroperand modes and the unusedParsedWorkload::new/entries.How
The change is a port: every planner module that took
QueryExpr/SummaryNodenow takesRc<OperatorNode>, using the same strategies and cost model. Tests that drive the planner (frontend planning tests, physical-operator tests that plan before executing, integration tests, devtools) are ported to the unified entry points. The legacy frontend modules and theQueryExpr/SummaryNodeIRs stayed compiled in this PR; #665 moved the unified modules to the crate roots, and #543 removed the legacy IRs.Before this PR
e2e_planlowered toQueryExpr, planned and costedSummaryNodes, and the runtime compiled aPostAsapDAG. The unified IR from #539–#541 was only reachable from its own tests. Standalone scalar queries were copied through unplanned, an exact-state root was exported as a bareSummaryAgg, and a filtered COUNT/SUM ranking could be planned as a heavy-hitter sketch over every row.After this PR
e2e_planand the planner produce and run unifiedOperatorNodeplans; the runtime path isPhysicalASAPDAG→ the physical planner. Public API changes for planner callers:OperatorNode/QueryRoot;FrontendInput::Metricsqlcarriesnow_ms.search_workload,search_workload_with,search_workload_with_targets,explain_replacementsandexplain_replacements_withreturnResult(SearchError).is_logical_rewriteis now the methodReplacementSubDAG::is_logical_rewrite.PlanOutput.scalar_plansandPhysicalASAPDAG.scalar_rootsare new.readout→evaluationrenames listed above.Later changes: #665 moved the unified frontends and physical planner to the crate roots; #667 removed the summary maintenance lifecycle (wire version 10); #674 retired the legacy search as the planner in favor of
run_stage1_to_stage3/Stage1ToStage3Pass(#675 names), which does not merge identical producers or scalar references until Pass 2 (#580); the scalar tests were renamed accordingly.Evidence
Execution examples: the planner, integration and devtools tests run on the unified IR.
crates/integration-tests/tests/operator_design_examples.rs(new) plans, exports and executes the #511 worked examples;operator_sharing.rs(new) checks that non-ASAP operators above and below summary operators can share inputs with them (#468). Screenshots and measurements: not applicable.Verification
subquery_time_shift_is_retained(a subquery's offset stays aTimeShift) anddoes_not_promote_a_filtered_count_rankingwere carried over from the legacy tests.scalar_queries_read_planned_sub_dags(["1", "scalar(quantile(0.99, rate(x[5m])))", "quantile(0.99, rate(x[5m]))"]keeps entry order and the scalar reads its planned sub-DAG),combined_scalar_references_keep_summary_candidates,identical_scalar_references_satisfy_the_plan_contract,scalar_reference_in_an_expression_is_a_scalar_input,scalar_references_are_one_input_per_distinct_reference,contract_rejects_a_scalar_reference_without_its_plan,contract_checks_scalar_plan_order_entry_and_count,frontier_from_timing_includes_ingestion_scalar_reference,a_quantile_read_by_a_scalar_expression_is_located, andscalar_roots_export_in_batch_order_and_reference_nodes(also rejects duplicate and descending keys).exact_state_roots_are_finalized(summary_sharing.rs) andassemble_selected_query_finalizes_only_an_exact_state_root(legacy search).global_limit_is_not_reused_for_a_per_group_limit,search_rejects_a_root_with_an_asap_operator,filtered_count_ranking_is_not_a_heavy_hitter(through the SQL frontend).crates/planner/tests/e2e_plan.rs,summary_sharing.rs.cargo fmt --check; workspace all-target Clippy with-D warnings;cargo test --workspace(2,010 passed, 0 failed, 2 ignored); dag-viewertest_render(31 passed);tools/verify_metricsql_external_consumer.shOK.Architectural decisions
unifiedpaths; moving them to the crate roots was refactor: move the unified frontends and physical planner to the crate roots #665, a pure move, so this diff carries only planner logic.QueryExproutput and cannot compile after the cutover.ScalarExprform with references to exported nodes.Limitations and follow-up
QueryExpr/SummaryExprIRs (merged).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