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;

Reply via email to