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
- 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
f306a96 to
c42fd93
Compare
148c49e to
578be5c
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
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