1use std::sync::Arc;
21
22use super::core::DataChunk;
23use crate::executor::base::MemoryBudget;
24use crate::executor::streaming::spill::{
25 schema_fingerprint, SpillManager, SpilledRun, COLLECTOR_RUN_ROWS_MAX,
26};
27use linkrs_core::error::QueryError;
28use linkrs_core::Value;
29
30use super::super::spill::{RunReader, RunWriter};
31
32pub const COLLECTOR_BLOCK_ROWS: usize = 4096;
34
35#[derive(Debug)]
37struct ActiveRun {
38 writer: RunWriter,
39 rows: u64,
40 reserved_bytes: u64,
43}
44
45#[derive(Debug, Default)]
47pub struct LocalChunkCollector {
48 blocks: Vec<Vec<Vec<Value>>>,
49 col_names: Vec<String>,
50 len: usize,
52 total_logical: u128,
53 spill_threshold_rows: Option<u64>,
54 spill_manager: Option<Arc<SpillManager>>,
55 spilled_runs: Vec<SpilledRun>,
56 active_writer: Option<ActiveRun>,
57 spill_fingerprint: Option<u64>,
58 spilled_rows: u64,
59 spilled_bytes: u64,
60 drain_cursor: usize,
61}
62
63impl LocalChunkCollector {
64 pub fn new(col_names: Vec<String>) -> Self {
65 Self {
66 blocks: Vec::new(),
67 col_names,
68 len: 0,
69 total_logical: 0,
70 spill_threshold_rows: None,
71 spill_manager: None,
72 spilled_runs: Vec::new(),
73 active_writer: None,
74 spill_fingerprint: None,
75 spilled_rows: 0,
76 spilled_bytes: 0,
77 drain_cursor: 0,
78 }
79 }
80
81 pub fn attach_spill_manager(&mut self, manager: Arc<SpillManager>) {
88 if self.spill_threshold_rows.is_none() {
89 self.spill_threshold_rows = Some(manager.collector_spill_threshold());
90 }
91 self.spill_manager = Some(manager);
92 }
93
94 pub fn push_chunk(&mut self, chunk: &mut DataChunk) -> Result<(), QueryError> {
98 if self.col_names.is_empty() {
99 self.col_names = chunk.col_names();
100 }
101 self.total_logical += u128::from(chunk.logical_len());
102 let rows = chunk.expand_visible_rows();
103 if rows.is_empty() {
104 return Ok(());
105 }
106 if self.spill_manager.is_none() {
107 self.push_rows_memory(rows);
108 return Ok(());
109 }
110 if self.is_spilled() {
111 self.write_rows_spilled(rows)?;
112 return Ok(());
113 }
114 if self.spill_threshold_exceeded() {
115 self.spill_memory_blocks()?;
116 self.write_rows_spilled(rows)?;
117 } else {
118 self.push_rows_memory(rows);
119 }
120 Ok(())
121 }
122
123 pub fn merge(&mut self, mut other: LocalChunkCollector) -> Result<(), QueryError> {
132 let self_spilled = self.is_spilled();
133 let other_spilled = other.is_spilled();
134 if self_spilled || other_spilled {
135 if !self.col_names.is_empty()
136 && !other.col_names.is_empty()
137 && self.col_names != other.col_names
138 {
139 return Err(QueryError::execution(
140 "spill run: cannot merge collectors with different column names".to_string(),
141 ));
142 }
143 match (self.spill_manager.clone(), other.spill_manager.clone()) {
144 (Some(a), Some(b)) => {
145 if !Arc::ptr_eq(&a, &b) {
146 return Err(QueryError::execution(
147 "spill run: cannot merge collectors from different spill managers"
148 .to_string(),
149 ));
150 }
151 }
152 (None, Some(manager)) => {
153 if self.len == 0 && self.total_logical == 0 {
154 self.spill_manager = Some(manager.clone());
155 if self.spill_threshold_rows.is_none() {
156 self.spill_threshold_rows = other.spill_threshold_rows;
157 }
158 } else {
159 return Err(QueryError::execution(
160 "spill run: cannot merge spilled collector without a spill manager"
161 .to_string(),
162 ));
163 }
164 }
165 (Some(_), None) => {
166 debug_assert!(
167 !other_spilled,
168 "spilled runs without a spill manager must not exist"
169 );
170 }
171 (None, None) => {
172 debug_assert!(
173 !self_spilled && !other_spilled,
174 "spilled state requires a spill manager"
175 );
176 }
177 }
178 if self.col_names.is_empty() {
179 self.col_names = std::mem::take(&mut other.col_names);
180 }
181 self.finalize_active_writer()?;
182 other.finalize_active_writer()?;
183 let other_remaining: Vec<SpilledRun> =
184 other.spilled_runs[other.drain_cursor..].to_vec();
185 self.spilled_runs.extend(other_remaining);
186 self.spilled_rows += other.spilled_rows;
187 self.spilled_bytes += other.spilled_bytes;
188 } else if self.col_names.is_empty() {
189 self.col_names = std::mem::take(&mut other.col_names);
190 }
191 self.total_logical += other.total_logical;
192 self.len += other.len;
193 if self.blocks.is_empty() {
194 self.blocks = other.blocks;
195 return Ok(());
196 }
197 for mut block in other.blocks {
198 if block.is_empty() {
199 continue;
200 }
201 let tail_has_room = self
202 .blocks
203 .last()
204 .is_some_and(|b| b.len() < COLLECTOR_BLOCK_ROWS);
205 if !tail_has_room {
206 self.blocks.push(std::mem::take(&mut block));
207 continue;
208 }
209 let tail = self.blocks.last_mut().expect("tail block must exist");
211 while tail.len() < COLLECTOR_BLOCK_ROWS && !block.is_empty() {
212 let room = COLLECTOR_BLOCK_ROWS - tail.len();
216 let take = room.min(block.len());
217 tail.extend(block.drain(..take));
218 }
219 if !block.is_empty() {
220 self.blocks.push(block);
221 }
222 }
223 Ok(())
224 }
225
226 pub fn total_logical_rows(&self) -> u128 {
228 self.total_logical
229 }
230
231 pub fn len(&self) -> usize {
233 self.len
234 }
235
236 pub fn is_empty(&self) -> bool {
237 self.len == 0
238 }
239
240 pub fn col_names(&self) -> &[String] {
241 &self.col_names
242 }
243
244 pub fn finish_spill(&mut self) -> Result<(), QueryError> {
249 self.finalize_active_writer()
250 }
251
252 pub fn into_rows(mut self) -> Result<(Vec<Vec<Value>>, Vec<String>), QueryError> {
255 self.finalize_active_writer()?;
256 let mut out = Vec::with_capacity(self.len);
257 for run in &self.spilled_runs[self.drain_cursor..] {
258 let mut reader = RunReader::open(run)?;
259 out.extend(reader.read_all()?);
260 }
261 for block in self.blocks {
262 out.extend(block);
263 }
264 Ok((out, self.col_names))
265 }
266
267 pub fn drain_rows(&mut self) -> Result<Vec<Vec<Value>>, QueryError> {
272 if self.drain_cursor < self.spilled_runs.len() {
273 let run = self.spilled_runs[self.drain_cursor].clone();
274 let mut reader = RunReader::open(&run)?;
275 let rows = reader.read_all()?;
276 self.drain_cursor += 1;
277 self.len = self.len.saturating_sub(rows.len());
278 return Ok(rows);
279 }
280 if self.active_writer.is_some() {
281 self.finalize_active_writer()?;
282 return self.drain_rows();
283 }
284 if !self.blocks.is_empty() {
285 let blocks = std::mem::take(&mut self.blocks);
286 let mut out = Vec::new();
287 for block in blocks {
288 out.extend(block);
289 }
290 self.len = self.len.saturating_sub(out.len());
291 return Ok(out);
292 }
293 Ok(Vec::new())
294 }
295
296 pub fn into_blocks(self) -> (Vec<Vec<Vec<Value>>>, Vec<String>) {
301 debug_assert!(
302 !self.is_spilled(),
303 "into_blocks drops spilled runs; use into_rows or drain_rows"
304 );
305 (self.blocks, self.col_names)
306 }
307
308 pub fn iter_rows(&self) -> impl Iterator<Item = &Vec<Value>> {
313 self.blocks.iter().flatten()
314 }
315
316 pub fn num_blocks(&self) -> usize {
317 self.blocks.len()
318 }
319
320 pub fn is_spilled(&self) -> bool {
322 !self.spilled_runs.is_empty() || self.active_writer.is_some()
323 }
324
325 pub fn spilled_rows(&self) -> u64 {
327 self.spilled_rows
328 }
329
330 pub fn spilled_bytes(&self) -> u64 {
332 self.spilled_bytes
333 }
334
335 pub fn spilled_run_count(&self) -> usize {
337 self.spilled_runs.len()
338 }
339
340 pub fn set_spill_threshold_rows(&mut self, threshold: u64) {
343 self.spill_threshold_rows = Some(threshold);
344 }
345
346 pub fn spill_threshold_exceeded(&self) -> bool {
348 match self.spill_threshold_rows {
349 Some(t) => u128::from(t) < self.total_logical,
350 None => false,
351 }
352 }
353
354 fn push_rows_memory(&mut self, mut rows: Vec<Vec<Value>>) {
355 if rows.is_empty() {
356 return;
357 }
358 self.len += rows.len();
359 rows.reverse();
361 while !rows.is_empty() {
362 let tail_full = self
363 .blocks
364 .last()
365 .is_none_or(|b| b.len() >= COLLECTOR_BLOCK_ROWS);
366 if tail_full {
367 self.blocks
368 .push(Vec::with_capacity(rows.len().min(COLLECTOR_BLOCK_ROWS)));
369 }
370 let tail = self.blocks.last_mut().expect("tail block must exist");
371 while tail.len() < COLLECTOR_BLOCK_ROWS {
372 if let Some(row) = rows.pop() {
373 tail.push(row);
374 } else {
375 break;
376 }
377 }
378 }
379 }
380
381 fn spill_fingerprint(&mut self) -> u64 {
382 if let Some(fp) = self.spill_fingerprint {
383 return fp;
384 }
385 let fp = schema_fingerprint(&self.col_names);
386 self.spill_fingerprint = Some(fp);
387 fp
388 }
389
390 fn ensure_active_writer(&mut self) -> Result<(), QueryError> {
391 if self.active_writer.is_some() {
392 return Ok(());
393 }
394 let manager = self.spill_manager.clone().ok_or_else(|| {
395 QueryError::execution("spill run: spill manager not available".to_string())
396 })?;
397 let fp = self.spill_fingerprint();
398 let writer = manager.create_run_writer(fp)?;
399 self.active_writer = Some(ActiveRun {
400 writer,
401 rows: 0,
402 reserved_bytes: 0,
403 });
404 Ok(())
405 }
406
407 fn rotate_active_writer_if_full(&mut self) -> Result<(), QueryError> {
408 let full = self
409 .active_writer
410 .as_ref()
411 .is_some_and(|a| a.rows >= COLLECTOR_RUN_ROWS_MAX);
412 if full {
413 self.finalize_active_writer()?;
414 }
415 Ok(())
416 }
417
418 fn finalize_active_writer(&mut self) -> Result<(), QueryError> {
419 let Some(active) = self.active_writer.take() else {
420 return Ok(());
421 };
422 let manager = self.spill_manager.clone().ok_or_else(|| {
423 QueryError::execution("spill run: spill manager not available".to_string())
424 })?;
425 let rows = active.rows;
426 let reserved = active.reserved_bytes;
427 manager.disk_quota().release(reserved);
431 let run = manager.finalize_run(active.writer).map_err(|e| {
434 let msg = e.to_string();
435 if msg.contains("Disk quota") {
436 e
437 } else {
438 QueryError::execution(format!("spill run: {}", msg))
439 }
440 })?;
441 self.spilled_rows += rows;
442 self.spilled_bytes += run.byte_size;
443 self.spilled_runs.push(run);
444 Ok(())
445 }
446
447 fn write_one_row(&mut self, row: Vec<Value>) -> Result<(), QueryError> {
448 self.rotate_active_writer_if_full()?;
449 self.ensure_active_writer()?;
450 let manager = self.spill_manager.clone().ok_or_else(|| {
451 QueryError::execution("spill run: spill manager not available".to_string())
452 })?;
453 let estimate = MemoryBudget::estimate_row_memory(&row) as u64;
458 manager.disk_quota().try_reserve(estimate)?;
459 let active = self.active_writer.as_mut().ok_or_else(|| {
460 QueryError::execution("spill run: collector writer missing after ensure".to_string())
461 })?;
462 active.writer.write_row(&row)?;
463 active.rows += 1;
464 active.reserved_bytes += estimate;
465 Ok(())
466 }
467
468 fn write_rows_spilled(&mut self, rows: Vec<Vec<Value>>) -> Result<(), QueryError> {
469 self.ensure_active_writer()?;
470 self.len += rows.len();
471 for row in rows {
472 self.write_one_row(row)?;
473 }
474 Ok(())
475 }
476
477 fn spill_memory_blocks(&mut self) -> Result<(), QueryError> {
478 if self.blocks.is_empty() {
479 return Ok(());
480 }
481 self.ensure_active_writer()?;
482 let blocks = std::mem::take(&mut self.blocks);
483 for block in blocks {
484 for row in block {
485 self.write_one_row(row)?;
486 }
487 }
488 Ok(())
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::executor::streaming::slot::SlotLayout;
496 use crate::executor::streaming::spill::{DiskQuota, SpillConfig, SpillManager};
497 use std::sync::Arc;
498
499 fn chunk_with(rows: Vec<Vec<Value>>, selection: Option<Vec<usize>>, mult: u64) -> DataChunk {
500 let layout = Arc::new(SlotLayout::from_names(&["a".to_string()]));
501 let mut c = DataChunk::new_with_layout(rows, layout);
502 if let Some(sel) = selection {
503 c = c.with_selection(sel);
504 }
505 c.with_multiplicity(mult)
506 }
507
508 fn spill_manager(query_id: u64) -> Arc<SpillManager> {
509 Arc::new(SpillManager::new(SpillConfig::default(), query_id).unwrap())
510 }
511
512 #[test]
513 fn expands_selection_and_multiplicity_once() {
514 let mut c = chunk_with(
515 vec![
516 vec![Value::Int(1)],
517 vec![Value::Int(2)],
518 vec![Value::Int(3)],
519 ],
520 Some(vec![0, 2]),
521 2,
522 );
523 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
524 collector.push_chunk(&mut c).unwrap();
525 assert_eq!(collector.total_logical_rows(), 4);
526 assert_eq!(collector.len(), 4);
527 assert!(c.rows.is_empty());
528 }
529
530 #[test]
531 fn merge_sums_counts() {
532 let mut a = LocalChunkCollector::new(vec!["a".to_string()]);
533 let mut b = LocalChunkCollector::new(vec!["a".to_string()]);
534 let mut c1 = chunk_with(vec![vec![Value::Int(1)]], None, 1);
535 let mut c2 = chunk_with(vec![vec![Value::Int(2)]], None, 3);
536 a.push_chunk(&mut c1).unwrap();
537 b.push_chunk(&mut c2).unwrap();
538 a.merge(b).unwrap();
539 assert_eq!(a.total_logical_rows(), 4);
540 assert_eq!(a.len(), 4);
541 }
542
543 #[test]
544 fn splits_large_push_into_blocks() {
545 let rows: Vec<Vec<Value>> = (0..5000).map(|i| vec![Value::Int(i)]).collect();
546 let mut c = chunk_with(rows, None, 1);
547 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
548 collector.push_chunk(&mut c).unwrap();
549 assert_eq!(collector.len(), 5000);
550 assert_eq!(collector.num_blocks(), 2);
551 let (flat, _) = collector.into_rows().unwrap();
552 assert_eq!(flat.len(), 5000);
553 assert_eq!(flat[0], vec![Value::Int(0)]);
554 assert_eq!(flat[4999], vec![Value::Int(4999)]);
555 }
556
557 #[test]
558 fn merge_moves_blocks_and_iter_rows_preserves_order() {
559 let mut a = LocalChunkCollector::new(vec!["a".to_string()]);
560 let mut b = LocalChunkCollector::new(vec!["a".to_string()]);
561 let mut c1 = chunk_with(vec![vec![Value::Int(1)], vec![Value::Int(2)]], None, 1);
562 let mut c2 = chunk_with(vec![vec![Value::Int(3)]], None, 1);
563 a.push_chunk(&mut c1).unwrap();
564 b.push_chunk(&mut c2).unwrap();
565 a.merge(b).unwrap();
566 let ordered: Vec<i32> = a
567 .iter_rows()
568 .map(|r| match &r[0] {
569 Value::Int(v) => *v,
570 _ => -1,
571 })
572 .collect();
573 assert_eq!(ordered, vec![1, 2, 3]);
574 let (flat, _) = a.into_rows().unwrap();
575 assert_eq!(flat.len(), 3);
576 }
577
578 #[test]
579 fn spill_threshold_hook_defaults_off() {
580 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
581 assert!(!collector.is_spilled());
582 assert!(!collector.spill_threshold_exceeded());
583 collector.set_spill_threshold_rows(2);
584 let mut c = chunk_with(
585 vec![
586 vec![Value::Int(1)],
587 vec![Value::Int(2)],
588 vec![Value::Int(3)],
589 ],
590 None,
591 1,
592 );
593 collector.push_chunk(&mut c).unwrap();
594 assert!(collector.spill_threshold_exceeded());
595 assert!(!collector.is_spilled());
596 }
597
598 #[test]
599 fn spill_through_preserves_order() {
600 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
601 collector.attach_spill_manager(spill_manager(4201));
602 collector.set_spill_threshold_rows(10);
603 for batch in 0..5 {
604 let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::Int(batch * 10 + i)]).collect();
605 let mut c = chunk_with(rows, None, 1);
606 collector.push_chunk(&mut c).unwrap();
607 }
608 assert!(collector.is_spilled());
609 collector.finish_spill().unwrap();
610 assert_eq!(collector.spilled_rows(), 50);
611 assert!(collector.spilled_bytes() > 0);
612 let (flat, names) = collector.into_rows().unwrap();
613 assert_eq!(flat.len(), 50);
614 assert_eq!(names, vec!["a".to_string()]);
615 for (i, row) in flat.iter().enumerate() {
616 assert_eq!(row, &vec![Value::Int(i as i32)]);
617 }
618 }
619
620 #[test]
621 fn spill_run_rotation_keeps_writer_alive() {
622 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
623 collector.attach_spill_manager(spill_manager(4204));
624 let total = (COLLECTOR_RUN_ROWS_MAX + 10) as usize;
625 let rows: Vec<Vec<Value>> = (0..total).map(|i| vec![Value::Int(i as i32)]).collect();
626 collector.write_rows_spilled(rows).unwrap();
627 collector.finish_spill().unwrap();
628 assert!(collector.spilled_run_count() >= 2);
629 let (flat, _) = collector.into_rows().unwrap();
630 assert_eq!(flat.len(), total);
631 assert_eq!(flat[0], vec![Value::Int(0)]);
632 assert_eq!(flat[total - 1], vec![Value::Int((total - 1) as i32)]);
633 }
634
635 #[test]
636 fn spill_drain_matches_into_rows() {
637 let mut full = LocalChunkCollector::new(vec!["a".to_string()]);
638 full.attach_spill_manager(spill_manager(4202));
639 full.set_spill_threshold_rows(5);
640 let mut drained = LocalChunkCollector::new(vec!["a".to_string()]);
641 drained.attach_spill_manager(spill_manager(4203));
642 drained.set_spill_threshold_rows(5);
643 for batch in 0..3 {
644 let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::Int(batch * 10 + i)]).collect();
645 let mut c1 = chunk_with(rows.clone(), None, 1);
646 let mut c2 = chunk_with(rows, None, 1);
647 full.push_chunk(&mut c1).unwrap();
648 drained.push_chunk(&mut c2).unwrap();
649 }
650 let (expected, _) = full.into_rows().unwrap();
651 let mut got = Vec::new();
652 loop {
653 let segment = drained.drain_rows().unwrap();
654 if segment.is_empty() {
655 break;
656 }
657 got.extend(segment);
658 }
659 assert_eq!(got, expected);
660 }
661
662 #[test]
663 fn spill_merge_memory_and_spilled() {
664 let mut a = LocalChunkCollector::new(vec!["a".to_string()]);
665 a.attach_spill_manager(spill_manager(4204));
666 a.set_spill_threshold_rows(2);
667 let mut c1 = chunk_with(
668 vec![
669 vec![Value::Int(1)],
670 vec![Value::Int(2)],
671 vec![Value::Int(3)],
672 ],
673 None,
674 1,
675 );
676 a.push_chunk(&mut c1).unwrap();
677 assert!(a.is_spilled());
678
679 let mut b = LocalChunkCollector::new(vec!["a".to_string()]);
680 let mut c2 = chunk_with(vec![vec![Value::Int(4)]], None, 1);
681 b.push_chunk(&mut c2).unwrap();
682
683 b.attach_spill_manager(spill_manager(4205));
685 assert!(a.merge(b).is_err());
686 }
687
688 #[test]
689 fn spill_merge_same_manager_preserves_order() {
690 let manager = spill_manager(4206);
691 let mut a = LocalChunkCollector::new(vec!["a".to_string()]);
692 a.attach_spill_manager(manager.clone());
693 a.set_spill_threshold_rows(2);
694 let mut c1 = chunk_with(
695 vec![
696 vec![Value::Int(1)],
697 vec![Value::Int(2)],
698 vec![Value::Int(3)],
699 ],
700 None,
701 1,
702 );
703 a.push_chunk(&mut c1).unwrap();
704
705 let mut b = LocalChunkCollector::new(vec!["a".to_string()]);
706 b.attach_spill_manager(manager);
707 b.set_spill_threshold_rows(100);
708 let mut c2 = chunk_with(vec![vec![Value::Int(4)]], None, 1);
709 b.push_chunk(&mut c2).unwrap();
710
711 a.merge(b).unwrap();
712 let (flat, _) = a.into_rows().unwrap();
713 assert_eq!(
714 flat,
715 vec![
716 vec![Value::Int(1)],
717 vec![Value::Int(2)],
718 vec![Value::Int(3)],
719 vec![Value::Int(4)],
720 ]
721 );
722 }
723
724 #[test]
725 fn spill_merge_column_mismatch_rejected() {
726 let manager = spill_manager(4207);
727 let mut a = LocalChunkCollector::new(vec!["a".to_string()]);
728 a.attach_spill_manager(manager.clone());
729 a.set_spill_threshold_rows(1);
730 let mut c1 = chunk_with(vec![vec![Value::Int(1)], vec![Value::Int(2)]], None, 1);
731 a.push_chunk(&mut c1).unwrap();
732 assert!(a.is_spilled());
733
734 let mut b = LocalChunkCollector::new(vec!["b".to_string()]);
735 b.attach_spill_manager(manager);
736 let mut c2 = chunk_with(vec![vec![Value::Int(3)]], None, 1);
737 b.push_chunk(&mut c2).unwrap();
738
739 assert!(a.merge(b).is_err());
740 }
741
742 #[test]
743 fn spill_quota_error_surfaces() {
744 let quota = DiskQuota::new(1);
745 let config = SpillConfig {
746 temp_dir: None,
747 max_spill_files: 64,
748 collect_spill_rows: Some(1),
749 };
750 let manager = Arc::new(SpillManager::new_with_quota(config, 4208, quota).unwrap());
751 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
752 collector.attach_spill_manager(manager);
753 let rows: Vec<Vec<Value>> = (0..50).map(|i| vec![Value::Int(i)]).collect();
755 let mut c = chunk_with(rows, None, 1);
756 assert!(collector.push_chunk(&mut c).is_err());
757 }
758
759 #[test]
760 fn no_manager_keeps_memory_behavior() {
761 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
762 collector.set_spill_threshold_rows(1);
763 let mut c = chunk_with(vec![vec![Value::Int(1)], vec![Value::Int(2)]], None, 1);
764 collector.push_chunk(&mut c).unwrap();
765 assert!(!collector.is_spilled());
766 assert_eq!(collector.spilled_rows(), 0);
767 let (flat, _) = collector.into_rows().unwrap();
768 assert_eq!(flat.len(), 2);
769 }
770
771 fn spill_with_rows(query_id: u64, n: i32) -> (LocalChunkCollector, Arc<SpillManager>) {
772 let manager = spill_manager(query_id);
773 let mut collector = LocalChunkCollector::new(vec!["a".to_string()]);
774 collector.attach_spill_manager(manager.clone());
775 collector.set_spill_threshold_rows(1);
776 let rows: Vec<Vec<Value>> = (0..n).map(|i| vec![Value::Int(i)]).collect();
777 let mut c = chunk_with(rows, None, 1);
778 collector.push_chunk(&mut c).unwrap();
779 collector.finish_spill().unwrap();
780 assert!(collector.is_spilled());
781 (collector, manager)
782 }
783
784 fn corrupt_run_headers(manager: &SpillManager, offset: u64) {
785 let entries = std::fs::read_dir(manager.base_dir()).unwrap();
786 let mut corrupted = 0;
787 for entry in entries.flatten() {
788 let path = entry.path();
789 if path.extension().and_then(|e| e.to_str()) != Some("run") {
790 continue;
791 }
792 let mut bytes = std::fs::read(&path).unwrap();
793 assert!(bytes.len() > offset as usize);
794 bytes[offset as usize] ^= 0xff;
795 std::fs::write(&path, &bytes).unwrap();
796 corrupted += 1;
797 }
798 assert!(corrupted > 0, "expected at least one run file to corrupt");
799 }
800
801 fn corrupt_run_bodies(manager: &SpillManager, offset: u64) {
802 let entries = std::fs::read_dir(manager.base_dir()).unwrap();
803 let mut corrupted = 0;
804 for entry in entries.flatten() {
805 let path = entry.path();
806 if path.extension().and_then(|e| e.to_str()) != Some("run") {
807 continue;
808 }
809 let mut bytes = std::fs::read(&path).unwrap();
810 if bytes.len() <= offset as usize {
811 continue;
812 }
813 bytes[offset as usize] ^= 0xff;
814 std::fs::write(&path, &bytes).unwrap();
815 corrupted += 1;
816 }
817 assert!(corrupted > 0, "expected at least one run file to corrupt");
818 }
819
820 fn repair_run_header_checksums(manager: &SpillManager) {
821 fn fnv1a_64_local(data: &[u8]) -> u64 {
822 let mut hash = 0xcbf29ce484222325u64;
823 for &b in data {
824 hash ^= b as u64;
825 hash = hash.wrapping_mul(0x100000001b3);
826 }
827 hash
828 }
829 let entries = std::fs::read_dir(manager.base_dir()).unwrap();
830 for entry in entries.flatten() {
831 let path = entry.path();
832 if path.extension().and_then(|e| e.to_str()) != Some("run") {
833 continue;
834 }
835 let mut bytes = std::fs::read(&path).unwrap();
836 assert!(bytes.len() >= 48);
837 let checksum = fnv1a_64_local(&bytes[0..40]);
838 bytes[40..48].copy_from_slice(&checksum.to_le_bytes());
839 std::fs::write(&path, &bytes).unwrap();
840 }
841 }
842
843 #[test]
844 fn spilled_checksum_failure_surfaces_at_replay() {
845 let (collector, manager) = spill_with_rows(4211, 20);
846 corrupt_run_bodies(&manager, 57);
849 let err = collector.into_rows().unwrap_err();
850 assert!(
851 err.to_string().contains("checksum mismatch"),
852 "unexpected error: {}",
853 err
854 );
855 }
856
857 #[test]
858 fn spilled_fingerprint_failure_surfaces_at_replay() {
859 let (collector, manager) = spill_with_rows(4212, 20);
860 corrupt_run_headers(&manager, 8);
864 repair_run_header_checksums(&manager);
865 let err = collector.into_rows().unwrap_err();
866 assert!(
867 err.to_string().contains("fingerprint mismatch"),
868 "unexpected error: {}",
869 err
870 );
871 }
872}