Repository navigation
feat(planner): physical ASAP-aware optimization and plan selection for candidate logical ASAP DAGs - #576
Merged
Conversation
This was referenced Oct 3, 2026
Merged
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
3 times, most recently
from
October 10, 2026 14:32
d613b0f to
243c791
Compare
zzylol
force-pushed
the
stack/509-c2-stage1-workload
branch
2 times, most recently
from
October 11, 2026 17:11
0150354 to
db2f3c4
Compare
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 11, 2026 17:11
243c791 to
bd70f05
Compare
zzylol
force-pushed
the
stack/509-c2-stage1-workload
branch
from
October 13, 2026 02:58
db2f3c4 to
ec00b3c
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
… 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 (Stage 3, #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
zzylol
force-pushed
the
stack/509-c2-stage1-workload
branch
from
October 13, 2026 04:12
ec00b3c to
03ebf0e
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
… 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
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 13, 2026 04:46
bd70f05 to
fd2e79f
Compare
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 13, 2026 05:24
fd2e79f to
99a1b3c
Compare
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>
…ction (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>
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>
…the export removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…#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
…#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
…th 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
… 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
…age 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
…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
…imization
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
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
…ote 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
…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
…olumns 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
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 13, 2026 15:02
99a1b3c to
60197da
Compare
… 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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
marked this pull request as ready for review
October 13, 2026 15:24
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
Plan selection charges a NotMaterialized node once per consuming query, so the charging helper checks NodeCost.multiplier and the Example 4 A3 ambiguity is recorded as decided. Example 1 checks that exact Q2 sorts by the ranked value and returns [job, topk_10]. The stage viewer guide's tab 1 row now describes the actual all-exact result. 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
Plan selection charges a NotMaterialized node once per consuming query, so the charging helper checks NodeCost.multiplier and the Example 4 A3 ambiguity is recorded as decided. Example 1 checks that exact Q2 sorts by the ranked value and returns [job, topk_10]. The stage viewer guide's tab 1 row now describes the actual all-exact result. 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
…stages (#577) * test: acceptance spec and pending tests for planner-layering Example 1 Define MVP acceptance for #509 Example 1 before the Phase C stage APIs exist: 1 -> 6 -> 6 -> 1 candidates (Pass 1 x identical-expression sharing; physical operator implementation only, no materialization). - Spec with per-stage candidate tables, invariants and doc ambiguities. - Integration tests against todo!() stage stubs, ignored until the stages land; workload and Stage 0 frontend-shape tests run now. - Expected-only asap-stage-pipeline/v1 fixture for the DAG viewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: run the Example 1 acceptance tests against the real stages Replace the todo!() stubs with adapters over Stage 0-3, un-ignore the 11 tests that pass, and give each still-ignored test the precise difference between the implementation and the spec. Add runtime capability checks: the selected plan compiles in the physical planner; compiling every candidate stays ignored with the two runtime gaps it finds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: read Example 1 logical stages as flat DAGs after the export removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: Example 1 acceptance on the composed stages (1 -> 3 -> 3 -> 1) The tests run Pass 1 and composition, optimize_physical_asap and select_plan through a shared tests/planner_layering_common pipeline, and follow the decided behavior: three Pass 1 candidates (Q2 exact, CMS+heap, CountSketch+heap), every sub-DAG NotMaterialized, P1 selected, CMS+heap invalid (weights not proven non-negative) and CountSketch+heap invalid (a Top-K output has no guarantee and no deployment evidence). Hydra (#605), Pass 2 sharing (#588) and the two runtime gaps (#593, #579) stay ignored with the PR that delivers them. Tests are named after the stages; the compile check no longer uses the PromQL fallback. The spec lists where the doc and the planner differ. The expected-only viewer fixture is removed: the generated four-stage fixture replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: port #590's Example 2-4 acceptance specs and tests The specs and tests from #590 run the current stages through the shared planner_layering_common pipeline (Pass 1 and composition, optimize_physical_asap, select_plan); tests are named after the stages. Materialization is read from the physical DAG (#667); window forms and retention stay pending adapters. Behavior that works passes (29 tests); every expectation that needs a feature not in this stack is ignored with the PR that delivers it (29 tests): Pass 2 sharing (#588, #595, #596), window composition (#601, #628, #634), materialization enumeration and MatAtQuery (#604, #625), UnivMon sizing and accuracy (#621), SQL recognition and COUNT(*) composition (#598, #603) and plan selection row estimates (#632). Each spec's "Today" column states the current output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * feat(devtools): stage_pipeline examples for #509 Example 3 Ported from #590: --example planner-layering-3a (Pattern A, ad hoc batch of five p99 reports) and planner-layering-3b (Pattern B, repeating 5-min p99), the Example 3 acceptance tests' workloads. Example 1's output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: follow #576's per-consumer charging and exact top-k columns Plan selection charges a NotMaterialized node once per consuming query, so the charging helper checks NodeCost.multiplier and the Example 4 A3 ambiguity is recorded as decided. Example 1 checks that exact Q2 sorts by the ranked value and returns [job, topk_10]. The stage viewer guide's tab 1 row now describes the actual all-exact result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * refactor(devtools): one planning pipeline for stage_pipeline and the acceptance tests run_planning_stages and the #509 example workloads move into the asap-devtools library; stage_pipeline serializes the run and the planner-layering tests adapt it. The shared data workload drops ingestion_rate for data at rest, which validate() requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(devtools): select over every candidate; --max-displayed limits only the document stage_pipeline capped enumeration at 64, so Example 3a (243 candidates) selected P41 instead of the cheapest P122 (KLL for all five queries). The pipeline now enumerates, optimizes and prices every candidate; the document shows the first --max-displayed (default 64) plus the selected one, with `combinations` and `capped`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs: Example 2's SQL candidates compose; the SQL test waits only on #598 Running the ignored SQL test composes every candidate and fails only on Q2's options (per-source counts until #598), so #603 is not a blocker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: name stages by number and name; say query-language frontends The frontends' Run field, id and locals become stage0_query_language_frontends, and test names carry stage0..stage3 with the stage name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: charging helper follows plan selection's per-consumer rule With a materialized node the helper accepted any multiplier >= 1. It now computes each node's computations as plan selection does (walks from query roots and materialized nodes' inputs, stopping at materialized nodes), checked on a hand-built candidate with a materialized node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: Example 1 picks Q2 candidates by (option, no shared input) physical_with and the invalid-candidate check no longer assume one candidate per Q2 option, so #588's shared-input variants need no change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs: Example 3 Pattern B charges each node once per consuming query Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(stage-viewer): rows 2, 3a and 3b describe the planner's current output Read from uncapped runs: Example 2's sketch plans are rejected for having no (ε, δ) guarantee, 3a selects P122 (first of 108 tied cheapest plans, nothing shared yet) and 3b selects query-time KLL; panes and the latency check come with the window-summary PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(stage-viewer): row 2 from the SQL run; 3a ties 32 plans, not 108 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test: Example 1 counts only unshared candidates; stage3_ charging-helper names The 3-logical and 3-physical counts now hold when #588 adds shared-input variants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * refactor(devtools): PlanningStagesRun fields carry stage number and name Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(devtools): unique temp file per stage_pipeline run cargo test runs the tests as threads of one process, so two runs of the same example shared a file name and one deleted the other's output. 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 was referenced Oct 13, 2026
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 has four stages. Before this PR the planner stopped after logical ASAP-aware optimization (#575): it produced candidate logical ASAP DAGs but no physical plans, and it selected no plan. This PR adds Stage 2 (physical ASAP-aware optimization) and Stage 3 (plan selection) as an MVP, so a workload goes from the frontends to one selected plan.
What
stage2_physical_asap_optimization::optimize_physical_asap): builds one candidate physical ASAP DAG for each candidate logical ASAP DAG.Materialization::NotMaterializedand runs at query time. EnumeratingMatAtIngestionandMatAtQueryis left to Stage 2 materialization: ingestion vs query time per summary #604.Aggregate{[TopK]}becomes a per-groupSort(descending, by the ranked measure) followed by a per-groupLimit, the shape the PromQL frontend uses fortopk. The ranked measure is found by looking through feat(planner): compose candidate logical ASAP DAGs from Pass 1 per-computation candidates #575's composition (a renamingProject, aFilter,FinalizeExactAccumulatororSummaryEstimate) to the reduction: under aReduceit is the measure column, otherwise the sample value. A restoringProjectover theLimitgives roots and consumers the logical[keys.., topk_k]columns;topk_kkeeps the ranked value's type and nullability. A summary staysSummaryAgg→SummaryEstimate.HAVING, a per-entity reduction) fail withPhysicalASAPOptimizationError::ExactTopKand their reason.PhysicalASAPOptimizationError. The caller prunes that candidate alone and records the error as its reason.stage3_plan_selection::select_plan): the only stage that computes cost.AccuracyEvidenceProvider::output_guarantee(new,Noneby default), shows that it does. Checks on individual estimates inside the plan never accept on their own: a difference of two quantile estimates is rejected under the built-in models.scalar(v)inheritsv's guarantee. Any other scalar expression over approximate inputs has no guarantee until Compose accuracy guarantees through arithmetic and comparisons over approximate values #669.analytical_cost, and the per-node costs are summed. The cheapest is selected. Every other candidate is listed with a reason: invalid (with the failed check) or valid but costlier.stage_pipelinedevtool emits four documents:stage0_query_language_frontends,stage1_logical_asap_optimization,stage2_physical_asap_optimization(candidates plusprunedwith reasons, no cost), andstage3_plan_selection(costs, selected, rejected). When plan selection fails, it still emitsstage3_plan_selection, withselected: nulland the rejections (or anerrorfor any other failure). PromQL rows carry the series identity, as per-series state needs at runtime. The Example 1 fixturetools/dag-viewer/examples/planner-layering-example1.jsonis regenerated.How
Physical ASAP-aware optimization rewrites bottom-up with one memo for the whole workload, so sharing between query roots is kept. It then applies
apply_materialization_timingswithMaterializationAssignment::with_default(NotMaterialized)and exports the timed roots as onePhysicalASAPDAG.Plan selection walks each query root, first applying the Count-Min weight check, then the output-level accuracy check. Deployment evidence is read only here; #575's composition never reads it. Pricing charges each node once per computation: a materialized node once, a
NotMaterializednode once for each consumer that reaches it (query roots, scalar roots, materialized nodes), so a shared node read by two queries costs twice until it is materialized.Before this PR
stage_pipelineon Example 1 stops at 3 candidate logical ASAP DAGs. There is no physical plan, no cost, and no selected plan.After this PR
Example 1 gives 3 → 3 → 1:
sort (partition by job) → limit 10, under aProjectrestoring[job, topk_10].Q1 exact (Sum acc, Rate acc) · Q2 exact (Sum acc)) is selected, at ≈79.40cpu_ms_per_workload_evaluation.q2: CmsWithHeap needs non-negative update weights, and these are not proven non-negative.q2: the output has no guarantee, and the deployment has no evidence that it meets target EpsilonDelta { epsilon: 0.01, delta: 0.001 }.Evidence
stage2_physical_asap_optimizationunit tests: exact top-k becomes sort then limit and a shared child stays shared; PromQL top-k over series sorts by the sample value, descending; top-k over a grouped aggregate sorts by its measure; a consumer of an exact top-k reads its logical columns; top-k with an extra measure orHAVINGfails withExactTopK; every node runs at query time.crates/integration-tests/tests/stage2_physical_asap_optimization_sql.rs(new): SQL heavy-hitter queries run through every Pass 1 choice and composition under exact and approximate targets; every exact top-k sorts by the ranked measure, descending, and every root outputs the logical columns — a group key namedvalue, an integer group key, two group keys, and aSUMranking.stage3_plan_selectiontests:count_min_is_admitted_without_a_target;not_materialized_shared_nodes_are_charged_per_query;scalar(v)inheritsv's guarantee and no other scalar expression does.stage_pipelinetests: a failed selection lists the rejections withselected: null; a run without physical candidates still emitsstage3_plan_selection.crates/devtools/tests/stage_pipeline.rs: the four-document output validates; physical ASAP-aware optimization implements or prunes each composed logical candidate exactly once with no cost; plan selection accounts for every physical candidate once and costs exactly the valid ones; the committed fixture is current.Verification
cargo fmt --all -- --check: OKcargo clippy --workspace --all-targets -- -D warnings: OKcargo test --workspace: 1610 passed, 0 failed, 2 ignoredtest_render: 29 OKArchitectural decisions
AccuracyModelstays theoretical; the deployment's empirical accuracy enters throughAccuracyEvidenceProvider::output_guaranteeand only in plan selection.Sort→Limitunder aProjectrestoring[keys.., topk_k].MatAtIngestion/MatAtQuery) once, aNotMaterializednode once per consumer that reaches it (each query root, scalar root or materialized node; counting stops at a materialized node). Each node'smultiplieris in thestage_pipelineoutput. Everything isNotMaterializeduntil Stage 2 materialization: ingestion vs query time per summary #604, so a shared node is charged once per consuming query (user decision).Later changes: #674 renamed the modules, test file and document keys by stage number and name (
stage2_physical_asap_optimization,stage3_plan_selection,stage0_query_language_frontends, …) and made these stages part of the planner pipeline, which the facade runs by default (run_stage1_to_stage3/Stage1ToStage3Pass, #675 names); #577 replaced the enumeration cap with the display cap--max-displayed, so plan selection prices every candidate.Limitations and follow-up
NotMaterialized. Enumeration ofMatAtIngestion/MatAtQueryand the timing split across query roots: Stage 2 materialization: ingestion vs query time per summary #604.prunedlist yet: feat(dag-viewer): redesign the viewer around the stage pipeline #574.Stack: tracker #528 · main → #576 → #577 → …
Human review — do not complete with an agent
🤖 Generated with Claude Code
https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW