Repository navigation
feat(runtime): export timed plans as PhysicalASAPDAG and compile them - #541
Conversation
5686cf7 to
d9223e3
Compare
b22fa53 to
d82bcde
Compare
26da80b to
51a5484
Compare
337a0ff to
a94bd59
Compare
51a5484 to
b58f2b4
Compare
a94bd59 to
d84830e
Compare
8fb402b to
2535016
Compare
d84830e to
b631137
Compare
2535016 to
e55e580
Compare
075d8e2 to
d9dc49c
Compare
e55e580 to
397e56a
Compare
d9dc49c to
04f9db2
Compare
008b0d0 to
e3febef
Compare
04f9db2 to
0ebce24
Compare
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>
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>
0ebce24 to
aaa8467
Compare
Review findings and fixesTwo review rounds. All fixes are in c42fd93 (
The DataFusion check of #539 also ran lowered SQL through this runtime: |
- timing: a SummaryAgg the lifecycle assignment recomputes at query time now forces the SummaryAgg consuming it to query time (`forces_query_time` reads the assignment), instead of failing with EvaluationUnderMaintenance. - physical export: `PhysicalASAPDAG::validate` requires each node's incoming edges to be exactly its operator's inputs with their roles, so swapped Left/Right edges cannot reorder `a - b` (EdgeRoleMismatch). - physical export: a DAG that fails validation (an empty batch, a hand-timed plan) is an `InvalidPhysicalDAG` error instead of a panic. - precompute: only a TimeRange over (a TimeShift over) a time series Scan takes the raw-sample input contract, as before the port; a table range needs a stored population. - compile: an input binding below a node the PromQL fallback evaluates is an error; the fallback reads raw series slots, so it was silently ignored. A binding below another bound input is still superseded by it, as in `cut`. - compile: drop the scalar-literal binary branch; nothing filled its map. - PromQL fallback: clamp's `NOT (max < min)` is now the IR `Filter` #540 emits; the fallback runs a filter on scalars alone, and the pointwise projection no longer filters on its own. - tests: `raw_series_contract_is_explicit` asserts that a consumed bare selector is instant selection (its comment said the opposite) and that a binding below the fallback is rejected; new `unified_precompute` test. - PromQL fallback: unary minus drops the metric name like every other pointwise projection, following #540 and the Prometheus 3.15 baseline (#511); pointwise_projection_names_and_dynamic_parameters expects it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
c42fd93 to
ccc1f83
Compare
578be5c to
7396e96
Compare
Third review and the fallback decisionDecision: ASAPPlanner has no execution fallback. It generates candidates, costs them, or says why a query has none; executing anything the native operators cannot run is the deployment runtime's job (e.g. the ASAPQuery backend). ccc1f83 (
The earlier fixes that lived in the fallback (unary minus |
Cut today's planner over to the unified OperatorNode IR. The planner calls the unified frontends (`asap_frontend_*::unified`) and returns OperatorNode / QueryRoot; ParsedWorkload keeps scalar roots; Pass 1 (ASAPStrategies), the identical-sub-DAG sharing, selection, DAG assembly and lifecycle costing all run on OperatorNode; and PlanOutput exposes the whole workload DAG. Planned summary states derive their coverage from the node (#646). The legacy physical planner read the planner's QueryExpr output, so it is removed here together with its expression compiler and raw sources; every caller uses the native compiler from #541 (`unified_physical_planner`, `unified_sources`), which reads the PhysicalASAPDAG export. Operator names follow the evaluation vocabulary (readout -> evaluation). The MetricsQL frontend input carries the planning clock and lowers through `lower_metricsql_query_workload`, so bare selectors get the workload's ingestion-interval lookback (#540). The legacy frontend modules and the QueryExpr/SummaryNode IRs stay compiled but are no longer re-exported from post_asap. The next PR moves the unified modules to the crate roots; a later one removes the legacy IRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
The logical export is phase-free. Physical planning still needs the lifecycle timing expansion; bring it back unchanged except for carrying coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The logical export carries no execution timing. Physical planning output needs it: reuse the logical payloads and node ids, and add each node's and edge's data state plus window compatibility, as the earlier post-ASAP export did. Coverage is carried through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match the logical export: a batch is one DAG whose roots are its queries, with shared sub-DAGs exported once. The runtime compiles all roots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s version DataFusion 54 requires chrono ^0.4.44, so the runtime crate's exact =0.4.39 pin no longer resolves; keep 0.4.39 as the minimum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SAP DAG Follow #537: the physical DAG now carries each node's operator as Operator<NodeId> (from ir::flat) instead of the removed wire mirror types. The runtime matches Operator::NonASAP / Operator::ASAP directly, and restore() rebuilds the in-memory DAG with map_children instead of a field-by-field conversion. Node ids are plain usize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…export Coverage is derived from the node (#646), so timed copies no longer carry it and PhysicalASAPDAGNode drops its coverage field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
- timing: a SummaryAgg the lifecycle assignment recomputes at query time now forces the SummaryAgg consuming it to query time (`forces_query_time` reads the assignment), instead of failing with EvaluationUnderMaintenance. - physical export: `PhysicalASAPDAG::validate` requires each node's incoming edges to be exactly its operator's inputs with their roles, so swapped Left/Right edges cannot reorder `a - b` (EdgeRoleMismatch). - physical export: a DAG that fails validation (an empty batch, a hand-timed plan) is an `InvalidPhysicalDAG` error instead of a panic. - precompute: only a TimeRange over (a TimeShift over) a time series Scan takes the raw-sample input contract, as before the port; a table range needs a stored population. - compile: an input binding below a node the PromQL fallback evaluates is an error; the fallback reads raw series slots, so it was silently ignored. A binding below another bound input is still superseded by it, as in `cut`. - compile: drop the scalar-literal binary branch; nothing filled its map. - PromQL fallback: clamp's `NOT (max < min)` is now the IR `Filter` #540 emits; the fallback runs a filter on scalars alone, and the pointwise projection no longer filters on its own. - tests: `raw_series_contract_is_explicit` asserts that a consumed bare selector is instant selection (its comment said the opposite) and that a binding below the fallback is rejected; new `unified_precompute` test. - PromQL fallback: unary minus drops the metric name like every other pointwise projection, following #540 and the Prometheus 3.15 baseline (#511); pointwise_projection_names_and_dynamic_parameters expects it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
ASAPPlanner generates candidates and computes their cost, or says why a query has none. Executing what the native operators cannot run belongs to the deployment runtime (ASAPQuery backend), not to the Planner. Remove unified_physical_planner::promql_fallback and its tests, and the fallback paths in compile: raw-series input slots, the fallback lowering and the below-fallback binding check. A node without a native lowering now fails compilation with Error::Invalid. Reject Planner node IDs above u32::MAX, which could collide with helper IDs, and error instead of debug-asserting when a node needs more than 2^16 helpers. Document that forces_query_time also forces an unaligned per-series PromQL BinaryOp to query time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
ccc1f83 to
ee0ee5a
Compare
Cut today's planner over to the unified OperatorNode IR. The planner calls the unified frontends (`asap_frontend_*::unified`) and returns OperatorNode / QueryRoot; ParsedWorkload keeps scalar roots; Pass 1 (ASAPStrategies), the identical-sub-DAG sharing, selection, DAG assembly and lifecycle costing all run on OperatorNode; and PlanOutput exposes the whole workload DAG. Planned summary states derive their coverage from the node (#646). The legacy physical planner read the planner's QueryExpr output, so it is removed here together with its expression compiler and raw sources; every caller uses the native compiler from #541 (`unified_physical_planner`, `unified_sources`), which reads the PhysicalASAPDAG export. Operator names follow the evaluation vocabulary (readout -> evaluation). The MetricsQL frontend input carries the planning clock and lowers through `lower_metricsql_query_workload`, so bare selectors get the workload's ingestion-interval lookback (#540). The legacy frontend modules and the QueryExpr/SummaryNode IRs stay compiled but are no longer re-exported from post_asap. The next PR moves the unified modules to the crate roots; a later one removes the legacy IRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Cut today's planner over to the unified OperatorNode IR. The planner calls the unified frontends (`asap_frontend_*::unified`) and returns OperatorNode / QueryRoot; ParsedWorkload keeps scalar roots; Pass 1 (ASAPStrategies), the identical-sub-DAG sharing, selection, DAG assembly and lifecycle costing all run on OperatorNode; and PlanOutput exposes the whole workload DAG. Planned summary states derive their coverage from the node (#646). The legacy physical planner read the planner's QueryExpr output, so it is removed here together with its expression compiler and raw sources; every caller uses the native compiler from #541 (`unified_physical_planner`, `unified_sources`), which reads the PhysicalASAPDAG export. Operator names follow the evaluation vocabulary (readout -> evaluation). The MetricsQL frontend input carries the planning clock and lowers through `lower_metricsql_query_workload`, so bare selectors get the workload's ingestion-interval lookback (#540). The legacy frontend modules and the QueryExpr/SummaryNode IRs stay compiled but are no longer re-exported from post_asap. The next PR moves the unified modules to the crate roots; a later one removes the legacy IRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Rename LifecycleAssignment to MaterializationAssignment and apply_lifecycle_timings to apply_materialization_timings. The default assignment is now all_query_time(): nothing is materialized until Stage 2 materialization (#509) decides per sub-DAG. all_ingestion_time() and set() assign maintenance explicitly. - validate_default becomes validate_maintained: candidate legality is still checked with every summary maintained, so candidate generation is unchanged. planned_data_state and fixed_window_rate_candidates use the same maintained assumption, and the maintained precompute compilers in promql_rows assign ingestion time explicitly. - PlanOutput::execution_timed_dag, show_post_asap_ir and the default test helpers now emit query-time summaries. - forces_query_time (#541) treats a SummaryAgg as query time when the assignment, including its default, puts it there. - Tests that exercise maintenance assign it explicitly (new `maintained` helpers); a new unit test pins the query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename LifecycleAssignment to MaterializationAssignment and apply_lifecycle_timings to apply_materialization_timings. The default assignment is now all_query_time(): nothing is materialized until Stage 2 materialization (#509) decides per sub-DAG. all_ingestion_time() and set() assign maintenance explicitly. - validate_default becomes validate_maintained: candidate legality is still checked with every summary maintained, so candidate generation is unchanged. planned_data_state and fixed_window_rate_candidates use the same maintained assumption, and the maintained precompute compilers in promql_rows assign ingestion time explicitly. - PlanOutput::execution_timed_dag, show_post_asap_ir and the default test helpers now emit query-time summaries. - forces_query_time (#541) treats a SummaryAgg as query time when the assignment, including its default, puts it there. - Tests that exercise maintenance assign it explicitly (new `maintained` helpers); a new unit test pins the query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(planner): plan one shared unified workload DAG Cut today's planner over to the unified OperatorNode IR. The planner calls the unified frontends (`asap_frontend_*::unified`) and returns OperatorNode / QueryRoot; ParsedWorkload keeps scalar roots; Pass 1 (ASAPStrategies), the identical-sub-DAG sharing, selection, DAG assembly and lifecycle costing all run on OperatorNode; and PlanOutput exposes the whole workload DAG. Planned summary states derive their coverage from the node (#646). The legacy physical planner read the planner's QueryExpr output, so it is removed here together with its expression compiler and raw sources; every caller uses the native compiler from #541 (`unified_physical_planner`, `unified_sources`), which reads the PhysicalASAPDAG export. Operator names follow the evaluation vocabulary (readout -> evaluation). The MetricsQL frontend input carries the planning clock and lowers through `lower_metricsql_query_workload`, so bare selectors get the workload's ingestion-interval lookback (#540). The legacy frontend modules and the QueryExpr/SummaryNode IRs stay compiled but are no longer re-exported from post_asap. The next PR moves the unified modules to the crate roots; a later one removes the legacy IRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * feat(planner): plan and export standalone scalar queries 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 * fix(planner): plan non-pass-through scalar references exact 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 * fix(planner): check scalar plans in the contract; remove dead code 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 * fix(planner): lower scalar(v) in Project/Filter expressions as scalar 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 * fix(planner): reuse a top-k only over the same partitions 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 * refactor(planner): tell logical rewrites apart by provenance `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 * fix(planner): return an error when search input already holds ASAP operators 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 * fix(planner): walk operator nodes read by scalar expressions 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 * fix(ir): do not promote a filtered count ranking to a heavy-hitter TopK 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 * Revert "fix(planner): plan non-pass-through scalar references exact" 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 * fix(planner): rank top-k only when the Limit partitions like its Sort 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 * fix(ir): export one ScalarRef edge per distinct referenced node 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 * test(planner): cover scalar inputs, top-k partitions, scalar plan contract, 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 * test(planner): a difference of two scalar quantiles reads summaries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): plan structurally identical scalar references once 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 * test(planner): reject a per-job Limit over a global Sort; doc cleanups Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): read exact-state query roots through their finalize 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 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rename LifecycleAssignment to MaterializationAssignment and apply_lifecycle_timings to apply_materialization_timings. The default assignment is now all_query_time(): nothing is materialized until Stage 2 materialization (#509) decides per sub-DAG. all_ingestion_time() and set() assign maintenance explicitly. - validate_default becomes validate_maintained: candidate legality is still checked with every summary maintained, so candidate generation is unchanged. planned_data_state and fixed_window_rate_candidates use the same maintained assumption, and the maintained precompute compilers in promql_rows assign ingestion time explicitly. - PlanOutput::execution_timed_dag, show_post_asap_ir and the default test helpers now emit query-time summaries. - forces_query_time (#541) treats a SummaryAgg as query time when the assignment, including its default, puts it there. - Tests that exercise maintenance assign it explicitly (new `maintained` helpers); a new unit test pins the query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename LifecycleAssignment to MaterializationAssignment and apply_lifecycle_timings to apply_materialization_timings. The default assignment is now all_query_time(): nothing is materialized until Stage 2 materialization (#509) decides per sub-DAG. all_ingestion_time() and set() assign maintenance explicitly. - validate_default becomes validate_maintained: candidate legality is still checked with every summary maintained, so candidate generation is unchanged. planned_data_state and fixed_window_rate_candidates use the same maintained assumption, and the maintained precompute compilers in promql_rows assign ingestion time explicitly. - PlanOutput::execution_timed_dag, show_post_asap_ir and the default test helpers now emit query-time summaries. - forces_query_time (#541) treats a SummaryAgg as query time when the assignment, including its default, puts it there. - Tests that exercise maintenance assign it explicitly (new `maintained` helpers); a new unit test pins the query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation options (#667) * refactor(planner): remove the summary maintenance lifecycle Stage 2 materialization (#509) will decide per sub-DAG whether and when to materialize, so the Planner no longer chooses a maintenance lifecycle. - MajorPass now runs search_workload_with_targets -> global_selection -> assemble_selected_dag per root -> share_common_sub_dags. - QueryLifecyclePlan becomes QueryPlan { entry_index, root }; PlanOutput's execution_timed_dag times the roots directly. LifecycleInput, the lifecycle errors and UserInput's `lifecycle` field are gone (public API break). - Delete summary_maintenance_lifecycle, summary_maintenance_cost, summary_maintenance_dag_export, post_asap::{summary_maintenance, summary_maintenance_lifecycle} and SummaryWindowFramework (the pane primitives stay), the lifecycle CostModel hooks, CandidateCostOverrides and EmpiricalEvidenceProvider::lifecycle_cost_inputs. - Delete the lifecycle e2e test and the viewer's lifecycle-plan UI; rewire e2e_plan, summary_sharing, operator_design_examples and weighted_topk_binding to the new pipeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ir): default to query-time materialization Rename LifecycleAssignment to MaterializationAssignment and apply_lifecycle_timings to apply_materialization_timings. The default assignment is now all_query_time(): nothing is materialized until Stage 2 materialization (#509) decides per sub-DAG. all_ingestion_time() and set() assign maintenance explicitly. - validate_default becomes validate_maintained: candidate legality is still checked with every summary maintained, so candidate generation is unchanged. planned_data_state and fixed_window_rate_candidates use the same maintained assumption, and the maintained precompute compilers in promql_rows assign ingestion time explicitly. - PlanOutput::execution_timed_dag, show_post_asap_ir and the default test helpers now emit query-time summaries. - forces_query_time (#541) treats a SummaryAgg as query time when the assignment, including its default, puts it there. - Tests that exercise maintenance assign it explicitly (new `maintained` helpers); a new unit test pins the query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: drop the summary maintenance lifecycle and document the query-time default Remove the lifecycle APIs, recipes and viewer section from the docs, delete the workload-demand-and-summary-lifecycle proposal, and point materialization questions to Stage 2 (#509). Rename LifecycleAssignment/apply_lifecycle_timings/ validate_default to their new names and describe the all-query-time default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: finish the Operator<NodeId> port after the legacy cleanup Follow #537: the remaining tests read Operator<NodeId> payloads from the physical DAG, and docs name ir::flat / ir::physical_export instead of the removed ir::export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ir): materialization per #509: NotMaterialized, MatAtIngestion, MatAtQuery options. Replace the two-way timing assignment with them: - New Materialization { NotMaterialized, MatAtIngestion(Medium), MatAtQuery(Medium) } and Medium { Memory, Disk }; only MatAtIngestion runs at ingestion time. - MaterializationAssignment maps a SummaryAgg or MaintainPopulation to its option (set); Default is NotMaterialized everywhere, and with_default gives unassigned sub-DAGs an option where the DAG allows it (validate_maintained, planned_data_state and the maintained precompute compilers use MatAtIngestion). all_query_time, all_ingestion_time and summary_timing are gone. - MaintainPopulation follows its assignment like SummaryAgg instead of always running at ingestion time. - An explicit MatAtIngestion over something that only exists at query time fails with UpstreamNotAtIngestion ("upstream of ingestion-time node not at ingestion time") instead of silently moving to query time. - OperatorNode and PhysicalASAPDAGNode carry their Materialization; PhysicalASAPDAG validation rejects a materialization inconsistent with the node's timing. - Remove PhysicalASAPDAG::with_execution_phases (#509 Assumption 8: the deployment executes the plan as given). Tests place summaries through the assignment instead. Retention and choosing the option per sub-DAG stay with Stage 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): materialize any node; keep explicit assignments across a split Per #509 ("Materialization decides, for each sub-DAG, whether its output is stored"; Example 4 stores panes), `MaterializationAssignment::set` accepts any node. `MatAtIngestion` puts it at ingestion time, or fails with `UpstreamNotAtIngestion` when something below can only run at query time, and a query-time consumer reads its stored output. An ingestion-pinned node (e.g. a fixed-window rate finalize) over a query-time state now fails the same way instead of with `IllegalChildDataState`. `split_shared_by_phase` returns the assignment remapped onto the rebuilt nodes, so an explicit `MatAtIngestion` is no longer silently lost, and the assignment holds `Rc` clones of its keys so a reused address cannot match a stale entry. Renames the remaining "maintained" names to the docs' vocabulary (`validate_at_ingestion`, `at_ingestion` test helpers), notes that `frontier_from_timing` does not store `MatAtQuery` outputs yet (Stage 2), and bumps PHYSICAL_ASAP_DAG_WIRE_VERSION to 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(integration): same answers with and without materialization Ports the four execution tests removed with the summary-maintenance lifecycle to MaterializationAssignment: NotMaterialized and MatAtIngestion placements of the KLL quantile, the current-series population, and sum_over_time arithmetic over one and over different selectors execute to the same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): honor explicit materialization on fixed-timing nodes; allow stored roots A node whose timing its candidate fixed keeps an agreeing explicit assignment and rejects a conflicting one (MaterializationConflictsWithFixedTiming) instead of dropping it. A root assigned MatAtIngestion is valid; only an ingestion-time root of rows that is not stored there errors (UnstoredIngestionRoot, was MaintenanceRowsAtRoot). 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>
Why
#509 (Stage 2) hands each candidate plan to the runtime as a
PhysicalASAPDAG: the #511 operator DAG plus, for every node, when it runs (ingestion or query time) and what it produces (rows or summary state). #537 exports only the logical DAG, with no timing, and the runtime still compiles the legacyPostAsapDAGbuilt fromSummaryExpr. To run plans built from the unified IR (#539, #540), the IR needs a timing pass and a physical export, and the runtime needs a compiler that reads that export.What
asap_types::ir::timing):apply_lifecycle_timingswrites anExecutionTiminginto every node of a plan from a per-summaryLifecycleAssignment, and validates every edge against the node kinds.asap_types::ir::physical_export):compile_physical_asap_dag/compile_physical_asap_workloadturn a timed plan, or a batch with one root per query, into a validatedPhysicalASAPDAG.asap_physical_operators::unified_physical_planner,unified_sources,expressions::unified_planner): compiles aPhysicalASAPDAGinto the existing physical operators, with precompute frontiers for maintained states. It is a reference executor for tests; executing anything it cannot lower natively is the deployment runtime's job.The planner still builds and runs legacy plans; it switches to these entry points at the cutover (#542).
How
Timing pass. The logical DAG carries no timing.
LifecycleAssignmentrecords, perSummaryAgg(by node identity), whether its state is maintained at ingestion time or recomputed at query time (default: maintained).apply_lifecycle_timingsthen times every node:SummaryEstimate/EvaluatePopulationrun at query time,MaintainPopulationat ingestion time;SummaryAggtakes its assignment, unless something below it can only exist at query time (an evaluation, or a state the assignment recomputes at query time), which moves it to query time too;A node reached by two consumers with different timings is a
ConflictingTimingerror;split_shared_by_phasecopies such a sub-DAG first.TimingMemoshares the pass across the roots of a batch, so a node shared by two roots stays one node.Physical export.
compile_physical_asap_workloadflattens the timed roots into nodes and edges (shared sub-DAGs once), and records each node'sExecutionDataState(timing + raw rows or summary state), each edge's role (Left/Right/Input/ScalarRef) and its grouping/window compatibility.PhysicalASAPDAG::validatechecks the roots, that each node's incoming edges are exactly its operator's inputs with their roles, and that no ingestion-time node reads a query-time value. A DAG that fails validation is anInvalidPhysicalDAGerror.Runtime compiler.
compile(dag, inputs, roots)walks the DAG from the selected roots down to the bound inputs and lowers each node to physical operators. Native lowering covers the relational and summary operators. A node with no native lowering (most PromQL vector expressions) fails compilation with an explicit error: ASAPPlanner generates and costs candidates, and the deployment runtime (e.g. the ASAPQuery backend) executes them, so there is no fallback evaluator in the planner. Node ids aboveu32::MAXare rejected, because the compiler numbers its helper operators above the Planner id range. Precompute: a frontier of nodes can be materialized (cut_candidate,enumerate_frontiers), and a boundary over a time-seriesTimeRangereads raw samples, while a table range needs a stored population.bind_with_data_sourcesresolvesScanleaves throughDataSources.The runtime crate's
chronopin is relaxed to0.4.39because DataFusion 54 (#614) needs^0.4.44.Key code interfaces
Timing —
crates/types/src/ir/timing.rs:Physical export —
crates/types/src/ir/physical_export.rs:Runtime —
crates/asap-physical-operators/src/unified_physical_planner/:Fields and errors
ExecutionDataState { timing, primitive }IngestionTime/QueryTime) and whether it produces raw rows or summary state.EdgeRoleLeft/Rightorder a binary operator,Inputis a single input,ScalarRefis a plan read from a scalar position.PhysicalASAPDAGValidationError::NoRootsPhysicalASAPDAGValidationError::EdgeRoleMismatchLeft/Right).ExecutionDataStateError::ConflictingTimingExecutionDataStateError::UntimedNodeExecutionDataStateError::InvalidPhysicalDAGcompilefails for a node the reference executor cannot lower; the deployment runtime executes such nodes.InputContractBefore this PR
The timing pass and physical export existed only for the legacy
SummaryExprgraph (PostAsapDAG), and the runtime compiled only that graph. A plan built from the unified IR (#539, #540) could be exported as a logical DAG (#537) but not timed, exported for execution, or run.After this PR
A unified-IR plan can be timed, exported as a validated
PhysicalASAPDAGwith one root per batch query, and the parts the reference executor supports natively can be compiled and executed, including plans with materialized precompute frontiers. The legacy runtime is unchanged; the planner switches at #542.Evidence
Execution examples from the tests. Screenshots and measurements: not applicable.
Scan → SummaryAgg(sum by key) → Finalize, default assignmentSummaryAggat ingestion time,Finalizeat query time; the export round-trips through JSONtimed_plan_exports_with_timingSummaryAggnode, two rootsbatch_shares_the_summary_and_keeps_one_root_per_queryLeft/Rightedges swappedEdgeRoleMismatchswapped_edge_roles_are_rejectedcompile_physical_asap_workload(&[])InvalidPhysicalDAG(NoRoots), not a panican_empty_batch_is_an_errorquery_time_state_moves_its_consuming_summary_to_query_timeu32::MAXErr, not a panic or an id collisionnode_ids_above_u32_are_rejectedTimeRangeover a tableScanas a precompute boundaryonly_a_time_series_range_reads_raw_samplesVerification
crates/types/src/ir/timing.rs(timing rules and conflicts),crates/types/tests/physical_export.rs(export, validation, batches, edge roles, empty batch).unified_precompute.rs(raw-sample contract of a precompute boundary),unified_node_ids.rs(ids aboveu32::MAXare rejected). Native lowering needs more tests: Runtime: tests for the native lowering of PhysicalASAPDAG #662.cargo fmt --check, workspace all-target Clippy with-D warnings, andcargo test --workspace(2,014 tests, 0 failures) on the PR head.Architectural decisions
OperatorNode.timing) by a separate pass over the logical DAG, not stored beside it, so the export and the runtime read one value per node.PhysicalASAPDAGper batch, one root per query, with shared sub-DAGs exported once, matching the logical export (feat(ir): add logical sub-DAG sharing and export without execution timing #537).unified_physical_planner, promoted tophysical_plannerwhen the legacy runtime is removed (refactor(planner): plan the workload on the unified operator IR #542 and the PR after it). Rejected: convertingPhysicalASAPDAGinto the legacyPostAsapDAG.Limitations and follow-up
PhysicalASAPDAG: PhysicalASAPDAG::validate does not check per-operator timing #656.ConflictingTiming: Timing: a node shared across batch queries with different timings fails with ConflictingTiming #654.split_shared_by_phasereturns a rebuilt DAG keyed by new node identities, so a non-default assignment has to be re-applied by the caller; Stage 2 materialization: ingestion vs query time per summary #604 (Stage 2 materialization) returns the remapped assignment. Nothing calls it with a non-default assignment before then.sum/avgover empty or all-NULL input return 0 / NaN instead of NULL, Int64avgcan overflow, and several SQL shapes are not executable yet: Runtime: sum/avg over empty input, avg overflow, and unsupported SQL shapes #659.compiled.rs, the value helpers) until refactor(planner): plan the workload on the unified operator IR #542 and the PR after it remove the legacy runtime.Stack: tracker #528 · previous: #540 · next: #542.
Human review — do not complete with an agent
🤖 Generated with Claude Code
https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW