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 233a3298 feat(c/sedona-proj): Implement item crs support for
ST_Transform (#531)
233a3298 is described below
commit 233a3298ee0730d707936c6ba7f9c6f597468f58
Author: Dewey Dunnington <[email protected]>
AuthorDate: Thu Jan 22 16:43:45 2026 -0600
feat(c/sedona-proj): Implement item crs support for ST_Transform (#531)
Co-authored-by: Copilot <[email protected]>
---
Cargo.lock | 1 +
c/sedona-proj/Cargo.toml | 1 +
c/sedona-proj/src/st_transform.rs | 1081 +++++++++++++++++++--------------
rust/sedona-functions/src/executor.rs | 11 +-
rust/sedona-testing/src/testers.rs | 12 +-
5 files changed, 658 insertions(+), 448 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bafb1b7e..9a8e0b9d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5302,6 +5302,7 @@ dependencies = [
"geo-types",
"proj-sys",
"rstest",
+ "sedona-common",
"sedona-expr",
"sedona-functions",
"sedona-geometry",
diff --git a/c/sedona-proj/Cargo.toml b/c/sedona-proj/Cargo.toml
index 4368a112..0a145fd1 100644
--- a/c/sedona-proj/Cargo.toml
+++ b/c/sedona-proj/Cargo.toml
@@ -51,6 +51,7 @@ datafusion-common = { workspace = true }
datafusion-expr = { workspace = true }
geo-traits = { workspace = true }
proj-sys = { version = "0.26.0", optional = true }
+sedona-common = { workspace = true }
sedona-expr = { workspace = true }
sedona-functions = { workspace = true }
sedona-geometry = { workspace = true }
diff --git a/c/sedona-proj/src/st_transform.rs
b/c/sedona-proj/src/st_transform.rs
index 47a2a793..b7fd90a6 100644
--- a/c/sedona-proj/src/st_transform.rs
+++ b/c/sedona-proj/src/st_transform.rs
@@ -15,31 +15,353 @@
// specific language governing permissions and limitations
// under the License.
use crate::transform::{ProjCrsEngine, ProjCrsEngineBuilder};
-use arrow_array::builder::BinaryBuilder;
+use arrow_array::builder::{BinaryBuilder, StringViewBuilder};
+use arrow_array::ArrayRef;
use arrow_schema::DataType;
-use datafusion_common::{DataFusionError, Result, ScalarValue};
+use datafusion_common::cast::{as_string_view_array, as_struct_array};
+use datafusion_common::{exec_err, DataFusionError, Result, ScalarValue};
use datafusion_expr::ColumnarValue;
-use geo_traits::to_geo::ToGeoGeometry;
+use sedona_common::sedona_internal_err;
+use sedona_expr::item_crs::make_item_crs;
use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
use sedona_functions::executor::WkbExecutor;
use sedona_geometry::transform::{transform, CachingCrsEngine, CrsEngine,
CrsTransform};
use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
-use sedona_schema::crs::deserialize_crs;
-use sedona_schema::datatypes::{Edges, SedonaType};
+use sedona_schema::crs::{deserialize_crs, Crs};
+use sedona_schema::datatypes::{Edges, SedonaType, WKB_GEOMETRY,
WKB_GEOMETRY_ITEM_CRS};
use sedona_schema::matchers::ArgMatcher;
use std::cell::OnceCell;
-use std::rc::Rc;
+use std::io::Write;
+use std::iter::zip;
use std::sync::{Arc, RwLock};
use wkb::reader::Wkb;
-#[derive(Debug)]
-struct STTransform {}
-
/// ST_Transform() implementation using the proj crate
pub fn st_transform_impl() -> ScalarKernelRef {
Arc::new(STTransform {})
}
+#[derive(Debug)]
+struct STTransform {}
+
+impl SedonaScalarKernel for STTransform {
+ fn return_type_from_args_and_scalars(
+ &self,
+ arg_types: &[SedonaType],
+ scalar_args: &[Option<&ScalarValue>],
+ ) -> Result<Option<SedonaType>> {
+ let inputs = zip(arg_types, scalar_args)
+ .map(|(arg_type, arg_scalar)|
ArgInput::from_return_type_arg(arg_type, *arg_scalar))
+ .collect::<Vec<_>>();
+
+ if inputs.len() == 2 {
+ match (inputs[0], inputs[1]) {
+ // ScalarCrs output always returns a Wkb output type with
concrete Crs
+ (ArgInput::Geo(_), ArgInput::ScalarCrs(scalar_value))
+ | (ArgInput::ItemCrs, ArgInput::ScalarCrs(scalar_value)) => {
+ Ok(Some(output_type_from_scalar_crs_value(scalar_value)?))
+ }
+
+ // Geo or ItemCrs with ArrayCrs output always return ItemCrs
output
+ (ArgInput::Geo(_), ArgInput::ArrayCrs)
+ | (ArgInput::ItemCrs, ArgInput::ArrayCrs) => {
+ Ok(Some(WKB_GEOMETRY_ITEM_CRS.clone()))
+ }
+ _ => Ok(None),
+ }
+ } else if inputs.len() == 3 {
+ match (inputs[0], inputs[1], inputs[2]) {
+ // ScalarCrs output always returns a Wkb output type with
concrete Crs
+ (ArgInput::Geo(_), ArgInput::ScalarCrs(_),
ArgInput::ScalarCrs(scalar_value))
+ | (ArgInput::Geo(_), ArgInput::ArrayCrs,
ArgInput::ScalarCrs(scalar_value))
+ | (ArgInput::ItemCrs, ArgInput::ScalarCrs(_),
ArgInput::ScalarCrs(scalar_value))
+ | (ArgInput::ItemCrs, ArgInput::ArrayCrs,
ArgInput::ScalarCrs(scalar_value)) => {
+ Ok(Some(output_type_from_scalar_crs_value(scalar_value)?))
+ }
+
+ // Geo or ItemCrs with ArrayCrs output always return ItemCrs
output
+ (ArgInput::Geo(_), ArgInput::ScalarCrs(_), ArgInput::ArrayCrs)
+ | (ArgInput::Geo(_), ArgInput::ArrayCrs, ArgInput::ArrayCrs)
+ | (ArgInput::ItemCrs, ArgInput::ScalarCrs(_),
ArgInput::ArrayCrs)
+ | (ArgInput::ItemCrs, ArgInput::ArrayCrs, ArgInput::ArrayCrs)
=> {
+ Ok(Some(WKB_GEOMETRY_ITEM_CRS.clone()))
+ }
+ _ => Ok(None),
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn invoke_batch_from_args(
+ &self,
+ arg_types: &[SedonaType],
+ args: &[ColumnarValue],
+ _return_type: &SedonaType,
+ _num_rows: usize,
+ ) -> Result<ColumnarValue> {
+ let inputs = zip(arg_types, args)
+ .map(|(arg_type, arg)| ArgInput::from_arg(arg_type, arg))
+ .collect::<Vec<_>>();
+
+ let executor = WkbExecutor::new(arg_types, args);
+ let mut builder = BinaryBuilder::with_capacity(
+ executor.num_iterations(),
+ WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
+ );
+
+ // Optimize the easy case, where we have exactly one transformation
and there are no
+ // null or missing CRSes to contend with.
+ let from_index = inputs.len() - 2;
+ let to_index = inputs.len() - 1;
+ let (from, to) = (inputs[from_index], inputs[to_index]);
+ if let (Some(from_constant), Some(to_constant)) =
(from.crs_constant()?, to.crs_constant()?)
+ {
+ let maybe_from_crs = deserialize_crs(&from_constant)?;
+ let maybe_to_crs = deserialize_crs(&to_constant)?;
+ if let (Some(from_crs), Some(to_crs)) = (maybe_from_crs,
maybe_to_crs) {
+ with_global_proj_engine(|engine| {
+ let crs_transform = engine
+ .get_transform_crs_to_crs(
+ &from_crs.to_crs_string(),
+ &to_crs.to_crs_string(),
+ None,
+ "",
+ )
+ .map_err(|e|
DataFusionError::Execution(format!("{e}")))?;
+ executor.execute_wkb_void(|maybe_wkb| {
+ match maybe_wkb {
+ Some(wkb) => {
+ invoke_scalar(&wkb, crs_transform.as_ref(),
&mut builder)?;
+ builder.append_value([]);
+ }
+ None => builder.append_null(),
+ }
+ Ok(())
+ })?;
+ Ok(())
+ })?;
+ return executor.finish(Arc::new(builder.finish()));
+ }
+ }
+
+ // Iterate over pairs of CRS strings
+ let from_crs_array = from.crs_array(&args[from_index],
executor.num_iterations())?;
+ let to_crs_array = to.crs_array(&args[to_index],
executor.num_iterations())?;
+ let from_crs_string_view_array =
as_string_view_array(&from_crs_array)?;
+ let to_crs_string_view_array = as_string_view_array(&to_crs_array)?;
+ let mut crs_to_crs_iter = zip(from_crs_string_view_array,
to_crs_string_view_array);
+
+ // We might need to build an output array of sanitized CRS strings
+ let mut maybe_crs_output = if matches!(to, ArgInput::ArrayCrs) {
+ Some(StringViewBuilder::with_capacity(executor.num_iterations()))
+ } else {
+ None
+ };
+
+ with_global_proj_engine(|engine| {
+ executor.execute_wkb_void(|maybe_wkb| {
+ match (maybe_wkb, crs_to_crs_iter.next().unwrap()) {
+ (Some(wkb), (Some(from_crs_str), Some(to_crs_str))) => {
+ let maybe_from_crs = deserialize_crs(from_crs_str)?;
+ let maybe_to_crs = deserialize_crs(to_crs_str)?;
+
+ if let Some(crs_output) = &mut maybe_crs_output {
+ if let Some(to_crs) = &maybe_to_crs {
+
crs_output.append_value(to_crs.to_authority_code()?.unwrap_or_else(||
to_crs.to_crs_string()));
+ } else {
+ crs_output.append_null();
+ }
+ }
+
+ if maybe_from_crs == maybe_to_crs {
+ invoke_noop(&wkb, &mut builder)?;
+ builder.append_value([]);
+ return Ok(());
+ }
+
+ let crs_transform = match (maybe_from_crs,
maybe_to_crs) {
+ (Some(from_crs), Some(to_crs)) => {
+ engine
+
.get_transform_crs_to_crs(&from_crs.to_crs_string(), &to_crs.to_crs_string(),
None, "")
+ .map_err(|e|
DataFusionError::Execution(format!("{e}")))?
+ },
+ _ => return exec_err!(
+ "Can't transform to or from an unset CRS. Do
you need to call ST_SetSRID on the input?"
+ )
+ };
+
+ invoke_scalar(&wkb, crs_transform.as_ref(), &mut
builder)?;
+ builder.append_value([]);
+ }
+ _ => {
+ if let Some(crs_output) = &mut maybe_crs_output {
+ crs_output.append_null();
+ }
+
+ builder.append_null()
+ },
+ }
+ Ok(())
+ })?;
+ Ok(())
+ })?;
+
+ let output_geometry = executor.finish(Arc::new(builder.finish()))?;
+ if let Some(mut crs_output) = maybe_crs_output {
+ let output_crs = executor.finish(Arc::new(crs_output.finish()))?;
+ make_item_crs(&WKB_GEOMETRY, output_geometry, &output_crs, None)
+ } else {
+ Ok(output_geometry)
+ }
+ }
+
+ fn return_type(&self, _args: &[SedonaType]) -> Result<Option<SedonaType>,
DataFusionError> {
+ sedona_internal_err!("Return type should only be called with args")
+ }
+
+ fn invoke_batch(
+ &self,
+ _arg_types: &[SedonaType],
+ _args: &[ColumnarValue],
+ ) -> Result<ColumnarValue> {
+ sedona_internal_err!("invoke_batch should only be called with args")
+ }
+}
+
+fn output_type_from_scalar_crs_value(scalar_arg: &ScalarValue) ->
Result<SedonaType> {
+ if let Some(crs_str) = parse_crs_from_scalar_crs_value(scalar_arg)? {
+ Ok(SedonaType::Wkb(Edges::Planar, deserialize_crs(&crs_str)?))
+ } else {
+ Ok(WKB_GEOMETRY)
+ }
+}
+
+fn parse_crs_from_scalar_crs_value(scalar_arg: &ScalarValue) ->
Result<Option<String>> {
+ if let ScalarValue::Utf8(maybe_to_crs_str) =
scalar_arg.cast_to(&DataType::Utf8)? {
+ if let Some(to_crs_str) = maybe_to_crs_str {
+ Ok(Some(
+ deserialize_crs(&to_crs_str)?
+ .map(|crs| crs.to_crs_string())
+ .unwrap_or("0".to_string()),
+ ))
+ } else {
+ Ok(None)
+ }
+ } else {
+ sedona_internal_err!("Expected scalar cast to utf8 to be a
ScalarValue::Utf8")
+ }
+}
+
+fn invoke_noop(wkb: &Wkb, builder: &mut impl Write) -> Result<()> {
+ builder
+ .write_all(wkb.buf())
+ .map_err(DataFusionError::IoError)
+}
+
+fn invoke_scalar(wkb: &Wkb, trans: &dyn CrsTransform, builder: &mut impl
Write) -> Result<()> {
+ transform(wkb, trans, builder)
+ .map_err(|err| DataFusionError::Execution(format!("Transform error:
{err}")))?;
+ Ok(())
+}
+
+/// Helper to label arguments because we have a lot argument types that are
valid
+#[derive(Debug, Clone, Copy)]
+enum ArgInput<'a> {
+ /// Geometry input. This currently only matches geometry and not geography
+ /// because CRS support for geography is less clear at the moment. Must be
+ /// the first argument (and not supported for other arguments).
+ Geo(&'a Crs),
+ /// Item-level CRS input. Must be the first argument if present (not
supported
+ /// for other arguments).
+ ItemCrs,
+ /// Scalar CRS input. Supported for second and third arguments. When
present
+ /// as the last argument (to), this forces type-level CRS output.
+ ScalarCrs(&'a ScalarValue),
+ /// Array CRS input. Supported for second and third arguments. When present
+ /// as the last (to) argument, this forces Item CRS output.
+ ArrayCrs,
+ /// Sentinel for anything else
+ Unsupported,
+}
+
+impl<'a> ArgInput<'a> {
+ fn from_return_type_arg(arg_type: &'a SedonaType, scalar_arg: Option<&'a
ScalarValue>) -> Self {
+ if ArgMatcher::is_item_crs().match_type(arg_type) {
+ Self::ItemCrs
+ } else if ArgMatcher::is_numeric().match_type(arg_type)
+ || ArgMatcher::is_string().match_type(arg_type)
+ {
+ if let Some(scalar_crs) = scalar_arg {
+ Self::ScalarCrs(scalar_crs)
+ } else {
+ Self::ArrayCrs
+ }
+ } else {
+ match arg_type {
+ SedonaType::Wkb(Edges::Planar, crs) |
SedonaType::WkbView(Edges::Planar, crs) => {
+ Self::Geo(crs)
+ }
+ _ => Self::Unsupported,
+ }
+ }
+ }
+
+ fn from_arg(arg_type: &'a SedonaType, arg: &'a ColumnarValue) -> Self {
+ if ArgMatcher::is_item_crs().match_type(arg_type) {
+ Self::ItemCrs
+ } else if ArgMatcher::is_numeric().match_type(arg_type)
+ || ArgMatcher::is_string().match_type(arg_type)
+ {
+ match arg {
+ ColumnarValue::Array(_) => Self::ArrayCrs,
+ ColumnarValue::Scalar(scalar_value) =>
Self::ScalarCrs(scalar_value),
+ }
+ } else {
+ match arg_type {
+ SedonaType::Wkb(_, crs) | SedonaType::WkbView(_, crs) =>
Self::Geo(crs),
+ _ => Self::Unsupported,
+ }
+ }
+ }
+
+ fn crs_constant(&self) -> Result<Option<String>> {
+ match self {
+ ArgInput::Geo(crs) => {
+ let crs_str = if let Some(crs) = crs {
+ crs.to_crs_string()
+ } else {
+ "0".to_string()
+ };
+
+ Ok(Some(crs_str))
+ }
+ ArgInput::ScalarCrs(scalar_value) =>
parse_crs_from_scalar_crs_value(scalar_value),
+ _ => Ok(None),
+ }
+ }
+
+ fn crs_array(&self, arg: &ColumnarValue, iterations: usize) ->
Result<ArrayRef> {
+ if let Some(crs_constant) = self.crs_constant()? {
+
ScalarValue::Utf8View(Some(crs_constant)).to_array_of_size(iterations)
+ } else if matches!(self, Self::ItemCrs) {
+ match arg {
+ ColumnarValue::Array(array) => {
+ let struct_array = as_struct_array(array)?;
+ Ok(struct_array.column(1).clone())
+ }
+ ColumnarValue::Scalar(ScalarValue::Struct(struct_array)) => {
+ Ok(struct_array.column(1).clone())
+ }
+ _ => sedona_internal_err!("Unexpected item_crs type"),
+ }
+ } else {
+ arg.cast_to(&DataType::Utf8View, None)?
+ .into_array(iterations)
+ }
+ }
+}
+
/// Configure the global PROJ engine
///
/// Provides an opportunity for a calling application to provide the
@@ -113,503 +435,370 @@ thread_local! {
};
}
-struct TransformArgIndexes {
- wkb: usize,
- first_crs: usize,
- second_crs: Option<usize>,
- lenient: Option<usize>,
-}
-
-impl TransformArgIndexes {
- fn new() -> Self {
- Self {
- wkb: 0,
- first_crs: 1,
- second_crs: None,
- lenient: None,
- }
- }
-}
-
-fn define_arg_indexes(arg_types: &[SedonaType], indexes: &mut
TransformArgIndexes) {
- indexes.wkb = 0;
- indexes.first_crs = 1;
-
- for (i, arg_type) in arg_types.iter().enumerate().skip(2) {
- if ArgMatcher::is_numeric().match_type(arg_type)
- || ArgMatcher::is_string().match_type(arg_type)
- {
- indexes.second_crs = Some(i);
- } else if *arg_type == SedonaType::Arrow(DataType::Boolean) {
- indexes.lenient = Some(i);
- }
- }
-}
-
-impl SedonaScalarKernel for STTransform {
- fn return_type(&self, _args: &[SedonaType]) -> Result<Option<SedonaType>,
DataFusionError> {
- Err(DataFusionError::Internal(
- "Return type should only be called with args".to_string(),
- ))
- }
- fn return_type_from_args_and_scalars(
- &self,
- arg_types: &[SedonaType],
- scalar_args: &[Option<&ScalarValue>],
- ) -> Result<Option<SedonaType>> {
- let matcher = ArgMatcher::new(
- vec![
- ArgMatcher::is_geometry(),
- ArgMatcher::or(vec![ArgMatcher::is_numeric(),
ArgMatcher::is_string()]),
- ArgMatcher::optional(ArgMatcher::or(vec![
- ArgMatcher::is_numeric(),
- ArgMatcher::is_string(),
- ])),
- ArgMatcher::optional(ArgMatcher::is_boolean()),
- ],
- SedonaType::Wkb(Edges::Planar, None),
- );
-
- if !matcher.matches(arg_types) {
- return Ok(None);
- }
-
- let mut indexes = TransformArgIndexes::new();
- define_arg_indexes(arg_types, &mut indexes);
-
- let scalar_arg_opt = if let Some(second_crs_index) =
indexes.second_crs {
- scalar_args.get(second_crs_index).unwrap()
- } else {
- scalar_args.get(indexes.first_crs).unwrap()
- };
-
- let crs_str_opt = if let Some(scalar_crs) = scalar_arg_opt {
- to_crs_str(scalar_crs)
- } else {
- None
- };
-
- // If there is no CRS argument, we cannot determine the return type.
- match crs_str_opt {
- Some(to_crs) => {
- let crs = deserialize_crs(&to_crs)?;
- Ok(Some(SedonaType::Wkb(Edges::Planar, crs)))
- }
- _ => Ok(Some(SedonaType::Wkb(Edges::Planar, None))),
- }
- }
-
- fn invoke_batch(
- &self,
- arg_types: &[SedonaType],
- args: &[ColumnarValue],
- ) -> Result<ColumnarValue> {
- let executor = WkbExecutor::new(arg_types, args);
- let mut builder = BinaryBuilder::with_capacity(
- executor.num_iterations(),
- WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
- );
-
- let mut indexes = TransformArgIndexes::new();
- define_arg_indexes(arg_types, &mut indexes);
-
- let first_crs = get_crs_str(args, indexes.first_crs).ok_or_else(|| {
- DataFusionError::Execution(
- "First CRS argument must be a string or numeric
scalar".to_string(),
- )
- })?;
-
- let lenient = indexes
- .lenient
- .is_some_and(|i| get_scalar_bool(args, i).unwrap_or(false));
-
- let second_crs = if let Some(second_crs_index) = indexes.second_crs {
- get_crs_str(args, second_crs_index)
- } else {
- None
- };
-
- with_global_proj_engine(|engine| {
- let crs_from_geo = parse_source_crs(&arg_types[indexes.wkb])?;
-
- let transform = match &second_crs {
- Some(to_crs) => get_transform_crs_to_crs(engine, &first_crs,
to_crs)?,
- None => get_transform_to_crs(engine, crs_from_geo, &first_crs,
lenient)?,
- };
-
- executor.execute_wkb_void(|maybe_wkb| {
- match maybe_wkb {
- Some(wkb) => invoke_scalar(&wkb, transform.as_ref(), &mut
builder)?,
- None => builder.append_null(),
- }
-
- Ok(())
- })?;
-
- Ok(())
- })?;
-
- executor.finish(Arc::new(builder.finish()))
- }
-}
-
-fn get_transform_to_crs(
- engine: &dyn CrsEngine,
- source_crs_opt: Option<String>,
- to_crs: &str,
- lenient: bool,
-) -> Result<Rc<dyn CrsTransform>, DataFusionError> {
- let from_crs = match source_crs_opt {
- Some(crs) => crs,
- None if lenient => "EPSG:4326".to_string(),
- None => {
- return Err(DataFusionError::Execution(
- "Source CRS is required when transforming to a
CRS".to_string(),
- ))
- }
- };
- get_transform_crs_to_crs(engine, &from_crs, to_crs)
-}
-
-fn get_transform_crs_to_crs(
- engine: &dyn CrsEngine,
- from_crs: &str,
- to_crs: &str,
-) -> Result<Rc<dyn CrsTransform>, DataFusionError> {
- engine
- .get_transform_crs_to_crs(from_crs, to_crs, None, "")
- .map_err(|err| DataFusionError::Execution(format!("Transform error:
{err}")))
-}
-
-fn invoke_scalar(wkb: &Wkb, trans: &dyn CrsTransform, builder: &mut
BinaryBuilder) -> Result<()> {
- let geo_geom = wkb.to_geometry();
- transform(&geo_geom, trans, builder)
- .map_err(|err| DataFusionError::Execution(format!("Transform error:
{err}")))?;
- builder.append_value([]);
- Ok(())
-}
-
-fn parse_source_crs(source_type: &SedonaType) -> Result<Option<String>> {
- match source_type {
- SedonaType::Wkb(_, Some(crs)) | SedonaType::WkbView(_, Some(crs)) => {
- Ok(Some(crs.to_crs_string()))
- }
- _ => Ok(None),
- }
-}
-
-fn to_crs_str(scalar_arg: &ScalarValue) -> Option<String> {
- if let Ok(ScalarValue::Utf8(Some(crs))) =
scalar_arg.cast_to(&DataType::Utf8) {
- if crs.chars().all(|c| c.is_ascii_digit()) {
- return Some(format!("EPSG:{crs}"));
- } else {
- return Some(crs);
- }
- }
-
- None
-}
-
-fn get_crs_str(args: &[ColumnarValue], index: usize) -> Option<String> {
- if let ColumnarValue::Scalar(scalar_crs) = &args[index] {
- return to_crs_str(scalar_crs);
- }
- None
-}
-
-fn get_scalar_bool(args: &[ColumnarValue], index: usize) -> Option<bool> {
- if let Some(ColumnarValue::Scalar(ScalarValue::Boolean(opt_bool))) =
args.get(index) {
- *opt_bool
- } else {
- None
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use arrow_array::create_array;
use arrow_array::ArrayRef;
- use arrow_schema::{DataType, Field};
- use datafusion_common::config::ConfigOptions;
- use datafusion_expr::{ColumnarValue, ReturnFieldArgs, ScalarFunctionArgs,
ScalarUDFImpl};
+ use arrow_schema::DataType;
use rstest::rstest;
use sedona_expr::scalar_udf::SedonaScalarUDF;
use sedona_schema::crs::lnglat;
use sedona_schema::crs::Crs;
use sedona_schema::datatypes::WKB_GEOMETRY;
- use sedona_testing::compare::assert_value_equal;
- use sedona_testing::{create::create_array, create::create_array_value};
+ use sedona_testing::compare::assert_array_equal;
+ use sedona_testing::create::create_array;
+ use sedona_testing::create::create_array_item_crs;
+ use sedona_testing::create::create_scalar;
+ use sedona_testing::testers::ScalarUdfTester;
const NAD83ZONE6PROJ: &str = "EPSG:2230";
const WGS84: &str = "EPSG:4326";
- #[rstest]
- fn invalid_arg_checks() {
- let udf: SedonaScalarUDF = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
-
- // No args
- let result = udf.return_field_from_args(ReturnFieldArgs {
- arg_fields: &[],
- scalar_arguments: &[],
- });
- assert!(
- result.is_err()
- && result
- .unwrap_err()
- .to_string()
- .contains("No kernel matching arguments")
+ #[test]
+ fn test_invoke_with_string() {
+ let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
+ let geometry_input = SedonaType::Wkb(Edges::Planar, lnglat());
+ let tester = ScalarUdfTester::new(
+ udf.into(),
+ vec![geometry_input.clone(), SedonaType::Arrow(DataType::Utf8)],
);
- // Too many args
- let arg_types = [
- WKB_GEOMETRY,
- SedonaType::Arrow(DataType::Utf8),
- SedonaType::Arrow(DataType::Utf8),
- SedonaType::Arrow(DataType::Boolean),
- SedonaType::Arrow(DataType::Int32),
- ];
- let arg_fields: Vec<Arc<Field>> = arg_types
- .iter()
- .map(|arg_type| Arc::new(arg_type.to_storage_field("",
true).unwrap()))
- .collect();
- let result = udf.return_field_from_args(ReturnFieldArgs {
- arg_fields: &arg_fields,
- scalar_arguments: &[None, None, None, None, None],
- });
- assert!(
- result.is_err()
- && result
- .unwrap_err()
- .to_string()
- .contains("No kernel matching arguments")
- );
+ // Return type with scalar to argument (returns type-level CRS)
+ let expected_return_type = SedonaType::Wkb(Edges::Planar,
get_crs(NAD83ZONE6PROJ));
+ let return_type = tester
+ .return_type_with_scalar_scalar(Option::<&str>::None,
Some(NAD83ZONE6PROJ))
+ .unwrap();
+ assert_eq!(return_type, expected_return_type);
- // First arg not geometry
- let arg_types = [
- SedonaType::Arrow(DataType::Utf8),
- SedonaType::Arrow(DataType::Utf8),
- ];
- let arg_fields: Vec<Arc<Field>> = arg_types
- .iter()
- .map(|arg_type| Arc::new(arg_type.to_storage_field("",
true).unwrap()))
- .collect();
- let result = udf.return_field_from_args(ReturnFieldArgs {
- arg_fields: &arg_fields,
- scalar_arguments: &[None, None],
- });
- assert!(
- result.is_err()
- && result
- .unwrap_err()
- .to_string()
- .contains("No kernel matching arguments")
+ // Return type with array to argument (returns item CRS)
+ let return_type = tester.return_type().unwrap();
+ assert_eq!(return_type, WKB_GEOMETRY_ITEM_CRS.clone());
+
+ // Invoke with scalar to argument (returns type-level CRS)
+ let expected_array = create_array(
+ &[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
+ &expected_return_type,
);
+ let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&geometry_input);
+ let result = tester.invoke_array_scalar(wkb, NAD83ZONE6PROJ).unwrap();
+ assert_array_equal(&result, &expected_array);
- // Second arg not string or numeric
- let arg_types = [WKB_GEOMETRY, SedonaType::Arrow(DataType::Boolean)];
- let arg_fields: Vec<Arc<Field>> = arg_types
- .iter()
- .map(|arg_type| Arc::new(arg_type.to_storage_field("",
true).unwrap()))
- .collect();
- let result = udf.return_field_from_args(ReturnFieldArgs {
- arg_fields: &arg_fields,
- scalar_arguments: &[None, None],
- });
- assert!(
- result.is_err()
- && result
- .unwrap_err()
- .to_string()
- .contains("No kernel matching arguments")
+ // Invoke with array to argument (returns item CRS)
+ let expected_array = create_array_item_crs(
+ &[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
+ [None, Some(NAD83ZONE6PROJ)],
+ &WKB_GEOMETRY,
);
+ let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&geometry_input);
+ let crs = create_array!(Utf8, [None, Some(NAD83ZONE6PROJ)]) as
ArrayRef;
+ let result = tester.invoke_array_array(wkb, crs).unwrap();
+ assert_array_equal(&result, &expected_array);
}
- #[rstest]
- fn test_invoke_batch_with_geo_crs() {
- // From-CRS pulled from sedona type
- let arg_types = [
- SedonaType::Wkb(Edges::Planar, lnglat()),
- SedonaType::Arrow(DataType::Utf8),
- ];
+ #[test]
+ fn test_invoke_with_srid() {
+ let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
+ let geometry_input = SedonaType::Wkb(Edges::Planar, lnglat());
+ let tester = ScalarUdfTester::new(
+ udf.into(),
+ vec![geometry_input.clone(), SedonaType::Arrow(DataType::UInt32)],
+ );
- let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&arg_types[0]);
+ // Return type with scalar to argument (returns type-level CRS)
+ let expected_return_type = SedonaType::Wkb(Edges::Planar,
get_crs(NAD83ZONE6PROJ));
+ let return_type = tester
+ .return_type_with_scalar_scalar(Option::<&str>::None, Some(2230))
+ .unwrap();
+ assert_eq!(return_type, expected_return_type);
- let scalar_args =
vec![ScalarValue::Utf8(Some(NAD83ZONE6PROJ.to_string()))];
+ // Return type with array to argument (returns item CRS)
+ let return_type = tester.return_type().unwrap();
+ assert_eq!(return_type, WKB_GEOMETRY_ITEM_CRS.clone());
- let expected = create_array_value(
+ // Invoke with scalar to argument (returns type-level CRS)
+ let expected_array = create_array(
&[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
- &SedonaType::Wkb(Edges::Planar, get_crs(NAD83ZONE6PROJ)),
+ &expected_return_type,
);
+ let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&geometry_input);
+ let result = tester.invoke_array_scalar(wkb, 2230).unwrap();
+ assert_array_equal(&result, &expected_array);
- let (result_type, result_col) =
- invoke_udf_test(wkb, scalar_args, arg_types.to_vec()).unwrap();
- assert_value_equal(&result_col, &expected);
- assert_eq!(
- result_type,
- SedonaType::Wkb(Edges::Planar, get_crs(NAD83ZONE6PROJ))
+ // Invoke with array to argument (returns item CRS)
+ let expected_array = create_array_item_crs(
+ &[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
+ [None, Some(NAD83ZONE6PROJ)],
+ &WKB_GEOMETRY,
);
+ let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&geometry_input);
+ let crs = create_array!(Int32, [None, Some(2230)]) as ArrayRef;
+ let result = tester.invoke_array_array(wkb, crs).unwrap();
+ assert_array_equal(&result, &expected_array);
}
- #[rstest]
- fn test_invoke_with_srids() {
- // Use an integer SRID for the to CRS
- let arg_types = [
- SedonaType::Wkb(Edges::Planar, lnglat()),
- SedonaType::Arrow(DataType::UInt32),
- ];
-
- let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&arg_types[0]);
-
- let scalar_args = vec![ScalarValue::UInt32(Some(2230))];
+ #[test]
+ fn test_invoke_with_item_crs() {
+ let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
+ let geometry_input = WKB_GEOMETRY_ITEM_CRS.clone();
+ let tester = ScalarUdfTester::new(
+ udf.into(),
+ vec![geometry_input.clone(), SedonaType::Arrow(DataType::Utf8)],
+ );
- let expected = create_array_value(
+ // Return type with scalar to argument (returns type-level CRS)
+ // This is the same as for normal input
+ let expected_return_type = SedonaType::Wkb(Edges::Planar,
get_crs(NAD83ZONE6PROJ));
+ let return_type = tester
+ .return_type_with_scalar_scalar(Option::<&str>::None,
Some(NAD83ZONE6PROJ))
+ .unwrap();
+ assert_eq!(return_type, expected_return_type);
+
+ // Return type with array to argument (returns item CRS)
+ // This is the same as for normal input
+ let return_type = tester.return_type().unwrap();
+ assert_eq!(return_type, WKB_GEOMETRY_ITEM_CRS.clone());
+
+ // Invoke with scalar to argument (returns type-level CRS)
+ let expected_array = create_array(
&[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
- &SedonaType::Wkb(Edges::Planar, get_crs(NAD83ZONE6PROJ)),
+ &expected_return_type,
);
-
- let (result_type, result_col) =
- invoke_udf_test(wkb, scalar_args, arg_types.to_vec()).unwrap();
- assert_value_equal(&result_col, &expected);
- assert_eq!(
- result_type,
- SedonaType::Wkb(Edges::Planar, get_crs(NAD83ZONE6PROJ))
+ let array_in = create_array_item_crs(
+ &[None, Some("POINT (79.3871 43.6426)")],
+ [None, Some("EPSG:4326")],
+ &WKB_GEOMETRY,
);
- }
+ let result = tester
+ .invoke_array_scalar(array_in, NAD83ZONE6PROJ)
+ .unwrap();
+ assert_array_equal(&result, &expected_array);
- #[rstest]
- fn test_invoke_batch_with_lenient() {
- let arg_types = [
- WKB_GEOMETRY,
- SedonaType::Arrow(DataType::Utf8),
- SedonaType::Arrow(DataType::Boolean),
- ];
-
- let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&WKB_GEOMETRY);
- let scalar_args = vec![
- ScalarValue::Utf8(Some(NAD83ZONE6PROJ.to_string())),
- ScalarValue::Boolean(Some(true)),
- ];
-
- let expected = create_array_value(
+ // Invoke with array to argument (returns item CRS)
+ let expected_array = create_array_item_crs(
&[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
- &SedonaType::Wkb(Edges::Planar,
Some(get_crs(NAD83ZONE6PROJ).unwrap())),
+ [None, Some(NAD83ZONE6PROJ)],
+ &WKB_GEOMETRY,
);
-
- let (result_type, result_col) =
- invoke_udf_test(wkb, scalar_args, arg_types.to_vec()).unwrap();
- assert_value_equal(&result_col, &expected);
- assert_eq!(
- result_type,
- SedonaType::Wkb(Edges::Planar,
Some(get_crs(NAD83ZONE6PROJ).unwrap()))
+ let array_in = create_array_item_crs(
+ &[None, Some("POINT (79.3871 43.6426)")],
+ [None, Some("EPSG:4326")],
+ &WKB_GEOMETRY,
);
+ let crs = create_array!(Utf8, [None, Some(NAD83ZONE6PROJ)]) as
ArrayRef;
+ let result = tester.invoke_array_array(array_in, crs).unwrap();
+ assert_array_equal(&result, &expected_array);
}
#[rstest]
- fn test_invoke_batch_one_crs_no_lenient() {
- let arg_types = [WKB_GEOMETRY, SedonaType::Arrow(DataType::Utf8)];
+ fn test_invoke_source_arg() {
+ let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
+ let geometry_input = WKB_GEOMETRY;
+ let tester = ScalarUdfTester::new(
+ udf.into(),
+ vec![
+ geometry_input.clone(),
+ SedonaType::Arrow(DataType::Utf8),
+ SedonaType::Arrow(DataType::Utf8),
+ ],
+ );
- let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&WKB_GEOMETRY);
- let scalar_args =
vec![ScalarValue::Utf8(Some(NAD83ZONE6PROJ.to_string()))];
+ // Return type with scalar to argument (returns type-level CRS)
+ // This is the same as for normal input
+ let expected_return_type = SedonaType::Wkb(Edges::Planar,
get_crs(NAD83ZONE6PROJ));
+ let return_type = tester
+ .return_type_with_scalar_scalar_scalar(
+ Option::<&str>::None,
+ Option::<&str>::None,
+ Some(NAD83ZONE6PROJ),
+ )
+ .unwrap();
+ assert_eq!(return_type, expected_return_type);
- let err = invoke_udf_test(wkb, scalar_args, arg_types.to_vec());
- assert!(
- matches!(err, Err(DataFusionError::Execution(_))),
- "Expected an Execution error"
- );
- }
+ // Return type with array to argument (returns item CRS)
+ // This is the same as for normal input
+ let return_type = tester.return_type().unwrap();
+ assert_eq!(return_type, WKB_GEOMETRY_ITEM_CRS.clone());
- #[rstest]
- fn test_invoke_batch_with_source_arg() {
- let arg_types = [
- WKB_GEOMETRY,
- SedonaType::Arrow(DataType::Utf8),
- SedonaType::Arrow(DataType::Utf8),
- ];
-
- let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&WKB_GEOMETRY);
-
- let scalar_args = vec![
- ScalarValue::Utf8(Some(WGS84.to_string())),
- ScalarValue::Utf8(Some(NAD83ZONE6PROJ.to_string())),
- ];
-
- // Note: would be nice to have an epsilon of tolerance when validating
- let expected = create_array_value(
+ // Invoke with scalar to argument (returns type-level CRS)
+ let expected_array = create_array(
&[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
- &SedonaType::Wkb(Edges::Planar,
Some(get_crs(NAD83ZONE6PROJ).unwrap())),
+ &expected_return_type,
);
+ let array_in = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&geometry_input);
+ let crs_from = create_array!(Utf8, [None, Some(WGS84)]) as ArrayRef;
+ let result = tester
+ .invoke_array_array_scalar(array_in, crs_from, NAD83ZONE6PROJ)
+ .unwrap();
+ assert_array_equal(&result, &expected_array);
+
+ // Invoke with array to argument (returns item CRS)
+ let expected_array = create_array_item_crs(
+ &[None, Some("POINT (-21508577.363421552 34067918.06097863)")],
+ [None, Some(NAD83ZONE6PROJ)],
+ &WKB_GEOMETRY,
+ );
+ let array_in = create_array(&[None, Some("POINT (79.3871 43.6426)")],
&WKB_GEOMETRY);
+ let crs_from = create_array!(Utf8, [None, Some(WGS84)]) as ArrayRef;
+ let crs_to = create_array!(Utf8, [None, Some(NAD83ZONE6PROJ)]) as
ArrayRef;
+ let result = tester
+ .invoke_arrays(vec![array_in, crs_from, crs_to])
+ .unwrap();
+ assert_array_equal(&result, &expected_array);
+ }
- let (result_type, result_col) =
- invoke_udf_test(wkb.clone(), scalar_args,
arg_types.to_vec()).unwrap();
- assert_value_equal(&result_col, &expected);
- assert_eq!(
- result_type,
- SedonaType::Wkb(Edges::Planar,
Some(get_crs(NAD83ZONE6PROJ).unwrap()))
+ #[test]
+ fn test_invoke_null_crs_to() {
+ let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![WKB_GEOMETRY, SedonaType::Arrow(DataType::Utf8)],
);
- // Test with integer SRIDs
- let arg_types = [
- WKB_GEOMETRY,
- SedonaType::Arrow(DataType::Int32),
- SedonaType::Arrow(DataType::Int32),
- ];
-
- let scalar_args = vec![
- ScalarValue::Int32(Some(4326)),
- ScalarValue::Int32(Some(2230)),
- ];
-
- let (result_type, result_col) =
- invoke_udf_test(wkb, scalar_args, arg_types.to_vec()).unwrap();
- assert_value_equal(&result_col, &expected);
- assert_eq!(
- result_type,
- SedonaType::Wkb(Edges::Planar,
Some(get_crs(NAD83ZONE6PROJ).unwrap()))
+ // A null scalar CRS should generate WKB_GEOMETRY output with a type
+ // level CRS that is unset; however, all the output will be null.
+ let result = tester
+ .invoke_scalar_scalar("POINT (0 1)", ScalarValue::Null)
+ .unwrap();
+ assert_eq!(result, create_scalar(None, &WKB_GEOMETRY));
+
+ let expected_array = create_array(&[None, None, None], &WKB_GEOMETRY);
+ let array_in = create_array(
+ &[
+ Some("POINT (0 1)"),
+ Some("POINT (1 2)"),
+ Some("POINT (2 3)"),
+ ],
+ &WKB_GEOMETRY,
+ );
+ let result = tester
+ .invoke_array_scalar(array_in, ScalarValue::Null)
+ .unwrap();
+ assert_array_equal(&result, &expected_array);
+
+ // This currently has a side effect of working even though there is not
+ // valid transform from lnglat() to an unset CRS (because no
transformations
+ // will ever take place).
+ let geometry_input = SedonaType::Wkb(Edges::Planar, lnglat());
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![geometry_input, SedonaType::Arrow(DataType::Utf8)],
);
+ let result = tester
+ .invoke_scalar_scalar("POINT (0 1)", ScalarValue::Null)
+ .unwrap();
+ assert_eq!(result, create_scalar(None, &WKB_GEOMETRY));
}
- fn get_crs(auth_code: &str) -> Crs {
- deserialize_crs(auth_code).unwrap()
+ #[test]
+ fn test_invoke_unset_crs_to() {
+ let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![WKB_GEOMETRY, SedonaType::Arrow(DataType::Int32)],
+ );
+
+ // A unset scalar CRS should generate WKB_GEOMETRY output with a type
+ // level CRS that is unset. This transformation is only valid if the
input
+ // also has unset CRSes (and the result is a noop).
+ let result = tester.invoke_scalar_scalar("POINT (0 1)", 0).unwrap();
+ assert_eq!(result, create_scalar(Some("POINT (0 1)"), &WKB_GEOMETRY));
+
+ let array_in = create_array(
+ &[
+ Some("POINT (0 1)"),
+ Some("POINT (1 2)"),
+ Some("POINT (2 3)"),
+ ],
+ &WKB_GEOMETRY,
+ );
+ let result = tester.invoke_array_scalar(array_in.clone(), 0).unwrap();
+ assert_array_equal(&result, &array_in);
+
+ // This should fail, because there is no valid transform between
lnglat()
+ // and an unset CRS.
+ let geometry_input = SedonaType::Wkb(Edges::Planar, lnglat());
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![geometry_input, SedonaType::Arrow(DataType::Int32)],
+ );
+ let err = tester.invoke_scalar_scalar("POINT (0 1)", 0).unwrap_err();
+ assert_eq!(
+ err.message(),
+ "Can't transform to or from an unset CRS. Do you need to call
ST_SetSRID on the input?"
+ );
}
- fn invoke_udf_test(
- wkb: ArrayRef,
- scalar_args: Vec<ScalarValue>,
- arg_types: Vec<SedonaType>,
- ) -> Result<(SedonaType, ColumnarValue)> {
+ #[test]
+ fn invalid_arg_types() {
let udf = SedonaScalarUDF::from_impl("st_transform",
st_transform_impl());
- let arg_fields: Vec<Arc<Field>> = arg_types
- .into_iter()
- .map(|arg_type| Arc::new(arg_type.to_storage_field("",
true).unwrap()))
- .collect();
- let row_count = wkb.len();
-
- let mut scalar_args_fields: Vec<Option<&ScalarValue>> = vec![None];
- let mut arg_vals: Vec<ColumnarValue> =
vec![ColumnarValue::Array(Arc::new(wkb))];
+ // No args
+ let tester = ScalarUdfTester::new(udf.clone().into(), vec![]);
+ let err = tester.return_type().unwrap_err();
+ assert_eq!(
+ err.message(),
+ "st_transform([]): No kernel matching arguments"
+ );
- for scalar_arg in &scalar_args {
- scalar_args_fields.push(Some(scalar_arg));
- arg_vals.push(scalar_arg.clone().into());
- }
+ // Too many args
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![
+ SedonaType::Arrow(DataType::Utf8),
+ SedonaType::Arrow(DataType::Utf8),
+ SedonaType::Arrow(DataType::Utf8),
+ SedonaType::Arrow(DataType::Utf8),
+ ],
+ );
+ let err = tester.return_type().unwrap_err();
+ assert_eq!(
+ err.message(),
+ "st_transform([Arrow(Utf8), Arrow(Utf8), Arrow(Utf8),
Arrow(Utf8)]): No kernel matching arguments"
+ );
- let return_field_args = ReturnFieldArgs {
- arg_fields: &arg_fields,
- scalar_arguments: &scalar_args_fields,
- };
+ // First arg not geometry
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![
+ SedonaType::Arrow(DataType::Utf8),
+ SedonaType::Arrow(DataType::Utf8),
+ ],
+ );
+ let err = tester.return_type().unwrap_err();
+ assert_eq!(
+ err.message(),
+ "st_transform([Arrow(Utf8), Arrow(Utf8)]): No kernel matching
arguments"
+ );
- let return_field = udf.return_field_from_args(return_field_args)?;
- let return_type = SedonaType::from_storage_field(&return_field)?;
+ // Second arg not string or numeric
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![WKB_GEOMETRY, SedonaType::Arrow(DataType::Boolean)],
+ );
+ let err = tester.return_type().unwrap_err();
+ assert_eq!(
+ err.message(),
+ "st_transform([Wkb(Planar, None), Arrow(Boolean)]): No kernel
matching arguments"
+ );
- let args = ScalarFunctionArgs {
- args: arg_vals,
- arg_fields: arg_fields.to_vec(),
- number_rows: row_count,
- return_field,
- config_options: Arc::new(ConfigOptions::default()),
- };
+ // third arg not string or numeric
+ let tester = ScalarUdfTester::new(
+ udf.clone().into(),
+ vec![
+ WKB_GEOMETRY,
+ SedonaType::Arrow(DataType::Utf8),
+ SedonaType::Arrow(DataType::Boolean),
+ ],
+ );
+ let err = tester.return_type().unwrap_err();
+ assert_eq!(
+ err.message(),
+ "st_transform([Wkb(Planar, None), Arrow(Utf8), Arrow(Boolean)]):
No kernel matching arguments"
+ );
+ }
- let value = udf.invoke_with_args(args)?;
- Ok((return_type, value))
+ fn get_crs(auth_code: &str) -> Crs {
+ deserialize_crs(auth_code).unwrap()
}
}
diff --git a/rust/sedona-functions/src/executor.rs
b/rust/sedona-functions/src/executor.rs
index 0686070f..f1d679d2 100644
--- a/rust/sedona-functions/src/executor.rs
+++ b/rust/sedona-functions/src/executor.rs
@@ -18,7 +18,7 @@ use std::iter::zip;
use arrow_array::ArrayRef;
use arrow_schema::DataType;
-use datafusion_common::cast::{as_binary_array, as_binary_view_array};
+use datafusion_common::cast::{as_binary_array, as_binary_view_array,
as_struct_array};
use datafusion_common::error::Result;
use datafusion_common::{DataFusionError, ScalarValue};
use datafusion_expr::ColumnarValue;
@@ -357,6 +357,15 @@ impl IterGeo for ArrayRef {
}
SedonaType::Wkb(_, _) => iter_wkb_binary(as_binary_array(self)?,
func),
SedonaType::WkbView(_, _) =>
iter_wkb_binary(as_binary_view_array(self)?, func),
+ SedonaType::Arrow(DataType::Struct(fields))
+ if fields.len() == 2 && fields[0].name() == "item" &&
fields[1].name() == "crs" =>
+ {
+ let struct_array = as_struct_array(self)?;
+ let item_type = SedonaType::from_storage_field(&fields[0])?;
+ struct_array
+ .column(0)
+ .iter_as_wkb_bytes(&item_type, num_iterations, func)
+ }
_ => {
// We could cast here as a fallback, iterate and cast
per-element, or
// implement iter_as_something_else()/supports_iter_xxx() when
more geo array types
diff --git a/rust/sedona-testing/src/testers.rs
b/rust/sedona-testing/src/testers.rs
index 3e939b32..54b947a3 100644
--- a/rust/sedona-testing/src/testers.rs
+++ b/rust/sedona-testing/src/testers.rs
@@ -570,8 +570,18 @@ impl ScalarUdfTester {
}
pub fn invoke(&self, args: Vec<ColumnarValue>) -> Result<ColumnarValue> {
- self.invoke_with_return_type(args, None)
+ let scalar_args = args
+ .iter()
+ .map(|arg| match arg {
+ ColumnarValue::Array(_) => None,
+ ColumnarValue::Scalar(scalar_value) =>
Some(scalar_value.clone()),
+ })
+ .collect::<Vec<_>>();
+
+ let return_type = self.return_type_with_scalars_inner(&scalar_args)?;
+ self.invoke_with_return_type(args, Some(return_type))
}
+
pub fn invoke_with_return_type(
&self,
args: Vec<ColumnarValue>,