xml-arrow 0.2.4

XML record conversion to Apache Arrow batches
Documentation
use arrow_array::{cast::AsArray, types::Int64Type, Array};
use arrow_schema::DataType::{Int64, Utf8};
use xml_arrow::{NamespaceBinding, XmlBatchBuilder, XmlField, XmlSchema};

#[test]
fn structs_preserve_parent_nulls_and_required_children() {
    let schema = XmlSchema::new(vec![XmlField::structure(
        "profile",
        "profile",
        true,
        vec![
            XmlField::new("id", "@id", Int64, false),
            XmlField::structure(
                "address",
                "address",
                true,
                vec![XmlField::new("city", "city", Utf8, false)],
            ),
        ],
    )])
    .unwrap();
    let mut b = XmlBatchBuilder::new(schema, 2).unwrap();
    b.append_xml(r#"<r><profile id="7"><address><city>Paris</city></address></profile></r>"#)
        .unwrap();
    b.append_xml("<r/>").unwrap();
    let batch = b.flush().unwrap().unwrap();
    let profile = batch.column(0).as_struct();
    assert_eq!(profile.column(0).as_primitive::<Int64Type>().value(0), 7);
    assert!(profile.is_null(1));
    let address = profile.column(1).as_struct();
    assert_eq!(address.column(0).as_string::<i32>().value(0), "Paris");
    assert!(address.is_null(1));
    b.append_xml(r#"<r><profile id="8"/></r>"#).unwrap();
    b.append_xml(
        r#"<r xmlns:n="http://www.w3.org/2001/XMLSchema-instance"><profile n:nil="true"/></r>"#,
    )
    .unwrap();
    let batch = b.flush().unwrap().unwrap();
    assert!(batch.column(0).as_struct().column(1).is_null(0));
    assert!(batch.column(0).is_null(1));
}

#[test]
fn lists_distinguish_empty_missing_nil_and_nullable_items() {
    let schema = XmlSchema::new(vec![XmlField::list(
        "tags",
        "tags",
        true,
        XmlField::new("item", "tag", Utf8, true),
    )])
    .unwrap();
    let mut b = XmlBatchBuilder::new(schema, 4).unwrap();
    for xml in [
        r#"<r xmlns:n="http://www.w3.org/2001/XMLSchema-instance"><tags><tag>one</tag><tag n:nil="1"/><tag/></tags></r>"#,
        "<r><tags/></r>",
        "<r/>",
        r#"<r xmlns:n="http://www.w3.org/2001/XMLSchema-instance"><tags n:nil="true"/></r>"#,
    ] {
        b.append_xml(xml).unwrap();
    }
    let batch = b.flush().unwrap().unwrap();
    let tags = batch.column(0).as_list::<i32>();
    assert_eq!(tags.value_offsets(), &[0, 3, 3, 3, 3]);
    assert!(!tags.is_null(1));
    assert!(tags.is_null(2));
    assert!(tags.is_null(3));
    assert_eq!(
        tags.values().as_string::<i32>().iter().collect::<Vec<_>>(),
        vec![Some("one"), None, Some("")]
    );
}

#[test]
fn list_of_structs_keeps_nested_lists_and_item_order() {
    let item = XmlField::structure(
        "item",
        "person",
        false,
        vec![
            XmlField::new("id", "@id", Int64, false),
            XmlField::list(
                "scores",
                "scores",
                false,
                XmlField::new("item", "score", Int64, false),
            ),
        ],
    );
    let schema = XmlSchema::new(vec![XmlField::list("people", "people", false, item)]).unwrap();
    let mut b = XmlBatchBuilder::new(schema, 2).unwrap();
    b.append_xml(r#"<r><people><person id="2"><scores><score>4</score><score>5</score></scores></person><person id="1"><scores/></person></people></r>"#).unwrap();
    let batch = b.flush().unwrap().unwrap();
    let people = batch.column(0).as_list::<i32>();
    let items = people.values().as_struct();
    assert_eq!(
        items
            .column(0)
            .as_primitive::<Int64Type>()
            .values()
            .as_ref(),
        &[2, 1]
    );
    let scores = items.column(1).as_list::<i32>();
    assert_eq!(scores.value_offsets(), &[0, 2, 2]);
    assert_eq!(
        scores
            .values()
            .as_primitive::<Int64Type>()
            .values()
            .as_ref(),
        &[4, 5]
    );
}

#[test]
fn nested_list_offsets_reset_after_flush() {
    let item = XmlField::list(
        "item",
        "row",
        true,
        XmlField::new("item", "v", Int64, false),
    );
    let schema = XmlSchema::new(vec![XmlField::list("matrix", "matrix", false, item)]).unwrap();
    let mut b = XmlBatchBuilder::new(schema, 2).unwrap();
    b.append_xml(r#"<r xmlns:n="http://www.w3.org/2001/XMLSchema-instance"><matrix><row><v>1</v></row><row/><row n:nil="true"/></matrix></r>"#)
        .unwrap();
    let first = b.flush().unwrap().unwrap();
    let matrix = first.column(0).as_list::<i32>();
    assert_eq!(matrix.value_offsets(), &[0, 3]);
    assert_eq!(
        matrix.values().as_list::<i32>().value_offsets(),
        &[0, 1, 1, 1]
    );
    assert!(!matrix.values().is_null(1));
    assert!(matrix.values().is_null(2));
    b.append_xml("<r><matrix><row><v>6</v><v>7</v></row></matrix></r>")
        .unwrap();
    let second = b.flush().unwrap().unwrap();
    let matrix = second.column(0).as_list::<i32>();
    assert_eq!(matrix.value_offsets(), &[0, 1]);
    let row = matrix.values().as_list::<i32>();
    assert_eq!(row.value_offsets(), &[0, 2]);
    assert_eq!(
        row.values().as_primitive::<Int64Type>().values().as_ref(),
        &[6, 7]
    );
}

#[test]
fn namespace_uris_match_across_prefixes_default_namespaces_and_projection() {
    let schema = XmlSchema::with_namespaces(
        vec![
            XmlField::new("unused", "unused", Int64, false),
            XmlField::structure(
                "person",
                "p:person",
                false,
                vec![
                    XmlField::new("qualified_id", "@m:id", Int64, false),
                    XmlField::new("id", "@id", Int64, false),
                    XmlField::new("name", "p:name", Utf8, false),
                    XmlField::new("plain", "name", Utf8, true),
                    XmlField::new("lang", "@xml:lang", Utf8, true),
                ],
            ),
        ],
        &[
            NamespaceBinding {
                prefix: "p",
                uri: "urn:person",
            },
            NamespaceBinding {
                prefix: "m",
                uri: "urn:meta",
            },
        ],
    )
    .unwrap();
    let mut b = XmlBatchBuilder::new_projected(schema, 2, &[1]).unwrap();
    b.append_xml(r#"<r xmlns:a="urn:person" xmlns:b="urn:meta"><a:person b:id="7" id="8" xml:lang="en"><a:name>Jane</a:name></a:person></r>"#).unwrap();
    b.append_xml(r#"<r xmlns="urn:person" xmlns:m="urn:meta"><person m:id="9" id="10"><name>Lee</name><name xmlns="">Plain</name></person></r>"#).unwrap();
    let batch = b.flush().unwrap().unwrap();
    let people = batch.column(0).as_struct();
    assert_eq!(
        people
            .column(0)
            .as_primitive::<Int64Type>()
            .values()
            .as_ref(),
        &[7, 9]
    );
    assert_eq!(
        people
            .column(1)
            .as_primitive::<Int64Type>()
            .values()
            .as_ref(),
        &[8, 10]
    );
    assert_eq!(
        people
            .column(2)
            .as_string::<i32>()
            .iter()
            .collect::<Vec<_>>(),
        vec![Some("Jane"), Some("Lee")]
    );
    assert_eq!(
        people
            .column(3)
            .as_string::<i32>()
            .iter()
            .collect::<Vec<_>>(),
        vec![None, Some("Plain")]
    );
    assert_eq!(
        people
            .column(4)
            .as_string::<i32>()
            .iter()
            .collect::<Vec<_>>(),
        vec![Some("en"), None]
    );
}

#[test]
fn invalid_nested_values_leave_the_batch_reusable() {
    let schema = XmlSchema::new(vec![
        XmlField::structure(
            "profile",
            "profile",
            false,
            vec![XmlField::new("id", "@id", Int64, false)],
        ),
        XmlField::list(
            "tags",
            "tags",
            false,
            XmlField::new("item", "tag", Int64, false),
        ),
    ])
    .unwrap();
    let mut b = XmlBatchBuilder::new(schema, 2).unwrap();
    b.append_xml(r#"<r><profile id="1"/><tags><tag>2</tag></tags></r>"#)
        .unwrap();
    for invalid in [
        "<profile/><tags/>",
        r#"<profile id="2"/><tags><tag>oops</tag></tags>"#,
        r#"<profile id="2"/><tags><tag n:nil="true"/></tags>"#,
        r#"<profile n:nil="true"/><tags/>"#,
        r#"<profile id="2" n:nil="maybe"/><tags/>"#,
        r#"<profile id="2"/><tags n:nil="true"><tag>2</tag></tags>"#,
        r#"<profile id="2">text<extra/></profile><tags/>"#,
        r#"<profile id="2"/><tags/><tags/>"#,
    ] {
        let xml =
            format!(r#"<r xmlns:n="http://www.w3.org/2001/XMLSchema-instance">{invalid}</r>"#);
        assert!(b.append_xml(&xml).is_err(), "{invalid}");
        assert_eq!(b.len(), 1);
    }
    b.append_xml(r#"<r xmlns:n="http://www.w3.org/2001/XMLSchema-instance"><profile id="3" n:nil="false"/><tags/></r>"#).unwrap();
    let batch = b.flush().unwrap().unwrap();
    assert_eq!(batch.num_rows(), 2);
    assert_eq!(
        batch
            .column(0)
            .as_struct()
            .column(0)
            .as_primitive::<Int64Type>()
            .values()
            .as_ref(),
        &[1, 3]
    );
    assert_eq!(batch.column(1).as_list::<i32>().value_offsets(), &[0, 1, 1]);
}