use crate::Result;
use crate::error::CoreError;
use arrow_array::RecordBatch;
use arrow_ipc::reader::StreamReader;
use arrow_ipc::writer::StreamWriter;
use arrow_schema::SchemaRef;
use std::io::Cursor;
pub fn to_binary_row(_schema: &SchemaRef, record: &RecordBatch) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut writer =
StreamWriter::try_new(&mut buf, &record.schema()).expect("IPC writer creation");
writer.write(record).expect("IPC write");
writer.finish().expect("IPC finish");
}
buf
}
pub fn to_binary_row_body(record: &RecordBatch) -> Option<Vec<u8>> {
let generator = arrow_ipc::writer::IpcDataGenerator {};
let mut dict_tracker = arrow_ipc::writer::DictionaryTracker::new(false);
let opts = arrow_ipc::writer::IpcWriteOptions::default();
let mut compression_context = arrow_ipc::writer::CompressionContext::default();
let (dictionaries, encoded) = generator
.encode(record, &mut dict_tracker, &opts, &mut compression_context)
.ok()?;
if !dictionaries.is_empty() {
return None; }
let mut buf = Vec::with_capacity(4 + encoded.ipc_message.len() + encoded.arrow_data.len());
buf.extend_from_slice(&(encoded.ipc_message.len() as u32).to_le_bytes());
buf.extend_from_slice(&encoded.ipc_message);
buf.extend_from_slice(&encoded.arrow_data);
Some(buf)
}
pub fn from_binary_body(bytes: &[u8], schema: SchemaRef) -> Result<RecordBatch> {
if bytes.len() < 4 {
return Err(CoreError::ReadFileSliceError(
"spill body decode: truncated length header".to_string(),
));
}
let msg_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let msg_end = 4 + msg_len;
if bytes.len() < msg_end {
return Err(CoreError::ReadFileSliceError(
"spill body decode: truncated IPC message".to_string(),
));
}
let message = arrow_ipc::root_as_message(&bytes[4..msg_end]).map_err(|e| {
CoreError::ReadFileSliceError(format!("spill body decode: bad IPC message: {e}"))
})?;
let record_batch = message.header_as_record_batch().ok_or_else(|| {
CoreError::ReadFileSliceError(
"spill body decode: message header is not a RecordBatch".to_string(),
)
})?;
let body = arrow_buffer::Buffer::from_vec(bytes[msg_end..].to_vec());
arrow_ipc::reader::read_record_batch(
&body,
record_batch,
schema,
&std::collections::HashMap::new(),
None,
&message.version(),
)
.map_err(|e| {
CoreError::ReadFileSliceError(format!("spill body decode: read_record_batch: {e}"))
})
}
#[allow(dead_code)]
pub fn seal(bytes: Vec<u8>) -> Vec<u8> {
bytes
}
pub fn from_binary(bytes: &[u8]) -> Result<RecordBatch> {
let cursor = Cursor::new(bytes);
let mut reader = StreamReader::try_new(cursor, None)
.map_err(|e| CoreError::ReadFileSliceError(format!("IPC reader creation failed: {e}")))?;
reader
.next()
.ok_or_else(|| {
CoreError::ReadFileSliceError("IPC stream contained no batches".to_string())
})?
.map_err(|e| CoreError::ReadFileSliceError(format!("IPC read failed: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::{Int64Array, StringArray};
use arrow_schema::{DataType, Field, Schema};
use std::sync::Arc;
fn make_test_batch() -> RecordBatch {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Utf8, false),
Field::new("value", DataType::Int64, false),
]));
RecordBatch::try_new(
schema,
vec![
Arc::new(StringArray::from(vec!["k1"])),
Arc::new(Int64Array::from(vec![42])),
],
)
.unwrap()
}
#[test]
fn test_to_binary_row_and_from_binary_roundtrip() {
let batch = make_test_batch();
let schema = batch.schema();
let bytes = to_binary_row(&schema, &batch);
assert!(!bytes.is_empty());
let sealed = seal(bytes);
let restored = from_binary(&sealed).unwrap();
assert_eq!(restored.num_rows(), 1);
assert_eq!(restored.schema(), schema);
assert_eq!(
restored
.column(0)
.as_any()
.downcast_ref::<StringArray>()
.unwrap()
.value(0),
"k1"
);
}
#[test]
fn test_to_binary_row_body_and_from_binary_body_roundtrip() {
let batch = make_test_batch();
let schema = batch.schema();
let bytes = to_binary_row_body(&batch).expect("no dictionaries, so body-only encodable");
assert!(!bytes.is_empty());
assert!(
bytes.len() < to_binary_row(&schema, &batch).len(),
"dropping the schema framing should make the blob smaller"
);
let restored = from_binary_body(&bytes, schema.clone()).unwrap();
assert_eq!(restored.num_rows(), 1);
assert_eq!(restored.schema(), schema);
assert_eq!(
restored
.column(1)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.value(0),
42
);
}
#[test]
fn test_from_binary_body_rejects_truncated_input() {
let schema = make_test_batch().schema();
for truncated in [&[][..], &[1, 2, 3][..], &[255, 255, 255, 255][..]] {
let err = from_binary_body(truncated, schema.clone()).unwrap_err();
assert!(
err.to_string().contains("spill body decode"),
"error should name the decode step, got: {err}"
);
}
}
}