From 5b020ded53a828abc8c7e4fd00e56c220c8a1b52 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Fri, 13 Feb 2026 23:25:14 +0800 Subject: [PATCH 1/4] feat(raster): add RS_BandPixelType, RS_BandNoDataValue, and RS_BandIsNoData UDFs --- .../benches/native-raster-functions.rs | 26 + rust/sedona-raster-functions/src/lib.rs | 1 + rust/sedona-raster-functions/src/register.rs | 3 + .../src/rs_band_accessors.rs | 738 ++++++++++++++++++ submodules/sedona-testing | 2 +- 5 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 rust/sedona-raster-functions/src/rs_band_accessors.rs diff --git a/rust/sedona-raster-functions/benches/native-raster-functions.rs b/rust/sedona-raster-functions/benches/native-raster-functions.rs index 37f32236f..e69fb86d7 100644 --- a/rust/sedona-raster-functions/benches/native-raster-functions.rs +++ b/rust/sedona-raster-functions/benches/native-raster-functions.rs @@ -20,6 +20,23 @@ use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*, BenchmarkAr fn criterion_benchmark(c: &mut Criterion) { let f = sedona_raster_functions::register::default_function_set(); + // RS_BandIsNoData + benchmark::scalar( + c, + &f, + "native-raster", + "rs_bandisnodata", + BenchmarkArgs::Array(Raster(64, 64)), + ); + // RS_BandNoDataValue + benchmark::scalar( + c, + &f, + "native-raster", + "rs_bandnodatavalue", + BenchmarkArgs::Array(Raster(64, 64)), + ); + // RS_BandPath benchmark::scalar( c, &f, @@ -34,6 +51,15 @@ fn criterion_benchmark(c: &mut Criterion) { "rs_bandpath", BenchmarkArgs::ArrayScalar(Raster(64, 64), Int32(1, 2)), ); + // RS_BandPixelType + benchmark::scalar( + c, + &f, + "native-raster", + "rs_bandpixeltype", + BenchmarkArgs::Array(Raster(64, 64)), + ); + 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 79b14983f..ef79f12a4 100644 --- a/rust/sedona-raster-functions/src/lib.rs +++ b/rust/sedona-raster-functions/src/lib.rs @@ -18,6 +18,7 @@ mod executor; pub mod raster_utils; pub mod register; +pub mod rs_band_accessors; pub mod rs_bandpath; pub mod rs_convexhull; pub mod rs_envelope; diff --git a/rust/sedona-raster-functions/src/register.rs b/rust/sedona-raster-functions/src/register.rs index a4d38a26b..9d1daaee3 100644 --- a/rust/sedona-raster-functions/src/register.rs +++ b/rust/sedona-raster-functions/src/register.rs @@ -38,6 +38,9 @@ pub fn default_function_set() -> FunctionSet { register_scalar_udfs!( function_set, + crate::rs_band_accessors::rs_bandpixeltype_udf, + crate::rs_band_accessors::rs_bandnodatavalue_udf, + crate::rs_band_accessors::rs_bandisnodata_udf, crate::rs_bandpath::rs_bandpath_udf, crate::rs_convexhull::rs_convexhull_udf, crate::rs_envelope::rs_envelope_udf, diff --git a/rust/sedona-raster-functions/src/rs_band_accessors.rs b/rust/sedona-raster-functions/src/rs_band_accessors.rs new file mode 100644 index 000000000..c546f80bc --- /dev/null +++ b/rust/sedona-raster-functions/src/rs_band_accessors.rs @@ -0,0 +1,738 @@ +// 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; +use std::vec; + +use crate::executor::RasterExecutor; +use arrow_array::builder::{BooleanBuilder, Float64Builder, StringBuilder}; +use arrow_array::{cast::AsArray, types::Int32Type, Array}; +use arrow_schema::DataType; +use datafusion_common::error::Result; +use datafusion_common::{exec_err, DataFusionError}; +use datafusion_expr::{ColumnarValue, Volatility}; +use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF}; +use sedona_raster::traits::RasterRef; +use sedona_schema::raster::{BandDataType, StorageType}; +use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Map a BandDataType to the Java-compatible pixel type name. +fn band_data_type_name(dt: &BandDataType) -> &'static str { + match dt { + 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 the Java Sedona + BandDataType::UInt32 => "UNSIGNED_32BITS", + BandDataType::UInt64 => "UNSIGNED_64BITS", + BandDataType::Int64 => "SIGNED_64BITS", + BandDataType::Int8 => "SIGNED_8BITS", + } +} + +/// Byte size of a single pixel for the given band data type. +fn data_type_byte_size(data_type: &BandDataType) -> usize { + match data_type { + BandDataType::UInt8 | BandDataType::Int8 => 1, + BandDataType::UInt16 | BandDataType::Int16 => 2, + BandDataType::UInt32 | BandDataType::Int32 | BandDataType::Float32 => 4, + BandDataType::UInt64 | BandDataType::Int64 | BandDataType::Float64 => 8, + } +} + +/// Convert raw nodata bytes to f64 for the given band data type. +fn bytes_to_f64(bytes: &[u8], band_type: &BandDataType) -> Result { + macro_rules! read_le_f64 { + ($t:ty, $n:expr) => {{ + let arr: [u8; $n] = bytes.try_into().map_err(|_| { + DataFusionError::Execution(format!( + "Invalid byte slice length for type {}, expected: {}, actual: {}", + stringify!($t), + $n, + bytes.len() + )) + })?; + Ok(<$t>::from_le_bytes(arr) as f64) + }}; + } + + match band_type { + BandDataType::UInt8 => { + if bytes.len() != 1 { + return Err(DataFusionError::Execution(format!( + "Invalid byte length for UInt8: expected 1, got {}", + bytes.len() + ))); + } + Ok(bytes[0] as f64) + } + BandDataType::Int8 => { + if bytes.len() != 1 { + return Err(DataFusionError::Execution(format!( + "Invalid byte length for Int8: expected 1, got {}", + bytes.len() + ))); + } + Ok(bytes[0] as i8 as f64) + } + BandDataType::UInt16 => read_le_f64!(u16, 2), + BandDataType::Int16 => read_le_f64!(i16, 2), + BandDataType::UInt32 => read_le_f64!(u32, 4), + BandDataType::Int32 => read_le_f64!(i32, 4), + BandDataType::UInt64 => read_le_f64!(u64, 8), + BandDataType::Int64 => read_le_f64!(i64, 8), + BandDataType::Float32 => read_le_f64!(f32, 4), + BandDataType::Float64 => read_le_f64!(f64, 8), + } +} + +/// Validate and return a (1-based) band index, erroring if out of range. +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(()) +} + +// =========================================================================== +// RS_BandPixelType +// =========================================================================== + +/// RS_BandPixelType() scalar UDF implementation +/// +/// Returns the pixel data type of the specified band as a string. +/// Accepts an optional band_index parameter (1-based, default is 1). +pub fn rs_bandpixeltype_udf() -> SedonaScalarUDF { + SedonaScalarUDF::new( + "rs_bandpixeltype", + vec![ + Arc::new(RsBandPixelType {}), + Arc::new(RsBandPixelTypeWithBand {}), + ], + Volatility::Immutable, + ) +} + +#[derive(Debug)] +struct RsBandPixelType {} + +impl SedonaScalarKernel for RsBandPixelType { + fn return_type(&self, args: &[SedonaType]) -> Result> { + 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 { + let executor = RasterExecutor::new(arg_types, args); + let mut builder = + StringBuilder::with_capacity(executor.num_iterations(), executor.num_iterations() * 20); + + executor + .execute_raster_void(|_i, raster_opt| get_pixel_type(raster_opt, 1, &mut builder))?; + + executor.finish(Arc::new(builder.finish())) + } +} + +#[derive(Debug)] +struct RsBandPixelTypeWithBand {} + +impl SedonaScalarKernel for RsBandPixelTypeWithBand { + fn return_type(&self, args: &[SedonaType]) -> Result> { + 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 { + let executor = RasterExecutor::new(arg_types, args); + let band_index_array = args[1].clone().into_array(executor.num_iterations())?; + let band_index_array = band_index_array.as_primitive::(); + + let mut builder = + StringBuilder::with_capacity(executor.num_iterations(), executor.num_iterations() * 20); + + executor.execute_raster_void(|i, raster_opt| { + let band_index = if band_index_array.is_null(i) { + 1 + } else { + band_index_array.value(i) + }; + get_pixel_type(raster_opt, band_index, &mut builder) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn get_pixel_type( + raster_opt: Option<&sedona_raster::array::RasterRefImpl<'_>>, + band_index: i32, + builder: &mut StringBuilder, +) -> Result<()> { + match raster_opt { + None => { + builder.append_null(); + Ok(()) + } + Some(raster) => { + let num_bands = raster.bands().len(); + validate_band_index(band_index, num_bands)?; + let band = raster.bands().band(band_index as usize)?; + let dt = band.metadata().data_type()?; + builder.append_value(band_data_type_name(&dt)); + Ok(()) + } + } +} + +// =========================================================================== +// RS_BandNoDataValue +// =========================================================================== + +/// RS_BandNoDataValue() scalar UDF implementation +/// +/// Returns the nodata value of the specified band as a Float64. +/// Returns null if the band has no nodata value defined. +/// Accepts an optional band_index parameter (1-based, default is 1). +pub fn rs_bandnodatavalue_udf() -> SedonaScalarUDF { + SedonaScalarUDF::new( + "rs_bandnodatavalue", + vec![ + Arc::new(RsBandNoDataValue {}), + Arc::new(RsBandNoDataValueWithBand {}), + ], + Volatility::Immutable, + ) +} + +#[derive(Debug)] +struct RsBandNoDataValue {} + +impl SedonaScalarKernel for RsBandNoDataValue { + fn return_type(&self, args: &[SedonaType]) -> Result> { + let matcher = ArgMatcher::new( + vec![ArgMatcher::is_raster()], + SedonaType::Arrow(DataType::Float64), + ); + matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = RasterExecutor::new(arg_types, args); + let mut builder = Float64Builder::with_capacity(executor.num_iterations()); + + executor + .execute_raster_void(|_i, raster_opt| get_nodata_value(raster_opt, 1, &mut builder))?; + + executor.finish(Arc::new(builder.finish())) + } +} + +#[derive(Debug)] +struct RsBandNoDataValueWithBand {} + +impl SedonaScalarKernel for RsBandNoDataValueWithBand { + fn return_type(&self, args: &[SedonaType]) -> Result> { + let matcher = ArgMatcher::new( + vec![ArgMatcher::is_raster(), ArgMatcher::is_integer()], + SedonaType::Arrow(DataType::Float64), + ); + matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = RasterExecutor::new(arg_types, args); + let band_index_array = args[1].clone().into_array(executor.num_iterations())?; + let band_index_array = band_index_array.as_primitive::(); + + let mut builder = Float64Builder::with_capacity(executor.num_iterations()); + + executor.execute_raster_void(|i, raster_opt| { + let band_index = if band_index_array.is_null(i) { + 1 + } else { + band_index_array.value(i) + }; + get_nodata_value(raster_opt, band_index, &mut builder) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn get_nodata_value( + raster_opt: Option<&sedona_raster::array::RasterRefImpl<'_>>, + band_index: i32, + builder: &mut Float64Builder, +) -> Result<()> { + match raster_opt { + None => { + builder.append_null(); + Ok(()) + } + Some(raster) => { + let num_bands = raster.bands().len(); + validate_band_index(band_index, num_bands)?; + let band = raster.bands().band(band_index as usize)?; + let band_meta = band.metadata(); + match band_meta.nodata_value() { + None => { + builder.append_null(); + } + Some(nodata_bytes) => { + let dt = band_meta.data_type()?; + let val = bytes_to_f64(nodata_bytes, &dt)?; + builder.append_value(val); + } + } + Ok(()) + } + } +} + +// =========================================================================== +// RS_BandIsNoData +// =========================================================================== + +/// RS_BandIsNoData() scalar UDF implementation +/// +/// Returns true if all pixels in the specified band equal the nodata value. +/// Returns false if the band has no nodata value defined. +/// Accepts an optional band_index parameter (1-based, default is 1). +pub fn rs_bandisnodata_udf() -> SedonaScalarUDF { + SedonaScalarUDF::new( + "rs_bandisnodata", + vec![ + Arc::new(RsBandIsNoData {}), + Arc::new(RsBandIsNoDataWithBand {}), + ], + Volatility::Immutable, + ) +} + +#[derive(Debug)] +struct RsBandIsNoData {} + +impl SedonaScalarKernel for RsBandIsNoData { + fn return_type(&self, args: &[SedonaType]) -> Result> { + let matcher = ArgMatcher::new( + vec![ArgMatcher::is_raster()], + SedonaType::Arrow(DataType::Boolean), + ); + matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = RasterExecutor::new(arg_types, args); + let mut builder = BooleanBuilder::with_capacity(executor.num_iterations()); + + executor + .execute_raster_void(|_i, raster_opt| get_is_nodata(raster_opt, 1, &mut builder))?; + + executor.finish(Arc::new(builder.finish())) + } +} + +#[derive(Debug)] +struct RsBandIsNoDataWithBand {} + +impl SedonaScalarKernel for RsBandIsNoDataWithBand { + fn return_type(&self, args: &[SedonaType]) -> Result> { + let matcher = ArgMatcher::new( + vec![ArgMatcher::is_raster(), ArgMatcher::is_integer()], + SedonaType::Arrow(DataType::Boolean), + ); + matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = RasterExecutor::new(arg_types, args); + let band_index_array = args[1].clone().into_array(executor.num_iterations())?; + let band_index_array = band_index_array.as_primitive::(); + + let mut builder = BooleanBuilder::with_capacity(executor.num_iterations()); + + executor.execute_raster_void(|i, raster_opt| { + let band_index = if band_index_array.is_null(i) { + 1 + } else { + band_index_array.value(i) + }; + get_is_nodata(raster_opt, band_index, &mut builder) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn get_is_nodata( + raster_opt: Option<&sedona_raster::array::RasterRefImpl<'_>>, + band_index: i32, + builder: &mut BooleanBuilder, +) -> Result<()> { + match raster_opt { + None => { + builder.append_null(); + Ok(()) + } + Some(raster) => { + let num_bands = raster.bands().len(); + validate_band_index(band_index, num_bands)?; + let band = raster.bands().band(band_index as usize)?; + let band_meta = band.metadata(); + + // If no nodata value defined, return false + let nodata_bytes = match band_meta.nodata_value() { + None => { + builder.append_value(false); + return Ok(()); + } + Some(b) => b, + }; + + // OutDbRef bands don't have inline pixel data + if band_meta.storage_type()? == StorageType::OutDbRef { + return exec_err!("RS_BandIsNoData does not support out-db raster bands"); + } + + let dt = band_meta.data_type()?; + let pixel_size = data_type_byte_size(&dt); + let data = band.data(); + + // Check every pixel against the nodata bytes + let all_nodata = data + .chunks_exact(pixel_size) + .all(|chunk| chunk == nodata_bytes); + + builder.append_value(all_nodata); + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow_array::{BooleanArray, Float64Array, Int32Array, StringArray, StructArray}; + use datafusion_common::ScalarValue; + use datafusion_expr::ScalarUDF; + use sedona_raster::builder::RasterBuilder; + use sedona_raster::traits::{BandMetadata, RasterMetadata}; + use sedona_schema::datatypes::RASTER; + use sedona_testing::compare::assert_array_equal; + use sedona_testing::rasters::generate_test_rasters; + use sedona_testing::testers::ScalarUdfTester; + + /// Build a single-row raster StructArray with custom metadata and band metadata. + fn build_custom_raster( + meta: &RasterMetadata, + band_meta: &BandMetadata, + data: &[u8], + crs: Option<&str>, + ) -> StructArray { + let mut builder = RasterBuilder::new(1); + builder.start_raster(meta, crs).expect("start raster"); + builder + .start_band(BandMetadata { + datatype: band_meta.datatype, + nodata_value: band_meta.nodata_value.clone(), + storage_type: band_meta.storage_type, + outdb_url: band_meta.outdb_url.clone(), + outdb_band_id: band_meta.outdb_band_id, + }) + .expect("start band"); + builder.band_data_writer().append_value(data); + builder.finish_band().expect("finish band"); + builder.finish_raster().expect("finish raster"); + builder.finish().expect("finish") + } + + // ----------------------------------------------------------------------- + // RS_BandPixelType tests + // ----------------------------------------------------------------------- + + #[test] + fn udf_bandpixeltype_metadata() { + let udf: ScalarUDF = rs_bandpixeltype_udf().into(); + assert_eq!(udf.name(), "rs_bandpixeltype"); + } + + #[test] + fn udf_bandpixeltype_default_band() { + let udf: ScalarUDF = rs_bandpixeltype_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + tester.assert_return_type(DataType::Utf8); + + // generate_test_rasters creates UInt16 bands + 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::() + .expect("Expected StringArray"); + + assert_eq!(string_array.value(0), "UNSIGNED_16BITS"); + assert!(string_array.is_null(1)); + assert_eq!(string_array.value(2), "UNSIGNED_16BITS"); + } + + #[test] + fn udf_bandpixeltype_with_band() { + let udf: ScalarUDF = rs_bandpixeltype_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER, SedonaType::Arrow(DataType::Int32)]); + + let rasters = generate_test_rasters(3, Some(1)).unwrap(); + let band_indices = Int32Array::from(vec![1, 1, 1]); + let result = tester + .invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)]) + .unwrap(); + let string_array = result + .as_any() + .downcast_ref::() + .expect("Expected StringArray"); + + assert_eq!(string_array.value(0), "UNSIGNED_16BITS"); + assert!(string_array.is_null(1)); + assert_eq!(string_array.value(2), "UNSIGNED_16BITS"); + } + + #[test] + fn udf_bandpixeltype_invalid_band_errors() { + let udf: ScalarUDF = rs_bandpixeltype_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER, SedonaType::Arrow(DataType::Int32)]); + + let rasters = generate_test_rasters(1, None).unwrap(); + let band_indices = Int32Array::from(vec![5]); // out of range + let result = tester.invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)]); + assert!(result.is_err()); + } + + #[test] + fn udf_bandpixeltype_null_scalar() { + let udf: ScalarUDF = rs_bandpixeltype_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + tester.assert_scalar_result_equals(result, ScalarValue::Utf8(None)); + } + + // ----------------------------------------------------------------------- + // RS_BandNoDataValue tests + // ----------------------------------------------------------------------- + + #[test] + fn udf_bandnodatavalue_metadata() { + let udf: ScalarUDF = rs_bandnodatavalue_udf().into(); + assert_eq!(udf.name(), "rs_bandnodatavalue"); + } + + #[test] + fn udf_bandnodatavalue_default_band() { + let udf: ScalarUDF = rs_bandnodatavalue_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + tester.assert_return_type(DataType::Float64); + + // generate_test_rasters creates bands with nodata = [0, 0] (UInt16 = 0) + let rasters = generate_test_rasters(3, Some(1)).unwrap(); + let expected: Arc = + Arc::new(Float64Array::from(vec![Some(0.0), None, Some(0.0)])); + let result = tester.invoke_array(Arc::new(rasters)).unwrap(); + assert_array_equal(&result, &expected); + } + + #[test] + fn udf_bandnodatavalue_no_nodata() { + // Create a raster without nodata + let meta = RasterMetadata { + width: 2, + height: 2, + 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 band_meta = BandMetadata { + datatype: BandDataType::UInt8, + nodata_value: None, + storage_type: StorageType::InDb, + outdb_url: None, + outdb_band_id: None, + }; + let data = vec![1u8, 2, 3, 4]; + let rasters = build_custom_raster(&meta, &band_meta, &data, Some("OGC:CRS84")); + + let udf: ScalarUDF = rs_bandnodatavalue_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + let result = tester.invoke_array(Arc::new(rasters)).unwrap(); + let float_array = result + .as_any() + .downcast_ref::() + .expect("Expected Float64Array"); + assert!(float_array.is_null(0)); + } + + #[test] + fn udf_bandnodatavalue_null_scalar() { + let udf: ScalarUDF = rs_bandnodatavalue_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + tester.assert_scalar_result_equals(result, ScalarValue::Float64(None)); + } + + // ----------------------------------------------------------------------- + // RS_BandIsNoData tests + // ----------------------------------------------------------------------- + + #[test] + fn udf_bandisnodata_metadata() { + let udf: ScalarUDF = rs_bandisnodata_udf().into(); + assert_eq!(udf.name(), "rs_bandisnodata"); + } + + #[test] + fn udf_bandisnodata_not_all_nodata() { + let udf: ScalarUDF = rs_bandisnodata_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + tester.assert_return_type(DataType::Boolean); + + // generate_test_rasters: pixel data is 0,1,2,... and nodata=0 + // So the first pixel matches nodata but subsequent don't -> false + let rasters = generate_test_rasters(3, Some(1)).unwrap(); + let expected: Arc = + Arc::new(BooleanArray::from(vec![Some(false), None, Some(false)])); + let result = tester.invoke_array(Arc::new(rasters)).unwrap(); + assert_array_equal(&result, &expected); + } + + #[test] + fn udf_bandisnodata_all_nodata() { + // Create a raster where all pixels equal nodata + let meta = RasterMetadata { + width: 2, + height: 2, + 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 nodata_bytes = 255u8.to_le_bytes().to_vec(); + let band_meta = BandMetadata { + datatype: BandDataType::UInt8, + nodata_value: Some(nodata_bytes), + storage_type: StorageType::InDb, + outdb_url: None, + outdb_band_id: None, + }; + // All pixels are 255 (same as nodata) + let data = vec![255u8, 255, 255, 255]; + let rasters = build_custom_raster(&meta, &band_meta, &data, Some("OGC:CRS84")); + + let udf: ScalarUDF = rs_bandisnodata_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + let result = tester.invoke_array(Arc::new(rasters)).unwrap(); + let bool_array = result + .as_any() + .downcast_ref::() + .expect("Expected BooleanArray"); + assert!(bool_array.value(0)); + } + + #[test] + fn udf_bandisnodata_no_nodata_defined() { + // No nodata defined -> return false + let meta = RasterMetadata { + width: 2, + height: 2, + 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 band_meta = BandMetadata { + datatype: BandDataType::UInt8, + nodata_value: None, + storage_type: StorageType::InDb, + outdb_url: None, + outdb_band_id: None, + }; + let data = vec![0u8, 0, 0, 0]; + let rasters = build_custom_raster(&meta, &band_meta, &data, Some("OGC:CRS84")); + + let udf: ScalarUDF = rs_bandisnodata_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + let result = tester.invoke_array(Arc::new(rasters)).unwrap(); + let bool_array = result + .as_any() + .downcast_ref::() + .expect("Expected BooleanArray"); + assert!(!bool_array.value(0)); + } + + #[test] + fn udf_bandisnodata_null_scalar() { + let udf: ScalarUDF = rs_bandisnodata_udf().into(); + let tester = ScalarUdfTester::new(udf, vec![RASTER]); + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + tester.assert_scalar_result_equals(result, ScalarValue::Boolean(None)); + } +} diff --git a/submodules/sedona-testing b/submodules/sedona-testing index c7bc17d71..143e4da79 160000 --- a/submodules/sedona-testing +++ b/submodules/sedona-testing @@ -1 +1 @@ -Subproject commit c7bc17d7109fc628959eb2850d4cfce3d483b1ee +Subproject commit 143e4da79f3a95267a5aaff3affb3495b2e58aae From aff7ff879a5a2e7501eda0f1e9c7cc0f2afc53b0 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Thu, 19 Feb 2026 18:39:42 +0800 Subject: [PATCH 2/4] refactor(raster): use shared utilities in RS_BandPixelType, RS_BandNoDataValue, RS_BandIsNoData Replace local band_data_type_name(), data_type_byte_size(), bytes_to_f64(), and validate_band_index() helpers with shared BandDataType.pixel_type_name(), BandDataType.byte_size(), BandMetadataRef.nodata_value_as_f64(), and raster_utils::validate_band_index() from pr15-raster-utilities. --- .../src/rs_band_accessors.rs | 111 ++---------------- submodules/sedona-testing | 2 +- 2 files changed, 10 insertions(+), 103 deletions(-) diff --git a/rust/sedona-raster-functions/src/rs_band_accessors.rs b/rust/sedona-raster-functions/src/rs_band_accessors.rs index c546f80bc..69749741f 100644 --- a/rust/sedona-raster-functions/src/rs_band_accessors.rs +++ b/rust/sedona-raster-functions/src/rs_band_accessors.rs @@ -19,106 +19,18 @@ use std::sync::Arc; use std::vec; use crate::executor::RasterExecutor; +use crate::raster_utils::validate_band_index; use arrow_array::builder::{BooleanBuilder, Float64Builder, StringBuilder}; use arrow_array::{cast::AsArray, types::Int32Type, Array}; use arrow_schema::DataType; use datafusion_common::error::Result; -use datafusion_common::{exec_err, DataFusionError}; +use datafusion_common::exec_err; use datafusion_expr::{ColumnarValue, Volatility}; use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF}; use sedona_raster::traits::RasterRef; -use sedona_schema::raster::{BandDataType, StorageType}; +use sedona_schema::raster::StorageType; use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher}; -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/// Map a BandDataType to the Java-compatible pixel type name. -fn band_data_type_name(dt: &BandDataType) -> &'static str { - match dt { - 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 the Java Sedona - BandDataType::UInt32 => "UNSIGNED_32BITS", - BandDataType::UInt64 => "UNSIGNED_64BITS", - BandDataType::Int64 => "SIGNED_64BITS", - BandDataType::Int8 => "SIGNED_8BITS", - } -} - -/// Byte size of a single pixel for the given band data type. -fn data_type_byte_size(data_type: &BandDataType) -> usize { - match data_type { - BandDataType::UInt8 | BandDataType::Int8 => 1, - BandDataType::UInt16 | BandDataType::Int16 => 2, - BandDataType::UInt32 | BandDataType::Int32 | BandDataType::Float32 => 4, - BandDataType::UInt64 | BandDataType::Int64 | BandDataType::Float64 => 8, - } -} - -/// Convert raw nodata bytes to f64 for the given band data type. -fn bytes_to_f64(bytes: &[u8], band_type: &BandDataType) -> Result { - macro_rules! read_le_f64 { - ($t:ty, $n:expr) => {{ - let arr: [u8; $n] = bytes.try_into().map_err(|_| { - DataFusionError::Execution(format!( - "Invalid byte slice length for type {}, expected: {}, actual: {}", - stringify!($t), - $n, - bytes.len() - )) - })?; - Ok(<$t>::from_le_bytes(arr) as f64) - }}; - } - - match band_type { - BandDataType::UInt8 => { - if bytes.len() != 1 { - return Err(DataFusionError::Execution(format!( - "Invalid byte length for UInt8: expected 1, got {}", - bytes.len() - ))); - } - Ok(bytes[0] as f64) - } - BandDataType::Int8 => { - if bytes.len() != 1 { - return Err(DataFusionError::Execution(format!( - "Invalid byte length for Int8: expected 1, got {}", - bytes.len() - ))); - } - Ok(bytes[0] as i8 as f64) - } - BandDataType::UInt16 => read_le_f64!(u16, 2), - BandDataType::Int16 => read_le_f64!(i16, 2), - BandDataType::UInt32 => read_le_f64!(u32, 4), - BandDataType::Int32 => read_le_f64!(i32, 4), - BandDataType::UInt64 => read_le_f64!(u64, 8), - BandDataType::Int64 => read_le_f64!(i64, 8), - BandDataType::Float32 => read_le_f64!(f32, 4), - BandDataType::Float64 => read_le_f64!(f64, 8), - } -} - -/// Validate and return a (1-based) band index, erroring if out of range. -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(()) -} - // =========================================================================== // RS_BandPixelType // =========================================================================== @@ -218,7 +130,7 @@ fn get_pixel_type( validate_band_index(band_index, num_bands)?; let band = raster.bands().band(band_index as usize)?; let dt = band.metadata().data_type()?; - builder.append_value(band_data_type_name(&dt)); + builder.append_value(dt.pixel_type_name()); Ok(()) } } @@ -322,15 +234,9 @@ fn get_nodata_value( validate_band_index(band_index, num_bands)?; let band = raster.bands().band(band_index as usize)?; let band_meta = band.metadata(); - match band_meta.nodata_value() { - None => { - builder.append_null(); - } - Some(nodata_bytes) => { - let dt = band_meta.data_type()?; - let val = bytes_to_f64(nodata_bytes, &dt)?; - builder.append_value(val); - } + match band_meta.nodata_value_as_f64()? { + None => builder.append_null(), + Some(val) => builder.append_value(val), } Ok(()) } @@ -451,7 +357,7 @@ fn get_is_nodata( } let dt = band_meta.data_type()?; - let pixel_size = data_type_byte_size(&dt); + let pixel_size = dt.byte_size(); let data = band.data(); // Check every pixel against the nodata bytes @@ -474,6 +380,7 @@ mod tests { use sedona_raster::builder::RasterBuilder; use sedona_raster::traits::{BandMetadata, RasterMetadata}; use sedona_schema::datatypes::RASTER; + use sedona_schema::raster::BandDataType; use sedona_testing::compare::assert_array_equal; use sedona_testing::rasters::generate_test_rasters; use sedona_testing::testers::ScalarUdfTester; diff --git a/submodules/sedona-testing b/submodules/sedona-testing index 143e4da79..c7bc17d71 160000 --- a/submodules/sedona-testing +++ b/submodules/sedona-testing @@ -1 +1 @@ -Subproject commit 143e4da79f3a95267a5aaff3affb3495b2e58aae +Subproject commit c7bc17d7109fc628959eb2850d4cfce3d483b1ee From 61fcb0c5694e56a9cab14ca6bce293b5d6a0535a Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Fri, 20 Feb 2026 01:25:05 +0800 Subject: [PATCH 3/4] Remove RS_BandIsNoData and fix behavior of handling out-of-bound band index --- rust/sedona-raster-functions/src/lib.rs | 1 - .../src/raster_utils.rs | 58 ---- rust/sedona-raster-functions/src/register.rs | 1 - .../src/rs_band_accessors.rs | 300 ++---------------- .../src/rs_bandpath.rs | 11 +- 5 files changed, 38 insertions(+), 333 deletions(-) delete mode 100644 rust/sedona-raster-functions/src/raster_utils.rs diff --git a/rust/sedona-raster-functions/src/lib.rs b/rust/sedona-raster-functions/src/lib.rs index ef79f12a4..f8f227149 100644 --- a/rust/sedona-raster-functions/src/lib.rs +++ b/rust/sedona-raster-functions/src/lib.rs @@ -16,7 +16,6 @@ // under the License. mod executor; -pub mod raster_utils; pub mod register; pub mod rs_band_accessors; pub mod rs_bandpath; diff --git a/rust/sedona-raster-functions/src/raster_utils.rs b/rust/sedona-raster-functions/src/raster_utils.rs deleted file mode 100644 index 1a0b44bd6..000000000 --- a/rust/sedona-raster-functions/src/raster_utils.rs +++ /dev/null @@ -1,58 +0,0 @@ -// 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 9d1daaee3..378b40abe 100644 --- a/rust/sedona-raster-functions/src/register.rs +++ b/rust/sedona-raster-functions/src/register.rs @@ -40,7 +40,6 @@ pub fn default_function_set() -> FunctionSet { function_set, crate::rs_band_accessors::rs_bandpixeltype_udf, crate::rs_band_accessors::rs_bandnodatavalue_udf, - crate::rs_band_accessors::rs_bandisnodata_udf, crate::rs_bandpath::rs_bandpath_udf, crate::rs_convexhull::rs_convexhull_udf, crate::rs_envelope::rs_envelope_udf, diff --git a/rust/sedona-raster-functions/src/rs_band_accessors.rs b/rust/sedona-raster-functions/src/rs_band_accessors.rs index 69749741f..52e401caa 100644 --- a/rust/sedona-raster-functions/src/rs_band_accessors.rs +++ b/rust/sedona-raster-functions/src/rs_band_accessors.rs @@ -19,16 +19,13 @@ use std::sync::Arc; use std::vec; use crate::executor::RasterExecutor; -use crate::raster_utils::validate_band_index; -use arrow_array::builder::{BooleanBuilder, Float64Builder, StringBuilder}; -use arrow_array::{cast::AsArray, types::Int32Type, Array}; +use arrow_array::builder::{Float64Builder, StringBuilder}; +use arrow_array::{cast::AsArray, types::Int32Type}; use arrow_schema::DataType; use datafusion_common::error::Result; -use datafusion_common::exec_err; 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}; // =========================================================================== @@ -101,13 +98,9 @@ impl SedonaScalarKernel for RsBandPixelTypeWithBand { let mut builder = StringBuilder::with_capacity(executor.num_iterations(), executor.num_iterations() * 20); - - executor.execute_raster_void(|i, raster_opt| { - let band_index = if band_index_array.is_null(i) { - 1 - } else { - band_index_array.value(i) - }; + let mut band_index_iter = band_index_array.iter(); + executor.execute_raster_void(|_, raster_opt| { + let band_index = band_index_iter.next().unwrap().unwrap_or(1); get_pixel_type(raster_opt, band_index, &mut builder) })?; @@ -127,7 +120,10 @@ fn get_pixel_type( } Some(raster) => { let num_bands = raster.bands().len(); - validate_band_index(band_index, num_bands)?; + if band_index < 1 || band_index > num_bands as i32 { + builder.append_null(); + return Ok(()); + } let band = raster.bands().band(band_index as usize)?; let dt = band.metadata().data_type()?; builder.append_value(dt.pixel_type_name()); @@ -205,13 +201,9 @@ impl SedonaScalarKernel for RsBandNoDataValueWithBand { let band_index_array = band_index_array.as_primitive::(); let mut builder = Float64Builder::with_capacity(executor.num_iterations()); - - executor.execute_raster_void(|i, raster_opt| { - let band_index = if band_index_array.is_null(i) { - 1 - } else { - band_index_array.value(i) - }; + let mut band_index_iter = band_index_array.iter(); + executor.execute_raster_void(|_, raster_opt| { + let band_index = band_index_iter.next().unwrap().unwrap_or(1); get_nodata_value(raster_opt, band_index, &mut builder) })?; @@ -231,7 +223,10 @@ fn get_nodata_value( } Some(raster) => { let num_bands = raster.bands().len(); - validate_band_index(band_index, num_bands)?; + if band_index < 1 || band_index > num_bands as i32 { + builder.append_null(); + return Ok(()); + } let band = raster.bands().band(band_index as usize)?; let band_meta = band.metadata(); match band_meta.nodata_value_as_f64()? { @@ -243,144 +238,15 @@ fn get_nodata_value( } } -// =========================================================================== -// RS_BandIsNoData -// =========================================================================== - -/// RS_BandIsNoData() scalar UDF implementation -/// -/// Returns true if all pixels in the specified band equal the nodata value. -/// Returns false if the band has no nodata value defined. -/// Accepts an optional band_index parameter (1-based, default is 1). -pub fn rs_bandisnodata_udf() -> SedonaScalarUDF { - SedonaScalarUDF::new( - "rs_bandisnodata", - vec![ - Arc::new(RsBandIsNoData {}), - Arc::new(RsBandIsNoDataWithBand {}), - ], - Volatility::Immutable, - ) -} - -#[derive(Debug)] -struct RsBandIsNoData {} - -impl SedonaScalarKernel for RsBandIsNoData { - fn return_type(&self, args: &[SedonaType]) -> Result> { - let matcher = ArgMatcher::new( - vec![ArgMatcher::is_raster()], - SedonaType::Arrow(DataType::Boolean), - ); - matcher.match_args(args) - } - - fn invoke_batch( - &self, - arg_types: &[SedonaType], - args: &[ColumnarValue], - ) -> Result { - let executor = RasterExecutor::new(arg_types, args); - let mut builder = BooleanBuilder::with_capacity(executor.num_iterations()); - - executor - .execute_raster_void(|_i, raster_opt| get_is_nodata(raster_opt, 1, &mut builder))?; - - executor.finish(Arc::new(builder.finish())) - } -} - -#[derive(Debug)] -struct RsBandIsNoDataWithBand {} - -impl SedonaScalarKernel for RsBandIsNoDataWithBand { - fn return_type(&self, args: &[SedonaType]) -> Result> { - let matcher = ArgMatcher::new( - vec![ArgMatcher::is_raster(), ArgMatcher::is_integer()], - SedonaType::Arrow(DataType::Boolean), - ); - matcher.match_args(args) - } - - fn invoke_batch( - &self, - arg_types: &[SedonaType], - args: &[ColumnarValue], - ) -> Result { - let executor = RasterExecutor::new(arg_types, args); - let band_index_array = args[1].clone().into_array(executor.num_iterations())?; - let band_index_array = band_index_array.as_primitive::(); - - let mut builder = BooleanBuilder::with_capacity(executor.num_iterations()); - - executor.execute_raster_void(|i, raster_opt| { - let band_index = if band_index_array.is_null(i) { - 1 - } else { - band_index_array.value(i) - }; - get_is_nodata(raster_opt, band_index, &mut builder) - })?; - - executor.finish(Arc::new(builder.finish())) - } -} - -fn get_is_nodata( - raster_opt: Option<&sedona_raster::array::RasterRefImpl<'_>>, - band_index: i32, - builder: &mut BooleanBuilder, -) -> Result<()> { - match raster_opt { - None => { - builder.append_null(); - Ok(()) - } - Some(raster) => { - let num_bands = raster.bands().len(); - validate_band_index(band_index, num_bands)?; - let band = raster.bands().band(band_index as usize)?; - let band_meta = band.metadata(); - - // If no nodata value defined, return false - let nodata_bytes = match band_meta.nodata_value() { - None => { - builder.append_value(false); - return Ok(()); - } - Some(b) => b, - }; - - // OutDbRef bands don't have inline pixel data - if band_meta.storage_type()? == StorageType::OutDbRef { - return exec_err!("RS_BandIsNoData does not support out-db raster bands"); - } - - let dt = band_meta.data_type()?; - let pixel_size = dt.byte_size(); - let data = band.data(); - - // Check every pixel against the nodata bytes - let all_nodata = data - .chunks_exact(pixel_size) - .all(|chunk| chunk == nodata_bytes); - - builder.append_value(all_nodata); - Ok(()) - } - } -} - #[cfg(test)] mod tests { use super::*; - use arrow_array::{BooleanArray, Float64Array, Int32Array, StringArray, StructArray}; - use datafusion_common::ScalarValue; + use arrow_array::{Array, Float64Array, Int32Array, StringArray, StructArray}; use datafusion_expr::ScalarUDF; use sedona_raster::builder::RasterBuilder; use sedona_raster::traits::{BandMetadata, RasterMetadata}; use sedona_schema::datatypes::RASTER; - use sedona_schema::raster::BandDataType; + use sedona_schema::raster::{BandDataType, StorageType}; use sedona_testing::compare::assert_array_equal; use sedona_testing::rasters::generate_test_rasters; use sedona_testing::testers::ScalarUdfTester; @@ -459,22 +325,16 @@ mod tests { } #[test] - fn udf_bandpixeltype_invalid_band_errors() { + fn udf_bandpixeltype_non_existing_band() { let udf: ScalarUDF = rs_bandpixeltype_udf().into(); let tester = ScalarUdfTester::new(udf, vec![RASTER, SedonaType::Arrow(DataType::Int32)]); let rasters = generate_test_rasters(1, None).unwrap(); let band_indices = Int32Array::from(vec![5]); // out of range - let result = tester.invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)]); - assert!(result.is_err()); - } - - #[test] - fn udf_bandpixeltype_null_scalar() { - let udf: ScalarUDF = rs_bandpixeltype_udf().into(); - let tester = ScalarUdfTester::new(udf, vec![RASTER]); - let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); - tester.assert_scalar_result_equals(result, ScalarValue::Utf8(None)); + let result = tester + .invoke_arrays(vec![Arc::new(rasters), Arc::new(band_indices)]) + .unwrap(); + assert!(result.is_null(0)); } // ----------------------------------------------------------------------- @@ -535,111 +395,19 @@ mod tests { } #[test] - fn udf_bandnodatavalue_null_scalar() { + fn udf_bandnodatavalue_non_existing_band() { let udf: ScalarUDF = rs_bandnodatavalue_udf().into(); - let tester = ScalarUdfTester::new(udf, vec![RASTER]); - let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); - tester.assert_scalar_result_equals(result, ScalarValue::Float64(None)); - } - - // ----------------------------------------------------------------------- - // RS_BandIsNoData tests - // ----------------------------------------------------------------------- - - #[test] - fn udf_bandisnodata_metadata() { - let udf: ScalarUDF = rs_bandisnodata_udf().into(); - assert_eq!(udf.name(), "rs_bandisnodata"); - } - - #[test] - fn udf_bandisnodata_not_all_nodata() { - let udf: ScalarUDF = rs_bandisnodata_udf().into(); - let tester = ScalarUdfTester::new(udf, vec![RASTER]); - tester.assert_return_type(DataType::Boolean); + let tester = ScalarUdfTester::new(udf, vec![RASTER, SedonaType::Arrow(DataType::Int32)]); + tester.assert_return_type(DataType::Float64); - // generate_test_rasters: pixel data is 0,1,2,... and nodata=0 - // So the first pixel matches nodata but subsequent don't -> false + // generate_test_rasters creates bands with nodata = [0, 0] (UInt16 = 0) let rasters = generate_test_rasters(3, Some(1)).unwrap(); - let expected: Arc = - Arc::new(BooleanArray::from(vec![Some(false), None, Some(false)])); - let result = tester.invoke_array(Arc::new(rasters)).unwrap(); - assert_array_equal(&result, &expected); - } - - #[test] - fn udf_bandisnodata_all_nodata() { - // Create a raster where all pixels equal nodata - let meta = RasterMetadata { - width: 2, - height: 2, - 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 nodata_bytes = 255u8.to_le_bytes().to_vec(); - let band_meta = BandMetadata { - datatype: BandDataType::UInt8, - nodata_value: Some(nodata_bytes), - storage_type: StorageType::InDb, - outdb_url: None, - outdb_band_id: None, - }; - // All pixels are 255 (same as nodata) - let data = vec![255u8, 255, 255, 255]; - let rasters = build_custom_raster(&meta, &band_meta, &data, Some("OGC:CRS84")); - - let udf: ScalarUDF = rs_bandisnodata_udf().into(); - let tester = ScalarUdfTester::new(udf, vec![RASTER]); - let result = tester.invoke_array(Arc::new(rasters)).unwrap(); - let bool_array = result - .as_any() - .downcast_ref::() - .expect("Expected BooleanArray"); - assert!(bool_array.value(0)); - } - - #[test] - fn udf_bandisnodata_no_nodata_defined() { - // No nodata defined -> return false - let meta = RasterMetadata { - width: 2, - height: 2, - 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 band_meta = BandMetadata { - datatype: BandDataType::UInt8, - nodata_value: None, - storage_type: StorageType::InDb, - outdb_url: None, - outdb_band_id: None, - }; - let data = vec![0u8, 0, 0, 0]; - let rasters = build_custom_raster(&meta, &band_meta, &data, Some("OGC:CRS84")); - - let udf: ScalarUDF = rs_bandisnodata_udf().into(); - let tester = ScalarUdfTester::new(udf, vec![RASTER]); - let result = tester.invoke_array(Arc::new(rasters)).unwrap(); - let bool_array = result - .as_any() - .downcast_ref::() - .expect("Expected BooleanArray"); - assert!(!bool_array.value(0)); - } - - #[test] - fn udf_bandisnodata_null_scalar() { - let udf: ScalarUDF = rs_bandisnodata_udf().into(); - let tester = ScalarUdfTester::new(udf, vec![RASTER]); - let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); - tester.assert_scalar_result_equals(result, ScalarValue::Boolean(None)); + let bands = Int32Array::from(vec![0, 1, 2]); // out of range band index + let result = tester + .invoke_array_array(Arc::new(rasters), Arc::new(bands)) + .unwrap(); + assert!(result.is_null(0)); + assert!(result.is_null(1)); + assert!(result.is_null(2)); } } diff --git a/rust/sedona-raster-functions/src/rs_bandpath.rs b/rust/sedona-raster-functions/src/rs_bandpath.rs index 16d9b79b7..4b08d8699 100644 --- a/rust/sedona-raster-functions/src/rs_bandpath.rs +++ b/rust/sedona-raster-functions/src/rs_bandpath.rs @@ -18,7 +18,7 @@ use std::{sync::Arc, vec}; use crate::executor::RasterExecutor; use arrow_array::builder::StringBuilder; -use arrow_array::{cast::AsArray, types::Int32Type, Array}; +use arrow_array::{cast::AsArray, types::Int32Type}; use arrow_schema::DataType; use datafusion_common::error::Result; use datafusion_expr::{ColumnarValue, Volatility}; @@ -104,12 +104,9 @@ impl SedonaScalarKernel for RsBandPathWithBandIndex { 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) - }; + let mut band_index_iter = band_index_array.iter(); + executor.execute_raster_void(|_, raster_opt| { + let band_index = band_index_iter.next().unwrap().unwrap_or(1); get_band_path(raster_opt, band_index, &mut builder) })?; From dbcec3064b63a84d175a3142e8ab9e620209da02 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Sat, 21 Feb 2026 01:55:39 +0800 Subject: [PATCH 4/4] Remove benchmark for stale function --- .../benches/native-raster-functions.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/rust/sedona-raster-functions/benches/native-raster-functions.rs b/rust/sedona-raster-functions/benches/native-raster-functions.rs index e69fb86d7..5afda2b09 100644 --- a/rust/sedona-raster-functions/benches/native-raster-functions.rs +++ b/rust/sedona-raster-functions/benches/native-raster-functions.rs @@ -20,14 +20,6 @@ use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*, BenchmarkAr fn criterion_benchmark(c: &mut Criterion) { let f = sedona_raster_functions::register::default_function_set(); - // RS_BandIsNoData - benchmark::scalar( - c, - &f, - "native-raster", - "rs_bandisnodata", - BenchmarkArgs::Array(Raster(64, 64)), - ); // RS_BandNoDataValue benchmark::scalar( c,