Skip to main content

akar_storage/
spiller.rs

1//! Spiller — disk spilling and stream-merge for memory-constrained batch ingestion.
2//!
3//! When a `ColumnChunk` or `NodeGroup` exceeds the configured memory threshold
4//! during `COPY FROM` or bulk inserts, the spiller serializes the in-memory data
5//! to a temporary file. Once all rows are ingested, a multi-way stream-merge
6//! reads back all spill files plus the final in-memory buffer, deduplicates by
7//! primary key, and writes the merged result to the persistent `Column` via
8//! `BufferManager`.
9//!
10//! # Strategy
11//!
12//! This is a simple `Vec<Value>` → JSON-lines → disk approach. Each spill file
13//! contains one JSON object per row. This is intentionally not Arrow-CSR format
14//! — that can be a future optimization.
15
16use 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/// Metadata for a single spill file on disk.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct SpillFile {
29    /// Absolute path to the temporary spill file.
30    pub path: PathBuf,
31    /// Number of rows in this spill file.
32    pub row_count: usize,
33    /// Optional sort key column index (for PK-ordered merge).
34    pub sort_key_column: Option<usize>,
35}
36
37/// The spiller manages temporary files for memory-constrained batch operations.
38///
39/// Each `spill()` call serializes a `ColumnChunk` to a new temp file and clears
40/// the chunk's in-memory buffer. Spill files are named `spill_00001.jsonl` etc.
41#[derive(Debug)]
42pub struct Spiller {
43    /// Directory where spill files are written.
44    tmp_dir: PathBuf,
45    /// Monotonically increasing counter for unique spill file names.
46    spill_counter: AtomicU64,
47    /// Maximum in-memory bytes per ColumnChunk before spilling triggers.
48    /// When a chunk's estimated memory exceeds this, `spill()` is called.
49    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    /// Create a new spiller that writes to `tmp_dir`.
64    ///
65    /// `memory_threshold` is the estimated byte limit for a `ColumnChunk`
66    /// before triggering a spill. Use 0 to disable spilling entirely.
67    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    /// Return the next unique spill file path.
78    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    /// Estimate the in-memory size of a `ColumnChunk`'s buffered values.
84    ///
85    /// This is a rough estimate: each `Value` variant has different sizes.
86    /// We use a conservative estimate of 64 bytes per value on average.
87    pub fn estimated_chunk_size(chunk: &ColumnChunk) -> u64 {
88        chunk.num_values() as u64 * 64
89    }
90
91    /// Spill a `ColumnChunk` to disk: serialize its values to a JSON-lines file
92    /// and clear the in-memory buffer.
93    ///
94    /// Returns the `SpillFile` metadata on success.
95    ///
96    /// If the chunk is empty, returns `None`.
97    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        // Write as JSON-lines: one line per row, each line is a JSON array of column values
107        // For a single-column chunk, each line is just one value.
108        // Multi-column chunks are handled by the caller (NodeGroup) which spills
109        // all columns together in a coordinated way.
110        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        // Re-allocate the chunk's buffer
120        let _ = chunk; // chunk is already drained by `drain()`
121
122        Ok(Some(SpillFile {
123            path,
124            row_count,
125            sort_key_column: None,
126        }))
127    }
128
129    /// Spill all columns of a NodeGroup-style set of column chunks together.
130    ///
131    /// Each row is serialized as a JSON array `[col0, col1, ..., colN]`.
132    /// This is used by `NodeGroup` to spill multi-column data.
133    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        // Drain all columns
143        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    /// Restore a single-column `ColumnChunk` from a spill file.
171    ///
172    /// Each line of the JSON-lines file is deserialized as a single `Value`.
173    /// Returns a new `ColumnChunk` with the restored values.
174    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    /// Restore a multi-column result from a spill file containing JSON arrays.
196    ///
197    /// Returns one `ColumnChunk` per column.
198    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    /// Remove a spill file from disk.
224    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    /// Remove all spill files in the temp directory.
230    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    /// Check whether a chunk's estimated size exceeds the memory threshold.
239    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        // Best-effort cleanup of the temp directory
250        let _ = fs::remove_dir_all(&self.tmp_dir);
251    }
252}
253
254// ---------------------------------------------------------------------------
255// Multi-way stream-merge
256// ---------------------------------------------------------------------------
257
258/// A file handle paired with the next buffered row, for streaming merge.
259struct MergeCursor {
260    /// Source spill file path (for logging).
261    _source: PathBuf,
262    /// Reader over the JSON-lines file.
263    reader: BufReader<fs::File>,
264    /// The next buffered row (None if exhausted).
265    current: Option<Vec<Value>>,
266    /// Column index to use as sort key (for ordering).
267    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, // EOF
290                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                            // Try as single value (single-column spill files)
299                            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
340/// Multi-way streaming merge of multiple spill files.
341///
342/// Reads N spill files + one optional in-memory buffer, merges them in
343/// sort-key order (ascending), and optionally deduplicates by primary key.
344pub struct MultiWayStreamMerge {
345    cursors: Vec<MergeCursor>,
346    /// The in-memory buffer (last "run" to merge).
347    in_memory_rows: Vec<Vec<Value>>,
348    in_memory_idx: usize,
349    sort_key_col: usize,
350    dedup: bool,
351    /// Last emitted sort key (for dedup).
352    last_key: Option<i64>,
353}
354
355impl MultiWayStreamMerge {
356    /// Create a new multi-way stream merge.
357    ///
358    /// * `spill_files` — list of spill files to read from disk.
359    /// * `in_memory` — optional final in-memory buffer (rows as `Vec<Vec<Value>>`).
360    /// * `sort_key_col` — column index to use as the sort/merge key.
361    /// * `dedup` — if true, consecutive duplicate sort keys are skipped.
362    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    /// Get the next row from the merge.
385    ///
386    /// Returns `None` when all sources are exhausted.
387    pub fn next_tuple(&mut self) -> Option<Vec<Value>> {
388        loop {
389            // Find the cursor with the smallest sort key
390            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            // Dedup: skip if same sort key as last emitted row
410            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                        // Duplicate — skip and continue
430                        continue;
431                    }
432                    self.last_key = Some(k);
433                }
434            }
435
436            return Some(row);
437        }
438    }
439
440    /// Find the source with the smallest sort key among all cursors and the in-memory buffer.
441    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        // Check in-memory buffer
462        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// ---------------------------------------------------------------------------
504// Tests
505// ---------------------------------------------------------------------------
506
507#[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); // Very low threshold
562
563        let mut chunk = ColumnChunk::new();
564        // 100 values * 64 bytes = 6400 bytes → should spill
565        for i in 0..100 {
566            chunk.append(Value::Int64(i));
567        }
568        assert!(spiller.should_spill(&chunk));
569
570        // Disabled spilling
571        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        // Create 3 spill files with sorted data
590        let mut files = Vec::new();
591
592        // File 1: values [1, 4, 7]
593        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        // File 2: values [2, 5, 8]
600        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        // File 3: values [3, 6, 9]
607        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        // Merge with no dedup
614        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        // Verify sorted order
622        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        // File 1: [1, 2, 3]
635        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        // File 2: [2, 3, 4] (overlaps with file 1)
642        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        // Merge WITH dedup
649        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        // Spill file: [1, 3, 5]
668        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        // In-memory: [2, 4, 6]
675        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        // After Spiller is dropped, the temp directory should be cleaned up
715        assert!(!spill_dir.exists());
716    }
717}