|
4 | 4 | //! execution timing). |
5 | 5 |
|
6 | 6 | use std::collections::HashSet; |
| 7 | +use std::ops::Range; |
7 | 8 | use std::rc::Rc; |
8 | 9 |
|
9 | 10 | use serde::{Deserialize, Serialize}; |
@@ -110,7 +111,9 @@ impl OperatorNode { |
110 | 111 | /// ASAP operator, ...). |
111 | 112 | pub fn new(operator: Operator) -> Result<Self, SchemaDerivationError> { |
112 | 113 | 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) |
114 | 117 | } |
115 | 118 |
|
116 | 119 | /// Build a node with caller-supplied output names and qualifiers. For |
@@ -145,32 +148,41 @@ impl OperatorNode { |
145 | 148 | self |
146 | 149 | } |
147 | 150 |
|
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( |
152 | 156 | mut self, |
153 | | - coverage: SummaryCoverage, |
| 157 | + time_range: Option<Range<i64>>, |
154 | 158 | ) -> 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)?); |
168 | 160 | Ok(self) |
169 | 161 | } |
170 | 162 |
|
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 | + } |
174 | 186 | } |
175 | 187 |
|
176 | 188 | pub fn non_asap(&self) -> Option<&NonASAPOp> { |
@@ -213,14 +225,18 @@ impl OperatorNode { |
213 | 225 | /// structural schema metadata. Only names and qualifiers that override |
214 | 226 | /// the old derived schema are retained, for either operator category. A |
215 | 227 | /// 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. |
218 | 231 | pub fn with_new_children( |
219 | 232 | &self, |
220 | 233 | f: impl FnMut(&Rc<OperatorNode>) -> Rc<OperatorNode>, |
221 | 234 | ) -> Result<Self, SchemaDerivationError> { |
222 | 235 | let previous = self.operator.output_schema()?; |
223 | 236 | 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 | + } |
224 | 240 | for (i, (derived, retained)) in previous.fields.iter().zip(&self.schema.fields).enumerate() |
225 | 241 | { |
226 | 242 | let renamed = retained.name != derived.name; |
@@ -324,14 +340,15 @@ impl OperatorNode { |
324 | 340 | "invalid time or identity column in schema".into(), |
325 | 341 | )); |
326 | 342 | } |
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, |
330 | 349 | } |
331 | | - None if node.requires_coverage() => return Err(CoverageError::Missing.into()), |
332 | | - None => {} |
| 350 | + .into()); |
333 | 351 | } |
334 | | - node.operator.validate_inputs()?; |
335 | 352 | if node.result_kind != node.operator.output_kind() { |
336 | 353 | return Err(SchemaDerivationError::InvalidScalarSignature( |
337 | 354 | "retained result kind disagrees with operation".into(), |
|
0 commit comments