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 37c30f36 feat(rust/sedona-raster-functions): Add RS_SRID and RS_CRS
(#430)
37c30f36 is described below
commit 37c30f36ee278aa9420f8dca718ee46f5071d12a
Author: jp <[email protected]>
AuthorDate: Thu Dec 18 18:39:36 2025 -0800
feat(rust/sedona-raster-functions): Add RS_SRID and RS_CRS (#430)
---
Cargo.lock | 1 +
rust/sedona-raster-functions/Cargo.toml | 1 +
.../benches/native-raster-functions.rs | 3 +
rust/sedona-raster-functions/src/executor.rs | 8 +-
rust/sedona-raster-functions/src/lib.rs | 1 +
rust/sedona-raster-functions/src/register.rs | 2 +
rust/sedona-raster-functions/src/rs_example.rs | 2 +-
rust/sedona-raster-functions/src/rs_srid.rs | 264 +++++++++++++++++++++
rust/sedona-testing/src/rasters.rs | 3 +-
9 files changed, 281 insertions(+), 4 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index dd9d16c0..2c61044f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5154,6 +5154,7 @@ dependencies = [
"sedona-raster",
"sedona-schema",
"sedona-testing",
+ "serde_json",
]
[[package]]
diff --git a/rust/sedona-raster-functions/Cargo.toml
b/rust/sedona-raster-functions/Cargo.toml
index a77ca4be..3eedf160 100644
--- a/rust/sedona-raster-functions/Cargo.toml
+++ b/rust/sedona-raster-functions/Cargo.toml
@@ -41,6 +41,7 @@ sedona-expr = { workspace = true }
sedona-geometry = { workspace = true }
sedona-raster = { workspace = true }
sedona-schema = { workspace = true }
+serde_json = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
diff --git a/rust/sedona-raster-functions/benches/native-raster-functions.rs
b/rust/sedona-raster-functions/benches/native-raster-functions.rs
index 2b1bff3d..cb4cb730 100644
--- a/rust/sedona-raster-functions/benches/native-raster-functions.rs
+++ b/rust/sedona-raster-functions/benches/native-raster-functions.rs
@@ -19,6 +19,8 @@ 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_crs", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_envelope", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_height", Raster(64, 64));
benchmark::scalar(
@@ -47,6 +49,7 @@ fn criterion_benchmark(c: &mut Criterion) {
benchmark::scalar(c, &f, "native-raster", "rs_scaley", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_skewx", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_skewy", Raster(64, 64));
+ benchmark::scalar(c, &f, "native-raster", "rs_srid", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_upperleftx", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_upperlefty", Raster(64, 64));
benchmark::scalar(c, &f, "native-raster", "rs_width", Raster(64, 64));
diff --git a/rust/sedona-raster-functions/src/executor.rs
b/rust/sedona-raster-functions/src/executor.rs
index 4cf0f801..917a1118 100644
--- a/rust/sedona-raster-functions/src/executor.rs
+++ b/rust/sedona-raster-functions/src/executor.rs
@@ -95,8 +95,12 @@ impl<'a, 'b> RasterExecutor<'a, 'b> {
ColumnarValue::Scalar(scalar_value) => match scalar_value {
ScalarValue::Struct(arc_struct) => {
let raster_array =
RasterStructArray::new(arc_struct.as_ref());
- let raster = raster_array.get(0)?;
- func(0, Some(raster))
+ if raster_array.is_null(0) {
+ func(0, None)
+ } else {
+ let raster = raster_array.get(0)?;
+ func(0, Some(raster))
+ }
}
ScalarValue::Null => func(0, None),
_ => Err(DataFusionError::Internal(
diff --git a/rust/sedona-raster-functions/src/lib.rs
b/rust/sedona-raster-functions/src/lib.rs
index f6d34fa0..2d231384 100644
--- a/rust/sedona-raster-functions/src/lib.rs
+++ b/rust/sedona-raster-functions/src/lib.rs
@@ -22,4 +22,5 @@ pub mod rs_example;
pub mod rs_geotransform;
pub mod rs_rastercoordinate;
pub mod rs_size;
+pub mod rs_srid;
pub mod rs_worldcoordinate;
diff --git a/rust/sedona-raster-functions/src/register.rs
b/rust/sedona-raster-functions/src/register.rs
index 7699076b..8685b62e 100644
--- a/rust/sedona-raster-functions/src/register.rs
+++ b/rust/sedona-raster-functions/src/register.rs
@@ -52,6 +52,8 @@ pub fn default_function_set() -> FunctionSet {
crate::rs_rastercoordinate::rs_worldtorastercoordy_udf,
crate::rs_size::rs_height_udf,
crate::rs_size::rs_width_udf,
+ crate::rs_srid::rs_crs_udf,
+ crate::rs_srid::rs_srid_udf,
crate::rs_worldcoordinate::rs_rastertoworldcoord_udf,
crate::rs_worldcoordinate::rs_rastertoworldcoordx_udf,
crate::rs_worldcoordinate::rs_rastertoworldcoordy_udf,
diff --git a/rust/sedona-raster-functions/src/rs_example.rs
b/rust/sedona-raster-functions/src/rs_example.rs
index d5bce5c8..4379192a 100644
--- a/rust/sedona-raster-functions/src/rs_example.rs
+++ b/rust/sedona-raster-functions/src/rs_example.rs
@@ -83,7 +83,7 @@ impl SedonaScalarKernel for RsExample {
skew_x: 1.0,
skew_y: 1.0,
};
- let crs = lnglat().unwrap().to_json();
+ let crs = lnglat().unwrap().to_crs_string();
builder.start_raster(&raster_metadata, Some(&crs))?;
let nodata_value = 127u8;
for band_id in 1..=3 {
diff --git a/rust/sedona-raster-functions/src/rs_srid.rs
b/rust/sedona-raster-functions/src/rs_srid.rs
new file mode 100644
index 00000000..b3e3709c
--- /dev/null
+++ b/rust/sedona-raster-functions/src/rs_srid.rs
@@ -0,0 +1,264 @@
+// 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::builder::UInt32Builder;
+use arrow_schema::DataType;
+use datafusion_common::error::Result;
+use datafusion_common::DataFusionError;
+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::crs::deserialize_crs;
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+
+/// RS_SRID() scalar UDF implementation
+///
+/// Extract the SRID (Spatial Reference ID) of the raster
+pub fn rs_srid_udf() -> SedonaScalarUDF {
+ SedonaScalarUDF::new(
+ "rs_srid",
+ vec![Arc::new(RsSrid {})],
+ Volatility::Immutable,
+ Some(rs_srid_doc()),
+ )
+}
+
+/// RS_CRS() scalar UDF implementation
+///
+/// Extract the CRS (Coordinate Reference System) of the raster
+pub fn rs_crs_udf() -> SedonaScalarUDF {
+ SedonaScalarUDF::new(
+ "rs_crs",
+ vec![Arc::new(RsCrs {})],
+ Volatility::Immutable,
+ Some(rs_crs_doc()),
+ )
+}
+
+fn rs_srid_doc() -> Documentation {
+ Documentation::builder(
+ DOC_SECTION_OTHER,
+ "Return the spatial reference system identifier (SRID) of the
raster".to_string(),
+ "RS_SRID(raster: Raster)".to_string(),
+ )
+ .with_argument("raster", "Raster: Input raster")
+ .with_sql_example("SELECT RS_SRID(RS_Example())".to_string())
+ .build()
+}
+
+fn rs_crs_doc() -> Documentation {
+ Documentation::builder(
+ DOC_SECTION_OTHER,
+ "Return the coordinate reference system (CRS) of the
raster".to_string(),
+ "RS_CRS(raster: Raster)".to_string(),
+ )
+ .with_argument("raster", "Raster: Input raster")
+ .with_sql_example("SELECT RS_CRS(RS_Example())".to_string())
+ .build()
+}
+
+#[derive(Debug)]
+struct RsSrid {}
+
+impl SedonaScalarKernel for RsSrid {
+ fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+ let matcher = ArgMatcher::new(
+ vec![ArgMatcher::is_raster()],
+ SedonaType::Arrow(DataType::UInt32),
+ );
+
+ 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 =
UInt32Builder::with_capacity(executor.num_iterations());
+
+ executor.execute_raster_void(|_i, raster_opt| {
+ match raster_opt {
+ None => builder.append_null(),
+ Some(raster) => {
+ match raster.crs() {
+ None => {
+ // When no CRS is set, SRID is 0
+ builder.append_value(0);
+ }
+ Some(crs_str) => {
+ let crs = deserialize_crs(crs_str).map_err(|e| {
+ DataFusionError::Execution(format!(
+ "Failed to deserialize CRS: {}",
+ e
+ ))
+ })?;
+
+ match crs {
+ Some(crs_ref) => {
+ let srid = crs_ref.srid().map_err(|e| {
+ DataFusionError::Execution(format!(
+ "Failed to get SRID from CRS: {}",
+ e
+ ))
+ })?;
+
+ match srid {
+ Some(srid_val) =>
builder.append_value(srid_val),
+ None => {
+ return
Err(DataFusionError::Execution(
+ "CRS has no SRID".to_string(),
+ ))
+ }
+ }
+ }
+ None => builder.append_value(0),
+ }
+ }
+ }
+ }
+ }
+ Ok(())
+ })?;
+
+ executor.finish(Arc::new(builder.finish()))
+ }
+}
+
+#[derive(Debug)]
+struct RsCrs {}
+
+impl SedonaScalarKernel for RsCrs {
+ 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 = "EPSG:4326".len() * executor.num_iterations();
+ let mut builder =
+ StringBuilder::with_capacity(executor.num_iterations(),
preallocate_bytes);
+
+ executor.execute_raster_void(|_i, raster_opt| {
+ match raster_opt {
+ None => builder.append_null(),
+ Some(raster) => match raster.crs() {
+ None => builder.append_null(),
+ Some(crs_str) => {
+ let crs = deserialize_crs(crs_str).map_err(|e| {
+ DataFusionError::Execution(format!("Failed to
deserialize CRS: {}", e))
+ })?;
+
+ let crs_string = crs
+ .ok_or_else(|| {
+ DataFusionError::Execution(
+ "Failed to parse non-null CRS
string".to_string(),
+ )
+ })?
+ .to_crs_string();
+ builder.append_value(crs_string);
+ }
+ },
+ }
+ Ok(())
+ })?;
+
+ executor.finish(Arc::new(builder.finish()))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use arrow_array::{StringArray, UInt32Array};
+ use datafusion_common::ScalarValue;
+ use datafusion_expr::ScalarUDF;
+ use sedona_schema::datatypes::RASTER;
+ use sedona_testing::compare::assert_array_equal;
+ use sedona_testing::rasters::generate_test_rasters;
+ use sedona_testing::testers::ScalarUdfTester;
+
+ #[test]
+ fn udf_metadata() {
+ let udf: ScalarUDF = rs_srid_udf().into();
+ assert_eq!(udf.name(), "rs_srid");
+ assert!(udf.documentation().is_some());
+
+ let udf: ScalarUDF = rs_crs_udf().into();
+ assert_eq!(udf.name(), "rs_crs");
+ assert!(udf.documentation().is_some());
+ }
+
+ #[test]
+ fn udf_srid() {
+ let udf: ScalarUDF = rs_srid_udf().into();
+ let tester = ScalarUdfTester::new(udf, vec![RASTER]);
+
+ tester.assert_return_type(DataType::UInt32);
+
+ // Test with rasters that have CRS set (generate_test_rasters sets
OGC:CRS84 which maps to 4326)
+ let rasters = generate_test_rasters(3, Some(1)).unwrap();
+ let expected: Arc<dyn arrow_array::Array> =
+ Arc::new(UInt32Array::from(vec![Some(4326), None, Some(4326)]));
+
+ let result = tester.invoke_array(Arc::new(rasters)).unwrap();
+ assert_array_equal(&result, &expected);
+
+ // Test with null scalar
+ let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+ tester.assert_scalar_result_equals(result, ScalarValue::UInt32(None));
+ }
+
+ #[test]
+ fn udf_crs() {
+ let udf: ScalarUDF = rs_crs_udf().into();
+ let tester = ScalarUdfTester::new(udf, vec![RASTER]);
+
+ tester.assert_return_type(DataType::Utf8);
+
+ // Test with rasters that have CRS set (generate_test_rasters sets
OGC:CRS84)
+ let rasters = generate_test_rasters(3, Some(1)).unwrap();
+ let expected_crs = "OGC:CRS84".to_string();
+ let expected: Arc<dyn arrow_array::Array> =
Arc::new(StringArray::from(vec![
+ Some(expected_crs.clone()),
+ None,
+ Some(expected_crs.clone()),
+ ]));
+
+ let result = tester.invoke_array(Arc::new(rasters)).unwrap();
+ assert_array_equal(&result, &expected);
+
+ // 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-testing/src/rasters.rs
b/rust/sedona-testing/src/rasters.rs
index 0ef30f8c..777ce1ae 100644
--- a/rust/sedona-testing/src/rasters.rs
+++ b/rust/sedona-testing/src/rasters.rs
@@ -91,6 +91,7 @@ pub fn generate_tiled_rasters(
let (x_tiles, y_tiles) = number_of_tiles;
let mut raster_builder = RasterBuilder::new(x_tiles * y_tiles);
let band_count = 3;
+ let crs = lnglat().unwrap().to_crs_string();
for tile_y in 0..y_tiles {
for tile_x in 0..x_tiles {
@@ -108,7 +109,7 @@ pub fn generate_tiled_rasters(
skew_y: 0.0,
};
- raster_builder.start_raster(&raster_metadata, None)?;
+ raster_builder.start_raster(&raster_metadata, Some(&crs))?;
for _ in 0..band_count {
// Set a nodata value appropriate for the data type