Repository navigation
Description
Problem, by example
The PromQL query from #1011, with max_error 0.01 and failure_probability 0.001:
avg by (job) (quantile_over_time(0.5, data[5m]) / quantile_over_time(0.5, data[5m]) + quantile_over_time(0.9, data[5m]))
Lowered two ways on main (d728d05cf). Measured with a scratch crate and with stage_pipeline:
| Path | Schema of the per-series quantile 0.5 node |
CSE result on main |
|---|---|---|
Test helper test_support::lower_promql |
fields [ts, value, job], unique_keys: [], closed: false |
the two quantiles are 2 groups (8 in total) |
Production (stage_pipeline, the planner facade) |
fields [ts, value, job, $promql_series_identity], unique_keys: [[3, 0]], closed: true |
1 group read twice (7 in total) |
Hundreds of Stage 1–3 tests plan over the first schema, which production never produces, so they test behaviour that never runs. #1011's own example (8 groups) was measured on the test shape.
Why it happens
- The frontend (
lower_promql_query_workload,crates/frontend-promql/src/lib.rs:108-133) does not add the series identity on scans. - Production adds it in a later step,
with_promql_series_identity(crates/types/src/ir/schema/schema_support.rs:40-70). That step adds the$promql_series_identitycolumn, closes the schema and adds the key[identity, ts]; parents inherit the key (for exampleaggregate_schema.rs:50). - Two production paths run that step, with different failure policies:
- devtools
lower_promql_roots(crates/devtools/src/planning_stages.rs:38-49) returns an error; - the facade (
crates/planner/src/lib.rs:322-334) returns the raw roots, andStage1ToStage3Pass::optimize(crates/plan-selection/src/search/pipeline.rs:340-350) adds the identity there, silently keeping the raw root when the step fails.
- devtools
- The test helpers stop after the frontend:
test_support::lower_promql(crates/logical-asap-optimization/src/test_support.rs:21-47);fixtures::lower_promqlandlower_promql_root(crates/integration-tests/src/lib.rs:27-65);devtools::lower_promql_with_sample_interval(crates/devtools/src/lib.rs:38);- direct
lower_promql_query_workloadplusrun_stage1_to_stage3_withincrates/planner/tests/{summary_sharing,plan_stages,deployment_accuracy_model}.rs.
- The frontend is also inconsistent by itself. It already adds the identity under scalar-op and
PromqlMapnodes (crates/frontend-common/src/resolve.rs:156-176), so in testsrate(x) * 2has the production shape andrate(x)does not. - By a grep estimate, about 350–450 Stage 1–3 tests run on the test shape. The
planner_layering_example*tests already use the production path, and the frontend-shape tests (about 60, inintegration-tests/tests/{aggregate,binary_op,nested,scan,schema,time_range}.rs) correctly use the raw output.
Behaviour that differs between the two shapes:
| Behaviour | Code | Test shape | Production shape |
|---|---|---|---|
CSE legality on main |
types/src/ir/cse.rs:219 |
per-series nodes are not shared | shared |
| Pass 2 equal per-series children | asap_aware_cse.rs:794-797 |
PerEntity same-rows rule never fires |
fires |
| Top-k heap item | per_computation_candidates.rs:1095-1107 |
EntityIdentity::PromqlLabelSet |
Column($promql_series_identity) |
| Keyed filtered group (key-HAVING) | per_computation_candidates.rs:1698 |
rejected (no key) | can qualify |
| Binary-op timing checks | types/src/ir/properties/timing.rs:270-277 |
per-series row checks skipped | applied |
| Executor | executor/src/operators/aggregate/mod.rs:49,115 |
reads the identity column, which test plans do not have | as planned |
For example, promql_to_post_asap.rs:234 asserts a PromqlLabelSet heap item, which production never plans.
Proposal
Have one PromQL planning lowering, used by production and by the planner test helpers:
- Add
lower_promql_planning_roots(workload) -> Result<Vec<QueryRoot>, PromqlError>toasap-frontend-promql: the frontend lowering, thenwith_promql_series_identityon every operator root (and on the operator nodes a scalar root reads). It needs onlyasap-types, which the crate already depends on. A_with_histogramsvariant goes with it only if a caller needs one. - Call it from devtools (
planning_stages.rs, replacinglower_promql_roots) and from the facade (planner/src/lib.rs). - Delete the identity step from
Stage1ToStage3Pass::optimize, so the roots a stage receives are what the frontend handed it. - Pick one failure policy for the step. Recommended: return an error, as devtools does today. The facade's silent fallback plans over a shape the executor does not run. The alternative is to keep the raw root, documented.
- Point the planner test helpers at it:
test_support::lower_promql,fixtures::lower_promql/lower_promql_root,devtools::lower_promql_with_sample_interval, and the threeplanner/testsfiles. The frontend-shape tests keep the rawlower_promql_query_workload.
Adding the identity twice is harmless: a scan that already has the column is returned unchanged (schema_support.rs:44-53). So test files that already add it after the helper keep working.
Scope and acceptance
- In scope: the new function; the facade, devtools and pass changes; one failure policy; the helpers; updating test expectations that pinned the test shape (column positions,
unique_keys,closed,PromqlLabelSet, CSE or reuse outcomes, prices). The estimate is dozens of tests, not yet counted by running them. - Out of scope: SQL and MetricsQL lowering, unless they show the same gap; changing what
with_promql_series_identityadds. - Acceptance:
- A test lowers Stage 0 review: frontends and the logical memo (hash-consed groups) #1011's query through
test_support::lower_promqland asserts the quantile node has$promql_series_identityand a unique key. It fails onmaintoday. LogicalMemo::from_rootsgives 7 groups for that query through every helper.rg with_promql_series_identityfinds it only inasap-typesand inlower_promql_planning_roots(plus tests that check the step itself).- The facade and
stage_pipelinegive the same roots for the same workload.
- A test lowers Stage 0 review: frontends and the logical memo (hash-consed groups) #1011's query through
Status and related work
- Found 2026-10-28 while measuring fix(cse): share equal nodes that return the same rows for the same input, not only keyed ones #1032 against Stage 0 review: frontends and the logical memo (hash-consed groups) #1011's example. The decision to use the single shared lowering function (above) rather than only patching the two helpers was made 2026-10-29. The failure policy is still open; the recommendation is to return an error.
- Related: Stage 0 review: frontends and the logical memo (hash-consed groups) #1011 (Stage 0 review; checklist 2 asks for one frontend entry-point shape), fix(cse): share equal nodes that return the same rows for the same input, not only keyed ones #1032 (CSE legality, whose only effect on the test path is this gap), [Refactor] Repo-wide naming survey: overloaded and misleading names #948 (naming survey).
🤖 Generated with Claude Code
Activity
test(devtools): keyless ratio-of-quantiles memo has 25 choices; only …
fix(cse): share equal nodes that return the same rows for the same in…