graphforge-cli 0.6.0-rc.3

GraphForge command-line interface
//! UUID and GeoArrow preservation in native CLI result output.

use super::*;

#[test]
fn canonical_json_uuid_conversion_is_exact_and_rejects_malformed_hex() {
    let ty = DataType::FixedSizeBinary(16);
    assert_eq!(
        canonical_json_value(
            serde_json::Value::String("000102030405060708090a0b0c0d0e0f".into()),
            &ty,
        )
        .unwrap(),
        serde_json::Value::String("00010203-0405-0607-0809-0a0b0c0d0e0f".into())
    );
    for value in ["00", "zz0102030405060708090a0b0c0d0e0f"] {
        assert!(matches!(
            canonical_json_value(serde_json::Value::String(value.into()), &ty),
            Err(graphforge_api::GfError::Execution(_))
        ));
    }
    let ordinary = serde_json::json!({"nested": [true, 1, null]});
    assert_eq!(
        canonical_json_value(ordinary.clone(), &DataType::Utf8).unwrap(),
        ordinary
    );
}

#[test]
#[allow(clippy::too_many_lines)]
fn arrow_result_export_preserves_geoarrow_fields_values_and_nulls() {
    use std::collections::HashMap;
    use std::io::Cursor;

    use arrow::array::{
        Array, FixedSizeListArray, Float64Array, LargeListArray, ListArray, StructArray,
    };
    use arrow::ipc::reader::StreamReader;
    use graphforge_api::{PropValue, SpatialValue};

    fn flatten_value(array: &dyn Array, row: usize, output: &mut Vec<f64>) {
        if let Some(values) = array.as_any().downcast_ref::<Float64Array>() {
            output.push(values.value(row));
        } else if let Some(values) = array.as_any().downcast_ref::<StructArray>() {
            for column in values.columns() {
                flatten_value(column.as_ref(), row, output);
            }
        } else if let Some(values) = array.as_any().downcast_ref::<ListArray>() {
            let values = values.value(row);
            for index in 0..values.len() {
                flatten_value(values.as_ref(), index, output);
            }
        } else if let Some(values) = array.as_any().downcast_ref::<LargeListArray>() {
            let values = values.value(row);
            for index in 0..values.len() {
                flatten_value(values.as_ref(), index, output);
            }
        } else if let Some(values) = array.as_any().downcast_ref::<FixedSizeListArray>() {
            let values = values.value(row);
            for index in 0..values.len() {
                flatten_value(values.as_ref(), index, output);
            }
        } else {
            panic!(
                "unexpected GeoArrow coordinate array: {:?}",
                array.data_type()
            );
        }
    }

    let fixture: serde_json::Value = serde_json::from_str(include_str!(
        "../../../tests/contracts/geoarrow-interchange-v1.json"
    ))
    .unwrap();
    let cases = fixture["cases"].as_array().unwrap();
    let properties = cases
        .iter()
        .map(|case| {
            let name = case["name"].as_str().unwrap().to_owned();
            let spatial: SpatialValue = serde_json::from_value(serde_json::json!({
                "spatial_type": {
                    "geometry": case["geometry"],
                    "crs": case["crs"],
                },
                "coordinates": case["coordinates"],
                    "extension_name": case.get("preservedOnly").and_then(serde_json::Value::as_bool).unwrap_or(false).then(|| case["extensionName"].clone()),
                    "extension_metadata": case.get("preservedOnly").and_then(serde_json::Value::as_bool).unwrap_or(false).then(|| case["extensionMetadata"].clone()),
            }))
            .unwrap();
            (name, PropValue::Spatial(spatial))
        })
        .collect::<HashMap<_, _>>();
    let graph = GraphForge::new(None).unwrap();
    graph.add_node("Geometry", &properties).unwrap();
    graph.add_node("Geometry", &HashMap::new()).unwrap();
    let projection = cases
        .iter()
        .map(|case| {
            let name = case["name"].as_str().unwrap();
            format!("n.{name} AS {name}")
        })
        .collect::<Vec<_>>()
        .join(", ");
    let result = graph
        .execute(&format!(
            "MATCH (n:Geometry) RETURN {projection} ORDER BY n.node_uuid"
        ))
        .unwrap();
    let mut ipc = Vec::new();
    write_result(&result, &mut ipc).unwrap();
    let batches = StreamReader::try_new(Cursor::new(ipc), None)
        .unwrap()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    let expected_batches = fixture["rows"]["batchSizes"]
        .as_array()
        .unwrap()
        .iter()
        .map(|value| usize::try_from(value.as_u64().unwrap()).unwrap())
        .collect::<Vec<_>>();
    assert_eq!(
        batches
            .iter()
            .map(arrow::array::RecordBatch::num_rows)
            .collect::<Vec<_>>(),
        expected_batches
    );
    assert_eq!(
        batches
            .iter()
            .map(arrow::array::RecordBatch::num_rows)
            .sum::<usize>(),
        2
    );
    for case in cases {
        let name = case["name"].as_str().unwrap();
        let field = batches[0].schema().field_with_name(name).unwrap().clone();
        assert_eq!(
            field.metadata()["ARROW:extension:name"],
            case["extensionName"]
        );
        assert_eq!(
            field.metadata()["ARROW:extension:metadata"],
            case["extensionMetadata"]
        );
        let column = batches[0].column_by_name(name).unwrap();
        let mut coordinates = Vec::new();
        flatten_value(
            column.as_ref(),
            usize::try_from(fixture["rows"]["populated"].as_u64().unwrap()).unwrap(),
            &mut coordinates,
        );
        let expected = case["flat"]
            .as_array()
            .unwrap()
            .iter()
            .map(|value| value.as_f64().unwrap())
            .collect::<Vec<_>>();
        assert_eq!(coordinates, expected);
        assert!(
            column.is_null(usize::try_from(fixture["rows"]["null"].as_u64().unwrap()).unwrap())
        );
    }
}