use std::error::Error;
use std::fs::File;
use std::io::{self, Write};
use std::sync::Arc;
use arrow::array::{ArrayRef, Int32Builder, StringBuilder};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::record_batch::RecordBatch;
use csv::WriterBuilder;
use parquet::arrow::arrow_writer::ArrowWriter;
use parquet::file::properties::WriterProperties;
use crate::cli::MANDATORY_HEADERS;
use crate::record::DataRecord;
enum WriterImpl {
Csv(csv::Writer<Box<dyn Write>>),
Delimited(Box<dyn Write>, String),
Parquet {
writer: ArrowWriter<File>,
schema: SchemaRef,
buffer: Vec<DataRecord>,
batch_size: usize,
},
}
pub struct OutputWriter {
writer_impl: WriterImpl,
sorted_tags: Vec<String>,
}
impl OutputWriter {
pub fn new(
output_path: Option<String>,
format: crate::cli::OutputFormat,
custom_delimiter: String,
no_quotes: bool,
sorted_tags: &[String],
) -> Result<Self, Box<dyn Error>> {
let writer_impl = match format {
crate::cli::OutputFormat::Csv => {
let writer: Box<dyn Write> = match &output_path {
Some(path) => Box::new(File::create(path)?),
None => Box::new(io::BufWriter::new(io::stdout().lock())),
};
let mut builder = WriterBuilder::new();
builder.delimiter(b',');
if no_quotes {
builder.quote_style(csv::QuoteStyle::Never);
}
WriterImpl::Csv(builder.from_writer(writer))
}
crate::cli::OutputFormat::Tsv
| crate::cli::OutputFormat::Psv
| crate::cli::OutputFormat::Custom => {
let writer: Box<dyn Write> = match &output_path {
Some(path) => Box::new(File::create(path)?),
None => Box::new(io::BufWriter::new(io::stdout().lock())),
};
let delimiter = match format {
crate::cli::OutputFormat::Tsv => "\t".to_string(),
crate::cli::OutputFormat::Psv => "|".to_string(),
_ => custom_delimiter,
};
WriterImpl::Delimited(writer, delimiter)
}
crate::cli::OutputFormat::Parquet => {
let path = output_path.ok_or("Parquet output requires a file path.")?;
let file = File::create(path)?;
let mut fields: Vec<Field> = MANDATORY_HEADERS
.iter()
.map(|(name, dtype)| Field::new(*name, dtype.clone(), true))
.collect();
fields.extend(
sorted_tags
.iter()
.map(|tag| Field::new(tag, DataType::Utf8, true)),
);
let schema = Arc::new(Schema::new(fields));
let props = WriterProperties::builder().build();
let writer = ArrowWriter::try_new(file, schema.clone(), Some(props))?;
WriterImpl::Parquet {
writer,
schema,
buffer: Vec::with_capacity(1024),
batch_size: 1024,
}
}
};
Ok(OutputWriter {
writer_impl,
sorted_tags: sorted_tags.to_vec(),
})
}
pub fn write_header(&mut self) -> Result<(), Box<dyn Error>> {
let mandatory_names: Vec<String> = MANDATORY_HEADERS
.iter()
.map(|(name, _)| name.to_string())
.collect();
match &mut self.writer_impl {
WriterImpl::Csv(csv_writer) => {
let mut record = mandatory_names.clone();
record.extend(self.sorted_tags.iter().cloned());
csv_writer.write_record(&record)?;
}
WriterImpl::Delimited(writer, delimiter) => {
let mut full_header = mandatory_names.join(delimiter);
if !self.sorted_tags.is_empty() {
full_header.push_str(delimiter);
full_header.push_str(&self.sorted_tags.join(delimiter));
}
writeln!(writer, "{full_header}")?;
}
WriterImpl::Parquet { .. } => {
}
}
Ok(())
}
pub fn write_record(&mut self, record: &DataRecord) -> Result<(), Box<dyn Error>> {
match &mut self.writer_impl {
WriterImpl::Csv(csv_writer) => {
let mut values: Vec<String> = record.mandatory_fields.clone();
values.extend(
self.sorted_tags
.iter()
.map(|tag| record.optional_fields.get(tag).cloned().unwrap_or_default()),
);
csv_writer.write_record(&values)?
}
WriterImpl::Delimited(writer, delimiter) => {
let mut values: Vec<String> = record.mandatory_fields.clone();
values.extend(
self.sorted_tags
.iter()
.map(|tag| record.optional_fields.get(tag).cloned().unwrap_or_default()),
);
writeln!(writer, "{}", values.join(delimiter))?
}
WriterImpl::Parquet {
buffer, batch_size, ..
} => {
buffer.push(record.clone());
if buffer.len() >= *batch_size {
self.flush_parquet_buffer()?;
}
}
}
Ok(())
}
fn flush_parquet_buffer(&mut self) -> Result<(), Box<dyn Error>> {
let WriterImpl::Parquet {
writer,
schema,
buffer,
..
} = &mut self.writer_impl
else {
return Ok(());
};
if buffer.is_empty() {
return Ok(());
}
let mut columns: Vec<ArrayRef> = Vec::with_capacity(schema.fields().len());
for (i, (_, dtype)) in MANDATORY_HEADERS.iter().enumerate() {
match dtype {
DataType::Int32 => {
let mut builder = Int32Builder::new();
for rec in buffer.iter() {
let text = rec
.mandatory_fields
.get(i)
.map(String::as_str)
.unwrap_or("");
if text.is_empty() {
builder.append_null();
} else {
match text.parse::<i32>() {
Ok(value) => builder.append_value(value),
Err(_) => builder.append_null(),
}
}
}
columns.push(Arc::new(builder.finish()));
}
DataType::Utf8 => {
let mut builder = StringBuilder::new();
for rec in buffer.iter() {
let text = rec
.mandatory_fields
.get(i)
.map(String::as_str)
.unwrap_or("");
if text.is_empty() {
builder.append_null();
} else {
builder.append_value(text);
}
}
columns.push(Arc::new(builder.finish()));
}
other => {
let mut builder = StringBuilder::new();
for rec in buffer.iter() {
let text = rec
.mandatory_fields
.get(i)
.map(String::as_str)
.unwrap_or("");
if text.is_empty() {
builder.append_null();
} else {
builder.append_value(text);
}
}
columns.push(Arc::new(builder.finish()));
eprintln!(
"Warning: unsupported mandatory data type in schema: {:?}",
other
);
}
}
}
for tag in &self.sorted_tags {
let mut builder = StringBuilder::new();
for rec in buffer.iter() {
match rec.optional_fields.get(tag).map(String::as_str) {
Some(value) if !value.is_empty() => builder.append_value(value),
_ => builder.append_null(),
}
}
columns.push(Arc::new(builder.finish()));
}
let batch = RecordBatch::try_new(schema.clone(), columns)?;
writer.write(&batch)?;
buffer.clear();
Ok(())
}
pub fn flush(mut self) -> Result<(), Box<dyn Error>> {
self.flush_parquet_buffer()?;
match self.writer_impl {
WriterImpl::Csv(mut csv_writer) => csv_writer.flush()?,
WriterImpl::Delimited(mut writer, _) => writer.flush()?,
WriterImpl::Parquet { writer, .. } => {
writer.close()?;
}
}
Ok(())
}
}