This is an automated email from the ASF dual-hosted git repository.
jiayu 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 5d3cfba Add ST_SRID (#31)
5d3cfba is described below
commit 5d3cfba252ffdb0d1a8ac1fd2126fb829ca459df
Author: jp <[email protected]>
AuthorDate: Fri Sep 5 14:25:11 2025 -0700
Add ST_SRID (#31)
---
rust/sedona-functions/src/lib.rs | 1 +
rust/sedona-functions/src/register.rs | 1 +
rust/sedona-functions/src/st_srid.rs | 155 ++++++++++++++++++++++++++++++++++
rust/sedona-schema/src/crs.rs | 41 ++++++++-
4 files changed, 194 insertions(+), 4 deletions(-)
diff --git a/rust/sedona-functions/src/lib.rs b/rust/sedona-functions/src/lib.rs
index c8d95db..1041fb1 100644
--- a/rust/sedona-functions/src/lib.rs
+++ b/rust/sedona-functions/src/lib.rs
@@ -43,6 +43,7 @@ mod st_perimeter;
mod st_point;
mod st_pointzm;
mod st_setsrid;
+mod st_srid;
mod st_transform;
pub mod st_union_aggr;
mod st_xyzm;
diff --git a/rust/sedona-functions/src/register.rs
b/rust/sedona-functions/src/register.rs
index 3f7f931..b5208ac 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -86,6 +86,7 @@ pub fn default_function_set() -> FunctionSet {
crate::st_pointzm::st_pointzm_udf,
crate::st_transform::st_transform_udf,
crate::st_setsrid::st_set_srid_udf,
+ crate::st_srid::st_srid_udf,
crate::st_xyzm::st_m_udf,
crate::st_xyzm::st_x_udf,
crate::st_xyzm::st_y_udf,
diff --git a/rust/sedona-functions/src/st_srid.rs
b/rust/sedona-functions/src/st_srid.rs
new file mode 100644
index 0000000..56dfedb
--- /dev/null
+++ b/rust/sedona-functions/src/st_srid.rs
@@ -0,0 +1,155 @@
+// 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 arrow_array::builder::UInt32Builder;
+use std::{sync::Arc, vec};
+
+use crate::executor::WkbExecutor;
+use arrow_schema::DataType;
+use datafusion_common::{DataFusionError, Result};
+use datafusion_expr::{
+ scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation,
Volatility,
+};
+use sedona_expr::scalar_udf::{ArgMatcher, SedonaScalarKernel, SedonaScalarUDF};
+use sedona_schema::datatypes::SedonaType;
+
+/// ST_Srid() scalar UDF implementation
+///
+/// Scalar function to return the SRID of a geometry or geography
+pub fn st_srid_udf() -> SedonaScalarUDF {
+ SedonaScalarUDF::new(
+ "st_srid",
+ vec![Arc::new(StSrid {})],
+ Volatility::Immutable,
+ Some(st_srid_doc()),
+ )
+}
+
+fn st_srid_doc() -> Documentation {
+ Documentation::builder(
+ DOC_SECTION_OTHER,
+ "Return the spatial reference system identifier (SRID) of the
geometry.",
+ "ST_SRID (geom: Geometry)",
+ )
+ .with_argument("geom", "geometry: Input geometry or geography")
+ .with_sql_example("SELECT ST_SRID(polygon)".to_string())
+ .build()
+}
+
+#[derive(Debug)]
+struct StSrid {}
+
+impl SedonaScalarKernel for StSrid {
+ fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+ let matcher = ArgMatcher::new(
+ vec![ArgMatcher::is_geometry_or_geography()],
+ SedonaType::Arrow(DataType::UInt32),
+ );
+
+ matcher.match_args(args)
+ }
+
+ fn invoke_batch(
+ &self,
+ arg_types: &[SedonaType],
+ args: &[ColumnarValue],
+ ) -> Result<ColumnarValue> {
+ let executor = WkbExecutor::new(arg_types, args);
+ let mut builder =
UInt32Builder::with_capacity(executor.num_iterations());
+ let srid_opt = match &arg_types[0] {
+ SedonaType::Wkb(_, Some(crs)) | SedonaType::WkbView(_, Some(crs))
=> {
+ match crs.srid()? {
+ Some(srid) => Some(srid),
+ None => return Err(DataFusionError::Execution("CRS has no
SRID".to_string())),
+ }
+ }
+ _ => Some(0),
+ };
+
+ executor.execute_wkb_void(|maybe_wkb| {
+ match maybe_wkb {
+ Some(_wkb) => {
+ builder.append_option(srid_opt);
+ }
+ _ => builder.append_null(),
+ }
+
+ Ok(())
+ })?;
+
+ executor.finish(Arc::new(builder.finish()))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use datafusion_common::ScalarValue;
+ use datafusion_expr::ScalarUDF;
+ use sedona_schema::crs::deserialize_crs;
+ use sedona_schema::datatypes::Edges;
+ use sedona_testing::testers::ScalarUdfTester;
+ use std::str::FromStr;
+
+ use super::*;
+
+ #[test]
+ fn udf_metadata() {
+ let udf: ScalarUDF = st_srid_udf().into();
+ assert_eq!(udf.name(), "st_srid");
+ assert!(udf.documentation().is_some())
+ }
+
+ #[test]
+ fn udf() {
+ let udf: ScalarUDF = st_srid_udf().into();
+
+ // Test that when no CRS is set, SRID is 0
+ let sedona_type = SedonaType::Wkb(Edges::Planar, None);
+ let tester = ScalarUdfTester::new(udf.clone(), vec![sedona_type]);
+ tester.assert_return_type(DataType::UInt32);
+ let result = tester
+ .invoke_scalar("POLYGON ((0 0, 1 0, 0 1, 0 0))")
+ .unwrap();
+ tester.assert_scalar_result_equals(result, 0_u32);
+
+ // Test that NULL input returns NULL output
+ let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+ tester.assert_scalar_result_equals(result, ScalarValue::Null);
+
+ // Test with a CRS with an EPSG code
+ let crs_value = serde_json::Value::String("EPSG:4837".to_string());
+ let crs = deserialize_crs(&crs_value).unwrap();
+ let sedona_type = SedonaType::Wkb(Edges::Planar, crs.clone());
+ let tester = ScalarUdfTester::new(udf.clone(), vec![sedona_type]);
+ let result = tester
+ .invoke_scalar("POLYGON ((0 0, 1 0, 0 1, 0 0))")
+ .unwrap();
+ tester.assert_scalar_result_equals(result, 4837_u32);
+
+ // Test with a CRS but null geom
+ let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+ tester.assert_scalar_result_equals(result, ScalarValue::Null);
+
+ // Call with a CRS with no SRID (should error)
+ let crs_value = serde_json::Value::from_str("{}");
+ let crs = deserialize_crs(&crs_value.unwrap()).unwrap();
+ let sedona_type = SedonaType::Wkb(Edges::Planar, crs.clone());
+ let tester = ScalarUdfTester::new(udf.clone(), vec![sedona_type]);
+ let result = tester.invoke_scalar("POINT (0 1)");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("CRS has no SRID"));
+ }
+}
diff --git a/rust/sedona-schema/src/crs.rs b/rust/sedona-schema/src/crs.rs
index 80e0884..5c81cb1 100644
--- a/rust/sedona-schema/src/crs.rs
+++ b/rust/sedona-schema/src/crs.rs
@@ -88,6 +88,7 @@ pub trait CoordinateReferenceSystem: Debug {
fn to_json(&self) -> String;
fn to_authority_code(&self) -> Result<Option<String>>;
fn crs_equals(&self, other: &dyn CoordinateReferenceSystem) -> bool;
+ fn srid(&self) -> Result<Option<u32>>;
}
/// Concrete implementation of a default longitude/latitude coordinate
reference system
@@ -207,6 +208,15 @@ impl CoordinateReferenceSystem for AuthorityCode {
(_, _) => false,
}
}
+
+ /// Get the SRID if authority is EPSG
+ fn srid(&self) -> Result<Option<u32>> {
+ if self.authority.eq_ignore_ascii_case("EPSG") {
+ Ok(self.code.parse::<u32>().ok())
+ } else {
+ Ok(None)
+ }
+ }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -272,6 +282,20 @@ impl CoordinateReferenceSystem for ProjJSON {
false
}
}
+
+ fn srid(&self) -> Result<Option<u32>> {
+ let authority_code_opt = self.to_authority_code()?;
+ if let Some(authority_code) = authority_code_opt {
+ if LngLat::is_authority_code_lnglat(&authority_code) {
+ return Ok(Some(4326));
+ }
+ if let Some((_, code)) =
AuthorityCode::split_auth_code(&authority_code) {
+ return Ok(code.parse::<u32>().ok());
+ }
+ }
+
+ Ok(None)
+ }
}
pub const OGC_CRS84_PROJJSON: &str =
r#"{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","type":"GeographicCRS","name":"WGS
84 (CRS84)","datum_ensemble":{"name":"World Geodetic System 1984
ensemble","members":[{"name":"World Geodetic System 1984
(Transit)","id":{"authority":"EPSG","code":1166}},{"name":"World Geodetic
System 1984 (G730)","id":{"authority":"EPSG","code":1152}},{"name":"World
Geodetic System 1984
(G873)","id":{"authority":"EPSG","code":1153}},{"name":"World [...]
@@ -279,6 +303,7 @@ pub const OGC_CRS84_PROJJSON: &str =
r#"{"$schema":"https://proj.org/schemas/v0.
#[cfg(test)]
mod test {
use super::*;
+ const EPSG_6318_PROJJSON: &str = r#"{"$schema":
"https://proj.org/schemas/v0.4/projjson.schema.json","type":
"GeographicCRS","name": "NAD83(2011)","datum": {"type":
"GeodeticReferenceFrame","name": "NAD83 (National Spatial Reference System
2011)","ellipsoid": {"name": "GRS 1980","semi_major_axis":
6378137,"inverse_flattening": 298.257222101}},"coordinate_system": {"subtype":
"ellipsoidal","axis": [{"name": "Geodetic latitude","abbreviation":
"Lat","direction": "north","unit": "degree [...]
#[test]
fn deserialize() {
@@ -304,6 +329,7 @@ mod test {
fn crs_projjson() {
let projjson = OGC_CRS84_PROJJSON.parse::<ProjJSON>().unwrap();
assert_eq!(projjson.to_authority_code().unwrap().unwrap(),
"OGC:CRS84");
+ assert_eq!(projjson.srid().unwrap(), Some(4326));
let json_value: Value =
serde_json::from_str(OGC_CRS84_PROJJSON).unwrap();
let json_value_roundtrip: Value =
serde_json::from_str(&projjson.to_json()).unwrap();
@@ -317,7 +343,11 @@ mod test {
.to_authority_code()
.unwrap()
.is_none());
- assert!(!projjson.crs_equals(&projjson_without_identifier))
+ assert!(!projjson.crs_equals(&projjson_without_identifier));
+
+ let projjson = EPSG_6318_PROJJSON.parse::<ProjJSON>().unwrap();
+ assert_eq!(projjson.to_authority_code().unwrap().unwrap(),
"EPSG:6318");
+ assert_eq!(projjson.srid().unwrap(), Some(6318));
}
#[test]
@@ -328,6 +358,7 @@ mod test {
};
assert!(auth_code.crs_equals(&auth_code));
assert!(!auth_code.crs_equals(LngLat::crs().unwrap().as_ref()));
+ assert_eq!(auth_code.srid().unwrap(), Some(4269));
assert_eq!(
auth_code.to_authority_code().unwrap(),
@@ -345,18 +376,20 @@ mod test {
assert!(AuthorityCode::is_authority_code(&auth_code_parsed.unwrap()));
let value: Value = serde_json::from_str("\"EPSG:4269\"").unwrap();
- let new_crs = deserialize_crs(&value).unwrap();
+ let new_crs = deserialize_crs(&value).unwrap().unwrap();
assert_eq!(
- new_crs.unwrap().to_authority_code().unwrap(),
+ new_crs.to_authority_code().unwrap(),
Some("EPSG:4269".to_string())
);
+ assert_eq!(new_crs.srid().unwrap(), Some(4269));
// Ensure we can also just pass a code here
let value: Value = serde_json::from_str("\"4269\"").unwrap();
let new_crs = deserialize_crs(&value).unwrap();
assert_eq!(
- new_crs.unwrap().to_authority_code().unwrap(),
+ new_crs.clone().unwrap().to_authority_code().unwrap(),
Some("EPSG:4269".to_string())
);
+ assert_eq!(new_crs.unwrap().srid().unwrap(), Some(4269));
}
}