1use crate::column_chunk::ColumnChunk;
17use akar_common::error::StorageError;
18use akar_common::types::Value;
19use serde::{Deserialize, Serialize};
20use serde_json;
21use std::fs;
22use std::io::{BufRead, BufReader, Write};
23use std::path::{Path, PathBuf};
24use std::sync::atomic::{AtomicU64, Ordering};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct SpillFile {
29 pub path: PathBuf,
31 pub row_count: usize,
33 pub sort_key_column: Option<usize>,
35}
36
37#[derive(Debug)]
42pub struct Spiller {
43 tmp_dir: PathBuf,
45 spill_counter: AtomicU64,
47 pub memory_threshold: u64,
50}
51
52impl Clone for Spiller {
53 fn clone(&self) -> Self {
54 Self {
55 tmp_dir: self.tmp_dir.clone(),
56 spill_counter: AtomicU64::new(self.spill_counter.load(Ordering::Relaxed)),
57 memory_threshold: self.memory_threshold,
58 }
59 }
60}
61
62impl Spiller {
63 pub fn new(tmp_dir: impl Into<PathBuf>, memory_threshold: u64) -> Self {
68 let tmp_dir = tmp_dir.into();
69 let _ = fs::create_dir_all(&tmp_dir);
70 Self {
71 tmp_dir,
72 spill_counter: AtomicU64::new(1),
73 memory_threshold,
74 }
75 }
76
77 fn next_spill_path(&self) -> PathBuf {
79 let n = self.spill_counter.fetch_add(1, Ordering::Relaxed);
80 self.tmp_dir.join(format!("spill_{n:05}.jsonl"))
81 }
82
83 pub fn estimated_chunk_size(chunk: &ColumnChunk) -> u64 {
88 chunk.num_values() as u64 * 64
89 }
90
91 pub fn spill(&self, chunk: &mut ColumnChunk) -> Result<Option<SpillFile>, StorageError> {
98 if chunk.is_empty() {
99 return Ok(None);
100 }
101
102 let path = self.next_spill_path();
103 let values: Vec<Value> = chunk.drain();
104 let row_count = values.len();
105
106 let mut file = fs::File::create(&path)
111 .map_err(|e| StorageError::Spiller(format!("Failed to create spill file {:?}: {e}", path)))?;
112
113 for value in &values {
114 let line = serde_json::to_string(value)
115 .map_err(|e| StorageError::Spiller(format!("Failed to serialize value: {e}")))?;
116 writeln!(file, "{line}").map_err(|e| StorageError::Spiller(format!("Failed to write spill file: {e}")))?;
117 }
118
119 let _ = chunk; Ok(Some(SpillFile {
123 path,
124 row_count,
125 sort_key_column: None,
126 }))
127 }
128
129 pub fn spill_columns(&self, chunks: &mut [ColumnChunk]) -> Result<Option<SpillFile>, StorageError> {
134 if chunks.is_empty() || chunks[0].is_empty() {
135 return Ok(None);
136 }
137
138 let path = self.next_spill_path();
139 let num_rows = chunks[0].num_values();
140 let num_cols = chunks.len();
141
142 let mut drained: Vec<Vec<Value>> = chunks.iter_mut().map(|c| c.drain()).collect();
144
145 let mut file = fs::File::create(&path)
146 .map_err(|e| StorageError::Spiller(format!("Failed to create spill file {:?}: {e}", path)))?;
147
148 for row in 0..num_rows {
149 let mut row_values = Vec::with_capacity(num_cols);
150 for col in 0..num_cols {
151 let val = if row < drained[col].len() {
152 std::mem::replace(&mut drained[col][row], Value::Null)
153 } else {
154 Value::Null
155 };
156 row_values.push(val);
157 }
158 let line = serde_json::to_string(&row_values)
159 .map_err(|e| StorageError::Spiller(format!("Failed to serialize row: {e}")))?;
160 writeln!(file, "{line}").map_err(|e| StorageError::Spiller(format!("Failed to write spill file: {e}")))?;
161 }
162
163 Ok(Some(SpillFile {
164 path,
165 row_count: num_rows,
166 sort_key_column: None,
167 }))
168 }
169
170 pub fn restore(&self, spill: &SpillFile) -> Result<ColumnChunk, StorageError> {
175 let file = fs::File::open(&spill.path)
176 .map_err(|e| StorageError::Spiller(format!("Failed to open spill file {:?}: {e}", spill.path)))?;
177 let reader = BufReader::new(file);
178 let mut values = Vec::with_capacity(spill.row_count);
179
180 for line in reader.lines() {
181 let line = line.map_err(|e| StorageError::Spiller(format!("Failed to read spill file: {e}")))?;
182 let line = line.trim().to_string();
183 if line.is_empty() {
184 continue;
185 }
186 let value: Value = serde_json::from_str(&line)
187 .map_err(|e| StorageError::Spiller(format!("Failed to deserialize value from spill file: {e}")))?;
188 values.push(value);
189 }
190
191 let chunk = ColumnChunk::from(values);
192 Ok(chunk)
193 }
194
195 pub fn restore_columns(&self, spill: &SpillFile, num_cols: usize) -> Result<Vec<ColumnChunk>, StorageError> {
199 let file = fs::File::open(&spill.path)
200 .map_err(|e| StorageError::Spiller(format!("Failed to open spill file {:?}: {e}", spill.path)))?;
201 let reader = BufReader::new(file);
202
203 let mut columns: Vec<Vec<Value>> = (0..num_cols).map(|_| Vec::new()).collect();
204
205 for line in reader.lines() {
206 let line = line.map_err(|e| StorageError::Spiller(format!("Failed to read spill file: {e}")))?;
207 let line = line.trim().to_string();
208 if line.is_empty() {
209 continue;
210 }
211 let row: Vec<Value> = serde_json::from_str(&line)
212 .map_err(|e| StorageError::Spiller(format!("Failed to deserialize row from spill file: {e}")))?;
213 for (col, value) in row.into_iter().enumerate() {
214 if col < num_cols {
215 columns[col].push(value);
216 }
217 }
218 }
219
220 Ok(columns.into_iter().map(ColumnChunk::from).collect())
221 }
222
223 pub fn cleanup(&self, spill: &SpillFile) -> Result<(), StorageError> {
225 fs::remove_file(&spill.path)
226 .map_err(|e| StorageError::Spiller(format!("Failed to remove spill file {:?}: {e}", spill.path)))
227 }
228
229 pub fn cleanup_all(&self) -> Result<(), StorageError> {
231 if self.tmp_dir.exists() {
232 fs::remove_dir_all(&self.tmp_dir)
233 .map_err(|e| StorageError::Spiller(format!("Failed to remove spill dir {:?}: {e}", self.tmp_dir)))?;
234 }
235 Ok(())
236 }
237
238 pub fn should_spill(&self, chunk: &ColumnChunk) -> bool {
240 if self.memory_threshold == 0 {
241 return false;
242 }
243 Self::estimated_chunk_size(chunk) > self.memory_threshold
244 }
245}
246
247impl Drop for Spiller {
248 fn drop(&mut self) {
249 let _ = fs::remove_dir_all(&self.tmp_dir);
251 }
252}
253
254struct MergeCursor {
260 _source: PathBuf,
262 reader: BufReader<fs::File>,
264 current: Option<Vec<Value>>,
266 sort_key_col: usize,
268}
269
270impl MergeCursor {
271 fn new(path: &Path, sort_key_col: usize) -> Result<Self, StorageError> {
272 let file = fs::File::open(path)
273 .map_err(|e| StorageError::Spiller(format!("Failed to open merge source {:?}: {e}", path)))?;
274 let mut reader = BufReader::new(file);
275 let current = Self::read_next_row(&mut reader);
276 Ok(Self {
277 _source: path.to_path_buf(),
278 reader,
279 current,
280 sort_key_col,
281 })
282 }
283
284 fn read_next_row(reader: &mut BufReader<fs::File>) -> Option<Vec<Value>> {
285 let mut line = String::new();
286 loop {
287 line.clear();
288 match reader.read_line(&mut line) {
289 Ok(0) => return None, Ok(_) => {
291 let trimmed = line.trim();
292 if trimmed.is_empty() {
293 continue;
294 }
295 match serde_json::from_str::<Vec<Value>>(trimmed) {
296 Ok(row) => return Some(row),
297 Err(_) => {
298 match serde_json::from_str::<Value>(trimmed) {
300 Ok(val) => return Some(vec![val]),
301 Err(_) => continue,
302 }
303 }
304 }
305 }
306 Err(_) => return None,
307 }
308 }
309 }
310
311 fn advance(&mut self) {
312 self.current = Self::read_next_row(&mut self.reader);
313 }
314
315 fn is_exhausted(&self) -> bool {
316 self.current.is_none()
317 }
318
319 fn sort_key(&self) -> Option<i64> {
320 self.current.as_ref().and_then(|row| {
321 if self.sort_key_col < row.len() {
322 match &row[self.sort_key_col] {
323 Value::Int64(v) => Some(*v),
324 Value::Int32(v) => Some(*v as i64),
325 Value::Int16(v) => Some(*v as i64),
326 Value::Int8(v) => Some(*v as i64),
327 Value::UInt64(v) => Some(*v as i64),
328 Value::UInt32(v) => Some(*v as i64),
329 Value::UInt16(v) => Some(*v as i64),
330 Value::UInt8(v) => Some(*v as i64),
331 _ => None,
332 }
333 } else {
334 None
335 }
336 })
337 }
338}
339
340pub struct MultiWayStreamMerge {
345 cursors: Vec<MergeCursor>,
346 in_memory_rows: Vec<Vec<Value>>,
348 in_memory_idx: usize,
349 sort_key_col: usize,
350 dedup: bool,
351 last_key: Option<i64>,
353}
354
355impl MultiWayStreamMerge {
356 pub fn new(
363 spill_files: &[SpillFile],
364 in_memory: Option<Vec<Vec<Value>>>,
365 sort_key_col: usize,
366 dedup: bool,
367 ) -> Result<Self, StorageError> {
368 let mut cursors = Vec::new();
369 for sf in spill_files {
370 if sf.row_count > 0 {
371 cursors.push(MergeCursor::new(&sf.path, sort_key_col)?);
372 }
373 }
374 Ok(Self {
375 cursors,
376 in_memory_rows: in_memory.unwrap_or_default(),
377 in_memory_idx: 0,
378 sort_key_col,
379 dedup,
380 last_key: None,
381 })
382 }
383
384 pub fn next_tuple(&mut self) -> Option<Vec<Value>> {
388 loop {
389 let smallest = self.find_smallest();
391 let row = match smallest {
392 MergeSource::Cursor(idx) => {
393 let row = self.cursors[idx].current.take()?;
394 self.cursors[idx].advance();
395 row
396 }
397 MergeSource::InMemory => {
398 if self.in_memory_idx < self.in_memory_rows.len() {
399 let row = self.in_memory_rows[self.in_memory_idx].clone();
400 self.in_memory_idx += 1;
401 row
402 } else {
403 return None;
404 }
405 }
406 MergeSource::None => return None,
407 };
408
409 if self.dedup {
411 let key = if self.sort_key_col < row.len() {
412 match &row[self.sort_key_col] {
413 Value::Int64(v) => Some(*v),
414 Value::Int32(v) => Some(*v as i64),
415 Value::Int16(v) => Some(*v as i64),
416 Value::Int8(v) => Some(*v as i64),
417 Value::UInt64(v) => Some(*v as i64),
418 Value::UInt32(v) => Some(*v as i64),
419 Value::UInt16(v) => Some(*v as i64),
420 Value::UInt8(v) => Some(*v as i64),
421 _ => None,
422 }
423 } else {
424 None
425 };
426
427 if let Some(k) = key {
428 if self.last_key == Some(k) {
429 continue;
431 }
432 self.last_key = Some(k);
433 }
434 }
435
436 return Some(row);
437 }
438 }
439
440 fn find_smallest(&self) -> MergeSource {
442 let mut best: Option<(MergeSource, i64)> = None;
443
444 for (idx, cursor) in self.cursors.iter().enumerate() {
445 if cursor.is_exhausted() {
446 continue;
447 }
448 if let Some(key) = cursor.sort_key() {
449 match best {
450 Some((_, best_key)) if key < best_key => {
451 best = Some((MergeSource::Cursor(idx), key));
452 }
453 None => {
454 best = Some((MergeSource::Cursor(idx), key));
455 }
456 _ => {}
457 }
458 }
459 }
460
461 if self.in_memory_idx < self.in_memory_rows.len() {
463 let row = &self.in_memory_rows[self.in_memory_idx];
464 let key = if self.sort_key_col < row.len() {
465 match &row[self.sort_key_col] {
466 Value::Int64(v) => Some(*v),
467 Value::Int32(v) => Some(*v as i64),
468 Value::Int16(v) => Some(*v as i64),
469 Value::Int8(v) => Some(*v as i64),
470 Value::UInt64(v) => Some(*v as i64),
471 Value::UInt32(v) => Some(*v as i64),
472 Value::UInt16(v) => Some(*v as i64),
473 Value::UInt8(v) => Some(*v as i64),
474 _ => None,
475 }
476 } else {
477 None
478 };
479
480 if let Some(k) = key {
481 match best {
482 Some((_, best_key)) if k < best_key => {
483 best = Some((MergeSource::InMemory, k));
484 }
485 None => {
486 best = Some((MergeSource::InMemory, k));
487 }
488 _ => {}
489 }
490 }
491 }
492
493 best.map_or(MergeSource::None, |(src, _)| src)
494 }
495}
496
497enum MergeSource {
498 Cursor(usize),
499 InMemory,
500 None,
501}
502
503#[cfg(test)]
508mod tests {
509 use super::*;
510 #[test]
511 fn test_spill_and_restore_single_column() {
512 let tmp = tempfile::tempdir().unwrap();
513 let spiller = Spiller::new(tmp.path(), 1024);
514
515 let mut chunk = ColumnChunk::new();
516 for i in 0..10 {
517 chunk.append(Value::Int64(i));
518 }
519 assert_eq!(chunk.num_values(), 10);
520
521 let spill = spiller.spill(&mut chunk).unwrap().unwrap();
522 assert!(chunk.is_empty());
523 assert_eq!(spill.row_count, 10);
524 assert!(spill.path.exists());
525
526 let restored = spiller.restore(&spill).unwrap();
527 assert_eq!(restored.num_values(), 10);
528 assert_eq!(restored.get(0), Some(&Value::Int64(0)));
529 assert_eq!(restored.get(9), Some(&Value::Int64(9)));
530
531 spiller.cleanup(&spill).unwrap();
532 }
533
534 #[test]
535 fn test_spill_and_restore_columns() {
536 let tmp = tempfile::tempdir().unwrap();
537 let spiller = Spiller::new(tmp.path(), 1024);
538
539 let mut chunk0 = ColumnChunk::new();
540 let mut chunk1 = ColumnChunk::new();
541 for i in 0..5 {
542 chunk0.append(Value::Int64(i));
543 chunk1.append(Value::String(format!("val_{i}")));
544 }
545
546 let spill = spiller.spill_columns(&mut [chunk0, chunk1]).unwrap().unwrap();
547 assert_eq!(spill.row_count, 5);
548
549 let restored = spiller.restore_columns(&spill, 2).unwrap();
550 assert_eq!(restored.len(), 2);
551 assert_eq!(restored[0].num_values(), 5);
552 assert_eq!(restored[0].get(0), Some(&Value::Int64(0)));
553 assert_eq!(restored[1].get(4), Some(&Value::String("val_4".into())));
554
555 spiller.cleanup(&spill).unwrap();
556 }
557
558 #[test]
559 fn test_should_spill() {
560 let tmp = tempfile::tempdir().unwrap();
561 let spiller = Spiller::new(tmp.path(), 128); let mut chunk = ColumnChunk::new();
564 for i in 0..100 {
566 chunk.append(Value::Int64(i));
567 }
568 assert!(spiller.should_spill(&chunk));
569
570 let spiller_disabled = Spiller::new(tmp.path(), 0);
572 assert!(!spiller_disabled.should_spill(&chunk));
573 }
574
575 #[test]
576 fn test_spill_empty_chunk() {
577 let tmp = tempfile::tempdir().unwrap();
578 let spiller = Spiller::new(tmp.path(), 1024);
579 let mut chunk = ColumnChunk::new();
580 let result = spiller.spill(&mut chunk).unwrap();
581 assert!(result.is_none());
582 }
583
584 #[test]
585 fn test_multi_way_merge() {
586 let tmp = tempfile::tempdir().unwrap();
587 let spiller = Spiller::new(tmp.path(), 1024);
588
589 let mut files = Vec::new();
591
592 let mut c1 = ColumnChunk::new();
594 for v in [1i64, 4, 7] {
595 c1.append(Value::Int64(v));
596 }
597 files.push(spiller.spill(&mut c1).unwrap().unwrap());
598
599 let mut c2 = ColumnChunk::new();
601 for v in [2i64, 5, 8] {
602 c2.append(Value::Int64(v));
603 }
604 files.push(spiller.spill(&mut c2).unwrap().unwrap());
605
606 let mut c3 = ColumnChunk::new();
608 for v in [3i64, 6, 9] {
609 c3.append(Value::Int64(v));
610 }
611 files.push(spiller.spill(&mut c3).unwrap().unwrap());
612
613 let mut merger = MultiWayStreamMerge::new(&files, None, 0, false).unwrap();
615 let mut merged = Vec::new();
616 while let Some(row) = merger.next_tuple() {
617 merged.push(row[0].clone());
618 }
619
620 assert_eq!(merged.len(), 9);
621 for i in 0..9 {
623 assert_eq!(merged[i], Value::Int64((i + 1) as i64), "Position {i}");
624 }
625 }
626
627 #[test]
628 fn test_multi_way_merge_with_dedup() {
629 let tmp = tempfile::tempdir().unwrap();
630 let spiller = Spiller::new(tmp.path(), 1024);
631
632 let mut files = Vec::new();
633
634 let mut c1 = ColumnChunk::new();
636 for v in [1i64, 2, 3] {
637 c1.append(Value::Int64(v));
638 }
639 files.push(spiller.spill(&mut c1).unwrap().unwrap());
640
641 let mut c2 = ColumnChunk::new();
643 for v in [2i64, 3, 4] {
644 c2.append(Value::Int64(v));
645 }
646 files.push(spiller.spill(&mut c2).unwrap().unwrap());
647
648 let mut merger = MultiWayStreamMerge::new(&files, None, 0, true).unwrap();
650 let mut merged = Vec::new();
651 while let Some(row) = merger.next_tuple() {
652 merged.push(row[0].clone());
653 }
654
655 assert_eq!(merged.len(), 4);
656 assert_eq!(merged[0], Value::Int64(1));
657 assert_eq!(merged[1], Value::Int64(2));
658 assert_eq!(merged[2], Value::Int64(3));
659 assert_eq!(merged[3], Value::Int64(4));
660 }
661
662 #[test]
663 fn test_merge_with_in_memory() {
664 let tmp = tempfile::tempdir().unwrap();
665 let spiller = Spiller::new(tmp.path(), 1024);
666
667 let mut c1 = ColumnChunk::new();
669 for v in [1i64, 3, 5] {
670 c1.append(Value::Int64(v));
671 }
672 let files = vec![spiller.spill(&mut c1).unwrap().unwrap()];
673
674 let in_memory: Vec<Vec<Value>> = vec![vec![Value::Int64(2)], vec![Value::Int64(4)], vec![Value::Int64(6)]];
676
677 let mut merger = MultiWayStreamMerge::new(&files, Some(in_memory), 0, false).unwrap();
678 let mut merged = Vec::new();
679 while let Some(row) = merger.next_tuple() {
680 merged.push(row[0].clone());
681 }
682
683 assert_eq!(merged.len(), 6);
684 for i in 0..6 {
685 assert_eq!(merged[i], Value::Int64((i + 1) as i64));
686 }
687 }
688
689 #[test]
690 fn test_cleanup_all() {
691 let tmp = tempfile::tempdir().unwrap();
692 let spiller = Spiller::new(tmp.path(), 1024);
693
694 let mut c = ColumnChunk::new();
695 c.append(Value::Int64(42));
696 let spill = spiller.spill(&mut c).unwrap().unwrap();
697 assert!(spill.path.exists());
698
699 spiller.cleanup_all().unwrap();
700 assert!(!spill.path.exists());
701 }
702
703 #[test]
704 fn test_cleanup_on_drop() {
705 let tmp = tempfile::tempdir().unwrap();
706 let spill_dir = tmp.path().join("spill_test");
707 {
708 let spiller = Spiller::new(&spill_dir, 1024);
709 let mut c = ColumnChunk::new();
710 c.append(Value::Int64(42));
711 spiller.spill(&mut c).unwrap();
712 assert!(spill_dir.exists());
713 }
714 assert!(!spill_dir.exists());
716 }
717}