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 bf760b1e feat(rust/sedona-functions): Add raster display support to
SD_Format (#591)
bf760b1e is described below
commit bf760b1e12dca7fbee2657363cc77b141fd8b44e
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Feb 19 04:05:36 2026 +0800
feat(rust/sedona-functions): Add raster display support to SD_Format (#591)
Co-authored-by: Dewey Dunnington <[email protected]>
---
Cargo.lock | 1 +
rust/sedona-functions/Cargo.toml | 1 +
rust/sedona-functions/src/sd_format.rs | 117 ++++++++++++++++++++++-
rust/sedona-raster/src/display.rs | 163 +++++++++++++++++++++++++++++++++
rust/sedona-raster/src/lib.rs | 1 +
5 files changed, 278 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 03686607..f518de7f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5264,6 +5264,7 @@ dependencies = [
"sedona-common",
"sedona-expr",
"sedona-geometry",
+ "sedona-raster",
"sedona-schema",
"sedona-testing",
"serde_json",
diff --git a/rust/sedona-functions/Cargo.toml b/rust/sedona-functions/Cargo.toml
index 41b26e24..365687df 100644
--- a/rust/sedona-functions/Cargo.toml
+++ b/rust/sedona-functions/Cargo.toml
@@ -48,6 +48,7 @@ geo-traits = { workspace = true }
sedona-common = { workspace = true }
sedona-expr = { workspace = true }
sedona-geometry = { workspace = true }
+sedona-raster = { workspace = true }
sedona-schema = { workspace = true }
wkb = { workspace = true }
wkt = { workspace = true }
diff --git a/rust/sedona-functions/src/sd_format.rs
b/rust/sedona-functions/src/sd_format.rs
index 8cafaaa2..2926efa9 100644
--- a/rust/sedona-functions/src/sd_format.rs
+++ b/rust/sedona-functions/src/sd_format.rs
@@ -14,7 +14,7 @@
// 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 std::{fmt::Write, sync::Arc, vec};
use crate::executor::WkbExecutor;
use arrow_array::{
@@ -28,6 +28,8 @@ use datafusion_common::{
};
use datafusion_expr::{ColumnarValue, Volatility};
use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_raster::array::RasterStructArray;
+use sedona_raster::display::RasterDisplay;
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
/// SD_Format() scalar UDF implementation
@@ -124,7 +126,7 @@ fn sedona_type_to_formatted_type(sedona_type: &SedonaType)
-> Result<SedonaType>
_ => Ok(sedona_type.clone()),
}
}
- SedonaType::Raster => internal_err!("SD_Format does not support Raster
types"),
+ SedonaType::Raster => Ok(SedonaType::Arrow(DataType::Utf8)),
}
}
@@ -142,6 +144,7 @@ fn columnar_value_to_formatted_value(
SedonaType::Wkb(_, _) | SedonaType::WkbView(_, _) => {
geospatial_value_to_formatted_value(sedona_type, columnar_value,
maybe_width_hint)
}
+ SedonaType::Raster => raster_value_to_formatted_value(columnar_value,
maybe_width_hint),
SedonaType::Arrow(arrow_type) => match arrow_type {
DataType::Struct(fields) => match columnar_value {
ColumnarValue::Array(array) => {
@@ -186,7 +189,6 @@ fn columnar_value_to_formatted_value(
},
_ => Ok(columnar_value.clone()),
},
- SedonaType::Raster => internal_err!("SD_Format does not support Raster
types"),
}
}
@@ -331,6 +333,53 @@ fn list_view_value_to_formatted_value<OffsetSize:
OffsetSizeTrait>(
))
}
+fn raster_value_to_formatted_value(
+ columnar_value: &ColumnarValue,
+ maybe_width_hint: Option<usize>,
+) -> Result<ColumnarValue> {
+ match columnar_value {
+ ColumnarValue::Array(array) => {
+ let struct_array = array.as_struct();
+ let raster_array = RasterStructArray::new(struct_array);
+ let min_output_size = match maybe_width_hint {
+ Some(width_hint) => raster_array.len() * width_hint,
+ None => raster_array.len() * 48,
+ };
+ let mut builder =
+ StringBuilder::with_capacity(raster_array.len(),
min_output_size.max(1));
+
+ for i in 0..raster_array.len() {
+ if raster_array.is_null(i) {
+ builder.append_null();
+ continue;
+ }
+
+ let raster = raster_array.get(i)?;
+ let mut limited_output =
+ LimitedSizeOutput::new(&mut builder,
maybe_width_hint.unwrap_or(usize::MAX));
+ let _ = write!(limited_output, "{}", RasterDisplay(&raster));
+ builder.append_value("");
+ }
+
+ Ok(ColumnarValue::Array(Arc::new(builder.finish())))
+ }
+ ColumnarValue::Scalar(ScalarValue::Struct(struct_array)) => {
+ let formatted = raster_value_to_formatted_value(
+ &ColumnarValue::Array(Arc::new(struct_array.as_ref().clone())),
+ maybe_width_hint,
+ )?;
+ if let ColumnarValue::Array(array) = formatted {
+ Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(
+ &array, 0,
+ )?))
+ } else {
+ internal_err!("Expected array formatted value for raster
scalar")
+ }
+ }
+ _ => internal_err!("Unsupported raster columnar value"),
+ }
+}
+
struct LimitedSizeOutput<'a, T> {
inner: &'a mut T,
current_item_size: usize,
@@ -370,9 +419,11 @@ mod tests {
use datafusion_expr::ScalarUDF;
use rstest::rstest;
use sedona_schema::datatypes::{
- WKB_GEOGRAPHY, WKB_GEOMETRY, WKB_VIEW_GEOGRAPHY, WKB_VIEW_GEOMETRY,
+ RASTER, WKB_GEOGRAPHY, WKB_GEOMETRY, WKB_VIEW_GEOGRAPHY,
WKB_VIEW_GEOMETRY,
+ };
+ use sedona_testing::{
+ create::create_array, rasters::generate_test_rasters,
testers::ScalarUdfTester,
};
- use sedona_testing::{create::create_array, testers::ScalarUdfTester};
use std::sync::Arc;
use super::*;
@@ -530,6 +581,62 @@ mod tests {
}
}
+ #[test]
+ fn sd_format_formats_raster_columns() {
+ let udf = sd_format_udf();
+ let tester = ScalarUdfTester::new(udf.into(), vec![RASTER]);
+
+ let raster_array = generate_test_rasters(3, Some(1)).unwrap();
+ let result =
tester.invoke_array(Arc::new(raster_array.clone())).unwrap();
+ let formatted = result.as_string::<i32>();
+
+ // Index 0: valid raster (no skew)
+ assert_eq!(formatted.value(0), "[1x2/1] @ [1 1.6 1.1 2] / OGC:CRS84");
+ // Index 1: null raster should produce null output
+ assert!(formatted.is_null(1));
+ // Index 2: valid raster (with skew)
+ assert_eq!(
+ formatted.value(2),
+ "[3x4/1] @ [3 2.4 3.84 4.24] skew=(0.06, 0.08) / OGC:CRS84"
+ );
+ }
+
+ #[test]
+ fn sd_format_formats_raster_columns_with_null() {
+ let udf = sd_format_udf();
+ let tester = ScalarUdfTester::new(udf.into(), vec![RASTER]);
+
+ // Generate 3 rasters with a null at index 1
+ let raster_array = generate_test_rasters(3, Some(1)).unwrap();
+ let result = tester.invoke_array(Arc::new(raster_array)).unwrap();
+ let formatted = result.as_string::<i32>();
+
+ // Index 0: valid raster (no skew)
+ assert!(formatted.value(0).starts_with("[1x2/"));
+ // Index 1: null raster should produce null output
+ assert!(formatted.is_null(1));
+ // Index 2: valid raster (with skew)
+ assert!(formatted.value(2).starts_with("[3x4/"));
+ }
+
+ #[test]
+ fn sd_format_formats_raster_columns_with_width_hint() {
+ let udf = sd_format_udf();
+ let tester =
+ ScalarUdfTester::new(udf.into(), vec![RASTER,
SedonaType::Arrow(DataType::Utf8)]);
+
+ let raster_array = generate_test_rasters(2, None).unwrap();
+ let result = tester
+ .invoke_array_scalar(Arc::new(raster_array), r#"{"width_hint":
10}"#)
+ .unwrap();
+ let formatted = result.as_string::<i32>();
+
+ // With a small width_hint, output should be truncated
+ let full_output = "[1x2/1] @ [1 1.6 1.1 2] / OGC:CRS84";
+ assert!(formatted.value(0).starts_with("["));
+ assert!(formatted.value(0).len() < full_output.len());
+ }
+
#[rstest]
fn sd_format_should_format_spatial_columns(
#[values(WKB_GEOMETRY, WKB_GEOGRAPHY, WKB_VIEW_GEOMETRY,
WKB_VIEW_GEOGRAPHY)]
diff --git a/rust/sedona-raster/src/display.rs
b/rust/sedona-raster/src/display.rs
new file mode 100644
index 00000000..400658a0
--- /dev/null
+++ b/rust/sedona-raster/src/display.rs
@@ -0,0 +1,163 @@
+// 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::fmt;
+
+use crate::affine_transformation::to_world_coordinate;
+use crate::traits::RasterRef;
+use sedona_schema::raster::StorageType;
+
+/// Wrapper for formatting a raster reference as a human-readable string.
+///
+/// # Format
+///
+/// Non-skewed rasters:
+/// ```text
+/// [WxH/nbands] @ [xmin ymin xmax ymax] / CRS
+/// ```
+///
+/// Skewed rasters (includes skew parameters):
+/// ```text
+/// [WxH/nbands] @ [xmin ymin xmax ymax] skew=(skew_x, skew_y) / CRS
+/// ```
+///
+/// With outdb bands:
+/// ```text
+/// [WxH/nbands] @ [xmin ymin xmax ymax] / CRS <outdb>
+/// ```
+///
+/// Without CRS:
+/// ```text
+/// [WxH/nbands] @ [xmin ymin xmax ymax]
+/// ```
+///
+/// # Examples
+///
+/// ```text
+/// [64x32/3] @ [43.08 79.07 171.08 143.07] / OGC:CRS84
+/// [3x4/1] @ [3 2.4 3.84 4.24] skew=(0.06, 0.08) / EPSG:2193
+/// [10x10/1] @ [0 0 10 10] / OGC:CRS84 <outdb>
+/// ```
+pub struct RasterDisplay<'a>(pub &'a dyn RasterRef);
+
+impl fmt::Display for RasterDisplay<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let raster = self.0;
+ let metadata = raster.metadata();
+ let bands = raster.bands();
+
+ let width = metadata.width();
+ let height = metadata.height();
+ let nbands = bands.len();
+
+ // Compute axis-aligned bounding box from 4 corners in world
coordinates.
+ // This handles both skewed and non-skewed rasters correctly.
+ let w = width as i64;
+ let h = height as i64;
+ let (ulx, uly) = to_world_coordinate(raster, 0, 0);
+ let (urx, ury) = to_world_coordinate(raster, w, 0);
+ let (lrx, lry) = to_world_coordinate(raster, w, h);
+ let (llx, lly) = to_world_coordinate(raster, 0, h);
+
+ let xmin = ulx.min(urx).min(lrx).min(llx);
+ let xmax = ulx.max(urx).max(lrx).max(llx);
+ let ymin = uly.min(ury).min(lry).min(lly);
+ let ymax = uly.max(ury).max(lry).max(lly);
+
+ let skew_x = metadata.skew_x();
+ let skew_y = metadata.skew_y();
+ let has_skew = skew_x != 0.0 || skew_y != 0.0;
+
+ let has_outdb = bands
+ .iter()
+ .any(|band| matches!(band.metadata().storage_type(),
Ok(StorageType::OutDbRef)));
+
+ // Write: [WxH/nbands] @ [xmin ymin xmax ymax]
+ write!(
+ f,
+ "[{width}x{height}/{nbands}] @ [{xmin} {ymin} {xmax} {ymax}]"
+ )?;
+
+ // Conditionally append skew info when the raster is rotated/skewed
+ if has_skew {
+ write!(f, " skew=({skew_x}, {skew_y})")?;
+ }
+
+ // Append CRS if present. For PROJJSON (starts with '{'), show compact
placeholder.
+ if let Some(crs) = raster.crs() {
+ if crs.starts_with('{') {
+ write!(f, " / {{...}}")?;
+ } else {
+ write!(f, " / {crs}")?;
+ }
+ }
+
+ if has_outdb {
+ write!(f, " <outdb>")?;
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::array::RasterStructArray;
+ use sedona_testing::rasters::generate_test_rasters;
+
+ #[test]
+ fn display_non_skewed_raster() {
+ // i=0: w=1, h=2, scale=(0.1, -0.2), skew=(0, 0), CRS=OGC:CRS84
+ // Bounds: xmin=1, ymin=1.6, xmax=1.1, ymax=2
+ let rasters = generate_test_rasters(1, None).unwrap();
+ let raster_array = RasterStructArray::new(&rasters);
+ let raster = raster_array.get(0).unwrap();
+
+ let display = format!("{}", RasterDisplay(&raster));
+ assert_eq!(display, "[1x2/1] @ [1 1.6 1.1 2] / OGC:CRS84");
+ }
+
+ #[test]
+ fn display_skewed_raster() {
+ // i=2: w=3, h=4, scale=(0.2, -0.4), skew=(0.06, 0.08), CRS=OGC:CRS84
+ // Corners: (3,4), (3.6,4.24), (3.84,2.64), (3.24,2.4)
+ // AABB: xmin=3, ymin=2.4, xmax=3.84, ymax=4.24
+ let rasters = generate_test_rasters(3, None).unwrap();
+ let raster_array = RasterStructArray::new(&rasters);
+ let raster = raster_array.get(2).unwrap();
+
+ let display = format!("{}", RasterDisplay(&raster));
+ assert_eq!(
+ display,
+ "[3x4/1] @ [3 2.4 3.84 4.24] skew=(0.06, 0.08) / OGC:CRS84"
+ );
+ }
+
+ #[test]
+ fn display_write_to_fmt_write() {
+ // Verify RasterDisplay works with any fmt::Write target (e.g., String)
+ let rasters = generate_test_rasters(1, None).unwrap();
+ let raster_array = RasterStructArray::new(&rasters);
+ let raster = raster_array.get(0).unwrap();
+
+ let mut buf = String::new();
+ use std::fmt::Write;
+ write!(buf, "{}", RasterDisplay(&raster)).unwrap();
+ assert_eq!(buf, "[1x2/1] @ [1 1.6 1.1 2] / OGC:CRS84");
+ }
+}
diff --git a/rust/sedona-raster/src/lib.rs b/rust/sedona-raster/src/lib.rs
index de2e9b2e..77db0c0d 100644
--- a/rust/sedona-raster/src/lib.rs
+++ b/rust/sedona-raster/src/lib.rs
@@ -18,4 +18,5 @@
pub mod affine_transformation;
pub mod array;
pub mod builder;
+pub mod display;
pub mod traits;