use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicU64;
use minarrow::Table;
use crate::models::encoders::csv::CsvEncodeOptions;
use crate::models::writers::csv::CsvWriter;
use crate::traits::chunked_table_writer::ChunkedTableWriter;
#[derive(Debug)]
pub struct ChunkedCsvWriter {
dir: PathBuf,
base: String,
options: CsvEncodeOptions,
next_index: AtomicU64,
}
impl ChunkedCsvWriter {
pub fn new<P: AsRef<Path>>(dir: P, base: &str, options: CsvEncodeOptions) -> io::Result<Self> {
let dir = dir.as_ref().to_path_buf();
fs::create_dir_all(&dir)?;
Ok(Self {
dir,
base: base.to_string(),
options,
next_index: AtomicU64::new(0),
})
}
}
impl ChunkedTableWriter for ChunkedCsvWriter {
type Error = io::Error;
fn extension() -> &'static str {
"csv"
}
fn dir(&self) -> &Path {
&self.dir
}
fn base(&self) -> &str {
&self.base
}
fn counter(&self) -> &AtomicU64 {
&self.next_index
}
fn write_chunk_at(&self, path: &Path, table: &Table) -> io::Result<()> {
let file = File::create(path)?;
let mut writer = CsvWriter::with_options(file, self.options.clone());
writer.write_table(table)?;
writer.flush()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::readers::chunked::csv::{ChunkedCsvReadOptions, ChunkedCsvReader};
use crate::traits::chunked_table_reader::ChunkedTableReader;
use minarrow::{Table, fa_i32};
#[test]
fn writes_indexed_chunk_files() {
let dir = std::env::temp_dir().join("lightstream_chunked_csv_test_writer");
let _ = fs::remove_dir_all(&dir);
let mut w = ChunkedCsvWriter::new(&dir, "part", CsvEncodeOptions::default()).unwrap();
let p0 = w
.write_chunk(&Table::new("b", Some(vec![fa_i32!("n", 0, 1, 2)])))
.unwrap();
let p1 = w
.write_chunk(&Table::new(
"b",
Some(vec![fa_i32!("n", 10, 11, 12)]),
))
.unwrap();
let p2 = w
.write_chunk(&Table::new(
"b",
Some(vec![fa_i32!("n", 20, 21, 22)]),
))
.unwrap();
assert_eq!(p0.file_name().unwrap(), "part-0000000000.csv");
assert_eq!(p1.file_name().unwrap(), "part-0000000001.csv");
assert_eq!(p2.file_name().unwrap(), "part-0000000002.csv");
assert_eq!(w.batches_written(), 3);
let body = fs::read_to_string(&p1).unwrap();
assert!(body.starts_with("n\n"));
assert!(body.contains("10\n11\n12"));
fs::remove_dir_all(&dir).ok();
}
#[test]
fn par_write_all_emits_files_in_order_and_advances_counter() {
let dir = std::env::temp_dir().join("lightstream_chunked_csv_par_writer");
let _ = fs::remove_dir_all(&dir);
let w = ChunkedCsvWriter::new(&dir, "part", CsvEncodeOptions::default()).unwrap();
let tables: Vec<Table> = (0..10i32)
.map(|i| Table::new("b", Some(vec![fa_i32!("n", i, i + 100)])))
.collect();
let refs: Vec<&Table> = tables.iter().collect();
let paths = w.par_write_all(&refs, None).unwrap();
assert_eq!(paths.len(), 10);
for (i, p) in paths.iter().enumerate() {
assert_eq!(
p.file_name().unwrap(),
std::ffi::OsString::from(format!("part-{i:010}.csv"))
);
}
assert_eq!(w.batches_written(), 10);
let st =
ChunkedCsvReader::par_load_batched(&dir, "part", ChunkedCsvReadOptions::default(), None)
.unwrap();
assert_eq!(st.batches.len(), 10);
fs::remove_dir_all(&dir).ok();
}
}