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 d5a36f0  feat(rust/sedona-raster-functions): Add a first raster 
function - RS_Width (#268)
d5a36f0 is described below

commit d5a36f0f443d6df11e3522e42b625826c48dc3af
Author: jp <[email protected]>
AuthorDate: Wed Nov 5 10:57:45 2025 -0800

    feat(rust/sedona-raster-functions): Add a first raster function - RS_Width 
(#268)
---
 Cargo.lock                                         |  20 ++-
 Cargo.toml                                         |   1 +
 .../Cargo.toml                                     |   7 +-
 rust/sedona-raster-functions/src/executor.rs       | 187 +++++++++++++++++++++
 .../src/lib.rs                                     |  12 +-
 .../src/register.rs}                               |  36 +++-
 rust/sedona-raster-functions/src/rs_size.rs        | 128 ++++++++++++++
 rust/sedona-raster/Cargo.toml                      |   3 +
 rust/sedona-raster/src/array.rs                    |  23 ++-
 rust/sedona-raster/src/builder.rs                  |   2 +-
 rust/sedona-schema/src/matchers.rs                 |  11 +-
 rust/sedona-testing/Cargo.toml                     |   1 +
 rust/sedona-testing/src/lib.rs                     |   1 +
 rust/sedona-testing/src/rasters.rs                 | 118 +++++++++++++
 14 files changed, 527 insertions(+), 23 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f34de99..bf0e67e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "abi_stable"
@@ -5127,6 +5127,23 @@ dependencies = [
  "arrow-schema",
  "sedona-common",
  "sedona-schema",
+ "sedona-testing",
+]
+
+[[package]]
+name = "sedona-raster-functions"
+version = "0.2.0"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-schema",
+ "datafusion-common",
+ "datafusion-expr",
+ "sedona-common",
+ "sedona-expr",
+ "sedona-raster",
+ "sedona-schema",
+ "sedona-testing",
 ]
 
 [[package]]
@@ -5224,6 +5241,7 @@ dependencies = [
  "sedona-common",
  "sedona-expr",
  "sedona-geometry",
+ "sedona-raster",
  "sedona-schema",
  "wkb",
  "wkt 0.14.0",
diff --git a/Cargo.toml b/Cargo.toml
index 909676e..5d4bc5c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ members = [
     "rust/sedona-geometry",
     "rust/sedona-geoparquet",
     "rust/sedona-raster",
+    "rust/sedona-raster-functions",
     "rust/sedona-schema",
     "rust/sedona-spatial-join",
     "rust/sedona-testing",
diff --git a/rust/sedona-raster/Cargo.toml 
b/rust/sedona-raster-functions/Cargo.toml
similarity index 83%
copy from rust/sedona-raster/Cargo.toml
copy to rust/sedona-raster-functions/Cargo.toml
index 91f35dc..d384e7e 100644
--- a/rust/sedona-raster/Cargo.toml
+++ b/rust/sedona-raster-functions/Cargo.toml
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 [package]
-name = "sedona-raster"
+name = "sedona-raster-functions"
 version.workspace = true
 homepage.workspace = true
 repository.workspace = true
@@ -31,5 +31,10 @@ result_large_err = "allow"
 arrow-schema = { workspace = true }
 arrow-array = { workspace = true }
 arrow-buffer = { workspace = true }
+datafusion-common = { workspace = true }
+datafusion-expr = { workspace = true }
 sedona-common = { path = "../sedona-common" }
+sedona-expr = { path = "../sedona-expr" }
+sedona-raster = { path = "../sedona-raster" }
 sedona-schema = { path = "../sedona-schema" }
+sedona-testing = { path = "../sedona-testing" }
diff --git a/rust/sedona-raster-functions/src/executor.rs 
b/rust/sedona-raster-functions/src/executor.rs
new file mode 100644
index 0000000..75123e5
--- /dev/null
+++ b/rust/sedona-raster-functions/src/executor.rs
@@ -0,0 +1,187 @@
+// 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::{Array, ArrayRef, StructArray};
+use datafusion_common::error::Result;
+use datafusion_common::{DataFusionError, ScalarValue};
+use datafusion_expr::ColumnarValue;
+use sedona_common::sedona_internal_err;
+use sedona_raster::array::{RasterRefImpl, RasterStructArray};
+use sedona_schema::datatypes::SedonaType;
+use sedona_schema::datatypes::RASTER;
+
+/// Helper for writing raster kernel implementations
+///
+/// The [RasterExecutor] provides a simplified interface for executing 
functions
+/// on raster arrays, handling the common pattern of downcasting to 
StructArray,
+/// creating raster iterators, and handling null values.
+pub struct RasterExecutor<'a, 'b> {
+    pub arg_types: &'a [SedonaType],
+    pub args: &'b [ColumnarValue],
+    num_iterations: usize,
+}
+
+impl<'a, 'b> RasterExecutor<'a, 'b> {
+    /// Create a new [RasterExecutor]
+    pub fn new(arg_types: &'a [SedonaType], args: &'b [ColumnarValue]) -> Self 
{
+        Self {
+            arg_types,
+            args,
+            num_iterations: Self::calc_num_iterations(args),
+        }
+    }
+
+    /// Return the number of iterations that will be performed
+    pub fn num_iterations(&self) -> usize {
+        self.num_iterations
+    }
+
+    /// Execute a function by iterating over rasters in the first argument
+    ///
+    /// This handles the common pattern of:
+    /// 1. Downcasting array to StructArray
+    /// 2. Creating raster iterator
+    /// 3. Iterating with null checks
+    /// 4. Calling the provided function with each raster
+    pub fn execute_raster_void<F>(&self, mut func: F) -> Result<()>
+    where
+        F: FnMut(usize, Option<RasterRefImpl<'_>>) -> Result<()>,
+    {
+        if self.arg_types[0] != RASTER {
+            return sedona_internal_err!("First argument must be a raster 
type");
+        }
+        let raster_array = match &self.args[0] {
+            ColumnarValue::Array(array) => array,
+            ColumnarValue::Scalar(_) => {
+                return Err(DataFusionError::NotImplemented(
+                    "Scalar raster input not yet supported".to_string(),
+                ));
+            }
+        };
+
+        // Downcast to StructArray (rasters are stored as structs)
+        let raster_struct = raster_array
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .ok_or_else(|| {
+                DataFusionError::Internal("Expected StructArray for raster 
data".to_string())
+            })?;
+
+        // Create raster iterator
+        let raster_array = RasterStructArray::new(raster_struct);
+
+        // Iterate through each raster in the array
+        for i in 0..self.num_iterations {
+            if raster_array.is_null(i) {
+                func(i, None)?;
+                continue;
+            }
+            let raster = raster_array.get(i)?;
+
+            func(i, Some(raster))?;
+        }
+
+        Ok(())
+    }
+
+    /// Finish an [ArrayRef] output as the appropriate [ColumnarValue]
+    ///
+    /// Converts the output into a [ColumnarValue::Scalar] if all arguments 
were scalars,
+    /// or a [ColumnarValue::Array] otherwise.
+    pub fn finish(&self, out: ArrayRef) -> Result<ColumnarValue> {
+        for arg in self.args {
+            match arg {
+                // If any argument was an array, we return an array
+                ColumnarValue::Array(_) => {
+                    return Ok(ColumnarValue::Array(out));
+                }
+                ColumnarValue::Scalar(_) => {}
+            }
+        }
+
+        // For all scalar arguments, we return a scalar
+        Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(&out, 0)?))
+    }
+
+    /// Calculates the number of iterations that should happen based on the
+    /// argument ColumnarValue types
+    fn calc_num_iterations(args: &[ColumnarValue]) -> usize {
+        for arg in args {
+            match arg {
+                // If any argument is an array, we have to iterate array.len() 
times
+                ColumnarValue::Array(array) => {
+                    return array.len();
+                }
+                ColumnarValue::Scalar(_) => {}
+            }
+        }
+
+        // All scalars: we iterate once
+        1
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use arrow_array::builder::UInt64Builder;
+    use arrow_array::UInt64Array;
+    use sedona_raster::traits::RasterRef;
+    use sedona_schema::datatypes::RASTER;
+    use sedona_testing::rasters::generate_test_rasters;
+    use std::sync::Arc;
+
+    #[test]
+    fn test_raster_executor_execute_raster_void() {
+        // 3 rasters, second one is null
+        let rasters = generate_test_rasters(3, Some(1)).unwrap();
+        let args = [ColumnarValue::Array(Arc::new(rasters))];
+        let arg_types = vec![RASTER];
+
+        let executor = RasterExecutor::new(&arg_types, &args);
+        assert_eq!(executor.num_iterations(), 3);
+
+        let mut builder = 
UInt64Builder::with_capacity(executor.num_iterations());
+        executor
+            .execute_raster_void(|_i, raster_opt| {
+                match raster_opt {
+                    None => builder.append_null(),
+                    Some(raster) => {
+                        let width = raster.metadata().width();
+                        builder.append_value(width);
+                    }
+                }
+                Ok(())
+            })
+            .unwrap();
+
+        let result = executor.finish(Arc::new(builder.finish())).unwrap();
+
+        let width_array = match &result {
+            ColumnarValue::Array(array) => array
+                .as_any()
+                .downcast_ref::<UInt64Array>()
+                .expect("Expected UInt64Array"),
+            ColumnarValue::Scalar(_) => panic!("Expected array, got scalar"),
+        };
+
+        assert_eq!(width_array.len(), 3);
+        assert_eq!(width_array.value(0), 1);
+        assert!(width_array.is_null(1));
+        assert_eq!(width_array.value(2), 3);
+    }
+}
diff --git a/rust/sedona-testing/src/lib.rs 
b/rust/sedona-raster-functions/src/lib.rs
similarity index 85%
copy from rust/sedona-testing/src/lib.rs
copy to rust/sedona-raster-functions/src/lib.rs
index 6652955..86aea00 100644
--- a/rust/sedona-testing/src/lib.rs
+++ b/rust/sedona-raster-functions/src/lib.rs
@@ -14,11 +14,7 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-pub mod benchmark_util;
-pub mod compare;
-pub mod create;
-pub mod data;
-pub mod datagen;
-pub mod fixtures;
-pub mod read;
-pub mod testers;
+
+mod executor;
+pub mod register;
+pub mod rs_size;
diff --git a/rust/sedona-testing/src/lib.rs 
b/rust/sedona-raster-functions/src/register.rs
similarity index 51%
copy from rust/sedona-testing/src/lib.rs
copy to rust/sedona-raster-functions/src/register.rs
index 6652955..7499892 100644
--- a/rust/sedona-testing/src/lib.rs
+++ b/rust/sedona-raster-functions/src/register.rs
@@ -14,11 +14,31 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-pub mod benchmark_util;
-pub mod compare;
-pub mod create;
-pub mod data;
-pub mod datagen;
-pub mod fixtures;
-pub mod read;
-pub mod testers;
+use sedona_expr::function_set::FunctionSet;
+
+/// Export the set of functions defined in this crate
+pub fn default_function_set() -> FunctionSet {
+    let mut function_set = FunctionSet::new();
+
+    macro_rules! register_scalar_udfs {
+        ($function_set:expr, $($udf:expr),* $(,)?) => {
+            $(
+                $function_set.insert_scalar_udf($udf());
+            )*
+        };
+    }
+
+    macro_rules! register_aggregate_udfs {
+        ($function_set:expr, $($udf:expr),* $(,)?) => {
+            $(
+                $function_set.insert_aggregate_udf($udf());
+            )*
+        };
+    }
+
+    register_scalar_udfs!(function_set, crate::rs_size::rs_width_udf,);
+
+    register_aggregate_udfs!(function_set,);
+
+    function_set
+}
diff --git a/rust/sedona-raster-functions/src/rs_size.rs 
b/rust/sedona-raster-functions/src/rs_size.rs
new file mode 100644
index 0000000..6b6d200
--- /dev/null
+++ b/rust/sedona-raster-functions/src/rs_size.rs
@@ -0,0 +1,128 @@
+// 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 std::{sync::Arc, vec};
+
+use crate::executor::RasterExecutor;
+use arrow_array::builder::UInt64Builder;
+use arrow_schema::DataType;
+use datafusion_common::error::Result;
+use datafusion_expr::{
+    scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, 
Volatility,
+};
+use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_raster::traits::RasterRef;
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+
+/// RS_Width() scalar UDF implementation
+///
+/// Extract the width of the raster
+pub fn rs_width_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "rs_width",
+        vec![Arc::new(RsWidth {})],
+        Volatility::Immutable,
+        Some(rs_width_doc()),
+    )
+}
+
+fn rs_width_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Return the width component of a raster".to_string(),
+        "RS_Width(raster: Raster)".to_string(),
+    )
+    .with_argument("raster", "Raster: Input raster")
+    .with_sql_example("SELECT RS_Width(raster)".to_string())
+    .build()
+}
+
+#[derive(Debug)]
+struct RsWidth {}
+
+impl SedonaScalarKernel for RsWidth {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_raster()],
+            SedonaType::Arrow(DataType::UInt64),
+        );
+
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = RasterExecutor::new(arg_types, args);
+        let mut builder = 
UInt64Builder::with_capacity(executor.num_iterations());
+
+        executor.execute_raster_void(|_i, raster_opt| {
+            match raster_opt {
+                None => builder.append_null(),
+                Some(raster) => {
+                    let width = raster.metadata().width();
+                    builder.append_value(width);
+                }
+            }
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use arrow_array::{Array, UInt64Array};
+    use datafusion_expr::ScalarUDF;
+    use sedona_schema::datatypes::RASTER;
+    use sedona_testing::rasters::generate_test_rasters;
+
+    #[test]
+    fn udf_size() {
+        let udf: ScalarUDF = rs_width_udf().into();
+        assert_eq!(udf.name(), "rs_width");
+        assert!(udf.documentation().is_some());
+    }
+
+    #[test]
+    fn udf_invoke() {
+        // 3 rasters, second one is null
+        let rasters = generate_test_rasters(3, Some(1)).unwrap();
+
+        // Create the UDF and invoke it
+        let kernel = RsWidth {};
+        let args = [ColumnarValue::Array(Arc::new(rasters))];
+        let arg_types = vec![RASTER];
+
+        let result = kernel.invoke_batch(&arg_types, &args).unwrap();
+
+        // Check the result
+        if let ColumnarValue::Array(result_array) = result {
+            let width_array = 
result_array.as_any().downcast_ref::<UInt64Array>().unwrap();
+
+            assert_eq!(width_array.len(), 3);
+            assert_eq!(width_array.value(0), 1); // First raster width
+            assert!(width_array.is_null(1)); // Second raster is null
+            assert_eq!(width_array.value(2), 3); // Third raster width
+        } else {
+            panic!("Expected array result");
+        }
+    }
+}
diff --git a/rust/sedona-raster/Cargo.toml b/rust/sedona-raster/Cargo.toml
index 91f35dc..1a8562f 100644
--- a/rust/sedona-raster/Cargo.toml
+++ b/rust/sedona-raster/Cargo.toml
@@ -33,3 +33,6 @@ arrow-array = { workspace = true }
 arrow-buffer = { workspace = true }
 sedona-common = { path = "../sedona-common" }
 sedona-schema = { path = "../sedona-schema" }
+
+[dev-dependencies]
+sedona-testing = { path = "../sedona-testing" }
diff --git a/rust/sedona-raster/src/array.rs b/rust/sedona-raster/src/array.rs
index 6e572e4..f7236a3 100644
--- a/rust/sedona-raster/src/array.rs
+++ b/rust/sedona-raster/src/array.rs
@@ -467,12 +467,19 @@ impl<'a> RasterStructArray<'a> {
     }
 
     /// Get a specific raster by index without consuming the iterator
-    pub fn get(&self, index: usize) -> Option<RasterRefImpl<'a>> {
+    pub fn get(&self, index: usize) -> Result<RasterRefImpl<'a>, ArrowError> {
         if index >= self.raster_array.len() {
-            return None;
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Invalid raster index: {}",
+                index
+            )));
         }
 
-        Some(RasterRefImpl::new(self.raster_array, index))
+        Ok(RasterRefImpl::new(self.raster_array, index))
+    }
+
+    pub fn is_null(&self, index: usize) -> bool {
+        self.raster_array.is_null(index)
     }
 }
 
@@ -482,6 +489,7 @@ mod tests {
     use crate::builder::RasterBuilder;
     use crate::traits::{BandMetadata, RasterMetadata};
     use sedona_schema::raster::{BandDataType, StorageType};
+    use sedona_testing::rasters::generate_test_rasters;
 
     #[test]
     fn test_array_basic_functionality() {
@@ -621,4 +629,13 @@ mod tests {
 
         assert_eq!(band_values, vec![0, 1, 2]);
     }
+
+    #[test]
+    fn test_raster_is_null() {
+        let raster_array = generate_test_rasters(2, Some(1)).unwrap();
+        let rasters = RasterStructArray::new(&raster_array);
+        assert_eq!(rasters.len(), 2);
+        assert!(!rasters.is_null(0));
+        assert!(rasters.is_null(1));
+    }
 }
diff --git a/rust/sedona-raster/src/builder.rs 
b/rust/sedona-raster/src/builder.rs
index 357486a..3ad1e37 100644
--- a/rust/sedona-raster/src/builder.rs
+++ b/rust/sedona-raster/src/builder.rs
@@ -256,7 +256,7 @@ impl RasterBuilder {
         self.band_offsets.push(current_offset);
 
         // Mark raster as null
-        self.raster_validity.append_value(false);
+        self.raster_validity.append_null();
 
         Ok(())
     }
diff --git a/rust/sedona-schema/src/matchers.rs 
b/rust/sedona-schema/src/matchers.rs
index 4935f43..ca7536c 100644
--- a/rust/sedona-schema/src/matchers.rs
+++ b/rust/sedona-schema/src/matchers.rs
@@ -21,7 +21,7 @@ use arrow_schema::DataType;
 use datafusion_common::{plan_err, Result};
 use sedona_common::sedona_internal_err;
 
-use crate::datatypes::{Edges, SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY};
+use crate::datatypes::{Edges, SedonaType, RASTER, WKB_GEOGRAPHY, WKB_GEOMETRY};
 
 /// Helper to match arguments and compute return types
 #[derive(Debug)]
@@ -173,6 +173,11 @@ impl ArgMatcher {
         Arc::new(IsGeography {})
     }
 
+    /// Matches any raster argument
+    pub fn is_raster() -> Arc<dyn TypeMatcher + Send + Sync> {
+        Self::is_exact(RASTER)
+    }
+
     /// Matches a null argument
     pub fn is_null() -> Arc<dyn TypeMatcher + Send + Sync> {
         Arc::new(IsNull {})
@@ -506,6 +511,10 @@ mod tests {
             ArgMatcher::is_boolean().type_if_null(),
             Some(SedonaType::Arrow(DataType::Boolean))
         );
+
+        assert!(ArgMatcher::is_raster().match_type(&RASTER));
+        
assert!(!ArgMatcher::is_raster().match_type(&SedonaType::Arrow(DataType::Int32)));
+        assert!(!ArgMatcher::is_raster().match_type(&WKB_GEOMETRY));
     }
 
     #[test]
diff --git a/rust/sedona-testing/Cargo.toml b/rust/sedona-testing/Cargo.toml
index d0c058f..f467aad 100644
--- a/rust/sedona-testing/Cargo.toml
+++ b/rust/sedona-testing/Cargo.toml
@@ -50,6 +50,7 @@ rand = { workspace = true }
 sedona-common = { path = "../sedona-common" }
 sedona-geometry = { path = "../sedona-geometry" }
 sedona-expr = { path = "../sedona-expr" }
+sedona-raster = { path = "../sedona-raster" }
 sedona-schema = { path = "../sedona-schema" }
 wkb = { workspace = true }
 wkt = { workspace = true }
diff --git a/rust/sedona-testing/src/lib.rs b/rust/sedona-testing/src/lib.rs
index 6652955..6fcd0d6 100644
--- a/rust/sedona-testing/src/lib.rs
+++ b/rust/sedona-testing/src/lib.rs
@@ -20,5 +20,6 @@ pub mod create;
 pub mod data;
 pub mod datagen;
 pub mod fixtures;
+pub mod rasters;
 pub mod read;
 pub mod testers;
diff --git a/rust/sedona-testing/src/rasters.rs 
b/rust/sedona-testing/src/rasters.rs
new file mode 100644
index 0000000..826024f
--- /dev/null
+++ b/rust/sedona-testing/src/rasters.rs
@@ -0,0 +1,118 @@
+// 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::StructArray;
+use arrow_schema::ArrowError;
+use sedona_raster::builder::RasterBuilder;
+use sedona_raster::traits::{BandMetadata, RasterMetadata};
+use sedona_schema::raster::{BandDataType, StorageType};
+
+/// Generate a StructArray of rasters with sequentially increasing dimensions 
and pixel values
+/// These tiny rasters are to provide fast, easy and predictable test data for 
unit tests.
+pub fn generate_test_rasters(
+    count: usize,
+    null_raster_index: Option<usize>,
+) -> Result<StructArray, ArrowError> {
+    let mut builder = RasterBuilder::new(count);
+    for i in 0..count {
+        // If a null raster index is specified and that matches the current 
index,
+        // append a null raster
+        if matches!(null_raster_index, Some(index) if index == i) {
+            builder.append_null()?;
+            continue;
+        }
+
+        let raster_metadata = RasterMetadata {
+            width: i as u64 + 1,
+            height: i as u64 + 2,
+            upperleft_x: i as f64 + 1.0,
+            upperleft_y: i as f64 + 2.0,
+            scale_x: i as f64 * 0.1,
+            scale_y: i as f64 * 0.2,
+            skew_x: i as f64 * 0.3,
+            skew_y: i as f64 * 0.4,
+        };
+        builder.start_raster(&raster_metadata, None)?;
+        builder.start_band(BandMetadata {
+            datatype: BandDataType::UInt16,
+            nodata_value: Some(vec![0u8; 2]),
+            storage_type: StorageType::InDb,
+            outdb_url: None,
+            outdb_band_id: None,
+        })?;
+
+        let pixel_count = (i + 1) * (i + 2); // width * height
+        let mut band_data = Vec::with_capacity(pixel_count * 2); // 2 bytes 
per u16
+        for pixel_value in 0..pixel_count as u16 {
+            band_data.extend_from_slice(&pixel_value.to_le_bytes());
+        }
+
+        builder.band_data_writer().append_value(&band_data);
+        builder.finish_band()?;
+        builder.finish_raster()?;
+    }
+
+    builder.finish()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use sedona_raster::array::RasterStructArray;
+    use sedona_raster::traits::RasterRef;
+
+    #[test]
+    fn test_generate_test_rasters() {
+        let count = 5;
+        let struct_array = generate_test_rasters(count, None).unwrap();
+        let raster_array = RasterStructArray::new(&struct_array);
+        assert_eq!(raster_array.len(), count);
+
+        for i in 0..count {
+            let raster = raster_array.get(i).unwrap();
+            let metadata = raster.metadata();
+            assert_eq!(metadata.width(), i as u64 + 1);
+            assert_eq!(metadata.height(), i as u64 + 2);
+            assert_eq!(metadata.upper_left_x(), i as f64 + 1.0);
+            assert_eq!(metadata.upper_left_y(), i as f64 + 2.0);
+            assert_eq!(metadata.scale_x(), (i as f64) * 0.1);
+            assert_eq!(metadata.scale_y(), (i as f64) * 0.2);
+            assert_eq!(metadata.skew_x(), (i as f64) * 0.3);
+            assert_eq!(metadata.skew_y(), (i as f64) * 0.4);
+
+            let bands = raster.bands();
+            let band = bands.band(1).unwrap();
+            let band_metadata = band.metadata();
+            assert_eq!(band_metadata.data_type(), BandDataType::UInt16);
+            assert_eq!(band_metadata.nodata_value(), Some(&[0u8, 0u8][..]));
+            assert_eq!(band_metadata.storage_type(), StorageType::InDb);
+            assert_eq!(band_metadata.outdb_url(), None);
+            assert_eq!(band_metadata.outdb_band_id(), None);
+
+            let band_data = band.data();
+            let expected_pixel_count = (i + 1) * (i + 2); // width * height
+
+            // Convert raw bytes back to u16 values for comparison
+            let mut actual_pixel_values = Vec::new();
+            for chunk in band_data.chunks_exact(2) {
+                let value = u16::from_le_bytes([chunk[0], chunk[1]]);
+                actual_pixel_values.push(value);
+            }
+            let expected_pixel_values: Vec<u16> = (0..expected_pixel_count as 
u16).collect();
+            assert_eq!(actual_pixel_values, expected_pixel_values);
+        }
+    }
+}

Reply via email to