Skip to content

Commit ba26c8b

Browse files
committed
feat(ir): define compatible logical summary merges
1 parent a718a09 commit ba26c8b

2 files changed

Lines changed: 102 additions & 14 deletions

File tree

‎crates/types/src/ir/asap.rs‎

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ pub enum ASAPOp {
3939
},
4040
/// Read an exact accumulator's state as its finalized value: the
4141
/// maintenance-to-read boundary before query-time operators.
42-
FinalizeExactAccumulator {
43-
child: Rc<OperatorNode>,
44-
},
42+
FinalizeExactAccumulator { child: Rc<OperatorNode> },
4543
/// Maintain the full declared population, including membership changes.
4644
MaintainPopulation {
4745
child: Rc<OperatorNode>,
@@ -52,10 +50,9 @@ pub enum ASAPOp {
5250
child: Rc<OperatorNode>,
5351
evaluation: PopulationStatistic,
5452
},
55-
// ── Reserved: migrated but unimplemented (§1.3 of the proposal) ──
56-
SummaryMerge {
57-
children: Vec<Rc<OperatorNode>>,
58-
},
53+
/// Merge compatible partial states for the same grouping and family.
54+
SummaryMerge { children: Vec<Rc<OperatorNode>> },
55+
// ── Reserved: migrated but unimplemented ──
5956
SummarySubtract {
6057
left: Rc<OperatorNode>,
6158
right: Rc<OperatorNode>,
@@ -186,11 +183,7 @@ impl ASAPOp {
186183
use ASAPOp::*;
187184
matches!(
188185
self,
189-
SummaryMerge { .. }
190-
| SummarySubtract { .. }
191-
| SummaryDelete { .. }
192-
| SummaryJoin { .. }
193-
| Extension { .. }
186+
SummarySubtract { .. } | SummaryDelete { .. } | SummaryJoin { .. } | Extension { .. }
194187
)
195188
}
196189

@@ -202,6 +195,14 @@ impl ASAPOp {
202195
pub fn produced_state(&self) -> Option<&FieldDataType> {
203196
match self {
204197
ASAPOp::SummaryAgg { family, .. } | ASAPOp::SummaryJoin { family, .. } => Some(family),
198+
ASAPOp::SummaryMerge { children } => children.first().and_then(|child| {
199+
child
200+
.schema
201+
.fields
202+
.iter()
203+
.find(|field| !field.is_plain())
204+
.map(|field| &field.dtype)
205+
}),
205206
_ => None,
206207
}
207208
}
@@ -405,8 +406,11 @@ impl ASAPOp {
405406
.output_schema()?
406407
}
407408
}
408-
SummaryMerge { .. }
409-
| SummarySubtract { .. }
409+
SummaryMerge { children } => {
410+
self.validate_inputs()?;
411+
children[0].schema.clone()
412+
}
413+
SummarySubtract { .. }
410414
| SummaryDelete { .. }
411415
| SummaryJoin { .. }
412416
| Extension { .. } => return Err(Self::unimplemented()),
@@ -444,6 +448,37 @@ impl ASAPOp {
444448
}
445449
};
446450
match self {
451+
SummaryMerge { children } => {
452+
let Some(first) = children.first() else {
453+
return Err(SchemaDerivationError::InvalidScalarSignature(
454+
"summary merge requires at least one state input".into(),
455+
));
456+
};
457+
// Matching state parameters and grouping positions are necessary;
458+
// matching names alone cannot prove two states compatible.
459+
if first
460+
.schema
461+
.fields
462+
.iter()
463+
.filter(|field| !field.is_plain())
464+
.count()
465+
!= 1
466+
{
467+
return Err(SchemaDerivationError::InvalidScalarSignature(
468+
"summary merge requires exactly one state column".into(),
469+
));
470+
}
471+
for child in children {
472+
needs_state(child, "SummaryMerge")?;
473+
if child.schema != first.schema {
474+
return Err(SchemaDerivationError::InvalidScalarSignature(
475+
"summary merge inputs must have identical state and grouping schemas"
476+
.into(),
477+
));
478+
}
479+
}
480+
Ok(())
481+
}
447482
SummaryEstimate {
448483
summary_input,
449484
query,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! Window composition merges compatible summary states without consuming raw rows.
2+
use asap_types::{
3+
ir::operator_properties::{Reduction, Source},
4+
ir::{ASAPOp, NonASAPOp, Operator, OperatorNode},
5+
post_asap::{GroupingStrategy, SketchAlgorithm, SketchKind, SketchParams, SummaryUpdate},
6+
pre_asap::{ColumnRef, DataType, Field, FieldDataType, Schema},
7+
};
8+
use std::rc::Rc;
9+
fn state(k: u32) -> Rc<OperatorNode> {
10+
let scan = OperatorNode::new_shared(Operator::NonASAP(NonASAPOp::Scan {
11+
source: Source::Table {
12+
table_ref: "latencies".into(),
13+
},
14+
predicates: vec![],
15+
schema: Schema::new(vec![Field::plain("value", DataType::Float64, false)]),
16+
}))
17+
.unwrap();
18+
OperatorNode::new_shared(Operator::ASAP(ASAPOp::SummaryAgg {
19+
child: scan,
20+
family: FieldDataType::Sketch(
21+
SketchKind::new(SketchAlgorithm::Kll, SketchParams::Kll { k }),
22+
Default::default(),
23+
),
24+
input: SummaryUpdate::column(ColumnRef::SampleValue),
25+
reduction: Reduction::by(vec![]),
26+
grouping: GroupingStrategy::default(),
27+
filter: None,
28+
}))
29+
.unwrap()
30+
}
31+
/// Two KLL panes compose into one typed logical state without timing assignment.
32+
#[test]
33+
fn compatible_panes_merge_structurally() {
34+
let root = OperatorNode::new_shared(Operator::ASAP(ASAPOp::SummaryMerge {
35+
children: vec![state(200), state(200)],
36+
}))
37+
.unwrap();
38+
root.validate_structure().unwrap();
39+
assert_eq!(root.schema.fields.len(), 1);
40+
}
41+
/// An empty merge, raw rows and differently sized state cannot masquerade as compatible panes.
42+
#[test]
43+
fn incompatible_merge_inputs_fail() {
44+
for children in [
45+
vec![],
46+
vec![state(200), state(300)],
47+
vec![state(200).children()[0].clone()],
48+
] {
49+
assert!(
50+
OperatorNode::new_shared(Operator::ASAP(ASAPOp::SummaryMerge { children })).is_err()
51+
);
52+
}
53+
}

0 commit comments

Comments
 (0)