1use std::collections::VecDeque;
32
33use columnar::{Columnar, Index, Len};
34use differential_dataflow::difference::Semigroup;
35use differential_dataflow::logging::{BatcherEvent, Logger};
36use differential_dataflow::trace::{Batcher, Description};
37use timely::Accountable;
38use timely::PartialOrder;
39use timely::container::{PushInto, SizableContainer};
40use timely::dataflow::channels::ContainerBytes;
41use timely::progress::Timestamp;
42use timely::progress::frontier::{Antichain, AntichainRef};
43
44use crate::column_pager::{self, ColumnPager, PagedColumn};
45use crate::columnar::Column;
46use crate::columnar::batcher::{empty_chunk, recycle_chunk};
47
48const STASH_CAP: usize = 2;
63
64const MAX_RECYCLE_BYTES: usize = 1 << 22;
71
72fn recycle_capped<C: Columnar>(chunk: Column<C>, stash: &mut Vec<Column<C>>) {
77 if stash.len() < STASH_CAP && chunk.length_in_bytes() <= MAX_RECYCLE_BYTES {
78 recycle_chunk(chunk, stash);
79 }
80}
81
82pub struct ColumnMergeBatcher<D, T, R>
95where
96 D: Columnar,
97 T: Columnar,
98 R: Columnar,
99{
100 chains: Vec<VecDeque<PagedColumn<(D, T, R)>>>,
101 lower: Antichain<T>,
102 frontier: Antichain<T>,
103 stash: Vec<Column<(D, T, R)>>,
111 pager_override: Option<ColumnPager>,
116 logger: Option<Logger>,
117 operator_id: usize,
118}
119
120impl<D, T, R> ColumnMergeBatcher<D, T, R>
121where
122 D: Columnar,
123 T: Columnar,
124 R: Columnar,
125{
126 pub fn set_pager(&mut self, pager: ColumnPager) {
130 self.pager_override = Some(pager);
131 }
132
133 fn pager(&self) -> ColumnPager {
137 self.pager_override
138 .clone()
139 .unwrap_or_else(column_pager::global_pager)
140 }
141
142 fn chain_push(&mut self, chain: VecDeque<PagedColumn<(D, T, R)>>) {
145 self.emit_account(&chain, 1);
146 self.chains.push(chain);
147 }
148
149 fn chain_pop(&mut self) -> Option<VecDeque<PagedColumn<(D, T, R)>>> {
160 let chain = self.chains.pop()?;
161 self.emit_account(&chain, -1);
162 Some(chain)
163 }
164
165 fn emit_account(&self, chain: &VecDeque<PagedColumn<(D, T, R)>>, diff: isize) {
168 let Some(logger) = &self.logger else {
169 return;
170 };
171 let (mut records, mut size, mut capacity, mut allocations) =
172 (0isize, 0isize, 0isize, 0isize);
173 for entry in chain {
174 let (r, s, c, a) = account_chunk(entry);
175 records = records.saturating_add_unsigned(r);
176 size = size.saturating_add_unsigned(s);
177 capacity = capacity.saturating_add_unsigned(c);
178 allocations = allocations.saturating_add_unsigned(a);
179 }
180 logger.log(BatcherEvent {
181 operator: self.operator_id,
182 records_diff: records.saturating_mul(diff),
183 size_diff: size.saturating_mul(diff),
184 capacity_diff: capacity.saturating_mul(diff),
185 allocations_diff: allocations.saturating_mul(diff),
186 });
187 }
188}
189
190impl<D, T, R> Drop for ColumnMergeBatcher<D, T, R>
191where
192 D: Columnar,
193 T: Columnar,
194 R: Columnar,
195{
196 fn drop(&mut self) {
197 while self.chain_pop().is_some() {}
200 }
201}
202
203fn account_chunk<C: Columnar>(entry: &PagedColumn<C>) -> (usize, usize, usize, usize) {
212 match entry {
213 PagedColumn::Resident(col, _) => {
214 let records = usize::try_from(col.record_count()).expect("non-negative");
215 let bytes = col.length_in_bytes();
216 (records, bytes, bytes, 1)
217 }
218 PagedColumn::Paged { .. } | PagedColumn::Compressed { .. } => (0, 0, 0, 0),
219 }
220}
221
222impl<D, T, R> Batcher for ColumnMergeBatcher<D, T, R>
223where
224 D: Columnar,
225 for<'a> columnar::Ref<'a, D>: Copy + Ord,
226 T: Columnar + Default + Timestamp + PartialOrder,
227 for<'a> columnar::Ref<'a, T>: Copy + Ord,
228 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
229 for<'a> columnar::Ref<'a, R>: Ord,
230{
231 type Output = Column<(D, T, R)>;
232 type Time = T;
233
234 fn new(logger: Option<Logger>, operator_id: usize) -> Self {
235 Self {
236 chains: Vec::new(),
237 lower: Antichain::from_elem(T::minimum()),
238 frontier: Antichain::new(),
239 stash: Vec::new(),
240 pager_override: None,
241 logger,
242 operator_id,
243 }
244 }
245
246 fn seal(
247 &mut self,
248 upper: Antichain<Self::Time>,
249 ) -> (Vec<Self::Output>, Description<Self::Time>) {
250 let pager = self.pager();
251 while self.chains.len() > 1 {
253 let a = self.chain_pop().unwrap();
254 let b = self.chain_pop().unwrap();
255 let merged = self.merge_by(a, b);
256 self.chain_push(merged);
257 }
258 let merged = self.chain_pop().unwrap_or_default();
259
260 let mut readied: Vec<Column<(D, T, R)>> = Vec::new();
264 let mut kept_chain: VecDeque<PagedColumn<(D, T, R)>> = VecDeque::new();
265 self.frontier.clear();
266 {
267 let pager = &pager;
268 let frontier = &mut self.frontier;
269 let stash = &mut self.stash;
270 extract_chain(
271 FetchIter::new(merged, pager),
272 upper.borrow(),
273 frontier,
274 |paged| readied.push(pager.take(paged)),
275 |paged| kept_chain.push_back(paged),
276 stash,
277 );
278 }
279
280 if !kept_chain.is_empty() {
281 self.chain_push(kept_chain);
282 }
283
284 let description = Description::new(
285 self.lower.clone(),
286 upper.clone(),
287 Antichain::from_elem(T::minimum()),
288 );
289 self.lower = upper;
290
291 self.stash.clear();
296
297 (readied, description)
298 }
299
300 fn frontier(&mut self) -> AntichainRef<'_, Self::Time> {
301 self.frontier.borrow()
302 }
303}
304
305impl<D, T, R> PushInto<Column<(D, T, R)>> for ColumnMergeBatcher<D, T, R>
306where
307 D: Columnar,
308 for<'a> columnar::Ref<'a, D>: Copy + Ord,
309 T: Columnar + Default + Clone + PartialOrder,
310 for<'a> columnar::Ref<'a, T>: Copy + Ord,
311 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
312{
313 fn push_into(&mut self, mut chunk: Column<(D, T, R)>) {
316 let pager = self.pager();
317 let paged = pager.page(&mut chunk);
318 self.insert_chain(VecDeque::from([paged]));
319 }
320}
321
322impl<D, T, R> ColumnMergeBatcher<D, T, R>
323where
324 D: Columnar,
325 for<'a> columnar::Ref<'a, D>: Copy + Ord,
326 T: Columnar + Default + Clone + PartialOrder,
327 for<'a> columnar::Ref<'a, T>: Copy + Ord,
328 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
329{
330 fn insert_chain(&mut self, chain: VecDeque<PagedColumn<(D, T, R)>>) {
333 if chain.is_empty() {
334 return;
335 }
336 self.chain_push(chain);
337 while self.chains.len() > 1
338 && self.chains[self.chains.len() - 1].len()
339 >= self.chains[self.chains.len() - 2].len() / 2
340 {
341 let a = self.chain_pop().unwrap();
342 let b = self.chain_pop().unwrap();
343 let merged = self.merge_by(a, b);
344 self.chain_push(merged);
345 }
346 }
347
348 fn merge_by(
352 &mut self,
353 a: VecDeque<PagedColumn<(D, T, R)>>,
354 b: VecDeque<PagedColumn<(D, T, R)>>,
355 ) -> VecDeque<PagedColumn<(D, T, R)>> {
356 let mut output: VecDeque<PagedColumn<(D, T, R)>> = VecDeque::new();
357 let pager = self.pager();
358 let pager = &pager;
359 let stash = &mut self.stash;
360 merge_chains(
361 FetchIter::new(a, pager),
362 FetchIter::new(b, pager),
363 |paged| output.push_back(paged),
364 stash,
365 );
366 output
367 }
368}
369
370pub struct FetchIter<'a, D, T, R>
376where
377 (D, T, R): Columnar,
378{
379 queue: VecDeque<PagedColumn<(D, T, R)>>,
380 pager: &'a ColumnPager,
381}
382
383impl<'a, D, T, R> FetchIter<'a, D, T, R>
384where
385 (D, T, R): Columnar,
386{
387 pub fn new(queue: VecDeque<PagedColumn<(D, T, R)>>, pager: &'a ColumnPager) -> Self {
389 Self { queue, pager }
390 }
391
392 pub fn pager(&self) -> &'a ColumnPager {
397 self.pager
398 }
399
400 pub fn into_paged(self) -> std::collections::vec_deque::IntoIter<PagedColumn<(D, T, R)>> {
405 self.queue.into_iter()
406 }
407}
408
409impl<D, T, R> Iterator for FetchIter<'_, D, T, R>
410where
411 (D, T, R): Columnar,
412{
413 type Item = Column<(D, T, R)>;
414
415 fn next(&mut self) -> Option<Self::Item> {
416 self.queue.pop_front().map(|p| self.pager.take(p))
417 }
418}
419
420pub fn merge_chains<D, T, R, Sink>(
435 list1: FetchIter<'_, D, T, R>,
436 list2: FetchIter<'_, D, T, R>,
437 mut sink: Sink,
438 stash: &mut Vec<Column<(D, T, R)>>,
439) where
440 D: Columnar,
441 for<'a> columnar::Ref<'a, D>: Copy + Ord,
442 T: Columnar + Default + Clone + PartialOrder,
443 for<'a> columnar::Ref<'a, T>: Copy + Ord,
444 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
445 Sink: FnMut(PagedColumn<(D, T, R)>),
446{
447 let pager = list1.pager();
448 let mut list1 = list1;
449 let mut list2 = list2;
450
451 let mut heads = [
452 list1.next().unwrap_or_default(),
453 list2.next().unwrap_or_default(),
454 ];
455 let mut positions = [0usize, 0usize];
456 let mut result: Column<(D, T, R)> = empty_chunk(stash);
457
458 loop {
459 let upper_l = heads[0].borrow().len();
460 let upper_r = heads[1].borrow().len();
461 if positions[0] >= upper_l || positions[1] >= upper_r {
462 break;
463 }
464
465 let lhs_passthrough = positions[0] == 0 && upper_l > 0 && {
467 let lhs = heads[0].borrow();
468 let rhs = heads[1].borrow();
469 let last_l = (lhs.0.get(upper_l - 1), lhs.1.get(upper_l - 1));
470 let cur_r = (rhs.0.get(positions[1]), rhs.1.get(positions[1]));
471 last_l < cur_r
472 };
473 if lhs_passthrough {
474 if !result.is_empty() {
475 sink(pager.page(&mut result));
476 if let Some(reuse) = stash.pop() {
477 result = reuse;
478 }
479 }
480 let mut head = std::mem::replace(&mut heads[0], list1.next().unwrap_or_default());
481 sink(pager.page(&mut head));
482 positions[0] = 0;
483 continue;
484 }
485
486 let rhs_passthrough = positions[1] == 0 && upper_r > 0 && {
487 let lhs = heads[0].borrow();
488 let rhs = heads[1].borrow();
489 let last_r = (rhs.0.get(upper_r - 1), rhs.1.get(upper_r - 1));
490 let cur_l = (lhs.0.get(positions[0]), lhs.1.get(positions[0]));
491 last_r < cur_l
492 };
493 if rhs_passthrough {
494 if !result.is_empty() {
495 sink(pager.page(&mut result));
496 if let Some(reuse) = stash.pop() {
497 result = reuse;
498 }
499 }
500 let mut head = std::mem::replace(&mut heads[1], list2.next().unwrap_or_default());
501 sink(pager.page(&mut head));
502 positions[1] = 0;
503 continue;
504 }
505
506 let yielded = result.merge_from(&mut heads, &mut positions);
507
508 if positions[0] >= heads[0].borrow().len() {
509 let old = std::mem::replace(&mut heads[0], list1.next().unwrap_or_default());
510 recycle_capped(old, stash);
511 positions[0] = 0;
512 }
513 if positions[1] >= heads[1].borrow().len() {
514 let old = std::mem::replace(&mut heads[1], list2.next().unwrap_or_default());
515 recycle_capped(old, stash);
516 positions[1] = 0;
517 }
518 if yielded || result.at_capacity() {
519 sink(pager.page(&mut result));
520 if let Some(reuse) = stash.pop() {
526 result = reuse;
527 }
528 }
529 }
530
531 drain_side(
535 &mut heads[0],
536 &mut positions[0],
537 list1,
538 &mut result,
539 &mut sink,
540 pager,
541 stash,
542 );
543 drain_side(
544 &mut heads[1],
545 &mut positions[1],
546 list2,
547 &mut result,
548 &mut sink,
549 pager,
550 stash,
551 );
552
553 if !result.is_empty() {
554 sink(pager.page(&mut result));
555 } else {
556 recycle_capped(result, stash);
560 }
561 let [h0, h1] = heads;
565 recycle_capped(h0, stash);
566 recycle_capped(h1, stash);
567}
568
569fn drain_side<D, T, R, Sink>(
573 head: &mut Column<(D, T, R)>,
574 pos: &mut usize,
575 rest: FetchIter<'_, D, T, R>,
576 result: &mut Column<(D, T, R)>,
577 sink: &mut Sink,
578 pager: &ColumnPager,
579 stash: &mut Vec<Column<(D, T, R)>>,
580) where
581 D: Columnar,
582 for<'a> columnar::Ref<'a, D>: Copy + Ord,
583 T: Columnar + Default + Clone + PartialOrder,
584 for<'a> columnar::Ref<'a, T>: Copy + Ord,
585 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
586 Sink: FnMut(PagedColumn<(D, T, R)>),
587{
588 if *pos < head.borrow().len() {
589 let _ = result.merge_from(std::slice::from_mut(head), std::slice::from_mut(pos));
591 }
592 if !result.is_empty() {
593 sink(pager.page(result));
594 if let Some(reuse) = stash.pop() {
595 *result = reuse;
596 }
597 }
598 for paged in rest.into_paged() {
599 sink(paged);
600 }
601}
602
603pub fn extract_chain<D, T, R, SinkShip, SinkKeep>(
612 merged: FetchIter<'_, D, T, R>,
613 upper: AntichainRef<T>,
614 frontier: &mut Antichain<T>,
615 mut ship: SinkShip,
616 mut keep: SinkKeep,
617 stash: &mut Vec<Column<(D, T, R)>>,
618) where
619 D: Columnar,
620 for<'a> columnar::Ref<'a, D>: Copy + Ord,
621 T: Columnar + Default + Clone + PartialOrder,
622 for<'a> columnar::Ref<'a, T>: Copy + Ord,
623 R: Columnar + Default + Semigroup + for<'a> Semigroup<columnar::Ref<'a, R>>,
624 SinkShip: FnMut(PagedColumn<(D, T, R)>),
625 SinkKeep: FnMut(PagedColumn<(D, T, R)>),
626{
627 let pager = merged.pager();
628 let mut keep_buf: Column<(D, T, R)> = empty_chunk(stash);
629 let mut ship_buf: Column<(D, T, R)> = empty_chunk(stash);
630
631 for mut buffer in merged {
632 let mut position = 0;
633 let len = buffer.borrow().len();
634 while position < len {
635 buffer.extract(&mut position, upper, frontier, &mut keep_buf, &mut ship_buf);
636 if keep_buf.at_capacity() {
637 keep(pager.page(&mut keep_buf));
638 if let Some(reuse) = stash.pop() {
639 keep_buf = reuse;
640 }
641 }
642 if ship_buf.at_capacity() {
643 ship(pager.page(&mut ship_buf));
644 if let Some(reuse) = stash.pop() {
645 ship_buf = reuse;
646 }
647 }
648 }
649 recycle_capped(buffer, stash);
651 }
652 if !keep_buf.is_empty() {
653 keep(pager.page(&mut keep_buf));
654 } else {
655 recycle_capped(keep_buf, stash);
656 }
657 if !ship_buf.is_empty() {
658 ship(pager.page(&mut ship_buf));
659 } else {
660 recycle_capped(ship_buf, stash);
661 }
662}
663
664#[cfg(test)]
665#[allow(clippy::clone_on_ref_ptr)]
666mod tests {
667 use std::sync::Arc;
668
669 use columnar::Index;
670
671 use super::*;
672 use crate::column_pager::{PageDecision, PageEvent, PageHint, PagingPolicy};
673
674 type KvUpdate = ((u64, u64), u64, i64);
675
676 fn col(rows: &[KvUpdate]) -> Column<KvUpdate> {
677 let mut c: Column<KvUpdate> = Default::default();
678 for &t in rows {
679 c.push_into(t);
680 }
681 c
682 }
683
684 fn collect_pc(chunks: &[PagedColumn<KvUpdate>], pager: &ColumnPager) -> Vec<KvUpdate> {
685 chunks
688 .iter()
689 .flat_map(|p| {
690 let view: Column<KvUpdate> = match p {
691 PagedColumn::Resident(c, _) => clone_column(c),
692 _ => pager.take(clone_paged(p)),
693 };
694 collect_column(&view).into_iter()
695 })
696 .collect()
697 }
698
699 fn collect_column(c: &Column<KvUpdate>) -> Vec<KvUpdate> {
700 c.borrow()
701 .into_index_iter()
702 .map(|((k, v), t, r)| {
703 (
704 (u64::into_owned(k), u64::into_owned(v)),
705 u64::into_owned(t),
706 i64::into_owned(r),
707 )
708 })
709 .collect()
710 }
711
712 fn clone_column(c: &Column<KvUpdate>) -> Column<KvUpdate> {
713 c.clone()
717 }
718
719 fn clone_paged(p: &PagedColumn<KvUpdate>) -> PagedColumn<KvUpdate> {
723 match p {
724 PagedColumn::Resident(c, _) => {
725 let mut c = c.clone();
727 ColumnPager::disabled().page(&mut c)
728 }
729 _ => panic!("clone_paged only supports Resident"),
732 }
733 }
734
735 struct ForcePagePolicy {
738 out: std::sync::atomic::AtomicUsize,
739 r#in: std::sync::atomic::AtomicUsize,
740 }
741 impl ForcePagePolicy {
742 fn new() -> Arc<Self> {
743 Arc::new(Self {
744 out: std::sync::atomic::AtomicUsize::new(0),
745 r#in: std::sync::atomic::AtomicUsize::new(0),
746 })
747 }
748 }
749 impl PagingPolicy for ForcePagePolicy {
750 fn decide(&self, _hint: PageHint) -> PageDecision {
751 PageDecision::Page {
752 backend: mz_ore::pager::Backend::Swap,
753 codec: None,
754 }
755 }
756 fn record(&self, event: PageEvent) {
757 use std::sync::atomic::Ordering;
758 match event {
759 PageEvent::PagedOut { .. } => {
760 self.out.fetch_add(1, Ordering::Relaxed);
761 }
762 PageEvent::PagedIn { .. } => {
763 self.r#in.fetch_add(1, Ordering::Relaxed);
764 }
765 _ => {}
766 }
767 }
768 }
769
770 fn to_chain(
772 cols: Vec<Column<KvUpdate>>,
773 pager: &ColumnPager,
774 ) -> VecDeque<PagedColumn<KvUpdate>> {
775 cols.into_iter().map(|mut c| pager.page(&mut c)).collect()
776 }
777
778 fn drive_merge(chain1: Vec<Column<KvUpdate>>, chain2: Vec<Column<KvUpdate>>) -> Vec<KvUpdate> {
780 let pager = ColumnPager::disabled();
781 let q1 = to_chain(chain1, &pager);
782 let q2 = to_chain(chain2, &pager);
783 let mut output: Vec<PagedColumn<KvUpdate>> = Vec::new();
784 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
785 merge_chains(
786 FetchIter::new(q1, &pager),
787 FetchIter::new(q2, &pager),
788 |paged| output.push(paged),
789 &mut stash,
790 );
791 collect_pc(&output, &pager)
792 }
793
794 #[mz_ore::test]
798 fn merge_chains_disjoint_ranges() {
799 let out = drive_merge(
800 vec![
801 col(&[((0, 0), 0, 1), ((1, 0), 0, 1)]),
802 col(&[((2, 0), 0, 1), ((3, 0), 0, 1)]),
803 ],
804 vec![
805 col(&[((10, 0), 0, 1), ((11, 0), 0, 1)]),
806 col(&[((12, 0), 0, 1), ((13, 0), 0, 1)]),
807 ],
808 );
809 let expected: Vec<_> = (0..4u64)
810 .map(|d| ((d, 0u64), 0u64, 1i64))
811 .chain((10..14u64).map(|d| ((d, 0u64), 0u64, 1i64)))
812 .collect();
813 assert_eq!(out, expected);
814 }
815
816 #[mz_ore::test]
817 fn merge_chains_interleaved() {
818 let out = drive_merge(
819 vec![
820 col(&[((0, 0), 0, 1), ((2, 0), 0, 1)]),
821 col(&[((4, 0), 0, 1), ((6, 0), 0, 1)]),
822 ],
823 vec![
824 col(&[((1, 0), 0, 1), ((3, 0), 0, 1)]),
825 col(&[((5, 0), 0, 1), ((7, 0), 0, 1)]),
826 ],
827 );
828 let expected: Vec<_> = (0..8u64).map(|d| ((d, 0u64), 0u64, 1i64)).collect();
829 assert_eq!(out, expected);
830 }
831
832 #[mz_ore::test]
836 fn merge_chains_equal_boundary() {
837 let out = drive_merge(
838 vec![col(&[((0, 0), 0, 1), ((5, 0), 0, 1)])],
839 vec![col(&[((5, 0), 0, 1), ((10, 0), 0, 1)])],
840 );
841 assert_eq!(out, vec![((0, 0), 0, 1), ((5, 0), 0, 2), ((10, 0), 0, 1)]);
842 }
843
844 #[mz_ore::test]
852 fn merge_chains_ships_fitting_align() {
853 let pager = ColumnPager::disabled();
854 let q1 = to_chain(vec![col(&[((0, 0), 0, 1), ((2, 0), 0, 1)])], &pager);
858 let q2 = to_chain(vec![col(&[((1, 0), 0, 1), ((3, 0), 0, 1)])], &pager);
859
860 let mut output: Vec<PagedColumn<KvUpdate>> = Vec::new();
861 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
862 merge_chains(
863 FetchIter::new(q1, &pager),
864 FetchIter::new(q2, &pager),
865 |paged| output.push(paged),
866 &mut stash,
867 );
868
869 assert!(!output.is_empty(), "merge produced no chunks");
870 for entry in &output {
871 match entry {
872 PagedColumn::Resident(col, _) => assert!(
873 matches!(col, Column::Align(_)),
874 "shipped chunk parked as non-Align resident: {:?}",
875 std::mem::discriminant(col),
876 ),
877 other => panic!(
878 "disabled pager should ship Resident, got a paged variant: {:?}",
879 std::mem::discriminant(other)
880 ),
881 }
882 }
883
884 assert_eq!(
886 collect_pc(&output, &pager),
887 vec![
888 ((0, 0), 0, 1),
889 ((1, 0), 0, 1),
890 ((2, 0), 0, 1),
891 ((3, 0), 0, 1)
892 ],
893 );
894 }
895
896 #[mz_ore::test]
899 fn merge_chains_force_paged_round_trip() {
900 let policy = ForcePagePolicy::new();
901 let pager = ColumnPager::new(policy.clone());
902 let q1 = to_chain(vec![col(&[((0, 0), 0, 1), ((2, 0), 0, 1)])], &pager);
903 let q2 = to_chain(vec![col(&[((1, 0), 0, 1), ((3, 0), 0, 1)])], &pager);
904
905 assert!(matches!(q1.front().unwrap(), PagedColumn::Paged { .. }));
907 assert!(matches!(q2.front().unwrap(), PagedColumn::Paged { .. }));
908
909 let mut output: Vec<PagedColumn<KvUpdate>> = Vec::new();
910 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
911 merge_chains(
912 FetchIter::new(q1, &pager),
913 FetchIter::new(q2, &pager),
914 |paged| output.push(paged),
915 &mut stash,
916 );
917
918 for p in &output {
920 assert!(matches!(p, PagedColumn::Paged { .. }));
921 }
922
923 let mut collected = Vec::new();
925 for p in output {
926 let c = pager.take(p);
927 collected.extend(collect_column(&c));
928 }
929 let expected: Vec<_> = (0..4u64).map(|d| ((d, 0u64), 0u64, 1i64)).collect();
930 assert_eq!(collected, expected);
931 }
932
933 #[mz_ore::test]
934 fn extract_chain_partitions_by_frontier() {
935 let pager = ColumnPager::disabled();
936 let data = vec![
937 ((0, 0), 0u64, 1i64),
938 ((1, 0), 1, 1),
939 ((2, 0), 2, 1),
940 ((3, 0), 3, 1),
941 ];
942 let chain = to_chain(vec![col(&data)], &pager);
943 let upper = Antichain::from_elem(2u64);
944 let mut frontier: Antichain<u64> = Antichain::new();
945 let mut ship: Vec<PagedColumn<KvUpdate>> = Vec::new();
946 let mut keep: Vec<PagedColumn<KvUpdate>> = Vec::new();
947 let mut stash: Vec<Column<KvUpdate>> = Vec::new();
948
949 extract_chain(
950 FetchIter::new(chain, &pager),
951 upper.borrow(),
952 &mut frontier,
953 |p| ship.push(p),
954 |p| keep.push(p),
955 &mut stash,
956 );
957
958 let shipped = collect_pc(&ship, &pager);
959 let kept = collect_pc(&keep, &pager);
960 for (_, t, _) in &shipped {
961 assert!(*t < 2, "shipped time {t} should be < upper");
962 }
963 for (_, t, _) in &kept {
964 assert!(*t >= 2, "kept time {t} should be >= upper");
965 }
966 assert_eq!(shipped.len() + kept.len(), data.len());
967 }
968
969 #[mz_ore::test]
970 fn batcher_seal_round_trip() {
971 let mut b: ColumnMergeBatcher<(u64, u64), u64, i64> =
972 differential_dataflow::trace::Batcher::new(None, 0);
973 let input1 = col(&[((1, 1), 0, 1), ((2, 0), 0, 1), ((3, 0), 0, 1)]);
977 let input2 = col(&[((2, 0), 0, 2), ((4, 0), 0, 1)]);
978 b.push_into(input1);
979 b.push_into(input2);
980
981 let upper = Antichain::from_elem(u64::MAX);
983 let (chain, _description) = differential_dataflow::trace::Batcher::seal(&mut b, upper);
984 let out: Vec<KvUpdate> = chain.iter().flat_map(collect_column).collect();
985
986 let mut expected = vec![
988 ((1u64, 1u64), 0u64, 1i64),
989 ((2, 0), 0, 3),
990 ((3, 0), 0, 1),
991 ((4, 0), 0, 1),
992 ];
993 expected.sort();
994 let mut out_sorted = out.clone();
995 out_sorted.sort();
996 assert_eq!(out_sorted, expected);
997 }
998
999 #[mz_ore::test]
1000 fn account_chunk_resident_vs_paged() {
1001 let policy = ForcePagePolicy::new();
1002 let pager_paged = ColumnPager::new(policy.clone());
1003 let pager_res = ColumnPager::disabled();
1004
1005 let mut c1 = col(&[((1, 1), 0, 1), ((2, 0), 0, 1), ((3, 0), 0, 1)]);
1006 let resident = pager_res.page(&mut c1);
1007 let (records, size, capacity, allocations) = account_chunk(&resident);
1008 assert_eq!(records, 3);
1009 assert!(size > 0);
1010 assert_eq!(size, capacity);
1011 assert_eq!(allocations, 1);
1012
1013 let mut c2 = col(&[((1, 1), 0, 1), ((2, 0), 0, 1)]);
1014 let paged = pager_paged.page(&mut c2);
1015 assert!(matches!(paged, PagedColumn::Paged { .. }));
1016 assert_eq!(account_chunk(&paged), (0, 0, 0, 0));
1018 }
1019
1020 #[mz_ore::test]
1021 fn batcher_seal_keeps_kept_chain_paged() {
1022 let policy = ForcePagePolicy::new();
1025 let pager = ColumnPager::new(policy.clone());
1026
1027 let mut b: ColumnMergeBatcher<(u64, u64), u64, i64> =
1028 differential_dataflow::trace::Batcher::new(None, 0);
1029 b.set_pager(pager);
1030
1031 let n: u64 = 200;
1034 for i in 0..n {
1035 let input = col(&[((i, 0), i % 10, 1)]);
1036 b.push_into(input);
1037 }
1038 let upper = Antichain::from_elem(5u64);
1039 let _ = differential_dataflow::trace::Batcher::seal(&mut b, upper);
1040
1041 let kept_records: usize = b
1043 .chains
1044 .iter()
1045 .flat_map(|c| c.iter())
1046 .map(|p| match p {
1047 PagedColumn::Paged { meta, .. } => {
1048 let _ = meta;
1051 1
1052 }
1053 PagedColumn::Compressed { meta, .. } => {
1054 let _ = meta;
1055 1
1056 }
1057 PagedColumn::Resident(_, _) => {
1058 panic!("kept chain entry was Resident under ForcePagePolicy");
1059 }
1060 })
1061 .sum();
1062 assert!(kept_records > 0, "expected at least one kept paged entry");
1064 assert!(policy.out.load(std::sync::atomic::Ordering::Relaxed) > 0);
1065 let _ = n;
1066 }
1067}