paleolimbot commented on code in PR #475:
URL: https://github.com/apache/sedona-db/pull/475#discussion_r2668791146


##########
r/sedonadb/src/rust/src/ffi.rs:
##########
@@ -78,6 +78,11 @@ pub fn import_scalar_udf(mut scalar_udf_xptr: savvy::Sexp) 
-> savvy::Result<Scal
     Ok(scalar_udf_impl.into())
 }
 
+pub fn import_arrow_field(mut xptr: savvy::Sexp) -> 
savvy::Result<arrow_schema::Field> {
+    let ffi_schema: &FFI_ArrowSchema = import_xptr(&mut xptr, 
"nanoarrow_schema")?;
+    arrow_schema::Field::try_from(ffi_schema).map_err(|e| savvy_err!("{e}"))
+}
+

Review Comment:
   Does `import_field()` work for this?



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;

Review Comment:
   It might be more appropriate to call this `input` (which is the term sf uses 
to describe this concept, sort of), or maybe `definition`. (A "proj string" 
carries the connotation specific formatting that is not how this is typically 
formatted here)



##########
r/sedonadb/tests/testthat/test-crs.R:
##########
@@ -0,0 +1,270 @@
+# 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.
+
+test_that("sd_parse_crs works for GeoArrow metadata with EPSG", {
+  meta <- '{"crs": {"id": {"authority": "EPSG", "code": 5070}, "name": "NAD83 
/ Conus Albers"}}'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))
+})
+
+test_that("sd_parse_crs works for Engineering CRS (no EPSG ID)", {
+  # A realistic example of a local engineering CRS that wouldn't have an EPSG 
code
+  meta <- '{
+    "crs": {
+      "type": "EngineeringCRS",
+      "name": "Construction Site Local Grid",
+      "datum": {
+        "type": "EngineeringDatum",
+        "name": "Local Datum"
+      },
+      "coordinate_system": {
+        "subtype": "Cartesian",
+        "axis": [
+          {"name": "Northing", "abbreviation": "N", "direction": "north", 
"unit": "metre"},
+          {"name": "Easting", "abbreviation": "E", "direction": "east", 
"unit": "metre"}
+        ]
+      }
+    }
+  }'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))
+})
+
+test_that("sd_parse_crs returns NULL if crs field is missing", {
+  expect_snapshot(sedonadb:::sd_parse_crs('{"something_else": 123}'))
+  expect_snapshot(sedonadb:::sd_parse_crs('{}'))
+})
+
+test_that("sd_parse_crs handles invalid JSON gracefully", {
+  expect_snapshot(
+    sedonadb:::sd_parse_crs('invalid json'),
+    error = TRUE
+  )
+})
+
+test_that("sd_parse_crs works with plain strings if that's what's in 'crs'", {
+  meta <- '{"crs": "EPSG:4326"}'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))
+})
+
+# Tests for CRS display in print.sedonadb_dataframe

Review Comment:
   nit: I like your other tests that put this comment inside the `test_that()` 
block (I think this is what I did for the tests in some of the other files too)



##########
r/sedonadb/tests/testthat/test-crs.R:
##########
@@ -0,0 +1,270 @@
+# 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.
+
+test_that("sd_parse_crs works for GeoArrow metadata with EPSG", {
+  meta <- '{"crs": {"id": {"authority": "EPSG", "code": 5070}, "name": "NAD83 
/ Conus Albers"}}'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))

Review Comment:
   ```suggestion
     expect_snapshot(sd_parse_crs(meta))
   ```
   
   (and elsewhere in this file!)
   
   You should be able to use `devtools::load_all()` to attach the namespace 
including internals + testthat for running these interactively.



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;
+
+                if let Some(auth_code) = auth_code {
+                    out.set_value(0, 
savvy::Sexp::try_from(auth_code.as_str())?)?;
+                } else {
+                    out.set_value(0, savvy::NullSexp)?;
+                }
+
+                if let Some(srid) = srid {
+                    out.set_value(1, savvy::Sexp::try_from(srid as i32)?)?;
+                } else {
+                    out.set_value(1, savvy::NullSexp)?;
+                }
+
+                if let Some(name) = name {
+                    out.set_value(2, savvy::Sexp::try_from(name)?)?;
+                } else {
+                    out.set_value(2, savvy::NullSexp)?;
+                }
+                out.set_value(3, 
savvy::Sexp::try_from(proj_string.as_str())?)?;
+
+                Ok(out.into())
+            }
+            None => Ok(savvy::NullSexp.into()),
+        }
+    } else {
+        Ok(savvy::NullSexp.into())
+    }
+}
+
+/// R-exposed wrapper for CRS (Coordinate Reference System) introspection
+///
+/// This wraps an Arc<dyn CoordinateReferenceSystem> and exposes its methods 
to R.
+#[savvy]
+pub struct SedonaCrsR {
+    inner: Arc<dyn CoordinateReferenceSystem + Send + Sync>,
+}
+
+#[savvy]
+impl SedonaCrsR {
+    /// Get the SRID (e.g., 4326 for WGS84) or NULL if not an EPSG code
+    fn srid(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.srid() {
+            Ok(Some(srid)) => savvy::Sexp::try_from(srid as i32),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!("Failed to get SRID: 
{e}"))),
+        }
+    }
+
+    /// Get the authority code (e.g., "EPSG:4326") or NULL if not available
+    fn authority_code(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.to_authority_code() {
+            Ok(Some(code)) => savvy::Sexp::try_from(code.as_str()),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!(
+                "Failed to get authority code: {e}"
+            ))),
+        }
+    }
+
+    /// Get the JSON representation of the CRS
+    fn to_json(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_json().as_str())
+    }
+
+    /// Get the PROJ-compatible CRS string representation
+    fn to_crs_string(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_crs_string().as_str())
+    }
+
+    /// Get a formatted display string (e.g., "EPSG:4326" or "{...}")
+    fn display(&self) -> savvy::Result<savvy::Sexp> {
+        let display = if let Ok(Some(auth)) = self.inner.to_authority_code() {
+            auth
+        } else {
+            format!("{}", self.inner.as_ref())
+        };
+        savvy::Sexp::try_from(display.as_str())
+    }
+}
+
+/// R-exposed wrapper for SedonaType introspection
+///
+/// This allows R code to inspect Arrow schema fields and determine
+/// if they are geometry types with CRS information.
+#[savvy]
+pub struct SedonaTypeR {
+    inner: sedona_schema::datatypes::SedonaType,
+    name: String,

Review Comment:
   I know it is pedantic and annoying, but the name is part of the `Field` and 
not the `Type`. We could either make a `Field` wrapper here
   
   
https://github.com/apache/sedona-db/blob/b254e8d884cd0d705a252e6127743e27de8789c8/python/sedonadb/src/schema.rs#L132-L135
   
   ...or just omit the `name` (you can access the name in R easily for the 
schema printing).



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;
+
+                if let Some(auth_code) = auth_code {
+                    out.set_value(0, 
savvy::Sexp::try_from(auth_code.as_str())?)?;
+                } else {
+                    out.set_value(0, savvy::NullSexp)?;
+                }
+
+                if let Some(srid) = srid {
+                    out.set_value(1, savvy::Sexp::try_from(srid as i32)?)?;
+                } else {
+                    out.set_value(1, savvy::NullSexp)?;
+                }
+
+                if let Some(name) = name {
+                    out.set_value(2, savvy::Sexp::try_from(name)?)?;
+                } else {
+                    out.set_value(2, savvy::NullSexp)?;
+                }
+                out.set_value(3, 
savvy::Sexp::try_from(proj_string.as_str())?)?;
+
+                Ok(out.into())
+            }
+            None => Ok(savvy::NullSexp.into()),
+        }
+    } else {
+        Ok(savvy::NullSexp.into())
+    }
+}
+
+/// R-exposed wrapper for CRS (Coordinate Reference System) introspection
+///
+/// This wraps an Arc<dyn CoordinateReferenceSystem> and exposes its methods 
to R.
+#[savvy]
+pub struct SedonaCrsR {

Review Comment:
   Can you move these classes to a separate module? (maybe `schema.rs`?)
   
   Optional nit: it's slightly more consistent with the type of wrapper class 
we've defined elsewhere to have this be `RSedonaCrs` (e.g., to match 
`PySedonaType` and friends).



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;
+
+                if let Some(auth_code) = auth_code {
+                    out.set_value(0, 
savvy::Sexp::try_from(auth_code.as_str())?)?;
+                } else {
+                    out.set_value(0, savvy::NullSexp)?;
+                }
+
+                if let Some(srid) = srid {
+                    out.set_value(1, savvy::Sexp::try_from(srid as i32)?)?;
+                } else {
+                    out.set_value(1, savvy::NullSexp)?;
+                }
+
+                if let Some(name) = name {
+                    out.set_value(2, savvy::Sexp::try_from(name)?)?;
+                } else {
+                    out.set_value(2, savvy::NullSexp)?;
+                }
+                out.set_value(3, 
savvy::Sexp::try_from(proj_string.as_str())?)?;
+
+                Ok(out.into())
+            }
+            None => Ok(savvy::NullSexp.into()),
+        }
+    } else {
+        Ok(savvy::NullSexp.into())
+    }
+}
+
+/// R-exposed wrapper for CRS (Coordinate Reference System) introspection
+///
+/// This wraps an Arc<dyn CoordinateReferenceSystem> and exposes its methods 
to R.
+#[savvy]
+pub struct SedonaCrsR {
+    inner: Arc<dyn CoordinateReferenceSystem + Send + Sync>,
+}
+
+#[savvy]
+impl SedonaCrsR {
+    /// Get the SRID (e.g., 4326 for WGS84) or NULL if not an EPSG code
+    fn srid(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.srid() {
+            Ok(Some(srid)) => savvy::Sexp::try_from(srid as i32),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!("Failed to get SRID: 
{e}"))),
+        }
+    }
+
+    /// Get the authority code (e.g., "EPSG:4326") or NULL if not available
+    fn authority_code(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.to_authority_code() {
+            Ok(Some(code)) => savvy::Sexp::try_from(code.as_str()),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!(
+                "Failed to get authority code: {e}"
+            ))),
+        }
+    }
+
+    /// Get the JSON representation of the CRS
+    fn to_json(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_json().as_str())
+    }
+
+    /// Get the PROJ-compatible CRS string representation
+    fn to_crs_string(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_crs_string().as_str())
+    }
+
+    /// Get a formatted display string (e.g., "EPSG:4326" or "{...}")
+    fn display(&self) -> savvy::Result<savvy::Sexp> {
+        let display = if let Ok(Some(auth)) = self.inner.to_authority_code() {
+            auth
+        } else {
+            format!("{}", self.inner.as_ref())
+        };
+        savvy::Sexp::try_from(display.as_str())
+    }
+}
+
+/// R-exposed wrapper for SedonaType introspection
+///
+/// This allows R code to inspect Arrow schema fields and determine
+/// if they are geometry types with CRS information.
+#[savvy]
+pub struct SedonaTypeR {
+    inner: sedona_schema::datatypes::SedonaType,
+    name: String,
+}
+
+#[savvy]
+impl SedonaTypeR {
+    /// Create a SedonaTypeR from a nanoarrow schema (external pointer)
+    ///
+    /// The schema should be a single field (column) schema, not a struct 
schema.
+    fn new(schema_xptr: savvy::Sexp) -> savvy::Result<SedonaTypeR> {
+        use sedona_schema::datatypes::SedonaType;
+
+        let field = crate::ffi::import_arrow_field(schema_xptr)?;
+        let name = field.name().clone();
+
+        // Use existing SedonaType infrastructure to parse the field
+        let inner = SedonaType::from_storage_field(&field)
+            .map_err(|e| savvy::Error::new(format!("Failed to create 
SedonaType: {e}")))?;
+
+        Ok(SedonaTypeR { inner, name })
+    }
+
+    /// Get the logical type name ("geometry", "geography", "utf8", etc.)
+    fn logical_type_name(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.logical_type_name().as_str())
+    }
+
+    /// Get the column name
+    fn name(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.name.as_str())
+    }
+
+    /// Get the CRS wrapper object, or NULL if no CRS is present
+    ///
+    /// This returns a SedonaCrsR object that can be used to inspect the CRS.
+    fn crs(&self) -> savvy::Result<SedonaCrsR> {
+        use sedona_schema::datatypes::SedonaType;
+
+        match &self.inner {
+            SedonaType::Wkb(_, crs) | SedonaType::WkbView(_, crs) => {
+                if let Some(crs_arc) = crs {
+                    Ok(SedonaCrsR {
+                        inner: crs_arc.clone(),
+                    })
+                } else {
+                    Err(savvy::Error::new("No CRS available for this geometry 
type"))

Review Comment:
   The docstring says this returns NULL for the "no crs" case. If this is hard 
to do with savvy maybe just update the docstring explaining that.



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;
+
+                if let Some(auth_code) = auth_code {
+                    out.set_value(0, 
savvy::Sexp::try_from(auth_code.as_str())?)?;
+                } else {
+                    out.set_value(0, savvy::NullSexp)?;
+                }
+
+                if let Some(srid) = srid {
+                    out.set_value(1, savvy::Sexp::try_from(srid as i32)?)?;
+                } else {
+                    out.set_value(1, savvy::NullSexp)?;
+                }
+
+                if let Some(name) = name {
+                    out.set_value(2, savvy::Sexp::try_from(name)?)?;
+                } else {
+                    out.set_value(2, savvy::NullSexp)?;
+                }
+                out.set_value(3, 
savvy::Sexp::try_from(proj_string.as_str())?)?;
+
+                Ok(out.into())
+            }
+            None => Ok(savvy::NullSexp.into()),
+        }
+    } else {
+        Ok(savvy::NullSexp.into())
+    }
+}
+
+/// R-exposed wrapper for CRS (Coordinate Reference System) introspection
+///
+/// This wraps an Arc<dyn CoordinateReferenceSystem> and exposes its methods 
to R.
+#[savvy]
+pub struct SedonaCrsR {
+    inner: Arc<dyn CoordinateReferenceSystem + Send + Sync>,
+}
+
+#[savvy]
+impl SedonaCrsR {
+    /// Get the SRID (e.g., 4326 for WGS84) or NULL if not an EPSG code
+    fn srid(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.srid() {
+            Ok(Some(srid)) => savvy::Sexp::try_from(srid as i32),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!("Failed to get SRID: 
{e}"))),
+        }
+    }
+
+    /// Get the authority code (e.g., "EPSG:4326") or NULL if not available
+    fn authority_code(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.to_authority_code() {
+            Ok(Some(code)) => savvy::Sexp::try_from(code.as_str()),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!(
+                "Failed to get authority code: {e}"
+            ))),
+        }
+    }
+
+    /// Get the JSON representation of the CRS
+    fn to_json(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_json().as_str())
+    }
+
+    /// Get the PROJ-compatible CRS string representation
+    fn to_crs_string(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_crs_string().as_str())
+    }
+
+    /// Get a formatted display string (e.g., "EPSG:4326" or "{...}")
+    fn display(&self) -> savvy::Result<savvy::Sexp> {
+        let display = if let Ok(Some(auth)) = self.inner.to_authority_code() {
+            auth
+        } else {
+            format!("{}", self.inner.as_ref())
+        };
+        savvy::Sexp::try_from(display.as_str())
+    }
+}
+
+/// R-exposed wrapper for SedonaType introspection
+///
+/// This allows R code to inspect Arrow schema fields and determine
+/// if they are geometry types with CRS information.
+#[savvy]
+pub struct SedonaTypeR {
+    inner: sedona_schema::datatypes::SedonaType,
+    name: String,
+}
+
+#[savvy]
+impl SedonaTypeR {
+    /// Create a SedonaTypeR from a nanoarrow schema (external pointer)
+    ///
+    /// The schema should be a single field (column) schema, not a struct 
schema.
+    fn new(schema_xptr: savvy::Sexp) -> savvy::Result<SedonaTypeR> {
+        use sedona_schema::datatypes::SedonaType;
+
+        let field = crate::ffi::import_arrow_field(schema_xptr)?;
+        let name = field.name().clone();
+
+        // Use existing SedonaType infrastructure to parse the field
+        let inner = SedonaType::from_storage_field(&field)
+            .map_err(|e| savvy::Error::new(format!("Failed to create 
SedonaType: {e}")))?;
+
+        Ok(SedonaTypeR { inner, name })
+    }
+
+    /// Get the logical type name ("geometry", "geography", "utf8", etc.)
+    fn logical_type_name(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.logical_type_name().as_str())
+    }
+
+    /// Get the column name
+    fn name(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.name.as_str())
+    }
+
+    /// Get the CRS wrapper object, or NULL if no CRS is present
+    ///
+    /// This returns a SedonaCrsR object that can be used to inspect the CRS.
+    fn crs(&self) -> savvy::Result<SedonaCrsR> {
+        use sedona_schema::datatypes::SedonaType;
+
+        match &self.inner {
+            SedonaType::Wkb(_, crs) | SedonaType::WkbView(_, crs) => {
+                if let Some(crs_arc) = crs {
+                    Ok(SedonaCrsR {
+                        inner: crs_arc.clone(),
+                    })
+                } else {
+                    Err(savvy::Error::new("No CRS available for this geometry 
type"))
+                }
+            }
+            _ => Err(savvy::Error::new("No CRS available for non-geometry 
types")),
+        }
+    }
+
+    /// Get a formatted CRS display string like " (CRS: EPSG:4326)" or empty 
string
+    fn crs_display(&self) -> savvy::Result<savvy::Sexp> {
+        use sedona_schema::datatypes::SedonaType;
+
+        match &self.inner {

Review Comment:
   Do we need this one (from R you can do `sd_type$crs()$display()`?)



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;
+
+                if let Some(auth_code) = auth_code {
+                    out.set_value(0, 
savvy::Sexp::try_from(auth_code.as_str())?)?;
+                } else {
+                    out.set_value(0, savvy::NullSexp)?;
+                }
+
+                if let Some(srid) = srid {
+                    out.set_value(1, savvy::Sexp::try_from(srid as i32)?)?;
+                } else {
+                    out.set_value(1, savvy::NullSexp)?;
+                }
+
+                if let Some(name) = name {
+                    out.set_value(2, savvy::Sexp::try_from(name)?)?;
+                } else {
+                    out.set_value(2, savvy::NullSexp)?;
+                }
+                out.set_value(3, 
savvy::Sexp::try_from(proj_string.as_str())?)?;
+
+                Ok(out.into())
+            }
+            None => Ok(savvy::NullSexp.into()),
+        }
+    } else {
+        Ok(savvy::NullSexp.into())
+    }
+}
+
+/// R-exposed wrapper for CRS (Coordinate Reference System) introspection
+///
+/// This wraps an Arc<dyn CoordinateReferenceSystem> and exposes its methods 
to R.
+#[savvy]
+pub struct SedonaCrsR {
+    inner: Arc<dyn CoordinateReferenceSystem + Send + Sync>,
+}
+
+#[savvy]
+impl SedonaCrsR {
+    /// Get the SRID (e.g., 4326 for WGS84) or NULL if not an EPSG code
+    fn srid(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.srid() {
+            Ok(Some(srid)) => savvy::Sexp::try_from(srid as i32),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!("Failed to get SRID: 
{e}"))),

Review Comment:
   The docstring says this should return NULL for this case?



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;

Review Comment:
   Do we still need this function or does `SedonaCrsR$display()` do what we 
need it to do?



##########
r/sedonadb/src/rust/src/lib.rs:
##########
@@ -67,3 +69,194 @@ fn configure_proj_shared(
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn parse_crs_metadata(crs_json: &str) -> savvy::Result<savvy::Sexp> {
+    use sedona_schema::crs::deserialize_crs_from_obj;
+
+    // The input is GeoArrow extension metadata, which is a JSON object like:
+    // {"crs": <PROJJSON or string>}
+    // We need to extract the "crs" field first.
+    let metadata: serde_json::Value = serde_json::from_str(crs_json)
+        .map_err(|e| savvy::Error::new(format!("Failed to parse metadata JSON: 
{e}")))?;
+
+    if let Some(crs_val) = metadata.get("crs") {
+        if crs_val.is_null() {
+            return Ok(savvy::NullSexp.into());
+        }
+
+        let crs = deserialize_crs_from_obj(crs_val)?;
+        match crs {
+            Some(crs_obj) => {
+                let auth_code = crs_obj.to_authority_code().ok().flatten();
+                let srid = crs_obj.srid().ok().flatten();
+                let name = crs_val.get("name").and_then(|v| v.as_str());
+                let proj_string = crs_obj.to_crs_string();
+
+                let mut out = savvy::OwnedListSexp::new(4, true)?;
+                out.set_name(0, "authority_code")?;
+                out.set_name(1, "srid")?;
+                out.set_name(2, "name")?;
+                out.set_name(3, "proj_string")?;
+
+                if let Some(auth_code) = auth_code {
+                    out.set_value(0, 
savvy::Sexp::try_from(auth_code.as_str())?)?;
+                } else {
+                    out.set_value(0, savvy::NullSexp)?;
+                }
+
+                if let Some(srid) = srid {
+                    out.set_value(1, savvy::Sexp::try_from(srid as i32)?)?;
+                } else {
+                    out.set_value(1, savvy::NullSexp)?;
+                }
+
+                if let Some(name) = name {
+                    out.set_value(2, savvy::Sexp::try_from(name)?)?;
+                } else {
+                    out.set_value(2, savvy::NullSexp)?;
+                }
+                out.set_value(3, 
savvy::Sexp::try_from(proj_string.as_str())?)?;
+
+                Ok(out.into())
+            }
+            None => Ok(savvy::NullSexp.into()),
+        }
+    } else {
+        Ok(savvy::NullSexp.into())
+    }
+}
+
+/// R-exposed wrapper for CRS (Coordinate Reference System) introspection
+///
+/// This wraps an Arc<dyn CoordinateReferenceSystem> and exposes its methods 
to R.
+#[savvy]
+pub struct SedonaCrsR {
+    inner: Arc<dyn CoordinateReferenceSystem + Send + Sync>,
+}
+
+#[savvy]
+impl SedonaCrsR {
+    /// Get the SRID (e.g., 4326 for WGS84) or NULL if not an EPSG code
+    fn srid(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.srid() {
+            Ok(Some(srid)) => savvy::Sexp::try_from(srid as i32),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!("Failed to get SRID: 
{e}"))),
+        }
+    }
+
+    /// Get the authority code (e.g., "EPSG:4326") or NULL if not available
+    fn authority_code(&self) -> savvy::Result<savvy::Sexp> {
+        match self.inner.to_authority_code() {
+            Ok(Some(code)) => savvy::Sexp::try_from(code.as_str()),
+            Ok(None) => Ok(savvy::NullSexp.into()),
+            Err(e) => Err(savvy::Error::new(format!(
+                "Failed to get authority code: {e}"
+            ))),
+        }
+    }
+
+    /// Get the JSON representation of the CRS
+    fn to_json(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_json().as_str())
+    }
+
+    /// Get the PROJ-compatible CRS string representation
+    fn to_crs_string(&self) -> savvy::Result<savvy::Sexp> {
+        savvy::Sexp::try_from(self.inner.to_crs_string().as_str())
+    }
+
+    /// Get a formatted display string (e.g., "EPSG:4326" or "{...}")
+    fn display(&self) -> savvy::Result<savvy::Sexp> {
+        let display = if let Ok(Some(auth)) = self.inner.to_authority_code() {
+            auth
+        } else {
+            format!("{}", self.inner.as_ref())
+        };
+        savvy::Sexp::try_from(display.as_str())
+    }
+}
+
+/// R-exposed wrapper for SedonaType introspection
+///
+/// This allows R code to inspect Arrow schema fields and determine
+/// if they are geometry types with CRS information.
+#[savvy]
+pub struct SedonaTypeR {
+    inner: sedona_schema::datatypes::SedonaType,
+    name: String,
+}
+
+#[savvy]
+impl SedonaTypeR {
+    /// Create a SedonaTypeR from a nanoarrow schema (external pointer)
+    ///
+    /// The schema should be a single field (column) schema, not a struct 
schema.
+    fn new(schema_xptr: savvy::Sexp) -> savvy::Result<SedonaTypeR> {
+        use sedona_schema::datatypes::SedonaType;
+
+        let field = crate::ffi::import_arrow_field(schema_xptr)?;
+        let name = field.name().clone();
+
+        // Use existing SedonaType infrastructure to parse the field
+        let inner = SedonaType::from_storage_field(&field)
+            .map_err(|e| savvy::Error::new(format!("Failed to create 
SedonaType: {e}")))?;

Review Comment:
   ```suggestion
               .map_err(|e| savvy_err!("Failed to create SedonaType: {e}"))?;
   ```
   
   (I've been trying to consistently use `savvy_err!()` elsewhere but I'm new 
to this so the conventions aren't perfect)



##########
r/sedonadb/tests/testthat/_snaps/crs.md:
##########
@@ -0,0 +1,211 @@
+# sd_parse_crs works for GeoArrow metadata with EPSG
+
+    Code
+      sedonadb:::sd_parse_crs(meta)
+    Output
+      $authority_code
+      [1] "EPSG:5070"
+      
+      $srid
+      [1] 5070
+      
+      $name
+      [1] "NAD83 / Conus Albers"
+      
+      $proj_string
+      [1] "{\"id\":{\"authority\":\"EPSG\",\"code\":5070},\"name\":\"NAD83 / 
Conus Albers\"}"
+      
+
+# sd_parse_crs works for Engineering CRS (no EPSG ID)
+
+    Code
+      sedonadb:::sd_parse_crs(meta)
+    Output
+      $authority_code
+      NULL
+      
+      $srid
+      NULL
+      
+      $name
+      [1] "Construction Site Local Grid"
+      
+      $proj_string
+      [1] 
"{\"coordinate_system\":{\"axis\":[{\"abbreviation\":\"N\",\"direction\":\"north\",\"name\":\"Northing\",\"unit\":\"metre\"},{\"abbreviation\":\"E\",\"direction\":\"east\",\"name\":\"Easting\",\"unit\":\"metre\"}],\"subtype\":\"Cartesian\"},\"datum\":{\"name\":\"Local
 Datum\",\"type\":\"EngineeringDatum\"},\"name\":\"Construction Site Local 
Grid\",\"type\":\"EngineeringCRS\"}"
+      
+
+# sd_parse_crs returns NULL if crs field is missing
+
+    Code
+      sedonadb:::sd_parse_crs("{\"something_else\": 123}")
+    Output
+      NULL
+
+---
+
+    Code
+      sedonadb:::sd_parse_crs("{}")
+    Output
+      NULL
+
+# sd_parse_crs handles invalid JSON gracefully
+
+    Code
+      sedonadb:::sd_parse_crs("invalid json")
+    Condition
+      Error:
+      ! Failed to parse metadata JSON: expected value at line 1 column 1
+
+# sd_parse_crs works with plain strings if that's what's in 'crs'
+
+    Code
+      sedonadb:::sd_parse_crs(meta)
+    Output
+      $authority_code
+      [1] "OGC:CRS84"
+      
+      $srid
+      [1] 4326
+      
+      $name
+      NULL
+      
+      $proj_string
+      [1] "OGC:CRS84"
+      
+
+# print.sedonadb_dataframe shows CRS info for geometry column with EPSG
+
+    Code
+      print(df, n = 0)
+    Output
+      # A sedonadb_dataframe: ? x 1
+      # Geometry: geom (CRS: OGC:CRS84)
+      +----------+
+      |   geom   |
+      | geometry |
+      +----------+
+      +----------+
+      Preview of up to 0 row(s)
+
+# print.sedonadb_dataframe shows CRS info with different SRID
+
+    Code
+      print(df, n = 0)
+    Output
+      # A sedonadb_dataframe: ? x 1
+      # Geometry: geom (CRS: EPSG:5070)
+      +----------+
+      |   geom   |
+      | geometry |
+      +----------+
+      +----------+
+      Preview of up to 0 row(s)
+
+# print.sedonadb_dataframe shows multiple geometry columns with CRS
+
+    Code
+      print(df, n = 0)
+    Output
+      # A sedonadb_dataframe: ? x 2
+      # Geometry: geom1 (CRS: OGC:CRS84), geom2 (CRS: EPSG:5070)
+      +----------+----------+
+      |   geom1  |   geom2  |
+      | geometry | geometry |
+      +----------+----------+
+      +----------+----------+
+      Preview of up to 0 row(s)
+
+# print.sedonadb_dataframe handles geometry without explicit CRS
+
+    Code
+      print(df, n = 0)
+    Output
+      # A sedonadb_dataframe: ? x 1
+      # Geometry: geom
+      +----------+
+      |   geom   |
+      | geometry |
+      +----------+
+      +----------+
+      Preview of up to 0 row(s)
+
+# print.sedonadb_dataframe respects width parameter for geometry line
+
+    Code
+      print(df, n = 0, width = 60)
+    Output
+      # A sedonadb_dataframe: ? x 2
+      # Geometry: very_long_geometry_column_name_1 (CRS: OGC:CR...
+      +-----------------------------+----------------------------+
+      | very_long_geometry_column_n | very_long_geometry_column_ |
+      |           ame_1...          |          name_2...         |
+      +-----------------------------+----------------------------+
+      +-----------------------------+----------------------------+
+      Preview of up to 0 row(s)
+

Review Comment:
   Should we add a snapshot test for printing without any geometry column?



##########
r/sedonadb/tests/testthat/test-crs.R:
##########
@@ -0,0 +1,270 @@
+# 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.
+
+test_that("sd_parse_crs works for GeoArrow metadata with EPSG", {
+  meta <- '{"crs": {"id": {"authority": "EPSG", "code": 5070}, "name": "NAD83 
/ Conus Albers"}}'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))
+})
+
+test_that("sd_parse_crs works for Engineering CRS (no EPSG ID)", {
+  # A realistic example of a local engineering CRS that wouldn't have an EPSG 
code
+  meta <- '{
+    "crs": {
+      "type": "EngineeringCRS",
+      "name": "Construction Site Local Grid",
+      "datum": {
+        "type": "EngineeringDatum",
+        "name": "Local Datum"
+      },
+      "coordinate_system": {
+        "subtype": "Cartesian",
+        "axis": [
+          {"name": "Northing", "abbreviation": "N", "direction": "north", 
"unit": "metre"},
+          {"name": "Easting", "abbreviation": "E", "direction": "east", 
"unit": "metre"}
+        ]
+      }
+    }
+  }'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))
+})
+
+test_that("sd_parse_crs returns NULL if crs field is missing", {
+  expect_snapshot(sedonadb:::sd_parse_crs('{"something_else": 123}'))
+  expect_snapshot(sedonadb:::sd_parse_crs('{}'))
+})
+
+test_that("sd_parse_crs handles invalid JSON gracefully", {
+  expect_snapshot(
+    sedonadb:::sd_parse_crs('invalid json'),
+    error = TRUE
+  )
+})
+
+test_that("sd_parse_crs works with plain strings if that's what's in 'crs'", {
+  meta <- '{"crs": "EPSG:4326"}'
+  expect_snapshot(sedonadb:::sd_parse_crs(meta))
+})
+
+# Tests for CRS display in print.sedonadb_dataframe
+
+test_that("print.sedonadb_dataframe shows CRS info for geometry column with 
EPSG", {
+  df <- sd_sql("SELECT ST_SetSRID(ST_Point(1, 2), 4326) as geom")
+  expect_snapshot(print(df, n = 0))
+})
+
+test_that("print.sedonadb_dataframe shows CRS info with different SRID", {
+  df <- sd_sql("SELECT ST_SetSRID(ST_Point(1, 2), 5070) as geom")
+  expect_snapshot(print(df, n = 0))
+})
+
+test_that("print.sedonadb_dataframe shows multiple geometry columns with CRS", 
{
+  df <- sd_sql(
+    "
+    SELECT
+      ST_SetSRID(ST_Point(1, 2), 4326) as geom1,
+      ST_SetSRID(ST_Point(3, 4), 5070) as geom2
+  "
+  )
+  expect_snapshot(print(df, n = 0))
+})
+
+test_that("print.sedonadb_dataframe handles geometry without explicit CRS", {
+  # ST_Point without ST_SetSRID may not have CRS metadata
+  df <- sd_sql("SELECT ST_Point(1, 2) as geom")
+  expect_snapshot(print(df, n = 0))
+})
+
+test_that("print.sedonadb_dataframe respects width parameter for geometry 
line", {
+  df <- sd_sql(
+    "
+    SELECT
+      ST_SetSRID(ST_Point(1, 2), 4326) as very_long_geometry_column_name_1,
+      ST_SetSRID(ST_Point(3, 4), 4326) as very_long_geometry_column_name_2
+  "
+  )
+  # Use a narrow width to trigger truncation
+  expect_snapshot(print(df, n = 0, width = 60))
+})
+
+# Additional edge cases for sd_parse_crs
+
+test_that("sd_parse_crs handles NULL input", {
+  expect_error(
+    sedonadb:::sd_parse_crs(NULL),
+    "must be character"
+  )
+})
+
+test_that("sd_parse_crs handles empty string", {
+  expect_snapshot(
+    sedonadb:::sd_parse_crs(""),
+    error = TRUE
+  )
+})

Review Comment:
   This test feels like it should be renamed (or the behaviour modified such 
that it handles the empty string)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to