Skip to content

Commit aa2ddc0

Browse files
zzylolclaude
andcommitted
feat(stage2): offer query time, kept for tumbling panes (Example 4 B3)
Stage 2 now offers a third materialization for the tumbling panes of a query repeating every pane width: built at query time and kept across evaluations. Each evaluation builds the newest pane from one pane width of raw data, merges it with the N - 1 kept panes, then keeps it and drops the oldest (Q59). The physical node carries a `kept` flag (omitted when false), set by Stage 2 and exported in the PhysicalASAPDAG. Stage 3 prices a kept chain like an ingestion-time one but at query time: the newest pane's build per evaluation, (N - 1) panes retained at cost_per_retained_byte_second and counted against the memory budget, and a scan (and raw retention) of one pane width. A deployment without query_time_retention, such as the reference executor, rejects the candidate with a reason (Q56, Q57). The viewer shows "query time, kept" in a kept node's timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4f65ff1 commit aa2ddc0

23 files changed

Lines changed: 139118 additions & 48275 deletions

File tree

‎crates/devtools/src/bin/stage_pipeline.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// cargo run -p asap-devtools --bin stage_pipeline -- \
2-
// --example planner-layering-1 --max-candidates 128 --out planner-layering-example1.json
2+
// --example planner-layering-1 --max-candidates 160 --out planner-layering-example1.json
33
// (also planner-layering-2: #509 Example 2, three SQL statistics over
44
// `flows`; planner-layering-3a and planner-layering-3b: Example 3, Patterns A
55
// and B; planner-layering-4a: Example 4, Pattern A repeated monthly;

‎crates/devtools/tests/stage_pipeline.rs‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ use serde_json::Value;
1010

1111
const COMMITTED: &str = "../../tools/dag-viewer/examples/planner-layering-example1.json";
1212

13-
/// Every Example 1 logical candidate (88) is displayed; the default cap is 64.
14-
const EXAMPLE1: [&str; 4] = ["--example", "planner-layering-1", "--max-candidates", "128"];
13+
/// Every Example 1 plan (136, from 88 logical candidates) is displayed; the
14+
/// default cap is 64.
15+
const EXAMPLE1: [&str; 4] = ["--example", "planner-layering-1", "--max-candidates", "160"];
1516

1617
fn generate(args: &[&str]) -> Value {
1718
// Tests run in parallel and may generate the same document.
@@ -75,9 +76,10 @@ fn example1_document_is_valid_and_committed_fixture_is_current() {
7576
.iter()
7677
.map(|p| p["from_logical"].as_str().unwrap())
7778
.collect();
78-
// One all-query-time candidate per logical one, plus one with Q2's sum
79-
// panes at ingestion time for each of the 24 with panes.
80-
assert_eq!(physical.len(), candidates.len() + 24);
79+
// One all-query-time candidate per logical one, plus, for each of the
80+
// 24 with panes, one with Q2's sum panes at ingestion time and one with
81+
// them kept at query time.
82+
assert_eq!(physical.len(), candidates.len() + 48);
8183
assert_eq!(sources, ids);
8284
for candidate in physical {
8385
assert!(candidate.get("cost").is_none(), "Stage 2 has no cost");

‎crates/executor/tests/hydra_cms_execution.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ fn node(id: u32, payload: Payload, output: Schema) -> PhysicalASAPDAGNode {
7373
id: LogicalASAPNodeId(id),
7474
payload,
7575
output_state: ExecutionDataState::QUERY_ROWS,
76+
kept: false,
7677
output_schema: output,
7778
guarantee: None,
7879
coverage: None,

‎crates/executor/tests/physical_dag.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ fn bind_post_asap_before_execution() {
499499
id: planner_types::ir::export::LogicalASAPNodeId(id),
500500
payload,
501501
output_state: ExecutionDataState::QUERY_ROWS,
502+
kept: false,
502503
output_schema: (*schema).clone(),
503504
guarantee: None,
504505
};
@@ -654,6 +655,7 @@ fn source_batches_must_match_the_bound_schema() {
654655
},
655656
},
656657
output_state: ExecutionDataState::QUERY_ROWS,
658+
kept: false,
657659
output_schema: (*expected).clone(),
658660
guarantee: None,
659661
}],
@@ -734,6 +736,7 @@ fn planner_semijoin_sort_limit_contract_at_both_phases() {
734736
payload,
735737
output_schema: (**schema).clone(),
736738
output_state: ExecutionDataState::QUERY_ROWS,
739+
kept: false,
737740
guarantee: None,
738741
};
739742
let edge =
@@ -1211,6 +1214,7 @@ fn grouped_temporal_schema_compiles_and_executes_topk() {
12111214
operator: operation,
12121215
},
12131216
output_state: ExecutionDataState::QUERY_ROWS,
1217+
kept: false,
12141218
output_schema: (*input).clone(),
12151219
guarantee: None,
12161220
};
@@ -1297,6 +1301,7 @@ fn certified_pruning_rejects_missing_authoritative_values_after_recovery() {
12971301
id: planner_types::ir::export::LogicalASAPNodeId(2),
12981302
output_schema: (*schema).clone(),
12991303
output_state: ExecutionDataState::QUERY_ROWS,
1304+
kept: false,
13001305
guarantee: None,
13011306
payload: PhysicalASAPOperatorPayload::Relational {
13021307
operator: planner_types::ir::export::NonASAPOpKind::Join {
@@ -1415,6 +1420,7 @@ fn compiled_ingestion_binary_preserves_alignment_and_rejects_missing_updates() {
14151420
id: planner_types::ir::export::LogicalASAPNodeId(2),
14161421
output_schema: (*input).clone(),
14171422
output_state: ExecutionDataState::INGESTION_ROWS,
1423+
kept: false,
14181424
guarantee: None,
14191425
payload: PhysicalASAPOperatorPayload::Relational {
14201426
operator: planner_types::ir::export::NonASAPOpKind::BinaryOp {

‎crates/executor/tests/physical_semantics.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ fn global_extrema_bind_with_planner_derived_schema() {
387387
},
388388
},
389389
output_state: ExecutionDataState::QUERY_ROWS,
390+
kept: false,
390391
output_schema: (*output).clone(),
391392
guarantee: None,
392393
};
@@ -771,6 +772,7 @@ fn exact_frequency_intents_execute_typed_keys_and_empty_input() {
771772
},
772773
},
773774
output_state: ExecutionDataState::QUERY_ROWS,
775+
kept: false,
774776
output_schema: (*schema(&[(name, DataType::Float64, false)])).clone(),
775777
guarantee: None,
776778
};
@@ -858,6 +860,7 @@ fn exact_cardinality_binds_and_executes_typed_tuples() {
858860
},
859861
},
860862
output_state: ExecutionDataState::QUERY_ROWS,
863+
kept: false,
861864
output_schema: (*schema(&[("distinct", DataType::Int64, false)])).clone(),
862865
guarantee: None,
863866
};

‎crates/executor/tests/precompute_population.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ fn finalized_shared_panes_rebuild_one_global_summary_after_recovery() {
5959
id: planner_types::ir::export::LogicalASAPNodeId(0),
6060
payload: PhysicalASAPOperatorPayload::SummaryMerge,
6161
output_state: ExecutionDataState::INGESTION_SUMMARY,
62+
kept: false,
6263
output_schema: state_schema.clone(),
6364
guarantee: None,
6465
},
@@ -67,6 +68,7 @@ fn finalized_shared_panes_rebuild_one_global_summary_after_recovery() {
6768
id: planner_types::ir::export::LogicalASAPNodeId(1),
6869
payload: PhysicalASAPOperatorPayload::FinalizeExactAccumulator,
6970
output_state: ExecutionDataState::INGESTION_ROWS,
71+
kept: false,
7072
output_schema: value_schema.clone(),
7173
guarantee: None,
7274
},
@@ -85,6 +87,7 @@ fn finalized_shared_panes_rebuild_one_global_summary_after_recovery() {
8587
},
8688
},
8789
output_state: ExecutionDataState::INGESTION_ROWS,
90+
kept: false,
8891
output_schema: value_schema.clone(),
8992
guarantee: None,
9093
},
@@ -102,6 +105,7 @@ fn finalized_shared_panes_rebuild_one_global_summary_after_recovery() {
102105
filter: None,
103106
},
104107
output_state: ExecutionDataState::INGESTION_SUMMARY,
108+
kept: false,
105109
output_schema: state_schema.clone(),
106110
guarantee: None,
107111
},
@@ -252,6 +256,7 @@ fn state_dag(
252256
id: planner_types::ir::export::LogicalASAPNodeId(0),
253257
payload: PhysicalASAPOperatorPayload::SummaryMerge,
254258
output_state: ExecutionDataState::INGESTION_SUMMARY,
259+
kept: false,
255260
output_schema: logical_schema(family.clone()),
256261
guarantee: None,
257262
}];
@@ -268,6 +273,7 @@ fn state_dag(
268273
id: planner_types::ir::export::LogicalASAPNodeId(read_id),
269274
payload: PhysicalASAPOperatorPayload::FinalizeExactAccumulator,
270275
output_state: ExecutionDataState::INGESTION_ROWS,
276+
kept: false,
271277
output_schema: logical_schema(FieldDataType::Plain(DataType::Float64)),
272278
guarantee: None,
273279
});
@@ -283,6 +289,7 @@ fn state_dag(
283289
filter: None,
284290
},
285291
output_state: ExecutionDataState::INGESTION_SUMMARY,
292+
kept: false,
286293
output_schema: logical_schema(target),
287294
guarantee: None,
288295
});

‎crates/executor/tests/promql_binary.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ fn program_for_bool(operator: BinaryOperator, return_bool: bool) -> CompiledPhys
7777
},
7878
},
7979
output_state: ExecutionDataState::QUERY_ROWS,
80+
kept: false,
8081
output_schema: (*schema).clone(),
8182
guarantee: None,
8283
};
@@ -390,6 +391,7 @@ fn stored_series_evaluations_support_filters_and_sets() {
390391
} else {
391392
ExecutionDataState::QUERY_ROWS
392393
},
394+
kept: false,
393395
output_schema: if id < 2 {
394396
(*state_schema).clone()
395397
} else {

‎crates/executor/tests/raw_scan.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ fn plan(
7474
id: planner_types::ir::export::LogicalASAPNodeId(id),
7575
payload,
7676
output_state: state,
77+
kept: false,
7778
output_schema: (**schema).clone(),
7879
guarantee: None,
7980
};

‎crates/executor/tests/summary_projection.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ fn post_asap_summary_projection_survives_recovery() {
6262
payload: PhysicalASAPOperatorPayload::SummaryMerge,
6363
output_schema: (*schema).clone(),
6464
output_state: ExecutionDataState::INGESTION_SUMMARY,
65+
kept: false,
6566
guarantee: None,
6667
},
6768
PhysicalASAPDAGNode {
@@ -81,6 +82,7 @@ fn post_asap_summary_projection_survives_recovery() {
8182
},
8283
output_schema: output.clone(),
8384
output_state: ExecutionDataState::INGESTION_SUMMARY,
85+
kept: false,
8486
guarantee: None,
8587
},
8688
],

‎crates/integration-tests/tests/planner_layering_common/mod.rs‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,9 @@ pub fn run_promql(workload: &PlanningWorkload) -> Run {
208208
}
209209

210210
/// Whether `form` answers a query window by merging several summaries.
211-
pub fn needs_merge(form: WindowForm, query_window_ms: u64) -> bool {
211+
pub fn needs_merge(form: WindowForm) -> bool {
212212
match form {
213213
WindowForm::None => false,
214-
WindowForm::Sliding { length_ms, .. } => length_ms < query_window_ms,
215214
WindowForm::Tumbling { .. } | WindowForm::ExponentialHistogram { .. } => true,
216215
}
217216
}
@@ -667,8 +666,6 @@ pub fn lookback_ms(workload: &PlanningWorkload, query: usize) -> u64 {
667666
pub enum WindowForm {
668667
/// Rebuilt from the query window's raw samples (no window summary).
669668
None,
670-
/// Windows of `length_ms` starting every `slide_ms`.
671-
Sliding { length_ms: u64, slide_ms: u64 },
672669
/// Back-to-back windows of `length_ms`.
673670
Tumbling { length_ms: u64 },
674671
/// EH buckets covering `horizon_ms` of history.
@@ -678,8 +675,8 @@ pub enum WindowForm {
678675
/// A build merged by a `SummaryMerge` is a tumbling pane, of the width its
679676
/// `TimeRange` input reads (#580: pane `i` is `TimeRange(w)` over
680677
/// `TimeShift(i·w)` over the scan); any other build is rebuilt from its
681-
/// query window. Sliding windows and Exponential Histograms are not planned
682-
/// yet.
678+
/// query window. Exponential Histograms are not planned yet; a sliding form
679+
/// is superseded by tumbling panes (Q63).
683680
pub fn window_form(dag: &impl ExportedDag, build: LogicalASAPNodeId) -> WindowForm {
684681
let merged = dag
685682
.consumers(build)
@@ -718,13 +715,14 @@ pub enum Materialization {
718715
NotMaterialized,
719716
}
720717

721-
/// Stage 2 runs a node at ingestion time or at query time, recomputed at
722-
/// each evaluation; it does not keep query-time output yet (Example 4 B3),
723-
/// so `QueryTimeKept` does not occur.
718+
/// Stage 2 runs a node at ingestion time or at query time; a query-time
719+
/// node is recomputed at each evaluation unless the export marks it kept
720+
/// across evaluations (Example 4 B3).
724721
pub fn materialization(p: &Physical, node: LogicalASAPNodeId) -> Materialization {
725722
let n = p.dag.nodes.iter().find(|n| n.id == node).expect("node");
726723
match n.output_state.timing {
727724
ExecutionTiming::IngestionTime => Materialization::IngestionTime,
725+
ExecutionTiming::QueryTime if n.kept => Materialization::QueryTimeKept,
728726
ExecutionTiming::QueryTime => Materialization::NotMaterialized,
729727
}
730728
}
@@ -734,10 +732,12 @@ pub fn materialization(p: &Physical, node: LogicalASAPNodeId) -> Materialization
734732
/// (`stage3-cost-model.md`): ingestion-time work read at query time through
735733
/// a merge of `N` panes of width `w` keeps `(N + 1) · w`; read directly, the
736734
/// window being built and the completed one, `2 · window`. Taken over every
737-
/// query-time reader the node's ingestion-time work feeds. `None` for a
738-
/// query-time node.
735+
/// query-time reader the node's ingestion-time work feeds. A kept pane of a
736+
/// merge of `N` panes of width `w` stays until the window no longer covers
737+
/// it: `N · w`, the newest pane built from raw data and `N − 1` kept. `None`
738+
/// for a node recomputed at each evaluation.
739739
pub fn retention_ms(p: &Physical, node: LogicalASAPNodeId) -> Option<u64> {
740-
if !runs_at_ingestion(p, node) {
740+
if materialization(p, node) == Materialization::NotMaterialized {
741741
return None;
742742
}
743743
// The longest raw range an ingestion-time node reads.
@@ -753,6 +753,15 @@ pub fn retention_ms(p: &Physical, node: LogicalASAPNodeId) -> Option<u64> {
753753
.max()
754754
.unwrap_or(0)
755755
};
756+
if materialization(p, node) == Materialization::QueryTimeKept {
757+
return p
758+
.dag
759+
.consumers(node)
760+
.into_iter()
761+
.filter(|&c| matches!(p.dag.payload(c), LogicalASAPOperatorPayload::SummaryMerge))
762+
.map(|merge| p.dag.producers(merge).len() as u64 * window(node))
763+
.max();
764+
}
756765
let mut kept = 0;
757766
let mut stack = vec![node];
758767
let mut seen = HashSet::new();

0 commit comments

Comments
 (0)