nd-arrow-array 0.5.0

N-dimensional Arrow arrays as an Arrow extension type
Documentation
use std::sync::Arc;

use arrow::array::{Float32Array, Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::ipc::reader::StreamReader;
use arrow::ipc::writer::StreamWriter;
use arrow::record_batch::RecordBatch;
use nd_arrow_array::extension::{is_global_attributes_field, is_nd_field};
use nd_arrow_array::{
    Attributes, Dimensions, NdArrayType, NdArrowArray, NdColumn, NdRecordBatch, encode_batches,
};

fn ipc_round_trip(batch: &RecordBatch) -> RecordBatch {
    let mut bytes = Vec::new();
    let mut writer = StreamWriter::try_new(&mut bytes, &batch.schema()).unwrap();
    writer.write(batch).unwrap();
    writer.finish().unwrap();
    drop(writer);

    let mut reader = StreamReader::try_new(bytes.as_slice(), None).unwrap();
    let read = reader.next().unwrap().unwrap();
    assert!(reader.next().is_none());
    read
}

fn sample_batch() -> NdRecordBatch {
    let schema = Arc::new(Schema::new(vec![
        Field::new("time", DataType::Int64, true),
        Field::new("station", DataType::Utf8, true),
        Field::new("temp", DataType::Float32, true),
    ]));
    let time = NdArrowArray::try_new(
        Arc::new(Int64Array::from(vec![0, 3600])),
        Dimensions::try_new([("time", 2)]).unwrap(),
    )
    .unwrap()
    .with_attributes(Attributes::new().with("units", "seconds since 1970-01-01"));
    let station = NdArrowArray::try_new(
        Arc::new(StringArray::from(vec!["A", "B", "C"])),
        Dimensions::try_new([("station", 3)]).unwrap(),
    )
    .unwrap();
    let temp = NdArrowArray::try_new(
        Arc::new(Float32Array::from(vec![
            Some(1.0),
            None,
            Some(3.0),
            Some(4.0),
            Some(5.0),
            None,
        ])),
        Dimensions::try_new([("time", 2), ("station", 3)]).unwrap(),
    )
    .unwrap()
    .with_attributes(
        Attributes::new()
            .with("units", "degC")
            .with("valid_range", vec![-50.0f32, 60.0]),
    );
    NdRecordBatch::try_new_inferred(schema, vec![time, station, temp])
        .unwrap()
        .with_attributes(
            Attributes::new()
                .with("title", "Station temperatures")
                .with("Conventions", "CF-1.8"),
        )
}

#[test]
fn nd_batch_survives_ipc() {
    let nd = sample_batch();
    let encoded = encode_batches(&[nd.clone(), nd.clone()]).unwrap();
    let read = ipc_round_trip(&encoded);

    assert_eq!(read.schema(), encoded.schema());
    for field in read.schema().fields() {
        if is_global_attributes_field(field) {
            continue;
        }
        assert!(
            is_nd_field(field),
            "{} lost the extension tag",
            field.name()
        );
        let ext: NdArrayType = field.try_extension_type().unwrap();
        assert_eq!(field.data_type(), &ext.storage_type());
    }

    let decoded = NdRecordBatch::try_from_record_batch(&read).unwrap();
    assert_eq!(decoded.len(), 2);
    for batch in decoded {
        assert_eq!(batch.target(), nd.target());
        assert_eq!(batch.attributes(), nd.attributes());
        assert_eq!(batch.columns(), nd.columns());
    }
}

#[test]
fn column_without_attributes_survives_ipc() {
    let array = NdArrowArray::try_new_scalar(Arc::new(Int64Array::from(vec![42]))).unwrap();
    let column = NdColumn::try_from_arrays([&array]).unwrap();
    assert!(column.extension_type().attribute_fields().is_empty());

    let schema = Arc::new(Schema::new(vec![column.to_field("x")]));
    let batch = RecordBatch::try_new(schema, vec![column.into_array_ref()]).unwrap();
    let read = ipc_round_trip(&batch);

    let decoded = NdColumn::try_from_array(read.column(0).as_ref()).unwrap();
    assert_eq!(decoded.value(0), array);
}

#[test]
fn non_nd_batch_rejected() {
    let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int64, true)]));
    let batch = RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(vec![1]))]).unwrap();
    assert!(NdRecordBatch::try_from_record_batch(&batch).is_err());
}