1use std::ops::{Add, Rem, Sub};
5
6use reifydb_core::window::{
7 accumulator::{
8 WindowAccumulator,
9 invertible::Multiset,
10 sealing::{SealingEndpoint, SealingMax, SealingMin},
11 },
12 span::Slot,
13};
14use reifydb_engine::flow::aggregate::SlotKind;
15use reifydb_value::{
16 reifydb_assertions,
17 value::{
18 Value,
19 datetime::DateTime,
20 duration::Duration,
21 number::safe::{add::SafeAdd, div::SafeDiv, sub::SafeSub},
22 },
23};
24use serde::{Deserialize, Serialize};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
27pub struct WindowSlotKey {
28 pub timestamp: DateTime,
29 pub seq: u64,
30}
31
32impl WindowSlotKey {
33 pub fn new(timestamp: DateTime, seq: u64) -> Self {
34 Self {
35 timestamp,
36 seq,
37 }
38 }
39}
40
41impl Add<Duration> for WindowSlotKey {
42 type Output = WindowSlotKey;
43 fn add(self, duration: Duration) -> WindowSlotKey {
44 WindowSlotKey {
45 timestamp: self.timestamp + duration,
46 seq: self.seq,
47 }
48 }
49}
50
51impl Sub<WindowSlotKey> for WindowSlotKey {
52 type Output = Duration;
53 fn sub(self, other: WindowSlotKey) -> Duration {
54 self.timestamp - other.timestamp
55 }
56}
57
58impl Sub<Duration> for WindowSlotKey {
59 type Output = WindowSlotKey;
60 fn sub(self, duration: Duration) -> WindowSlotKey {
61 WindowSlotKey {
62 timestamp: self.timestamp - duration,
63 seq: self.seq,
64 }
65 }
66}
67
68impl Rem<Duration> for WindowSlotKey {
69 type Output = Duration;
70 fn rem(self, duration: Duration) -> Duration {
71 self.timestamp % duration
72 }
73}
74
75impl Slot for WindowSlotKey {
76 type Duration = Duration;
77
78 fn order_key(&self) -> u64 {
79 self.timestamp.to_nanos()
80 }
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize)]
84pub enum AggregateSlot {
85 Count {
86 n: i64,
87 count_star: bool,
88 },
89 Sum {
90 accumulator: Value,
91 n: u64,
92 },
93 Avg {
94 sum: Value,
95 n: i64,
96 },
97 Min(Multiset<Value>),
98 Max(Multiset<Value>),
99 MinSealed(SealingMin<WindowSlotKey, Value>),
100 MaxSealed(SealingMax<WindowSlotKey, Value>),
101 First(SealingEndpoint<WindowSlotKey, Value>),
102 Last(SealingEndpoint<WindowSlotKey, Value>),
103}
104
105fn endpoint(lateness: Option<Duration>) -> SealingEndpoint<WindowSlotKey, Value> {
106 match lateness {
107 Some(l) => SealingEndpoint::with_lateness(l),
108 None => SealingEndpoint::default(),
109 }
110}
111
112impl AggregateSlot {
113 fn empty(kind: SlotKind, lateness: Option<Duration>) -> Self {
114 match kind {
115 SlotKind::Count {
116 count_star,
117 } => AggregateSlot::Count {
118 n: 0,
119 count_star,
120 },
121 SlotKind::Sum => AggregateSlot::Sum {
122 accumulator: Value::none(),
123 n: 0,
124 },
125 SlotKind::Avg => AggregateSlot::Avg {
126 sum: Value::none(),
127 n: 0,
128 },
129 SlotKind::Min => match lateness {
130 Some(l) => AggregateSlot::MinSealed(SealingMin::with_lateness(l)),
131 None => AggregateSlot::Min(Multiset::default()),
132 },
133 SlotKind::Max => match lateness {
134 Some(l) => AggregateSlot::MaxSealed(SealingMax::with_lateness(l)),
135 None => AggregateSlot::Max(Multiset::default()),
136 },
137 SlotKind::First => AggregateSlot::First(endpoint(lateness)),
138 SlotKind::Last => AggregateSlot::Last(endpoint(lateness)),
139 }
140 }
141
142 fn add(&mut self, coord: WindowSlotKey, input: &Option<Value>) {
143 match self {
144 AggregateSlot::Count {
145 n,
146 count_star,
147 } => {
148 if *count_star || present(input).is_some() {
149 *n += 1;
150 }
151 }
152 AggregateSlot::Sum {
153 accumulator,
154 n,
155 } => {
156 if let Some(v) = present(input) {
157 *accumulator = if *n == 0 {
158 widen(v)
159 } else {
160 accumulator.checked_add(v).unwrap_or_else(Value::none)
161 };
162 *n += 1;
163 }
164 }
165 AggregateSlot::Avg {
166 sum,
167 n,
168 } => {
169 if let Some(v) = present(input) {
170 *sum = if *n == 0 {
171 widen(v)
172 } else {
173 sum.checked_add(v).unwrap_or_else(Value::none)
174 };
175 *n += 1;
176 }
177 }
178 AggregateSlot::Min(set) | AggregateSlot::Max(set) => {
179 if let Some(v) = present(input) {
180 set.add(v.clone());
181 }
182 }
183 AggregateSlot::MinSealed(s) => {
184 if let Some(v) = present(input) {
185 s.add(&(coord, v.clone()));
186 }
187 }
188 AggregateSlot::MaxSealed(s) => {
189 if let Some(v) = present(input) {
190 s.add(&(coord, v.clone()));
191 }
192 }
193 AggregateSlot::First(e) | AggregateSlot::Last(e) => {
194 if let Some(v) = present(input) {
195 e.add(&(coord, v.clone()));
196 }
197 }
198 }
199 }
200
201 fn remove(&mut self, coord: WindowSlotKey, input: &Option<Value>) {
202 match self {
203 AggregateSlot::Count {
204 n,
205 count_star,
206 } => {
207 if *count_star || present(input).is_some() {
208 *n -= 1;
209 }
210 }
211 AggregateSlot::Sum {
212 accumulator,
213 n,
214 } => {
215 if let Some(v) = present(input) {
216 *n -= 1;
217 *accumulator = if *n == 0 {
218 Value::none()
219 } else {
220 accumulator.checked_sub(v).unwrap_or_else(Value::none)
221 };
222 }
223 }
224 AggregateSlot::Avg {
225 sum,
226 n,
227 } => {
228 if let Some(v) = present(input) {
229 *n -= 1;
230 *sum = if *n == 0 {
231 Value::none()
232 } else {
233 sum.checked_sub(v).unwrap_or_else(Value::none)
234 };
235 }
236 }
237 AggregateSlot::Min(set) | AggregateSlot::Max(set) => {
238 if let Some(v) = present(input) {
239 set.remove(v);
240 }
241 }
242 AggregateSlot::MinSealed(s) => {
243 if let Some(v) = present(input) {
244 s.remove(&(coord, v.clone()));
245 }
246 }
247 AggregateSlot::MaxSealed(s) => {
248 if let Some(v) = present(input) {
249 s.remove(&(coord, v.clone()));
250 }
251 }
252 AggregateSlot::First(e) | AggregateSlot::Last(e) => {
253 if let Some(v) = present(input) {
254 e.remove(&(coord, v.clone()));
255 }
256 }
257 }
258 }
259
260 fn merge(&mut self, other: &AggregateSlot) {
261 match (self, other) {
262 (
263 AggregateSlot::Count {
264 n,
265 ..
266 },
267 AggregateSlot::Count {
268 n: on,
269 ..
270 },
271 ) => *n += *on,
272 (
273 AggregateSlot::Sum {
274 accumulator,
275 n,
276 },
277 AggregateSlot::Sum {
278 accumulator: other_accumulator,
279 n: on,
280 },
281 ) => {
282 if *on > 0 {
283 *accumulator = if *n == 0 {
284 other_accumulator.clone()
285 } else {
286 accumulator.checked_add(other_accumulator).unwrap_or_else(Value::none)
287 };
288 *n += *on;
289 }
290 }
291 (
292 AggregateSlot::Avg {
293 sum,
294 n,
295 },
296 AggregateSlot::Avg {
297 sum: osum,
298 n: on,
299 },
300 ) => {
301 if *on > 0 {
302 *sum = if *n == 0 {
303 osum.clone()
304 } else {
305 sum.checked_add(osum).unwrap_or_else(Value::none)
306 };
307 *n += *on;
308 }
309 }
310 (
311 AggregateSlot::Min(set) | AggregateSlot::Max(set),
312 AggregateSlot::Min(oset) | AggregateSlot::Max(oset),
313 ) => set.merge(oset),
314 (AggregateSlot::MinSealed(a), AggregateSlot::MinSealed(b)) => a.absorb(b),
315 (AggregateSlot::MaxSealed(a), AggregateSlot::MaxSealed(b)) => a.absorb(b),
316 (
317 AggregateSlot::First(a) | AggregateSlot::Last(a),
318 AggregateSlot::First(b) | AggregateSlot::Last(b),
319 ) => a.absorb(b),
320 _ => {}
321 }
322 }
323
324 fn finalize(&self) -> Value {
325 match self {
326 AggregateSlot::Count {
327 n,
328 ..
329 } => Value::Int8(*n),
330 AggregateSlot::Sum {
331 accumulator,
332 ..
333 } => accumulator.clone(),
334 AggregateSlot::Avg {
335 sum,
336 n,
337 } => sum.checked_div(&Value::Int8(*n)).unwrap_or_else(Value::none),
338 AggregateSlot::Min(set) => set.min().cloned().unwrap_or_else(Value::none),
339 AggregateSlot::Max(set) => set.max().cloned().unwrap_or_else(Value::none),
340 AggregateSlot::MinSealed(s) => s.min().unwrap_or_else(Value::none),
341 AggregateSlot::MaxSealed(s) => s.max().unwrap_or_else(Value::none),
342 AggregateSlot::First(e) => e.open().cloned().unwrap_or_else(Value::none),
343 AggregateSlot::Last(e) => e.close().cloned().unwrap_or_else(Value::none),
344 }
345 }
346
347 fn is_empty(&self) -> bool {
348 match self {
349 AggregateSlot::Count {
350 n,
351 ..
352 } => *n == 0,
353 AggregateSlot::Sum {
354 n,
355 ..
356 } => *n == 0,
357 AggregateSlot::Avg {
358 n,
359 ..
360 } => *n == 0,
361 AggregateSlot::Min(set) | AggregateSlot::Max(set) => set.is_empty(),
362 AggregateSlot::MinSealed(s) => s.is_empty(),
363 AggregateSlot::MaxSealed(s) => s.is_empty(),
364 AggregateSlot::First(e) | AggregateSlot::Last(e) => e.is_empty(),
365 }
366 }
367}
368
369#[derive(Clone, Debug, Default, Serialize, Deserialize)]
370pub struct RowAccumulator {
371 slots: Vec<AggregateSlot>,
372}
373
374impl RowAccumulator {
375 pub fn new(kinds: &[SlotKind], lateness: Option<Duration>) -> Self {
376 Self {
377 slots: kinds.iter().map(|k| AggregateSlot::empty(*k, lateness)).collect(),
378 }
379 }
380
381 pub fn merge(&mut self, other: &RowAccumulator) {
382 for (slot, other_slot) in self.slots.iter_mut().zip(other.slots.iter()) {
383 slot.merge(other_slot);
384 }
385 }
386}
387
388impl WindowAccumulator for RowAccumulator {
389 type Contribution = (WindowSlotKey, Vec<Option<Value>>);
390 type Output = Vec<Value>;
391
392 fn add(&mut self, contribution: &Self::Contribution) {
393 let (coord, values) = contribution;
394 reifydb_assertions! {
395 assert!(
396 values.len() == self.slots.len(),
397 "RowAccumulator contribution length {} != slot count {}; the zip below truncates to the \
398 shorter side, so a default-constructed zero-slot accumulator (e.g. routed through an engine \
399 that builds empties via Default instead of new(kinds)) would silently swallow every \
400 contribution",
401 values.len(),
402 self.slots.len()
403 );
404 }
405 for (slot, input) in self.slots.iter_mut().zip(values.iter()) {
406 slot.add(*coord, input);
407 }
408 }
409
410 fn remove(&mut self, contribution: &Self::Contribution) {
411 let (coord, values) = contribution;
412 reifydb_assertions! {
413 assert!(
414 values.len() == self.slots.len(),
415 "RowAccumulator contribution length {} != slot count {}; the zip below truncates to the \
416 shorter side, so a default-constructed zero-slot accumulator (e.g. routed through an engine \
417 that builds empties via Default instead of new(kinds)) would silently swallow every \
418 retraction",
419 values.len(),
420 self.slots.len()
421 );
422 }
423 for (slot, input) in self.slots.iter_mut().zip(values.iter()) {
424 slot.remove(*coord, input);
425 }
426 }
427
428 fn finalize(&self) -> Option<Self::Output> {
429 if self.is_empty() {
430 return None;
431 }
432 Some(self.slots.iter().map(AggregateSlot::finalize).collect())
433 }
434
435 fn is_empty(&self) -> bool {
436 self.slots.iter().all(AggregateSlot::is_empty)
437 }
438}
439
440#[derive(Clone, Debug, Default, Serialize, Deserialize)]
441pub struct StampedAccumulator {
442 inner: RowAccumulator,
443 ts: u64,
444}
445
446impl StampedAccumulator {
447 pub fn new(kinds: &[SlotKind], lateness: Option<Duration>) -> Self {
448 Self {
449 inner: RowAccumulator::new(kinds, lateness),
450 ts: 0,
451 }
452 }
453
454 pub fn inner(&self) -> &RowAccumulator {
455 &self.inner
456 }
457}
458
459impl WindowAccumulator for StampedAccumulator {
460 type Contribution = ((WindowSlotKey, Vec<Option<Value>>), u64);
461 type Output = Vec<Value>;
462
463 fn add(&mut self, contribution: &Self::Contribution) {
464 self.inner.add(&contribution.0);
465 self.ts = self.ts.max(contribution.1);
466 }
467
468 fn remove(&mut self, contribution: &Self::Contribution) {
469 self.inner.remove(&contribution.0);
470 }
471
472 fn finalize(&self) -> Option<Self::Output> {
473 self.inner.finalize()
474 }
475
476 fn is_empty(&self) -> bool {
477 self.inner.is_empty()
478 }
479
480 fn stamp(&self) -> Option<u64> {
481 if self.inner.is_empty() {
482 None
483 } else {
484 Some(self.ts)
485 }
486 }
487}
488
489fn present(input: &Option<Value>) -> Option<&Value> {
490 match input {
491 Some(v) if !matches!(v, Value::None { .. }) => Some(v),
492 _ => None,
493 }
494}
495
496fn widen(v: &Value) -> Value {
497 v.checked_add(v).and_then(|two| two.checked_sub(v)).unwrap_or_else(|| v.clone())
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 fn i4(v: i32) -> Option<Value> {
505 Some(Value::Int4(v))
506 }
507
508 fn accumulator(kinds: &[SlotKind]) -> RowAccumulator {
509 RowAccumulator::new(kinds, None)
510 }
511
512 fn at(seq: u64) -> WindowSlotKey {
513 WindowSlotKey::new(DateTime::default(), seq)
514 }
515
516 fn coord(secs: u64) -> WindowSlotKey {
517 WindowSlotKey::new(DateTime::from_timestamp(secs as i64).unwrap(), secs)
518 }
519
520 fn add(a: &mut RowAccumulator, seq: u64, values: Vec<Option<Value>>) {
521 a.add(&(at(seq), values));
522 }
523
524 fn remove(a: &mut RowAccumulator, seq: u64, values: Vec<Option<Value>>) {
525 a.remove(&(at(seq), values));
526 }
527
528 #[test]
529 fn count_counts_rows_and_resets_on_empty() {
530 let mut a = accumulator(&[SlotKind::Count {
531 count_star: true,
532 }]);
533 assert!(a.is_empty());
534 add(&mut a, 0, vec![None]);
535 add(&mut a, 1, vec![None]);
536 assert_eq!(a.finalize(), Some(vec![Value::Int8(2)]));
537 remove(&mut a, 0, vec![None]);
538 remove(&mut a, 1, vec![None]);
539 assert!(a.is_empty());
540 assert_eq!(a.finalize(), None);
541 }
542
543 #[test]
544 fn count_col_ignores_none() {
545 let mut a = accumulator(&[SlotKind::Count {
546 count_star: false,
547 }]);
548 add(&mut a, 0, vec![i4(5)]);
549 add(&mut a, 1, vec![Some(Value::none())]); add(&mut a, 2, vec![i4(7)]);
551 assert_eq!(a.finalize(), Some(vec![Value::Int8(2)]));
552 }
553
554 #[test]
555 fn sum_has_stable_widened_type_and_inverts() {
556 let mut a = accumulator(&[SlotKind::Sum]);
557 add(&mut a, 0, vec![i4(5)]);
558 assert_eq!(a.finalize(), Some(vec![Value::Int16(5)]));
560 add(&mut a, 1, vec![i4(3)]);
561 assert_eq!(a.finalize(), Some(vec![Value::Int16(8)]));
562 remove(&mut a, 1, vec![i4(3)]);
564 assert_eq!(a.finalize(), Some(vec![Value::Int16(5)]));
565 }
566
567 #[test]
568 fn sum_skips_none() {
569 let mut a = accumulator(&[SlotKind::Sum]);
570 add(&mut a, 0, vec![i4(10)]);
571 add(&mut a, 1, vec![Some(Value::none())]);
572 assert_eq!(a.finalize(), Some(vec![Value::Int16(10)]));
573 }
574
575 #[test]
576 fn avg_is_decimal_and_inverts() {
577 let mut a = accumulator(&[SlotKind::Avg]);
578 add(&mut a, 0, vec![i4(2)]);
579 add(&mut a, 1, vec![i4(3)]);
580 let got = a.finalize().unwrap();
582 assert!(matches!(got[0], Value::Decimal(_)), "avg is Decimal, got {:?}", got[0]);
583 let expected = Value::Int16(5).checked_div(&Value::Int8(2)).unwrap();
584 assert_eq!(got[0], expected);
585 remove(&mut a, 1, vec![i4(3)]);
586 assert_eq!(a.finalize().unwrap()[0], Value::Int16(2).checked_div(&Value::Int8(1)).unwrap());
587 }
588
589 #[test]
590 fn min_max_via_multiset_invert() {
591 let mut a = accumulator(&[SlotKind::Min, SlotKind::Max]);
592 for (seq, v) in [5, 8, 6].into_iter().enumerate() {
593 add(&mut a, seq as u64, vec![i4(v), i4(v)]);
594 }
595 assert_eq!(a.finalize(), Some(vec![Value::Int4(5), Value::Int4(8)]));
596 remove(&mut a, 0, vec![i4(5), i4(5)]);
598 assert_eq!(a.finalize(), Some(vec![Value::Int4(6), Value::Int4(8)]));
599 }
600
601 #[test]
602 fn multi_slot_row_add_remove_inverse() {
603 let kinds = [
604 SlotKind::Count {
605 count_star: true,
606 },
607 SlotKind::Sum,
608 SlotKind::Min,
609 ];
610 let mut a = accumulator(&kinds);
611 add(&mut a, 0, vec![None, i4(100), i4(100)]);
612 let snap = a.finalize();
613 add(&mut a, 1, vec![None, i4(40), i4(40)]);
614 remove(&mut a, 1, vec![None, i4(40), i4(40)]);
615 assert_eq!(a.finalize(), snap, "add then remove restores all slots");
616 }
617
618 #[test]
619 fn merge_equals_accumulating_all_into_one() {
620 let kinds = [
621 SlotKind::Count {
622 count_star: true,
623 },
624 SlotKind::Sum,
625 SlotKind::Avg,
626 SlotKind::Min,
627 SlotKind::Max,
628 ];
629 let mut whole = accumulator(&kinds);
631 let rows = [(10, 10, 10), (40, 40, 40), (7, 7, 7), (99, 99, 99)];
632 for (seq, (s, mn, mx)) in rows.into_iter().enumerate() {
633 add(&mut whole, seq as u64, vec![None, i4(s), i4(s), i4(mn), i4(mx)]);
634 }
635 let mut left = accumulator(&kinds);
637 for (seq, (s, mn, mx)) in rows[..2].iter().enumerate() {
638 add(&mut left, seq as u64, vec![None, i4(*s), i4(*s), i4(*mn), i4(*mx)]);
639 }
640 let mut right = accumulator(&kinds);
641 for (seq, (s, mn, mx)) in rows[2..].iter().enumerate() {
642 add(&mut right, (seq + 2) as u64, vec![None, i4(*s), i4(*s), i4(*mn), i4(*mx)]);
643 }
644 left.merge(&right);
645 assert_eq!(
646 left.finalize(),
647 whole.finalize(),
648 "merge of two partials must equal one combined accumulator"
649 );
650 }
651
652 #[test]
653 fn merge_into_empty_takes_other_widened_sum() {
654 let kinds = [SlotKind::Sum];
655 let mut empty = accumulator(&kinds);
656 let mut other = accumulator(&kinds);
657 add(&mut other, 0, vec![i4(5)]);
658 empty.merge(&other);
659 assert_eq!(empty.finalize(), Some(vec![Value::Int16(5)]));
661 }
662
663 #[test]
664 fn empty_when_all_removed() {
665 let mut a = accumulator(&[SlotKind::Sum, SlotKind::Min]);
666 add(&mut a, 0, vec![i4(1), i4(1)]);
667 remove(&mut a, 0, vec![i4(1), i4(1)]);
668 assert!(a.is_empty());
669 assert_eq!(a.finalize(), None);
670 }
671
672 #[test]
673 fn first_last_track_endpoints_by_coordinate() {
674 let mut a = RowAccumulator::new(&[SlotKind::First, SlotKind::Last], None);
677 a.add(&(coord(20), vec![i4(20), i4(20)]));
678 a.add(&(coord(10), vec![i4(10), i4(10)]));
679 a.add(&(coord(30), vec![i4(30), i4(30)]));
680 assert_eq!(a.finalize(), Some(vec![Value::Int4(10), Value::Int4(30)]));
681 }
682
683 #[test]
684 fn lateness_seals_aged_min_max_and_drops_late_retraction() {
685 let lateness = Duration::from_seconds(5).unwrap();
690 let mut a = RowAccumulator::new(&[SlotKind::Max], Some(lateness));
691 a.add(&(coord(0), vec![i4(100)])); a.add(&(coord(10), vec![i4(50)]));
693 assert_eq!(a.finalize(), Some(vec![Value::Int4(100)]), "sealed max still dominates");
694 a.remove(&(coord(0), vec![i4(100)]));
696 assert_eq!(
697 a.finalize(),
698 Some(vec![Value::Int4(100)]),
699 "retraction older than lateness is a no-op, so the sealed max survives"
700 );
701 a.add(&(coord(12), vec![i4(70)]));
703 a.remove(&(coord(12), vec![i4(70)]));
704 assert_eq!(a.finalize(), Some(vec![Value::Int4(100)]));
705 }
706
707 #[test]
708 fn lateness_none_min_max_is_exact_under_retraction() {
709 let mut a = accumulator(&[SlotKind::Max]);
712 add(&mut a, 0, vec![i4(100)]);
713 add(&mut a, 1, vec![i4(50)]);
714 remove(&mut a, 0, vec![i4(100)]);
715 assert_eq!(a.finalize(), Some(vec![Value::Int4(50)]), "exact path retracts the old max");
716 }
717
718 #[test]
719 fn sealed_merge_matches_one_combined_accumulator() {
720 let lateness = Duration::from_seconds(60).unwrap();
723 let kinds = [SlotKind::Min, SlotKind::Max, SlotKind::First, SlotKind::Last];
724 let rows = [(5, 30), (8, 10), (3, 50), (12, 20)];
725 let mut whole = RowAccumulator::new(&kinds, Some(lateness));
726 for (i, (v, _)) in rows.iter().enumerate() {
727 whole.add(&(coord((i as u64) * 10), vec![i4(*v), i4(*v), i4(*v), i4(*v)]));
728 }
729 let mut left = RowAccumulator::new(&kinds, Some(lateness));
730 for (i, (v, _)) in rows[..2].iter().enumerate() {
731 left.add(&(coord((i as u64) * 10), vec![i4(*v), i4(*v), i4(*v), i4(*v)]));
732 }
733 let mut right = RowAccumulator::new(&kinds, Some(lateness));
734 for (i, (v, _)) in rows[2..].iter().enumerate() {
735 right.add(&(coord(((i + 2) as u64) * 10), vec![i4(*v), i4(*v), i4(*v), i4(*v)]));
736 }
737 left.merge(&right);
738 assert_eq!(left.finalize(), whole.finalize(), "sealed merge must equal one combined accumulator");
739 }
740}