Skip to content

Commit 6ec8858

Browse files
zzylolclaude
andcommitted
refactor(planner): tumbling panes derive their coverage; Example 3 tests read flat DAGs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
1 parent d3ce2bd commit 6ec8858

8 files changed

Lines changed: 62488 additions & 129751 deletions

File tree

‎crates/devtools/tests/stage_pipeline.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,14 @@ fn example3b_lists_tumbling_candidates() {
147147
]
148148
);
149149
for candidate in &stage1["candidates"].as_array().unwrap()[3..] {
150-
let dag: LogicalASAPDAG = serde_json::from_value(candidate["dag"].clone()).unwrap();
150+
let dag: FlatDag = serde_json::from_value(candidate["dag"].clone()).unwrap();
151151
let merges = dag
152152
.nodes
153153
.iter()
154154
.filter(|n| {
155155
matches!(
156-
n.payload,
157-
asap_types::ir::export::LogicalASAPOperatorPayload::SummaryMerge
156+
n.operator,
157+
asap_types::ir::Operator::ASAP(asap_types::ir::ASAPOp::SummaryMerge { .. })
158158
)
159159
})
160160
.count();

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

Lines changed: 117 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010
//! replaced. Tests that depend on them are `#[ignore]`d with the feature.
1111
#![allow(dead_code)]
1212

13+
use asap_types::ir::NonASAPOp;
1314
use std::collections::{BTreeMap, BTreeSet, HashSet};
1415

1516
use asap_physical_optimizer::implementation::physical_candidates::PhysicalCandidate;
1617
use asap_plan_selection::{plan_stages, PlanningModels, Selection};
17-
use asap_types::ir::export::{
18-
compile_logical_asap_workload, LogicalASAPDAG, LogicalASAPNodeId, LogicalASAPOperatorPayload,
19-
LogicalASAPQueryRoot, NonASAPOpKind, PhysicalASAPDAG,
20-
};
18+
use asap_types::ir::flat::{flatten, FlatDag};
19+
use asap_types::ir::physical_export::{PhysicalASAPDAG, PhysicalASAPNodeId};
2120
use asap_types::ir::properties::ExecutionTiming;
2221
use asap_types::ir::schema::{FieldDataType, SketchAlgorithm, SketchParams, SketchStatistic};
2322
use asap_types::ir::QueryRoot;
23+
use asap_types::ir::{ASAPOp, Operator};
2424
use asap_types::types::AccuracyTarget;
2525
use asap_types::workload::{
2626
AccuracyRequirement, BatchEntry, DataArrival, DataDistribution, DataWorkload, DurationMs,
@@ -29,6 +29,8 @@ use asap_types::workload::{
2929
RepeatedDemand, RepeatingEntry, RepetitionInterval, RootDemand, TimeSelection, TimestampMs,
3030
};
3131

32+
pub type Payload = Operator<PhysicalASAPNodeId>;
33+
3234
/// Every enumerated candidate is built and displayed; the largest example
3335
/// (Example 3, Pattern A) has 486 today.
3436
pub const MAX_CANDIDATES: usize = 4096;
@@ -234,17 +236,15 @@ pub fn lower_promql(workload: &PlanningWorkload) -> Vec<QueryRoot> {
234236
/// as `aggregate:<kind>`, for each node of one query's Stage 0 DAG, sorted
235237
/// (node ids are post-order; compare as a set of operations).
236238
pub fn stage0_operations(root: &QueryRoot) -> Vec<String> {
237-
let dag = asap_types::ir::export::compile_logical_asap_query(root).expect("compiles");
238-
let json = serde_json::to_value(&dag).expect("serializes");
239-
let mut ops: Vec<String> = json["nodes"]
240-
.as_array()
241-
.unwrap()
239+
let dag = flatten(std::slice::from_ref(root)).0;
240+
let mut ops: Vec<String> = dag
241+
.nodes
242242
.iter()
243243
.map(|n| {
244-
let op = &n["payload"]["operator"];
245-
match op["measures"][0]["kind"].as_str() {
244+
let json = serde_json::to_value(&n.operator).expect("serializes");
245+
match json["NonASAP"]["Aggregate"]["measures"][0]["kind"].as_str() {
246246
Some(measure) => format!("aggregate:{measure}"),
247-
None => op["kind"].as_str().unwrap_or("?").to_owned(),
247+
None => relational(&n.operator).unwrap_or_else(|| "?".into()),
248248
}
249249
})
250250
.collect();
@@ -261,8 +261,8 @@ pub struct Logical {
261261
pub id: String,
262262
/// From Pass 2's identical-expression variant.
263263
pub shared_input: bool,
264-
pub dag: LogicalASAPDAG,
265-
pub query_roots: Vec<LogicalASAPNodeId>,
264+
pub dag: FlatDag,
265+
pub query_roots: Vec<PhysicalASAPNodeId>,
266266
}
267267

268268
/// One Stage 2 candidate, derived from the Stage 1 candidate `from_logical`.
@@ -271,7 +271,7 @@ pub struct Physical {
271271
pub id: String,
272272
pub from_logical: String,
273273
pub dag: PhysicalASAPDAG,
274-
pub query_roots: Vec<LogicalASAPNodeId>,
274+
pub query_roots: Vec<PhysicalASAPNodeId>,
275275
pub stage2: PhysicalCandidate,
276276
}
277277

@@ -308,14 +308,17 @@ impl Run {
308308
}
309309
}
310310

311-
fn export(roots: &[QueryRoot]) -> (LogicalASAPDAG, Vec<LogicalASAPNodeId>) {
312-
let dag = compile_logical_asap_workload(roots).expect("logical export");
311+
fn export(roots: &[QueryRoot]) -> (FlatDag, Vec<PhysicalASAPNodeId>) {
312+
for root in roots {
313+
root.validate_structure().expect("valid DAG");
314+
}
315+
let dag = flatten(roots).0;
313316
let query_roots = dag
314317
.roots
315318
.iter()
316319
.map(|root| match root {
317-
LogicalASAPQueryRoot::Operator(id) => *id,
318-
LogicalASAPQueryRoot::Scalar(_) => panic!("Examples 2–4 have operator roots"),
320+
QueryRoot::Operator(id) => *id,
321+
QueryRoot::Scalar(_) => panic!("Examples 2–4 have operator roots"),
319322
})
320323
.collect();
321324
(dag, query_roots)
@@ -386,45 +389,61 @@ pub fn run_stages(workload: &PlanningWorkload, roots: Vec<QueryRoot>) -> Run {
386389

387390
// ── DAG helpers ──────────────────────────────────────────────────────────
388391

389-
/// The logical and physical exports share node ids, payloads and edge
390-
/// endpoints; the helpers below read only those.
392+
/// The logical (flat) and physical DAGs share node ids and payloads; the
393+
/// helpers below read only those and each node's producers and consumers.
391394
pub trait ExportedDag {
392-
fn producers(&self, consumer: LogicalASAPNodeId) -> Vec<LogicalASAPNodeId>;
393-
fn consumers(&self, producer: LogicalASAPNodeId) -> Vec<LogicalASAPNodeId>;
394-
fn node_ids(&self) -> Vec<LogicalASAPNodeId>;
395-
fn payload(&self, id: LogicalASAPNodeId) -> &LogicalASAPOperatorPayload;
396-
}
397-
398-
macro_rules! exported_dag {
399-
($t:ty) => {
400-
impl ExportedDag for $t {
401-
fn producers(&self, consumer: LogicalASAPNodeId) -> Vec<LogicalASAPNodeId> {
402-
let edges = self.edges.iter().filter(|e| e.consumer == consumer);
403-
edges.map(|e| e.producer).collect()
404-
}
405-
fn consumers(&self, producer: LogicalASAPNodeId) -> Vec<LogicalASAPNodeId> {
406-
let edges = self.edges.iter().filter(|e| e.producer == producer);
407-
edges.map(|e| e.consumer).collect()
408-
}
409-
fn node_ids(&self) -> Vec<LogicalASAPNodeId> {
410-
self.nodes.iter().map(|n| n.id).collect()
411-
}
412-
fn payload(&self, id: LogicalASAPNodeId) -> &LogicalASAPOperatorPayload {
413-
&self
414-
.nodes
415-
.iter()
416-
.find(|n| n.id == id)
417-
.expect("node")
418-
.payload
419-
}
420-
}
421-
};
395+
fn producers(&self, consumer: PhysicalASAPNodeId) -> Vec<PhysicalASAPNodeId>;
396+
fn consumers(&self, producer: PhysicalASAPNodeId) -> Vec<PhysicalASAPNodeId>;
397+
fn node_ids(&self) -> Vec<PhysicalASAPNodeId>;
398+
fn payload(&self, id: PhysicalASAPNodeId) -> &Payload;
399+
}
400+
401+
impl ExportedDag for FlatDag {
402+
fn producers(&self, consumer: PhysicalASAPNodeId) -> Vec<PhysicalASAPNodeId> {
403+
self.nodes[consumer]
404+
.operator
405+
.children()
406+
.into_iter()
407+
.copied()
408+
.collect()
409+
}
410+
fn consumers(&self, producer: PhysicalASAPNodeId) -> Vec<PhysicalASAPNodeId> {
411+
(0..self.nodes.len())
412+
.filter(|&id| self.producers(id).contains(&producer))
413+
.collect()
414+
}
415+
fn node_ids(&self) -> Vec<PhysicalASAPNodeId> {
416+
(0..self.nodes.len()).collect()
417+
}
418+
fn payload(&self, id: PhysicalASAPNodeId) -> &Payload {
419+
&self.nodes[id].operator
420+
}
421+
}
422+
423+
impl ExportedDag for PhysicalASAPDAG {
424+
fn producers(&self, consumer: PhysicalASAPNodeId) -> Vec<PhysicalASAPNodeId> {
425+
let edges = self.edges.iter().filter(|e| e.consumer == consumer);
426+
edges.map(|e| e.producer).collect()
427+
}
428+
fn consumers(&self, producer: PhysicalASAPNodeId) -> Vec<PhysicalASAPNodeId> {
429+
let edges = self.edges.iter().filter(|e| e.producer == producer);
430+
edges.map(|e| e.consumer).collect()
431+
}
432+
fn node_ids(&self) -> Vec<PhysicalASAPNodeId> {
433+
self.nodes.iter().map(|n| n.id).collect()
434+
}
435+
fn payload(&self, id: PhysicalASAPNodeId) -> &Payload {
436+
&self
437+
.nodes
438+
.iter()
439+
.find(|n| n.id == id)
440+
.expect("node")
441+
.payload
442+
}
422443
}
423-
exported_dag!(LogicalASAPDAG);
424-
exported_dag!(PhysicalASAPDAG);
425444

426445
/// Every node `root` depends on, including itself.
427-
pub fn closure(dag: &impl ExportedDag, root: LogicalASAPNodeId) -> HashSet<LogicalASAPNodeId> {
446+
pub fn closure(dag: &impl ExportedDag, root: PhysicalASAPNodeId) -> HashSet<PhysicalASAPNodeId> {
428447
let mut seen = HashSet::from([root]);
429448
let mut stack = vec![root];
430449
while let Some(node) = stack.pop() {
@@ -440,8 +459,8 @@ pub fn closure(dag: &impl ExportedDag, root: LogicalASAPNodeId) -> HashSet<Logic
440459
/// The queries (indexes into `query_roots`) whose result depends on `node`.
441460
pub fn readers(
442461
dag: &impl ExportedDag,
443-
query_roots: &[LogicalASAPNodeId],
444-
node: LogicalASAPNodeId,
462+
query_roots: &[PhysicalASAPNodeId],
463+
node: PhysicalASAPNodeId,
445464
) -> BTreeSet<usize> {
446465
query_roots
447466
.iter()
@@ -451,40 +470,46 @@ pub fn readers(
451470
.collect()
452471
}
453472

454-
/// The relational operator's wire `kind` (`"scan"`, `"time_range"`, …).
455-
pub fn relational(payload: &LogicalASAPOperatorPayload) -> Option<String> {
473+
/// The non-ASAP operator's kind in snake case (`"scan"`, `"time_range"`, …).
474+
pub fn relational(payload: &Payload) -> Option<String> {
456475
match payload {
457-
LogicalASAPOperatorPayload::Relational { .. } => {
458-
let json = serde_json::to_value(payload).expect("payload serializes");
459-
json["operator"]["kind"].as_str().map(str::to_owned)
476+
Operator::NonASAP(_) => {
477+
let mut out = String::new();
478+
for (i, c) in payload.kind_name().chars().enumerate() {
479+
if c.is_ascii_uppercase() && i > 0 {
480+
out.push('_');
481+
}
482+
out.push(c.to_ascii_lowercase());
483+
}
484+
Some(out)
460485
}
461-
_ => None,
486+
Operator::ASAP(_) => None,
462487
}
463488
}
464489

465-
pub fn is_summary(payload: &LogicalASAPOperatorPayload) -> bool {
490+
pub fn is_summary(payload: &Payload) -> bool {
466491
matches!(
467492
payload,
468-
LogicalASAPOperatorPayload::SummaryAgg {
493+
Operator::ASAP(ASAPOp::SummaryAgg {
469494
family: FieldDataType::Sketch(..),
470495
..
471-
} | LogicalASAPOperatorPayload::SummaryEstimate { .. }
472-
| LogicalASAPOperatorPayload::SummaryMerge
496+
}) | Operator::ASAP(ASAPOp::SummaryEstimate { .. })
497+
| Operator::ASAP(ASAPOp::SummaryMerge { .. })
473498
)
474499
}
475500

476501
/// The sketch build nodes (`SummaryAgg` over a sketch family) of `dag`,
477502
/// with their algorithm and parameters.
478503
pub fn sketch_builds(
479504
dag: &impl ExportedDag,
480-
) -> Vec<(LogicalASAPNodeId, SketchAlgorithm, SketchParams)> {
505+
) -> Vec<(PhysicalASAPNodeId, SketchAlgorithm, SketchParams)> {
481506
dag.node_ids()
482507
.into_iter()
483508
.filter_map(|id| match dag.payload(id) {
484-
LogicalASAPOperatorPayload::SummaryAgg {
509+
Operator::ASAP(ASAPOp::SummaryAgg {
485510
family: FieldDataType::Sketch(kind, _),
486511
..
487-
} => Some((id, kind.algorithm().clone(), kind.params().clone())),
512+
}) => Some((id, kind.algorithm().clone(), kind.params().clone())),
488513
_ => None,
489514
})
490515
.collect()
@@ -493,8 +518,8 @@ pub fn sketch_builds(
493518
/// The estimation nodes that read `build`, directly or through merges.
494519
pub fn estimates_of(
495520
dag: &impl ExportedDag,
496-
build: LogicalASAPNodeId,
497-
) -> Vec<(LogicalASAPNodeId, SketchStatistic)> {
521+
build: PhysicalASAPNodeId,
522+
) -> Vec<(PhysicalASAPNodeId, SketchStatistic)> {
498523
let mut out = Vec::new();
499524
let mut stack = vec![build];
500525
let mut seen = HashSet::new();
@@ -504,10 +529,10 @@ pub fn estimates_of(
504529
continue;
505530
}
506531
match dag.payload(consumer) {
507-
LogicalASAPOperatorPayload::SummaryEstimate { query } => {
532+
Operator::ASAP(ASAPOp::SummaryEstimate { query, .. }) => {
508533
out.push((consumer, query.clone()))
509534
}
510-
LogicalASAPOperatorPayload::SummaryMerge => stack.push(consumer),
535+
Operator::ASAP(ASAPOp::SummaryMerge { .. }) => stack.push(consumer),
511536
_ => {}
512537
}
513538
}
@@ -519,7 +544,7 @@ pub fn estimates_of(
519544
/// closure, or `"exact"` when it builds none.
520545
pub fn query_option(
521546
dag: &impl ExportedDag,
522-
query_roots: &[LogicalASAPNodeId],
547+
query_roots: &[PhysicalASAPNodeId],
523548
query: usize,
524549
) -> String {
525550
let reach = closure(dag, query_roots[query]);
@@ -539,8 +564,8 @@ pub fn query_option(
539564
/// Nodes reachable from more than one query root.
540565
pub fn cross_query_nodes(
541566
dag: &impl ExportedDag,
542-
query_roots: &[LogicalASAPNodeId],
543-
) -> BTreeSet<LogicalASAPNodeId> {
567+
query_roots: &[PhysicalASAPNodeId],
568+
) -> BTreeSet<PhysicalASAPNodeId> {
544569
dag.node_ids()
545570
.into_iter()
546571
.filter(|&id| readers(dag, query_roots, id).len() > 1)
@@ -551,7 +576,16 @@ pub fn assert_valid_and_uniquely_named(run: &Run) {
551576
let ids: BTreeSet<_> = run.logical.iter().map(|c| c.id.as_str()).collect();
552577
assert_eq!(ids.len(), run.logical.len(), "unique logical ids");
553578
for c in &run.logical {
554-
c.dag.validate().unwrap_or_else(|e| panic!("{}: {e}", c.id));
579+
for (id, node) in c.dag.nodes.iter().enumerate() {
580+
assert!(
581+
node.operator
582+
.children()
583+
.into_iter()
584+
.all(|child| *child < id),
585+
"{}: children come before their parents",
586+
c.id
587+
);
588+
}
555589
}
556590
}
557591

@@ -659,21 +693,19 @@ pub enum WindowForm {
659693
/// `TimeShift(i·w)` over the scan); any other build is rebuilt from its
660694
/// query window. Sliding windows and Exponential Histograms are not planned
661695
/// yet.
662-
pub fn window_form(dag: &impl ExportedDag, build: LogicalASAPNodeId) -> WindowForm {
696+
pub fn window_form(dag: &impl ExportedDag, build: PhysicalASAPNodeId) -> WindowForm {
663697
let merged = dag
664698
.consumers(build)
665699
.into_iter()
666-
.any(|c| matches!(dag.payload(c), LogicalASAPOperatorPayload::SummaryMerge));
700+
.any(|c| matches!(dag.payload(c), Operator::ASAP(ASAPOp::SummaryMerge { .. })));
667701
if !merged {
668702
return WindowForm::None;
669703
}
670704
let ranges: Vec<u64> = dag
671705
.producers(build)
672706
.into_iter()
673707
.filter_map(|input| match dag.payload(input) {
674-
LogicalASAPOperatorPayload::Relational {
675-
operator: NonASAPOpKind::TimeRange { range, .. },
676-
} => Some(range.as_millis() as u64),
708+
Operator::NonASAP(NonASAPOp::TimeRange { range, .. }) => Some(range.as_millis() as u64),
677709
_ => None,
678710
})
679711
.collect();
@@ -699,7 +731,7 @@ pub enum Materialization {
699731

700732
/// Pending (needs Stage 2 materialization): the export records only the
701733
/// execution timing, so a query-time node is never kept.
702-
pub fn materialization(p: &Physical, node: LogicalASAPNodeId) -> Materialization {
734+
pub fn materialization(p: &Physical, node: PhysicalASAPNodeId) -> Materialization {
703735
let n = p.dag.nodes.iter().find(|n| n.id == node).expect("node");
704736
match n.output_state.timing {
705737
ExecutionTiming::IngestionTime => Materialization::IngestionTime,
@@ -709,11 +741,11 @@ pub fn materialization(p: &Physical, node: LogicalASAPNodeId) -> Materialization
709741

710742
/// Pending (needs Stage 2 materialization): how long a materialized node's
711743
/// output is kept, in event time. `None` until Stage 2 records retention.
712-
pub fn retention_ms(_p: &Physical, _node: LogicalASAPNodeId) -> Option<u64> {
744+
pub fn retention_ms(_p: &Physical, _node: PhysicalASAPNodeId) -> Option<u64> {
713745
None
714746
}
715747

716-
pub fn runs_at_ingestion(p: &Physical, node: LogicalASAPNodeId) -> bool {
748+
pub fn runs_at_ingestion(p: &Physical, node: PhysicalASAPNodeId) -> bool {
717749
materialization(p, node) == Materialization::IngestionTime
718750
}
719751

0 commit comments

Comments
 (0)