This is an automated email from the ASF dual-hosted git repository.

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new 2939063  feat(rust/sedona-functions): Implement ST_Dump (#269)
2939063 is described below

commit 2939063fbac28dc06a03e1d18743fa327fbba7ed
Author: Hiroaki Yutani <[email protected]>
AuthorDate: Thu Nov 6 04:48:26 2025 +0900

    feat(rust/sedona-functions): Implement ST_Dump (#269)
    
    Co-authored-by: Dewey Dunnington <[email protected]>
---
 benchmarks/test_functions.py                      |  18 +
 python/sedonadb/tests/functions/test_functions.py | 136 +++++++
 rust/sedona-functions/src/lib.rs                  |   1 +
 rust/sedona-functions/src/register.rs             |   1 +
 rust/sedona-functions/src/st_dump.rs              | 437 ++++++++++++++++++++++
 5 files changed, 593 insertions(+)

diff --git a/benchmarks/test_functions.py b/benchmarks/test_functions.py
index e609039..aabae02 100644
--- a/benchmarks/test_functions.py
+++ b/benchmarks/test_functions.py
@@ -113,6 +113,24 @@ class TestBenchFunctions(TestBenchBase):
 
         benchmark(queries)
 
+    @pytest.mark.parametrize(
+        "eng", [SedonaDBSingleThread, PostGISSingleThread, DuckDBSingleThread]
+    )
+    @pytest.mark.parametrize(
+        "table",
+        [
+            "collections_simple",
+            "collections_complex",
+        ],
+    )
+    def test_st_dump(self, benchmark, eng, table):
+        eng = self._get_eng(eng)
+
+        def queries():
+            eng.execute_and_collect(f"SELECT ST_Dump(geom1) from {table}")
+
+        benchmark(queries)
+
     @pytest.mark.parametrize(
         "eng", [SedonaDBSingleThread, PostGISSingleThread, DuckDBSingleThread]
     )
diff --git a/python/sedonadb/tests/functions/test_functions.py 
b/python/sedonadb/tests/functions/test_functions.py
index a30f783..21b715c 100644
--- a/python/sedonadb/tests/functions/test_functions.py
+++ b/python/sedonadb/tests/functions/test_functions.py
@@ -634,6 +634,142 @@ def test_st_dimension(eng, geom, expected):
     eng.assert_query_result(f"SELECT ST_Dimension({geom_or_null(geom)})", 
expected)
 
 
[email protected]("eng", [SedonaDB, PostGIS])
+def test_st_dump(eng):
+    is_postgis = eng == PostGIS
+    eng = eng.create_or_skip()
+
+    cases = [
+        {"input": "POINT (1 2)", "expected": [{"path": [], "geom": "POINT (1 
2)"}]},
+        {
+            "input": "LINESTRING (1 1, 2 2)",
+            "expected": [{"path": [], "geom": "LINESTRING (1 1, 2 2)"}],
+        },
+        {
+            "input": "POLYGON ((1 1, 2 2, 2 1, 1 1))",
+            "expected": [{"path": [], "geom": "POLYGON ((1 1, 2 2, 2 1, 1 
1))"}],
+        },
+        {
+            "input": "MULTIPOINT (0 1, 1 2)",
+            "expected": [
+                {
+                    "path": [1],
+                    "geom": "POINT (0 1)",
+                },
+                {
+                    "path": [2],
+                    "geom": "POINT (1 2)",
+                },
+            ],
+        },
+        {
+            "input": "MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4))",
+            "expected": [
+                {
+                    "path": [1],
+                    "geom": "LINESTRING (1 1, 2 2)",
+                },
+                {
+                    "path": [2],
+                    "geom": "LINESTRING EMPTY",
+                },
+                {
+                    "path": [3],
+                    "geom": "LINESTRING (3 3, 4 4)",
+                },
+            ],
+        },
+        {
+            "input": "MULTIPOLYGON (((1 1, 2 2, 2 1, 1 1)), EMPTY, ((3 3, 4 4, 
4 3, 3 3)))",
+            "expected": [
+                {
+                    "path": [1],
+                    "geom": "POLYGON ((1 1, 2 2, 2 1, 1 1))",
+                },
+                {
+                    "path": [2],
+                    "geom": "POLYGON EMPTY",
+                },
+                {
+                    "path": [3],
+                    "geom": "POLYGON ((3 3, 4 4, 4 3, 3 3))",
+                },
+            ],
+        },
+        {
+            "input": "GEOMETRYCOLLECTION (POINT (1 2), MULTILINESTRING ((1 1, 
2 2), EMPTY, (3 3, 4 4)), LINESTRING (1 1, 2 2))",
+            "expected": [
+                {
+                    "path": [1],
+                    "geom": "POINT (1 2)",
+                },
+                {
+                    "path": [2, 1],
+                    "geom": "LINESTRING (1 1, 2 2)",
+                },
+                {
+                    "path": [2, 2],
+                    "geom": "LINESTRING EMPTY",
+                },
+                {
+                    "path": [2, 3],
+                    "geom": "LINESTRING (3 3, 4 4)",
+                },
+                {
+                    "path": [3],
+                    "geom": "LINESTRING (1 1, 2 2)",
+                },
+            ],
+        },
+        {
+            "input": "GEOMETRYCOLLECTION (POINT (1 2), GEOMETRYCOLLECTION 
(MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4)), LINESTRING (1 1, 2 2)))",
+            "expected": [
+                {
+                    "path": [1],
+                    "geom": "POINT (1 2)",
+                },
+                {
+                    "path": [2, 1, 1],
+                    "geom": "LINESTRING (1 1, 2 2)",
+                },
+                {
+                    "path": [2, 1, 2],
+                    "geom": "LINESTRING EMPTY",
+                },
+                {
+                    "path": [2, 1, 3],
+                    "geom": "LINESTRING (3 3, 4 4)",
+                },
+                {
+                    "path": [2, 2],
+                    "geom": "LINESTRING (1 1, 2 2)",
+                },
+            ],
+        },
+    ]
+
+    for case in cases:
+        if is_postgis:
+            result = eng.execute_and_collect(
+                f"SELECT ST_Dump({geom_or_null(case['input'])})"
+            )
+        else:
+            result = eng.execute_and_collect(
+                f"SELECT unnest(ST_Dump({geom_or_null(case['input'])}))"
+            )
+        df = eng.result_to_pandas(result)
+
+        for i in df.index:
+            actual = df.iat[i, 0]
+            expected = case["expected"][i]
+            assert list(actual.keys()) == ["path", "geom"]
+            if actual["path"].size == 0:
+                assert len(expected["path"]) == 0
+            else:
+                actual["path"] == expected["path"]
+            assert actual["geom"] == shapely.from_wkt(expected["geom"]).wkb
+
+
 @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
 @pytest.mark.parametrize(
     ("geom", "expected"),
diff --git a/rust/sedona-functions/src/lib.rs b/rust/sedona-functions/src/lib.rs
index ed93709..0014629 100644
--- a/rust/sedona-functions/src/lib.rs
+++ b/rust/sedona-functions/src/lib.rs
@@ -31,6 +31,7 @@ mod st_buffer;
 mod st_centroid;
 mod st_collect;
 mod st_dimension;
+mod st_dump;
 mod st_dwithin;
 pub mod st_envelope;
 pub mod st_envelope_aggr;
diff --git a/rust/sedona-functions/src/register.rs 
b/rust/sedona-functions/src/register.rs
index 7069fcb..ba9f82d 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -68,6 +68,7 @@ pub fn default_function_set() -> FunctionSet {
         crate::st_buffer::st_buffer_udf,
         crate::st_centroid::st_centroid_udf,
         crate::st_dimension::st_dimension_udf,
+        crate::st_dump::st_dump_udf,
         crate::st_dwithin::st_dwithin_udf,
         crate::st_envelope::st_envelope_udf,
         crate::st_flipcoordinates::st_flipcoordinates_udf,
diff --git a/rust/sedona-functions/src/st_dump.rs 
b/rust/sedona-functions/src/st_dump.rs
new file mode 100644
index 0000000..ca1d712
--- /dev/null
+++ b/rust/sedona-functions/src/st_dump.rs
@@ -0,0 +1,437 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+use arrow_array::{
+    builder::{BinaryBuilder, NullBufferBuilder, OffsetBufferBuilder, 
UInt32Builder},
+    ListArray, StructArray,
+};
+use arrow_schema::{DataType, Field, Fields};
+use datafusion_common::error::Result;
+use datafusion_expr::{
+    scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, 
Volatility,
+};
+use geo_traits::{
+    GeometryCollectionTrait, GeometryTrait, GeometryType, 
MultiLineStringTrait, MultiPointTrait,
+    MultiPolygonTrait,
+};
+use sedona_common::sedona_internal_err;
+use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
+use sedona_schema::{
+    datatypes::{SedonaType, WKB_GEOMETRY},
+    matchers::ArgMatcher,
+};
+use std::sync::Arc;
+
+use crate::executor::WkbExecutor;
+
+/// ST_Dump() scalar UDF
+///
+/// Native implementation to get all the points of a geometry as MULTIPOINT
+pub fn st_dump_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_dump",
+        vec![Arc::new(STDump)],
+        Volatility::Immutable,
+        Some(st_dump_doc()),
+    )
+}
+
+fn st_dump_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Extracts the components of a geometry.",
+        "ST_Dump (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry")
+    .with_sql_example("SELECT ST_Dump(ST_GeomFromWKT('MULTIPOINT (0 1, 2 3, 4 
5)'))")
+    .build()
+}
+
+#[derive(Debug)]
+struct STDump;
+
+// This enum is solely for passing the subset of wkb geometry to 
STDumpStructBuilder.
+// Maybe we can pass the underlying raw WKB bytes directly, but this just 
works for now.
+enum SingleWkb<'a> {
+    Point(&'a wkb::reader::Point<'a>),
+    LineString(&'a wkb::reader::LineString<'a>),
+    Polygon(&'a wkb::reader::Polygon<'a>),
+}
+
+// A builder for a list of the structs
+struct STDumpBuilder {
+    path_array_builder: UInt32Builder,
+    path_array_offsets_builder: OffsetBufferBuilder<i32>,
+    geom_builder: BinaryBuilder,
+    struct_offsets_builder: OffsetBufferBuilder<i32>,
+    null_builder: NullBufferBuilder,
+    parent_path: Vec<u32>,
+}
+
+impl STDumpBuilder {
+    fn new(num_iter: usize) -> Self {
+        let path_array_builder = UInt32Builder::with_capacity(num_iter);
+        let path_array_offsets_builder = OffsetBufferBuilder::new(num_iter);
+        let geom_builder =
+            BinaryBuilder::with_capacity(num_iter, WKB_MIN_PROBABLE_BYTES * 
num_iter);
+        let struct_offsets_builder = OffsetBufferBuilder::new(num_iter);
+        let null_builder = NullBufferBuilder::new(num_iter);
+
+        Self {
+            path_array_builder,
+            path_array_offsets_builder,
+            geom_builder,
+            struct_offsets_builder,
+            null_builder,
+            parent_path: Vec::new(), // Reusable buffer to avoid allocation 
per row
+        }
+    }
+
+    // This appends both path and geom at once.
+    fn append_single_struct(&mut self, cur_index: Option<u32>, wkb: 
SingleWkb<'_>) -> Result<()> {
+        self.path_array_builder.append_slice(&self.parent_path);
+        if let Some(cur_index) = cur_index {
+            self.path_array_builder.append_value(cur_index);
+            self.path_array_offsets_builder
+                .push_length(self.parent_path.len() + 1);
+        } else {
+            self.path_array_offsets_builder
+                .push_length(self.parent_path.len());
+        }
+
+        let write_result = match wkb {
+            SingleWkb::Point(point) => {
+                wkb::writer::write_point(&mut self.geom_builder, &point, 
&Default::default())
+            }
+            SingleWkb::LineString(line_string) => 
wkb::writer::write_line_string(
+                &mut self.geom_builder,
+                &line_string,
+                &Default::default(),
+            ),
+            SingleWkb::Polygon(polygon) => {
+                wkb::writer::write_polygon(&mut self.geom_builder, &polygon, 
&Default::default())
+            }
+        };
+        if let Err(e) = write_result {
+            return sedona_internal_err!("Failed to write WKB: {e}");
+        }
+
+        self.geom_builder.append_value([]);
+
+        Ok(())
+    }
+
+    fn append_structs(&mut self, wkb: &wkb::reader::Wkb<'_>) -> Result<i32> {
+        match wkb.as_type() {
+            GeometryType::Point(point) => {
+                self.append_single_struct(None, SingleWkb::Point(point))?;
+                Ok(1)
+            }
+            GeometryType::LineString(line_string) => {
+                self.append_single_struct(None, 
SingleWkb::LineString(line_string))?;
+                Ok(1)
+            }
+            GeometryType::Polygon(polygon) => {
+                self.append_single_struct(None, SingleWkb::Polygon(polygon))?;
+                Ok(1)
+            }
+            GeometryType::MultiPoint(multi_point) => {
+                for (index, point) in multi_point.points().enumerate() {
+                    self.append_single_struct(Some((index + 1) as _), 
SingleWkb::Point(&point))?;
+                }
+                Ok(multi_point.num_points() as _)
+            }
+            GeometryType::MultiLineString(multi_line_string) => {
+                for (index, line_string) in 
multi_line_string.line_strings().enumerate() {
+                    self.append_single_struct(
+                        Some((index + 1) as _),
+                        SingleWkb::LineString(line_string),
+                    )?;
+                }
+                Ok(multi_line_string.num_line_strings() as _)
+            }
+            GeometryType::MultiPolygon(multi_polygon) => {
+                for (index, polygon) in multi_polygon.polygons().enumerate() {
+                    self.append_single_struct(Some((index + 1) as _), 
SingleWkb::Polygon(polygon))?;
+                }
+                Ok(multi_polygon.num_polygons() as _)
+            }
+            GeometryType::GeometryCollection(geometry_collection) => {
+                let mut num_geometries: i32 = 0;
+
+                self.parent_path.push(0); // add an index for the next nested 
level
+
+                for geometry in geometry_collection.geometries() {
+                    // increment the index
+                    if let Some(index) = self.parent_path.last_mut() {
+                        *index += 1;
+                    }
+                    num_geometries += self.append_structs(geometry)?;
+                }
+
+                self.parent_path.truncate(self.parent_path.len() - 1); // 
clear the index before returning to the upper level
+
+                Ok(num_geometries)
+            }
+            _ => sedona_internal_err!("Invalid geometry type"),
+        }
+    }
+
+    fn append(&mut self, wkb: &wkb::reader::Wkb<'_>) -> Result<()> {
+        self.parent_path.clear();
+
+        let num_geometries = self.append_structs(wkb)?;
+        self.null_builder.append(true);
+        self.struct_offsets_builder
+            .push_length(num_geometries as usize);
+        Ok(())
+    }
+
+    fn append_null(&mut self) {
+        self.path_array_offsets_builder.push_length(0);
+        self.geom_builder.append_null();
+        self.struct_offsets_builder.push_length(1);
+        self.null_builder.append(false);
+    }
+
+    fn finish(mut self) -> ListArray {
+        let path_array = Arc::new(self.path_array_builder.finish());
+        let path_offsets = self.path_array_offsets_builder.finish();
+        let geom_array = self.geom_builder.finish();
+
+        let path_field = Arc::new(Field::new("item", DataType::UInt32, true));
+        let path_list = ListArray::new(path_field, path_offsets, path_array, 
None);
+
+        let fields = Fields::from(vec![
+            Field::new(
+                "path",
+                DataType::List(Arc::new(Field::new("item", DataType::UInt32, 
true))),
+                true,
+            ),
+            Field::new("geom", DataType::Binary, true),
+        ]);
+        let struct_array = StructArray::try_new(
+            fields.clone(),
+            vec![Arc::new(path_list), Arc::new(geom_array)],
+            None,
+        )
+        .unwrap();
+        let struct_offsets = self.struct_offsets_builder.finish();
+        let struct_field = Arc::new(Field::new("item", 
DataType::Struct(fields), true));
+        let nulls = self.null_builder.finish();
+        ListArray::new(struct_field, struct_offsets, Arc::new(struct_array), 
nulls)
+    }
+}
+
+impl SedonaScalarKernel for STDump {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(vec![ArgMatcher::is_geometry()], 
geometry_dump_type());
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+
+        let mut builder = STDumpBuilder::new(executor.num_iterations());
+
+        executor.execute_wkb_void(|maybe_wkb| {
+            if let Some(wkb) = maybe_wkb {
+                builder.append(&wkb)?;
+            } else {
+                builder.append_null();
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+fn geometry_dump_fields() -> Fields {
+    let path = Field::new(
+        "path",
+        DataType::List(Field::new("item", DataType::UInt32, true).into()),
+        true,
+    );
+    let geom = WKB_GEOMETRY.to_storage_field("geom", true).unwrap();
+    vec![path, geom].into()
+}
+
+fn geometry_dump_type() -> SedonaType {
+    let fields = geometry_dump_fields();
+    let struct_type = DataType::Struct(fields);
+
+    SedonaType::Arrow(DataType::List(Field::new("item", struct_type, 
true).into()))
+}
+
+#[cfg(test)]
+mod tests {
+    use arrow_array::{Array, ArrayRef, ListArray, StructArray, UInt32Array};
+    use datafusion_expr::ScalarUDF;
+    use rstest::rstest;
+    use sedona_schema::datatypes::WKB_VIEW_GEOMETRY;
+    use sedona_testing::{
+        compare::assert_array_equal, create::create_array, 
testers::ScalarUdfTester,
+    };
+
+    use super::*;
+
+    #[test]
+    fn udf_metadata() {
+        let st_dump_udf: ScalarUDF = st_dump_udf().into();
+        assert_eq!(st_dump_udf.name(), "st_dump");
+        assert!(st_dump_udf.documentation().is_some());
+    }
+
+    #[rstest]
+    fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) 
{
+        let tester = ScalarUdfTester::new(st_dump_udf().into(), 
vec![sedona_type.clone()]);
+
+        let input = create_array(
+            &[
+                Some("POINT (1 2)"),
+                Some("LINESTRING (1 1, 2 2)"),
+                Some("POLYGON ((1 1, 2 2, 2 1, 1 1))"),
+                Some("MULTIPOINT (1 1, 2 2)"),
+                Some("MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4))"),
+                Some("MULTIPOLYGON (((1 1, 2 2, 2 1, 1 1)), EMPTY, ((3 3, 4 4, 
4 3, 3 3)))"),
+                Some("GEOMETRYCOLLECTION (POINT (1 2), MULTILINESTRING ((1 1, 
2 2), EMPTY, (3 3, 4 4)), LINESTRING (1 1, 2 2))"),
+                Some("GEOMETRYCOLLECTION (POINT (1 2), GEOMETRYCOLLECTION 
(MULTILINESTRING ((1 1, 2 2), EMPTY, (3 3, 4 4)), LINESTRING (1 1, 2 2)))"),
+            ],
+            &sedona_type,
+        );
+        let result = tester.invoke_array(input).unwrap();
+        assert_dump_row(&result, 0, &[(&[], Some("POINT (1 2)"))]);
+        assert_dump_row(&result, 1, &[(&[], Some("LINESTRING (1 1, 2 2)"))]);
+        assert_dump_row(&result, 2, &[(&[], Some("POLYGON ((1 1, 2 2, 2 1, 1 
1))"))]);
+        assert_dump_row(
+            &result,
+            3,
+            &[(&[1], Some("POINT (1 1)")), (&[2], Some("POINT (2 2)"))],
+        );
+        assert_dump_row(
+            &result,
+            4,
+            &[
+                (&[1], Some("LINESTRING (1 1, 2 2)")),
+                (&[2], Some("LINESTRING EMPTY")),
+                (&[3], Some("LINESTRING (3 3, 4 4)")),
+            ],
+        );
+        assert_dump_row(
+            &result,
+            5,
+            &[
+                (&[1], Some("POLYGON ((1 1, 2 2, 2 1, 1 1))")),
+                (&[2], Some("POLYGON EMPTY")),
+                (&[3], Some("POLYGON ((3 3, 4 4, 4 3, 3 3)))")),
+            ],
+        );
+        assert_dump_row(
+            &result,
+            6,
+            &[
+                (&[1], Some("POINT (1 2)")),
+                (&[2, 1], Some("LINESTRING (1 1, 2 2)")),
+                (&[2, 2], Some("LINESTRING EMPTY")),
+                (&[2, 3], Some("LINESTRING (3 3, 4 4)")),
+                (&[3], Some("LINESTRING (1 1, 2 2)")),
+            ],
+        );
+        assert_dump_row(
+            &result,
+            7,
+            &[
+                (&[1], Some("POINT (1 2)")),
+                (&[2, 1, 1], Some("LINESTRING (1 1, 2 2)")),
+                (&[2, 1, 2], Some("LINESTRING EMPTY")),
+                (&[2, 1, 3], Some("LINESTRING (3 3, 4 4)")),
+                (&[2, 2], Some("LINESTRING (1 1, 2 2)")),
+            ],
+        );
+
+        let null_input = create_array(&[None], &sedona_type);
+        let result = tester.invoke_array(null_input).unwrap();
+        assert_dump_row_null(&result, 0);
+    }
+
+    fn assert_dump_row(result: &ArrayRef, row: usize, expected: &[(&[u32], 
Option<&str>)]) {
+        let list_array = result
+            .as_ref()
+            .as_any()
+            .downcast_ref::<ListArray>()
+            .expect("result should be a ListArray");
+        assert!(
+            !list_array.is_null(row),
+            "row {row} should not be null in dump result"
+        );
+        let dumped = list_array.value(row);
+        let dumped = dumped
+            .as_ref()
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .expect("list elements should be StructArray");
+        assert_eq!(dumped.len(), expected.len());
+
+        let path_array = dumped
+            .column(0)
+            .as_ref()
+            .as_any()
+            .downcast_ref::<ListArray>()
+            .expect("path should be a ListArray");
+        assert_eq!(path_array.len(), expected.len());
+        for (i, (expected_path, _)) in expected.iter().enumerate() {
+            let path_array_value = path_array.value(i);
+            let path_values = path_array_value
+                .as_ref()
+                .as_any()
+                .downcast_ref::<UInt32Array>()
+                .expect("path values should be UInt32Array");
+            assert_eq!(
+                path_values.len(),
+                expected_path.len(),
+                "unexpected path length at index {i}"
+            );
+            for (j, expected_value) in expected_path.iter().enumerate() {
+                assert_eq!(
+                    path_values.value(j),
+                    *expected_value,
+                    "unexpected path value at index {i}:{j}"
+                );
+            }
+        }
+
+        let expected_geom_values: Vec<Option<&str>> =
+            expected.iter().map(|(_, geom)| *geom).collect();
+        let expected_geom_array = create_array(&expected_geom_values, 
&WKB_GEOMETRY);
+        assert_array_equal(dumped.column(1), &expected_geom_array);
+    }
+
+    fn assert_dump_row_null(result: &ArrayRef, row: usize) {
+        let list_array = result
+            .as_ref()
+            .as_any()
+            .downcast_ref::<ListArray>()
+            .expect("result should be a ListArray");
+        assert!(list_array.is_null(row), "row {row} should be null");
+    }
+}

Reply via email to