Skip to content

Commit fea4cc0

Browse files
zzylolclaude
andcommitted
feat(ir): derive summary coverage from the subtree with one SummaryCoverage::derive
SummaryCoverage::derive(node, time_range) is the only way coverage is computed, for every summary node: - SummaryAgg: the one source scanned below it, the population read from its filters, its input and reduction (grouping keys named from its input schema) as columns, over the caller's time range. - SummaryMerge: the disjoint union of its inputs' coverage, which must read the same source and columns; its time range comes from the inputs. OperatorNode::new derives coverage (a SummaryAgg starts with no time bounds), with_time_range replaces with_coverage and sets a SummaryAgg's time range, validate_structure requires the retained coverage to equal the derived one (DerivedMismatch), and with_new_children derives it again, keeping the time range. A SummaryAgg whose source or population cannot be read has no coverage and cannot be merged (NoSingleSource, UnprovenPopulation); an unreadable population is never treated as unrestricted. Merges now nest. CoverageRegion.time_ms is renamed time_range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7ce0660 commit fea4cc0

9 files changed

Lines changed: 945 additions & 175 deletions

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use std::rc::Rc;
66
use serde::{Deserialize, Serialize};
77

88
use super::node::{OperatorNode, OperatorResultKind};
9-
use super::summary_coverage::CoverageError;
109
use crate::ir::operator_properties::Reduction;
1110
use crate::ir::SchemaDerivationError;
1211
use crate::post_asap::maintained_population::{MaintainedPopulation, PopulationStatistic};
@@ -488,14 +487,6 @@ impl ASAPOp {
488487
));
489488
}
490489
}
491-
// Equal schemas cannot tell a KLL over `latency` from one over
492-
// `size`; the columns in each input's coverage can. A nested
493-
// `SummaryMerge` carries no coverage yet (#646), so it is rejected.
494-
let columns = first.coverage.as_ref().ok_or(CoverageError::UnknownInput)?;
495-
for child in children {
496-
let coverage = child.coverage.as_ref().ok_or(CoverageError::UnknownInput)?;
497-
coverage.check_columns(columns)?;
498-
}
499490
Ok(())
500491
}
501492
SummaryEstimate {

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

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! execution timing).
55
66
use std::collections::HashSet;
7+
use std::ops::Range;
78
use std::rc::Rc;
89

910
use serde::{Deserialize, Serialize};
@@ -110,7 +111,9 @@ impl OperatorNode {
110111
/// ASAP operator, ...).
111112
pub fn new(operator: Operator) -> Result<Self, SchemaDerivationError> {
112113
let schema = operator.output_schema()?;
113-
Ok(Self::with_schema(operator, schema))
114+
let mut node = Self::with_schema(operator, schema);
115+
node.coverage = node.derived_coverage(None)?;
116+
Ok(node)
114117
}
115118

116119
/// Build a node with caller-supplied output names and qualifiers. For
@@ -145,32 +148,41 @@ impl OperatorNode {
145148
self
146149
}
147150

148-
/// Attach caller-established coverage. Required on summary nodes; see
149-
/// [`Self::requires_coverage`]. On a `SummaryAgg` the coverage columns
150-
/// must equal the node's `input` and `reduction`.
151-
pub fn with_coverage(
151+
/// Set the time range a `SummaryAgg`'s state covers (for example an
152+
/// ingested pane's bounds) and derive its coverage with
153+
/// [`SummaryCoverage::derive`]. Fails on any other node, or when the
154+
/// coverage cannot be derived.
155+
pub fn with_time_range(
152156
mut self,
153-
coverage: SummaryCoverage,
157+
time_range: Option<Range<i64>>,
154158
) -> Result<Self, SchemaDerivationError> {
155-
coverage.validate()?;
156-
if self.result_kind != OperatorResultKind::State {
157-
return Err(CoverageError::NotState.into());
158-
}
159-
if let Some(ASAPOp::SummaryAgg {
160-
input, reduction, ..
161-
}) = self.asap()
162-
{
163-
if &coverage.input != input || &coverage.group_by != reduction {
164-
return Err(CoverageError::ColumnMismatch.into());
165-
}
166-
}
167-
self.coverage = Some(coverage);
159+
self.coverage = Some(SummaryCoverage::derive(&self, time_range)?);
168160
Ok(self)
169161
}
170162

171-
/// Summary nodes whose state can be composed must declare coverage.
172-
pub fn requires_coverage(&self) -> bool {
173-
matches!(self.asap(), Some(ASAPOp::SummaryAgg { .. }))
163+
/// The coverage this node should carry: [`SummaryCoverage::derive`] for a
164+
/// `SummaryMerge`; for a `SummaryAgg` the same over `time_range`, or none
165+
/// when its source or population cannot be read (it then cannot be
166+
/// merged); none for any other node.
167+
fn derived_coverage(
168+
&self,
169+
time_range: Option<Range<i64>>,
170+
) -> Result<Option<SummaryCoverage>, CoverageError> {
171+
match self.asap() {
172+
Some(ASAPOp::SummaryAgg { .. }) => Ok(SummaryCoverage::derive(self, time_range).ok()),
173+
Some(ASAPOp::SummaryMerge { .. }) => SummaryCoverage::derive(self, None).map(Some),
174+
_ => Ok(None),
175+
}
176+
}
177+
178+
/// The time range of a `SummaryAgg`'s coverage; `None` otherwise.
179+
fn coverage_time_range(&self) -> Option<Range<i64>> {
180+
match self.asap() {
181+
Some(ASAPOp::SummaryAgg { .. }) => {
182+
self.coverage.as_ref()?.regions.first()?.time_range.clone()
183+
}
184+
_ => None,
185+
}
174186
}
175187

176188
pub fn non_asap(&self) -> Option<&NonASAPOp> {
@@ -213,14 +225,18 @@ impl OperatorNode {
213225
/// structural schema metadata. Only names and qualifiers that override
214226
/// the old derived schema are retained, for either operator category. A
215227
/// change in output arity with such overrides needs an explicit new
216-
/// naming assignment.
217-
/// `guarantee` and `timing` depend on the inputs and are cleared.
228+
/// naming assignment. `guarantee` and `timing` depend on the inputs and
229+
/// are cleared. Coverage is derived again from the new inputs, keeping a
230+
/// `SummaryAgg`'s time range; this fails when it can no longer be derived.
218231
pub fn with_new_children(
219232
&self,
220233
f: impl FnMut(&Rc<OperatorNode>) -> Rc<OperatorNode>,
221234
) -> Result<Self, SchemaDerivationError> {
222235
let previous = self.operator.output_schema()?;
223236
let mut rebuilt = Self::new(self.operator.map_children(f))?;
237+
if let (Some(ASAPOp::SummaryAgg { .. }), Some(_)) = (rebuilt.asap(), &self.coverage) {
238+
rebuilt = rebuilt.with_time_range(self.coverage_time_range())?;
239+
}
224240
for (i, (derived, retained)) in previous.fields.iter().zip(&self.schema.fields).enumerate()
225241
{
226242
let renamed = retained.name != derived.name;
@@ -324,14 +340,15 @@ impl OperatorNode {
324340
"invalid time or identity column in schema".into(),
325341
));
326342
}
327-
match &node.coverage {
328-
Some(coverage) => {
329-
(*node.as_ref()).clone().with_coverage(coverage.clone())?;
343+
node.operator.validate_inputs()?;
344+
let derived = node.derived_coverage(node.coverage_time_range())?;
345+
if node.coverage != derived {
346+
return Err(match node.coverage {
347+
None => CoverageError::Missing,
348+
Some(_) => CoverageError::DerivedMismatch,
330349
}
331-
None if node.requires_coverage() => return Err(CoverageError::Missing.into()),
332-
None => {}
350+
.into());
333351
}
334-
node.operator.validate_inputs()?;
335352
if node.result_kind != node.operator.output_kind() {
336353
return Err(SchemaDerivationError::InvalidScalarSignature(
337354
"retained result kind disagrees with operation".into(),

0 commit comments

Comments
 (0)