use std::io;
use minarrow::{Field, Table, TableV, Vec64};
use crate::compression::Compression;
use crate::enums::IPCMessageProtocol;
use crate::models::writers::ipc::table_stream::TableStreamWriter;
use crate::traits::stream_buffer::StreamBuffer;
use crate::utils::dict_values;
pub struct SyncTableWriter<W, B = Vec64<u8>>
where
W: io::Write,
B: StreamBuffer + Unpin + 'static,
{
inner: TableStreamWriter<B>,
sink: W,
finished: bool,
}
impl<W, B> SyncTableWriter<W, B>
where
W: io::Write,
B: StreamBuffer + Unpin + 'static,
{
pub fn new(
sink: W,
schema: Vec<Field>,
protocol: IPCMessageProtocol,
compression: Option<Compression>,
) -> Self {
Self {
inner: TableStreamWriter::new(schema, protocol, compression),
sink,
finished: false,
}
}
pub fn schema(&self) -> &[Field] {
self.inner.schema()
}
pub fn register_dictionary(&mut self, dict_id: i64, values: Vec<String>) {
self.inner.register_dictionary(dict_id, values);
}
pub fn write_table(&mut self, table: impl Into<TableV>) -> io::Result<()> {
let view: TableV = table.into();
for (col_idx, col) in view.cols.iter().enumerate() {
if let Some(values) = dict_values(col.as_tuple_ref().0) {
self.inner.register_dictionary(col_idx as i64, values);
}
}
self.inner.write(&view)?;
self.drain_frames()
}
pub fn write_all_tables<I>(&mut self, tables: I) -> io::Result<()>
where
I: IntoIterator<Item = Table>,
{
for table in tables {
self.write_table(table)?;
}
self.finish()
}
pub fn finish(&mut self) -> io::Result<()> {
if self.finished {
return Ok(());
}
self.inner.finish()?;
self.drain_frames()?;
self.sink.flush()?;
self.finished = true;
Ok(())
}
pub fn into_inner(mut self) -> io::Result<W> {
self.finish()?;
Ok(self.sink)
}
fn drain_frames(&mut self) -> io::Result<()> {
while let Some(frame) = self.inner.next_frame() {
let buf = frame?;
self.sink.write_all(buf.as_ref())?;
}
Ok(())
}
}
pub fn write_tables_to_file_sync(
file_path: &std::path::Path,
tables: &[Table],
schema: Vec<Field>,
) -> io::Result<()> {
let file = std::fs::File::create(file_path)?;
let mut writer = SyncTableWriter::<_, Vec64<u8>>::new(file, schema, IPCMessageProtocol::File, None);
for table in tables {
writer.write_table(table.clone())?;
}
writer.finish()
}
#[cfg(test)]
mod tests {
use super::SyncTableWriter;
use super::write_tables_to_file_sync;
use crate::enums::IPCMessageProtocol;
use minarrow::{Field, Table, Vec64, fa_i32};
use tempfile::NamedTempFile;
#[test]
fn writes_arrow_file_round_trips_via_file_reader() {
use crate::models::readers::ipc::file_table::FileTableReader;
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let table = Table::new("t", Some(vec![fa_i32!["n", 1, 2, 3]]));
let schema: Vec<Field> = table.cols.iter().map(|c| (*c.field).clone()).collect();
write_tables_to_file_sync(&path, std::slice::from_ref(&table), schema).unwrap();
let reader = FileTableReader::open(&path).unwrap();
assert_eq!(reader.num_batches(), 1);
let got = reader.read_batch(0).unwrap();
assert_eq!(got.n_rows, 3);
}
#[cfg(feature = "datetime")]
#[test]
fn timestamp_columns_round_trip_with_unit_and_timezone() {
use minarrow::enums::time_units::TimeUnit;
use minarrow::{Array, ArrowType, DatetimeArray, FieldArray, NumericArray};
use crate::models::readers::ipc::file_table::FileTableReader;
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let ts = FieldArray::new(
Field::new(
"ts_event",
ArrowType::Timestamp(TimeUnit::Nanoseconds, Some("UTC".into())),
false,
None,
),
Array::from_datetime_i64(DatetimeArray::from_slice(
&[1_782_999_000_000_000_000, 1_782_999_000_000_000_001],
Some(TimeUnit::Nanoseconds),
)),
);
let table = Table::new("t", Some(vec![ts]));
let schema: Vec<Field> = table.cols.iter().map(|c| (*c.field).clone()).collect();
write_tables_to_file_sync(&path, std::slice::from_ref(&table), schema).unwrap();
let reader = FileTableReader::open(&path).unwrap();
let got = reader.read_batch(0).unwrap();
assert_eq!(got.n_rows, 2);
assert_eq!(
got.cols[0].field.dtype,
ArrowType::Timestamp(TimeUnit::Nanoseconds, Some("UTC".into())),
);
match &got.cols[0].array {
Array::NumericArray(NumericArray::Int64(a)) => {
assert_eq!(a.data.as_slice(), &[1_782_999_000_000_000_000, 1_782_999_000_000_000_001]);
}
other => panic!("timestamp column decoded as {other:?}"),
}
}
#[test]
fn writes_multiple_tables_via_file_protocol() {
use crate::models::readers::ipc::file_table::FileTableReader;
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let t1 = Table::new("t", Some(vec![fa_i32!["n", 1, 2, 3]]));
let t2 = Table::new("t", Some(vec![fa_i32!["n", 4, 5]]));
let t3 = Table::new("t", Some(vec![fa_i32!["n", 6, 7, 8, 9]]));
let schema: Vec<Field> = t1.cols.iter().map(|c| (*c.field).clone()).collect();
let mut writer = SyncTableWriter::<_, Vec64<u8>>::new(
std::fs::File::create(&path).unwrap(),
schema,
IPCMessageProtocol::File,
None,
);
writer.write_table(t1.clone()).unwrap();
writer.write_table(t2.clone()).unwrap();
writer.write_table(t3.clone()).unwrap();
writer.finish().unwrap();
let reader = FileTableReader::open(&path).unwrap();
assert_eq!(reader.num_batches(), 3);
assert_eq!(reader.read_batch(0).unwrap().n_rows, 3);
assert_eq!(reader.read_batch(1).unwrap().n_rows, 2);
assert_eq!(reader.read_batch(2).unwrap().n_rows, 4);
}
#[test]
fn writes_stream_protocol_to_buffer() {
use crate::models::readers::ipc::table::TableReader;
use futures_util::StreamExt;
let t1 = Table::new("t", Some(vec![fa_i32!["n", 10, 20, 30]]));
let t2 = Table::new("t", Some(vec![fa_i32!["n", 40, 50]]));
let schema: Vec<Field> = t1.cols.iter().map(|c| (*c.field).clone()).collect();
let mut buf: Vec<u8> = Vec::new();
let mut writer =
SyncTableWriter::<_, Vec64<u8>>::new(&mut buf, schema, IPCMessageProtocol::Stream, None);
writer.write_table(t1.clone()).unwrap();
writer.write_table(t2.clone()).unwrap();
writer.finish().unwrap();
drop(writer);
let rt = tokio::runtime::Runtime::new().unwrap();
let mut reader = TableReader::<Vec64<u8>>::new(
std::io::Cursor::new(buf),
8 * 1024,
IPCMessageProtocol::Stream,
None,
);
let batches: Vec<Table> = rt.block_on(async move {
let mut out = Vec::new();
while let Some(batch) = reader.next().await {
out.push(batch.unwrap());
}
out
});
assert_eq!(batches.len(), 2);
assert_eq!(batches[0].n_rows, 3);
assert_eq!(batches[1].n_rows, 2);
}
#[test]
fn finish_is_idempotent() {
let file = NamedTempFile::new().unwrap();
let mut writer = SyncTableWriter::<_, Vec64<u8>>::new(
file.reopen().unwrap(),
vec![],
IPCMessageProtocol::Stream,
None,
);
writer.finish().unwrap();
writer.finish().unwrap();
}
}