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 963cd940 feat(rust/sedona-raster-functions): add RS_BandPath function 
(#603)
963cd940 is described below

commit 963cd940e0fb18a4626c13fbb7053884bc868a9b
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Feb 19 23:12:41 2026 +0800

    feat(rust/sedona-raster-functions): add RS_BandPath function (#603)
---
 .../benches/native-raster-functions.rs             |  14 +
 rust/sedona-raster-functions/src/lib.rs            |   2 +
 rust/sedona-raster-functions/src/raster_utils.rs   |  58 ++++
 rust/sedona-raster-functions/src/register.rs       |   1 +
 rust/sedona-raster-functions/src/rs_bandpath.rs    | 363 +++++++++++++++++++++
 rust/sedona-raster/src/traits.rs                   | 100 ++++++
 rust/sedona-schema/src/raster.rs                   |  57 ++++
 7 files changed, 595 insertions(+)

diff --git a/rust/sedona-raster-functions/benches/native-raster-functions.rs 
b/rust/sedona-raster-functions/benches/native-raster-functions.rs
index eae081ac..37f32236 100644
--- a/rust/sedona-raster-functions/benches/native-raster-functions.rs
+++ b/rust/sedona-raster-functions/benches/native-raster-functions.rs
@@ -20,6 +20,20 @@ use sedona_testing::benchmark_util::{benchmark, 
BenchmarkArgSpec::*, BenchmarkAr
 fn criterion_benchmark(c: &mut Criterion) {
     let f = sedona_raster_functions::register::default_function_set();
 
+    benchmark::scalar(
+        c,
+        &f,
+        "native-raster",
+        "rs_bandpath",
+        BenchmarkArgs::Array(Raster(64, 64)),
+    );
+    benchmark::scalar(
+        c,
+        &f,
+        "native-raster",
+        "rs_bandpath",
+        BenchmarkArgs::ArrayScalar(Raster(64, 64), Int32(1, 2)),
+    );
     benchmark::scalar(c, &f, "native-raster", "rs_convexhull", Raster(64, 64));
     benchmark::scalar(c, &f, "native-raster", "rs_crs", Raster(64, 64));
     benchmark::scalar(c, &f, "native-raster", "rs_envelope", Raster(64, 64));
diff --git a/rust/sedona-raster-functions/src/lib.rs 
b/rust/sedona-raster-functions/src/lib.rs
index 55325b1a..79b14983 100644
--- a/rust/sedona-raster-functions/src/lib.rs
+++ b/rust/sedona-raster-functions/src/lib.rs
@@ -16,7 +16,9 @@
 // under the License.
 
 mod executor;
+pub mod raster_utils;
 pub mod register;
+pub mod rs_bandpath;
 pub mod rs_convexhull;
 pub mod rs_envelope;
 pub mod rs_example;
diff --git a/rust/sedona-raster-functions/src/raster_utils.rs 
b/rust/sedona-raster-functions/src/raster_utils.rs
new file mode 100644
index 00000000..1a0b44bd
--- /dev/null
+++ b/rust/sedona-raster-functions/src/raster_utils.rs
@@ -0,0 +1,58 @@
+// 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 datafusion_common::error::Result;
+use datafusion_common::exec_err;
+
+/// Validate that a 1-based band index is within `[1, num_bands]`.
+pub fn validate_band_index(band_index: i32, num_bands: usize) -> Result<()> {
+    if band_index < 1 || band_index as usize > num_bands {
+        return exec_err!(
+            "Provided band index {} is not in the range [1, {}]",
+            band_index,
+            num_bands
+        );
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_validate_band_index_valid() {
+        assert!(validate_band_index(1, 3).is_ok());
+        assert!(validate_band_index(2, 3).is_ok());
+        assert!(validate_band_index(3, 3).is_ok());
+    }
+
+    #[test]
+    fn test_validate_band_index_zero() {
+        assert!(validate_band_index(0, 3).is_err());
+    }
+
+    #[test]
+    fn test_validate_band_index_negative() {
+        assert!(validate_band_index(-1, 3).is_err());
+    }
+
+    #[test]
+    fn test_validate_band_index_out_of_range() {
+        assert!(validate_band_index(4, 3).is_err());
+    }
+}
diff --git a/rust/sedona-raster-functions/src/register.rs 
b/rust/sedona-raster-functions/src/register.rs
index fc687e1b..a4d38a26 100644
--- a/rust/sedona-raster-functions/src/register.rs
+++ b/rust/sedona-raster-functions/src/register.rs
@@ -38,6 +38,7 @@ pub fn default_function_set() -> FunctionSet {
 
     register_scalar_udfs!(
         function_set,
+        crate::rs_bandpath::rs_bandpath_udf,
         crate::rs_convexhull::rs_convexhull_udf,
         crate::rs_envelope::rs_envelope_udf,
         crate::rs_example::rs_example_udf,
diff --git a/rust/sedona-raster-functions/src/rs_bandpath.rs 
b/rust/sedona-raster-functions/src/rs_bandpath.rs
new file mode 100644
index 00000000..16d9b79b
--- /dev/null
+++ b/rust/sedona-raster-functions/src/rs_bandpath.rs
@@ -0,0 +1,363 @@
+// 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::StringBuilder;
+use arrow_array::{cast::AsArray, types::Int32Type, Array};
+use arrow_schema::DataType;
+use datafusion_common::error::Result;
+use datafusion_expr::{ColumnarValue, Volatility};
+use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_raster::traits::RasterRef;
+use sedona_schema::raster::StorageType;
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+
+/// RS_BandPath() scalar UDF implementation
+///
+/// Returns the path to the raster file referenced by the out-db band.
+/// If the band is an in-db band, this function returns null.
+/// Accepts an optional band_index parameter (1-based, default is 1).
+pub fn rs_bandpath_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "rs_bandpath",
+        vec![
+            Arc::new(RsBandPath {}),
+            Arc::new(RsBandPathWithBandIndex {}),
+        ],
+        Volatility::Immutable,
+    )
+}
+
+/// One-argument kernel: RS_BandPath(raster) - uses band 1 by default
+#[derive(Debug)]
+struct RsBandPath {}
+
+const PREALLOC_SIZE_PER_PATH: usize = 256;
+
+impl SedonaScalarKernel for RsBandPath {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_raster()],
+            SedonaType::Arrow(DataType::Utf8),
+        );
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = RasterExecutor::new(arg_types, args);
+
+        let preallocate_bytes = PREALLOC_SIZE_PER_PATH * 
executor.num_iterations();
+        let mut builder =
+            StringBuilder::with_capacity(executor.num_iterations(), 
preallocate_bytes);
+
+        executor
+            .execute_raster_void(|_i, raster_opt| get_band_path(raster_opt, 1, 
&mut builder))?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+/// Two-argument kernel: RS_BandPath(raster, band_index)
+#[derive(Debug)]
+struct RsBandPathWithBandIndex {}
+
+impl SedonaScalarKernel for RsBandPathWithBandIndex {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_raster(), ArgMatcher::is_integer()],
+            SedonaType::Arrow(DataType::Utf8),
+        );
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = RasterExecutor::new(arg_types, args);
+
+        // Expand the band_index parameter to an array
+        let band_index_array = 
args[1].clone().into_array(executor.num_iterations())?;
+        let band_index_array = band_index_array.as_primitive::<Int32Type>();
+
+        let preallocate_bytes = PREALLOC_SIZE_PER_PATH * 
executor.num_iterations();
+        let mut builder =
+            StringBuilder::with_capacity(executor.num_iterations(), 
preallocate_bytes);
+
+        executor.execute_raster_void(|i, raster_opt| {
+            let band_index = if band_index_array.is_null(i) {
+                1 // Default to band 1 if null
+            } else {
+                band_index_array.value(i)
+            };
+            get_band_path(raster_opt, band_index, &mut builder)
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+/// Get the band path for a raster at the specified band index
+fn get_band_path(
+    raster_opt: Option<&sedona_raster::array::RasterRefImpl<'_>>,
+    band_index: i32,
+    builder: &mut StringBuilder,
+) -> Result<()> {
+    match raster_opt {
+        None => builder.append_null(),
+        Some(raster) => {
+            let bands = raster.bands();
+            let num_bands = bands.len() as i32;
+            if band_index < 1 || band_index > num_bands {
+                builder.append_null();
+            } else {
+                let band = bands.band(band_index as usize)?;
+                let band_metadata = band.metadata();
+
+                if band_metadata.storage_type()? == StorageType::OutDbRef {
+                    match band_metadata.outdb_url() {
+                        Some(url) => builder.append_value(url),
+                        None => builder.append_null(),
+                    }
+                } else {
+                    builder.append_null()
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use arrow_array::{Array, Int32Array, StringArray};
+    use datafusion_common::ScalarValue;
+    use datafusion_expr::ScalarUDF;
+    use sedona_schema::datatypes::RASTER;
+    use sedona_testing::rasters::generate_test_rasters;
+    use sedona_testing::testers::ScalarUdfTester;
+
+    #[test]
+    fn udf_metadata() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        assert_eq!(udf.name(), "rs_bandpath");
+    }
+
+    #[test]
+    fn udf_bandpath_indb_rasters_default_band() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        let tester = ScalarUdfTester::new(udf, vec![RASTER]);
+
+        tester.assert_return_type(DataType::Utf8);
+
+        // Test with in-db rasters - should all return null (default 
band_index = 1)
+        let rasters = generate_test_rasters(3, Some(1)).unwrap();
+        let result = tester.invoke_array(Arc::new(rasters)).unwrap();
+
+        let string_array = result
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .expect("Expected StringArray");
+
+        // All in-db rasters should return null
+        assert!(string_array.is_null(0));
+        assert!(string_array.is_null(1));
+        assert!(string_array.is_null(2));
+    }
+
+    #[test]
+    fn udf_bandpath_indb_rasters_with_band_index() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        let tester = ScalarUdfTester::new(udf, vec![RASTER, 
SedonaType::Arrow(DataType::Int32)]);
+
+        tester.assert_return_type(DataType::Utf8);
+
+        // Test with in-db rasters and explicit band index
+        let rasters = generate_test_rasters(3, Some(3)).unwrap(); // 3 bands
+        let band_indices = Int32Array::from(vec![1, 2, 3]);
+        let result = tester
+            .invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)])
+            .unwrap();
+
+        let string_array = result
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .expect("Expected StringArray");
+
+        // All in-db bands should return null regardless of band index
+        assert!(string_array.is_null(0));
+        assert!(string_array.is_null(1));
+        assert!(string_array.is_null(2));
+    }
+
+    #[test]
+    fn udf_bandpath_invalid_band_index() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        let tester = ScalarUdfTester::new(udf, vec![RASTER, 
SedonaType::Arrow(DataType::Int32)]);
+
+        // Test with invalid band indices (out of range)
+        let rasters = generate_test_rasters(3, Some(2)).unwrap(); // 2 bands
+        let band_indices = Int32Array::from(vec![0, 3, -1]); // All invalid 
indices
+        let result = tester
+            .invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)])
+            .unwrap();
+
+        let string_array = result
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .expect("Expected StringArray");
+
+        // Invalid band indices should return null
+        assert!(string_array.is_null(0)); // band 0 is invalid (1-based)
+        assert!(string_array.is_null(1)); // band 3 is out of range
+        assert!(string_array.is_null(2)); // negative band index is invalid
+    }
+
+    /// Build a raster array with out-db bands for testing RS_BandPath.
+    /// Returns a StructArray with 3 rasters:
+    ///   [0] OutDbRef band with URL "s3://bucket/raster_0.tif"
+    ///   [1] null raster
+    ///   [2] Two bands: InDb band 1, OutDbRef band 2 with URL 
"s3://bucket/raster_2.tif"
+    fn build_outdb_rasters() -> arrow_array::StructArray {
+        use sedona_raster::builder::RasterBuilder;
+        use sedona_raster::traits::{BandMetadata, RasterMetadata};
+        use sedona_schema::raster::{BandDataType, StorageType};
+
+        let metadata = RasterMetadata {
+            width: 4,
+            height: 4,
+            upperleft_x: 0.0,
+            upperleft_y: 0.0,
+            scale_x: 1.0,
+            scale_y: -1.0,
+            skew_x: 0.0,
+            skew_y: 0.0,
+        };
+
+        let mut builder = RasterBuilder::new(3);
+
+        // Raster 0: single OutDbRef band
+        builder.start_raster(&metadata, Some("EPSG:4326")).unwrap();
+        builder
+            .start_band(BandMetadata {
+                nodata_value: None,
+                storage_type: StorageType::OutDbRef,
+                datatype: BandDataType::Float32,
+                outdb_url: Some("s3://bucket/raster_0.tif".to_string()),
+                outdb_band_id: Some(1),
+            })
+            .unwrap();
+        builder.band_data_writer().append_value([]);
+        builder.finish_band().unwrap();
+        builder.finish_raster().unwrap();
+
+        // Raster 1: null
+        builder.append_null().unwrap();
+
+        // Raster 2: two bands — InDb (band 1) + OutDbRef (band 2)
+        builder.start_raster(&metadata, Some("EPSG:4326")).unwrap();
+        builder
+            .start_band(BandMetadata {
+                nodata_value: None,
+                storage_type: StorageType::InDb,
+                datatype: BandDataType::UInt8,
+                outdb_url: None,
+                outdb_band_id: None,
+            })
+            .unwrap();
+        builder.band_data_writer().append_value([0u8; 16]);
+        builder.finish_band().unwrap();
+        builder
+            .start_band(BandMetadata {
+                nodata_value: None,
+                storage_type: StorageType::OutDbRef,
+                datatype: BandDataType::Float32,
+                outdb_url: Some("s3://bucket/raster_2.tif".to_string()),
+                outdb_band_id: Some(3),
+            })
+            .unwrap();
+        builder.band_data_writer().append_value([]);
+        builder.finish_band().unwrap();
+        builder.finish_raster().unwrap();
+
+        builder.finish().unwrap()
+    }
+
+    #[test]
+    fn udf_bandpath_outdb_rasters_default_band() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        let tester = ScalarUdfTester::new(udf, vec![RASTER]);
+
+        let rasters = build_outdb_rasters();
+        let result = tester.invoke_array(Arc::new(rasters)).unwrap();
+
+        let string_array = result
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .expect("Expected StringArray");
+
+        // Raster 0: OutDbRef band 1 → returns URL
+        assert!(!string_array.is_null(0));
+        assert_eq!(string_array.value(0), "s3://bucket/raster_0.tif");
+        // Raster 1: null raster → null
+        assert!(string_array.is_null(1));
+        // Raster 2: band 1 is InDb → null
+        assert!(string_array.is_null(2));
+    }
+
+    #[test]
+    fn udf_bandpath_outdb_rasters_with_band_index() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        let tester = ScalarUdfTester::new(udf, vec![RASTER, 
SedonaType::Arrow(DataType::Int32)]);
+
+        let rasters = build_outdb_rasters();
+        // Ask for band 1, band 1, band 2 respectively
+        let band_indices = Int32Array::from(vec![1, 1, 2]);
+        let result = tester
+            .invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)])
+            .unwrap();
+
+        let string_array = result
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .expect("Expected StringArray");
+
+        // Raster 0, band 1: OutDbRef → URL
+        assert_eq!(string_array.value(0), "s3://bucket/raster_0.tif");
+        // Raster 1: null raster → null
+        assert!(string_array.is_null(1));
+        // Raster 2, band 2: OutDbRef → URL
+        assert_eq!(string_array.value(2), "s3://bucket/raster_2.tif");
+    }
+
+    #[test]
+    fn udf_bandpath_null_scalar() {
+        let udf: ScalarUDF = rs_bandpath_udf().into();
+        let tester = ScalarUdfTester::new(udf, vec![RASTER]);
+
+        // Test with null scalar
+        let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+        tester.assert_scalar_result_equals(result, ScalarValue::Utf8(None));
+    }
+}
diff --git a/rust/sedona-raster/src/traits.rs b/rust/sedona-raster/src/traits.rs
index 98010d92..f8541ff3 100644
--- a/rust/sedona-raster/src/traits.rs
+++ b/rust/sedona-raster/src/traits.rs
@@ -108,6 +108,69 @@ pub trait BandMetadataRef {
     fn outdb_url(&self) -> Option<&str>;
     /// OutDb band ID (only used when storage_type == OutDbRef)
     fn outdb_band_id(&self) -> Option<u32>;
+
+    /// No-data value interpreted as f64.
+    ///
+    /// Returns `Ok(None)` when no nodata value is defined, `Ok(Some(f64))` on
+    /// success, or an error when the raw bytes have an unexpected length for
+    /// the band's data type.
+    fn nodata_value_as_f64(&self) -> Result<Option<f64>, ArrowError> {
+        let bytes = match self.nodata_value() {
+            Some(b) => b,
+            None => return Ok(None),
+        };
+        let dt = self.data_type()?;
+        nodata_bytes_to_f64(bytes, &dt).map(Some)
+    }
+}
+
+/// Convert raw nodata bytes to f64 given a [`BandDataType`].
+///
+/// The bytes are expected to be in little-endian order and exactly match the
+/// byte size of the data type.
+fn nodata_bytes_to_f64(bytes: &[u8], dt: &BandDataType) -> Result<f64, 
ArrowError> {
+    macro_rules! read_le {
+        ($t:ty, $n:expr) => {{
+            let arr: [u8; $n] = bytes.try_into().map_err(|_| {
+                ArrowError::InvalidArgumentError(format!(
+                    "Invalid nodata byte length for {:?}: expected {}, got {}",
+                    dt,
+                    $n,
+                    bytes.len()
+                ))
+            })?;
+            Ok(<$t>::from_le_bytes(arr) as f64)
+        }};
+    }
+
+    match dt {
+        BandDataType::UInt8 => {
+            if bytes.len() != 1 {
+                return Err(ArrowError::InvalidArgumentError(format!(
+                    "Invalid nodata byte length for UInt8: expected 1, got {}",
+                    bytes.len()
+                )));
+            }
+            Ok(bytes[0] as f64)
+        }
+        BandDataType::Int8 => {
+            if bytes.len() != 1 {
+                return Err(ArrowError::InvalidArgumentError(format!(
+                    "Invalid nodata byte length for Int8: expected 1, got {}",
+                    bytes.len()
+                )));
+            }
+            Ok(bytes[0] as i8 as f64)
+        }
+        BandDataType::UInt16 => read_le!(u16, 2),
+        BandDataType::Int16 => read_le!(i16, 2),
+        BandDataType::UInt32 => read_le!(u32, 4),
+        BandDataType::Int32 => read_le!(i32, 4),
+        BandDataType::UInt64 => read_le!(u64, 8),
+        BandDataType::Int64 => read_le!(i64, 8),
+        BandDataType::Float32 => read_le!(f32, 4),
+        BandDataType::Float64 => read_le!(f64, 8),
+    }
 }
 
 /// Trait for iterating over bands within a raster
@@ -118,3 +181,40 @@ pub trait BandIterator<'a>: Iterator<Item = Box<dyn 
BandRef + 'a>> {
         self.len() == 0
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_nodata_bytes_to_f64_uint8() {
+        let val = nodata_bytes_to_f64(&[42], &BandDataType::UInt8).unwrap();
+        assert_eq!(val, 42.0);
+    }
+
+    #[test]
+    fn test_nodata_bytes_to_f64_int8() {
+        let val = nodata_bytes_to_f64(&[0xFE], &BandDataType::Int8).unwrap();
+        assert_eq!(val, -2.0);
+    }
+
+    #[test]
+    fn test_nodata_bytes_to_f64_float64() {
+        let bytes = (-9999.0_f64).to_le_bytes();
+        let val = nodata_bytes_to_f64(&bytes, &BandDataType::Float64).unwrap();
+        assert_eq!(val, -9999.0);
+    }
+
+    #[test]
+    fn test_nodata_bytes_to_f64_int32() {
+        let bytes = (-1_i32).to_le_bytes();
+        let val = nodata_bytes_to_f64(&bytes, &BandDataType::Int32).unwrap();
+        assert_eq!(val, -1.0);
+    }
+
+    #[test]
+    fn test_nodata_bytes_to_f64_wrong_length() {
+        let result = nodata_bytes_to_f64(&[1, 2, 3], &BandDataType::Float64);
+        assert!(result.is_err());
+    }
+}
diff --git a/rust/sedona-schema/src/raster.rs b/rust/sedona-schema/src/raster.rs
index f1bc0283..b5b8745c 100644
--- a/rust/sedona-schema/src/raster.rs
+++ b/rust/sedona-schema/src/raster.rs
@@ -105,6 +105,35 @@ pub enum BandDataType {
     Int8 = 10,
 }
 
+impl BandDataType {
+    /// Byte size of a single pixel for this data type.
+    pub fn byte_size(&self) -> usize {
+        match self {
+            BandDataType::UInt8 | BandDataType::Int8 => 1,
+            BandDataType::UInt16 | BandDataType::Int16 => 2,
+            BandDataType::UInt32 | BandDataType::Int32 | BandDataType::Float32 
=> 4,
+            BandDataType::UInt64 | BandDataType::Int64 | BandDataType::Float64 
=> 8,
+        }
+    }
+
+    /// Java/Sedona-compatible pixel type name (e.g. `"UNSIGNED_8BITS"`).
+    pub fn pixel_type_name(&self) -> &'static str {
+        match self {
+            BandDataType::UInt8 => "UNSIGNED_8BITS",
+            BandDataType::UInt16 => "UNSIGNED_16BITS",
+            BandDataType::Int16 => "SIGNED_16BITS",
+            BandDataType::Int32 => "SIGNED_32BITS",
+            BandDataType::Float32 => "REAL_32BITS",
+            BandDataType::Float64 => "REAL_64BITS",
+            // Extra types present in Rust but not in Java Sedona
+            BandDataType::UInt32 => "UNSIGNED_32BITS",
+            BandDataType::UInt64 => "UNSIGNED_64BITS",
+            BandDataType::Int64 => "SIGNED_64BITS",
+            BandDataType::Int8 => "SIGNED_8BITS",
+        }
+    }
+}
+
 /// Storage strategy for raster band data within Apache Arrow arrays.
 ///
 /// This enum defines how raster data is physically stored and accessed:
@@ -333,4 +362,32 @@ mod tests {
             panic!("Expected Struct type for band");
         }
     }
+
+    #[test]
+    fn test_band_data_type_byte_size() {
+        assert_eq!(BandDataType::UInt8.byte_size(), 1);
+        assert_eq!(BandDataType::Int8.byte_size(), 1);
+        assert_eq!(BandDataType::UInt16.byte_size(), 2);
+        assert_eq!(BandDataType::Int16.byte_size(), 2);
+        assert_eq!(BandDataType::UInt32.byte_size(), 4);
+        assert_eq!(BandDataType::Int32.byte_size(), 4);
+        assert_eq!(BandDataType::Float32.byte_size(), 4);
+        assert_eq!(BandDataType::UInt64.byte_size(), 8);
+        assert_eq!(BandDataType::Int64.byte_size(), 8);
+        assert_eq!(BandDataType::Float64.byte_size(), 8);
+    }
+
+    #[test]
+    fn test_band_data_type_pixel_type_name() {
+        assert_eq!(BandDataType::UInt8.pixel_type_name(), "UNSIGNED_8BITS");
+        assert_eq!(BandDataType::Int8.pixel_type_name(), "SIGNED_8BITS");
+        assert_eq!(BandDataType::UInt16.pixel_type_name(), "UNSIGNED_16BITS");
+        assert_eq!(BandDataType::Int16.pixel_type_name(), "SIGNED_16BITS");
+        assert_eq!(BandDataType::UInt32.pixel_type_name(), "UNSIGNED_32BITS");
+        assert_eq!(BandDataType::Int32.pixel_type_name(), "SIGNED_32BITS");
+        assert_eq!(BandDataType::Float32.pixel_type_name(), "REAL_32BITS");
+        assert_eq!(BandDataType::UInt64.pixel_type_name(), "UNSIGNED_64BITS");
+        assert_eq!(BandDataType::Int64.pixel_type_name(), "SIGNED_64BITS");
+        assert_eq!(BandDataType::Float64.pixel_type_name(), "REAL_64BITS");
+    }
 }

Reply via email to