use std::{
fs::{self, File, OpenOptions},
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
};
static FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct SpillFile {
path: PathBuf,
writer: Option<BufWriter<File>>,
reader: Option<BufReader<File>>,
bytes_written: usize,
created: bool,
}
impl SpillFile {
pub fn new(temp_dir: &Path) -> io::Result<Self> {
fs::create_dir_all(temp_dir)?;
let id = FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let filename = format!("vibesql_spill_{}_{}.tmp", pid, id);
let path = temp_dir.join(filename);
Ok(Self { path, writer: None, reader: None, bytes_written: 0, created: false })
}
pub fn with_suffix(temp_dir: &Path, suffix: &str) -> io::Result<Self> {
fs::create_dir_all(temp_dir)?;
let id = FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let filename = format!("vibesql_spill_{}_{}_{}.tmp", pid, id, suffix);
let path = temp_dir.join(filename);
Ok(Self { path, writer: None, reader: None, bytes_written: 0, created: false })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn bytes_written(&self) -> usize {
self.bytes_written
}
pub fn is_created(&self) -> bool {
self.created
}
fn ensure_writer(&mut self) -> io::Result<&mut BufWriter<File>> {
if self.writer.is_none() {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&self.path)?;
self.writer = Some(BufWriter::with_capacity(64 * 1024, file)); self.created = true;
}
Ok(self.writer.as_mut().unwrap())
}
pub fn write_all(&mut self, data: &[u8]) -> io::Result<()> {
let writer = self.ensure_writer()?;
writer.write_all(data)?;
self.bytes_written += data.len();
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
if let Some(writer) = self.writer.as_mut() {
writer.flush()?;
}
Ok(())
}
pub fn prepare_for_read(&mut self) -> io::Result<()> {
if let Some(mut writer) = self.writer.take() {
writer.flush()?;
}
if self.created {
let file = File::open(&self.path)?;
self.reader = Some(BufReader::with_capacity(64 * 1024, file)); }
Ok(())
}
pub fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
if let Some(reader) = self.reader.as_mut() {
reader.seek(pos)
} else if let Some(writer) = self.writer.as_mut() {
writer.seek(pos)
} else {
Ok(0)
}
}
pub fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.reader.is_none() {
self.prepare_for_read()?;
}
if let Some(reader) = self.reader.as_mut() {
reader.read(buf)
} else {
Ok(0)
}
}
pub fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
if self.reader.is_none() {
self.prepare_for_read()?;
}
if let Some(reader) = self.reader.as_mut() {
reader.read_exact(buf)
} else {
Err(io::Error::new(io::ErrorKind::UnexpectedEof, "spill file not created"))
}
}
pub fn read_to_vec(&mut self) -> io::Result<Vec<u8>> {
self.seek(SeekFrom::Start(0))?;
let mut data = Vec::with_capacity(self.bytes_written);
if let Some(reader) = self.reader.as_mut() {
reader.read_to_end(&mut data)?;
}
Ok(data)
}
fn delete(&mut self) {
self.writer = None;
self.reader = None;
if self.created {
let _ = fs::remove_file(&self.path);
}
}
}
impl Drop for SpillFile {
fn drop(&mut self) {
self.delete();
}
}
pub struct SpillFileSet {
temp_dir: PathBuf,
files: Vec<SpillFile>,
total_bytes: usize,
}
impl SpillFileSet {
pub fn new(temp_dir: PathBuf) -> Self {
Self { temp_dir, files: Vec::new(), total_bytes: 0 }
}
pub fn create_file(&mut self) -> io::Result<&mut SpillFile> {
let file = SpillFile::new(&self.temp_dir)?;
self.files.push(file);
Ok(self.files.last_mut().unwrap())
}
pub fn create_file_with_suffix(&mut self, suffix: &str) -> io::Result<&mut SpillFile> {
let file = SpillFile::with_suffix(&self.temp_dir, suffix)?;
self.files.push(file);
Ok(self.files.last_mut().unwrap())
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn total_bytes(&self) -> usize {
self.files.iter().map(|f| f.bytes_written()).sum()
}
pub fn files(&self) -> &[SpillFile] {
&self.files
}
pub fn files_mut(&mut self) -> &mut [SpillFile] {
&mut self.files
}
pub fn into_files(self) -> Vec<SpillFile> {
self.files
}
pub fn prepare_all_for_read(&mut self) -> io::Result<()> {
for file in &mut self.files {
file.prepare_for_read()?;
}
Ok(())
}
pub fn clear(&mut self) {
self.files.clear();
self.total_bytes = 0;
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn test_spill_file_create_and_write() {
let temp = TempDir::new().unwrap();
let mut file = SpillFile::new(temp.path()).unwrap();
assert!(!file.is_created());
assert_eq!(file.bytes_written(), 0);
file.write_all(b"hello world").unwrap();
file.flush().unwrap();
assert!(file.is_created());
assert_eq!(file.bytes_written(), 11);
assert!(file.path().exists());
}
#[test]
fn test_spill_file_read_back() {
let temp = TempDir::new().unwrap();
let mut file = SpillFile::new(temp.path()).unwrap();
let test_data = b"test data 12345";
file.write_all(test_data).unwrap();
file.flush().unwrap();
file.prepare_for_read().unwrap();
let read_data = file.read_to_vec().unwrap();
assert_eq!(read_data, test_data);
}
#[test]
fn test_spill_file_auto_delete() {
let temp = TempDir::new().unwrap();
let path;
{
let mut file = SpillFile::new(temp.path()).unwrap();
file.write_all(b"data").unwrap();
file.flush().unwrap();
path = file.path().to_path_buf();
assert!(path.exists());
}
assert!(!path.exists());
}
#[test]
fn test_spill_file_with_suffix() {
let temp = TempDir::new().unwrap();
let file = SpillFile::with_suffix(temp.path(), "sort_run").unwrap();
assert!(file.path().to_string_lossy().contains("sort_run"));
}
#[test]
fn test_spill_file_set() {
let temp = TempDir::new().unwrap();
let mut set = SpillFileSet::new(temp.path().to_path_buf());
assert!(set.is_empty());
{
let file = set.create_file().unwrap();
file.write_all(b"run1").unwrap();
}
{
let file = set.create_file().unwrap();
file.write_all(b"run22").unwrap();
}
assert_eq!(set.len(), 2);
assert_eq!(set.total_bytes(), 9);
set.clear();
assert!(set.is_empty());
}
#[test]
fn test_spill_file_sequential_read() {
let temp = TempDir::new().unwrap();
let mut file = SpillFile::new(temp.path()).unwrap();
file.write_all(b"chunk1").unwrap();
file.write_all(b"chunk2").unwrap();
file.write_all(b"chunk3").unwrap();
file.flush().unwrap();
file.prepare_for_read().unwrap();
let mut buf = [0u8; 6];
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"chunk1");
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"chunk2");
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"chunk3");
}
#[test]
fn test_spill_file_seek() {
let temp = TempDir::new().unwrap();
let mut file = SpillFile::new(temp.path()).unwrap();
file.write_all(b"0123456789").unwrap();
file.flush().unwrap();
file.prepare_for_read().unwrap();
file.seek(SeekFrom::Start(5)).unwrap();
let mut buf = [0u8; 5];
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"56789");
}
}