This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new beba39c8d feat: add geospatial read support [2/3] (#3341)
beba39c8d is described below

commit beba39c8d62810fa101740bb2dbb03ab0d8f07ac
Author: Xuanyi Li <[email protected]>
AuthorDate: Wed Oct 28 20:49:40 2026 +0000

    feat: add geospatial read support [2/3] (#3341)
    
    feat: add geospatial read support
---
 Cargo.lock                              |  47 +++++
 Cargo.toml                              |   1 +
 crates/iceberg/Cargo.toml               |   2 +
 crates/iceberg/public-api.txt           |   2 +
 crates/iceberg/src/arrow/schema.rs      | 339 +++++++++++++++++++++++++++++---
 crates/iceberg/src/arrow/value.rs       |  69 ++++---
 crates/iceberg/src/avro/schema.rs       |  18 +-
 crates/iceberg/src/spec/values/datum.rs |  12 +-
 crates/iceberg/src/spec/values/tests.rs |  16 +-
 9 files changed, 421 insertions(+), 85 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 859704c5a..dcd6452de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2382,6 +2382,26 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "geo-traits"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "2e7c353d12a704ccfab1ba8bfb1a7fe6cb18b665bf89d37f4f7890edcd260206"
+dependencies = [
+ "geo-types",
+]
+
+[[package]]
+name = "geo-types"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "94776032c45f950d30a13af6113c2ad5625316c9abfbccee4dd5a6695f8fe0f5"
+dependencies = [
+ "approx",
+ "num-traits",
+ "serde",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.17"
@@ -2884,6 +2904,7 @@ dependencies = [
  "once_cell",
  "ordered-float 4.6.0",
  "parquet",
+ "parquet-geospatial",
  "pretty_assertions",
  "rand 0.9.5",
  "regex",
@@ -4462,6 +4483,7 @@ dependencies = [
  "num-bigint 0.5.1",
  "num-integer",
  "num-traits",
+ "parquet-geospatial",
  "ring",
  "seq-macro",
  "simdutf8",
@@ -4471,6 +4493,19 @@ dependencies = [
  "zstd",
 ]
 
+[[package]]
+name = "parquet-geospatial"
+version = "59.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "634827365851180ca26bf54c76e970dc1c14c075684c0580ee97d8d0c953faf1"
+dependencies = [
+ "arrow-schema",
+ "geo-traits",
+ "serde",
+ "serde_json",
+ "wkb",
+]
+
 [[package]]
 name = "paste"
 version = "1.0.15"
@@ -7735,6 +7770,18 @@ version = "0.57.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
 
+[[package]]
+name = "wkb"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "a120b336c7ad17749026d50427c23d838ecb50cd64aaea6254b5030152f890a9"
+dependencies = [
+ "byteorder",
+ "geo-traits",
+ "num_enum",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "writeable"
 version = "0.6.3"
diff --git a/Cargo.toml b/Cargo.toml
index d4804d882..1a63d85be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -114,6 +114,7 @@ once_cell = "1.20"
 opendal = "0.58"
 ordered-float = "4"
 parquet = "59.2"
+parquet-geospatial = "59.2"
 pilota = "0.11.10"
 pretty_assertions = "1.4"
 proc-macro2 = "1"
diff --git a/crates/iceberg/Cargo.toml b/crates/iceberg/Cargo.toml
index e3591e996..fa5e6cce7 100644
--- a/crates/iceberg/Cargo.toml
+++ b/crates/iceberg/Cargo.toml
@@ -69,7 +69,9 @@ parquet = { workspace = true, features = [
   "async",
   "encryption",
   "arrow_canonical_extension_types",
+  "geospatial",
 ] }
+parquet-geospatial = { workspace = true }
 rand = { workspace = true }
 reqwest = { workspace = true }
 roaring = { workspace = true }
diff --git a/crates/iceberg/public-api.txt b/crates/iceberg/public-api.txt
index 701cb7f95..92267f0a9 100644
--- a/crates/iceberg/public-api.txt
+++ b/crates/iceberg/public-api.txt
@@ -1507,6 +1507,8 @@ pub fn 
iceberg::spec::geospatial::EdgeInterpolationAlgorithm::clone(&self) -> ic
 impl core::cmp::Eq for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
 impl core::cmp::PartialEq for 
iceberg::spec::geospatial::EdgeInterpolationAlgorithm
 pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::eq(&self, other: 
&iceberg::spec::geospatial::EdgeInterpolationAlgorithm) -> bool
+impl core::convert::From<parquet_geospatial::types::Edges> for 
iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::from(edges: 
parquet_geospatial::types::Edges) -> Self
 impl core::default::Default for 
iceberg::spec::geospatial::EdgeInterpolationAlgorithm
 pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::default() -> 
iceberg::spec::geospatial::EdgeInterpolationAlgorithm
 impl core::fmt::Debug for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
diff --git a/crates/iceberg/src/arrow/schema.rs 
b/crates/iceberg/src/arrow/schema.rs
index 34eb597d4..91b8d687a 100644
--- a/crates/iceberg/src/arrow/schema.rs
+++ b/crates/iceberg/src/arrow/schema.rs
@@ -32,14 +32,16 @@ use arrow_schema::{
 };
 use parquet::arrow::PARQUET_FIELD_ID_META_KEY;
 use parquet::file::statistics::Statistics;
+use parquet_geospatial::{WkbEdges, WkbMetadata, WkbType, WkbTypeHint};
 use uuid::Uuid;
 
 use crate::error::{Result, invalid_data};
 use crate::spec::decimal_utils::i128_from_be_bytes;
+use crate::spec::geospatial::EdgeInterpolationAlgorithm;
 use crate::spec::{
-    Datum, FIRST_FIELD_ID, LIST_FIELD_NAME, ListType, MAP_KEY_FIELD_NAME, 
MAP_VALUE_FIELD_NAME,
-    MapType, NestedField, NestedFieldRef, PrimitiveLiteral, PrimitiveType, 
Schema, SchemaVisitor,
-    StructType, Type, VariantType,
+    Datum, FIRST_FIELD_ID, GeographyType, GeometryType, LIST_FIELD_NAME, 
ListType,
+    MAP_KEY_FIELD_NAME, MAP_VALUE_FIELD_NAME, MapType, NestedField, 
NestedFieldRef,
+    PrimitiveLiteral, PrimitiveType, Schema, SchemaVisitor, StructType, Type, 
VariantType,
 };
 use crate::{Error, ErrorKind};
 
@@ -47,6 +49,7 @@ use crate::{Error, ErrorKind};
 pub const DEFAULT_MAP_FIELD_NAME: &str = "key_value";
 /// UTC time zone for Arrow timestamp type.
 pub const UTC_TIME_ZONE: &str = "+00:00";
+const UNSET_GEOSPATIAL_CRS: &str = "srid:0";
 
 /// The canonical Arrow [`arrow.parquet.variant`] extension type.
 ///
@@ -101,6 +104,59 @@ impl ExtensionType for VariantExtensionType {
     }
 }
 
+fn to_arrow_wkb_edges(algorithm: EdgeInterpolationAlgorithm) -> WkbEdges {
+    match algorithm {
+        EdgeInterpolationAlgorithm::Spherical => WkbEdges::Spherical,
+        EdgeInterpolationAlgorithm::Vincenty => WkbEdges::Vincenty,
+        EdgeInterpolationAlgorithm::Thomas => WkbEdges::Thomas,
+        EdgeInterpolationAlgorithm::Andoyer => WkbEdges::Andoyer,
+        EdgeInterpolationAlgorithm::Karney => WkbEdges::Karney,
+    }
+}
+
+impl From<WkbEdges> for EdgeInterpolationAlgorithm {
+    fn from(edges: WkbEdges) -> Self {
+        match edges {
+            WkbEdges::Spherical => Self::Spherical,
+            WkbEdges::Vincenty => Self::Vincenty,
+            WkbEdges::Thomas => Self::Thomas,
+            WkbEdges::Andoyer => Self::Andoyer,
+            WkbEdges::Karney => Self::Karney,
+        }
+    }
+}
+
+fn iceberg_crs_from_wkb_metadata(metadata: &WkbMetadata) -> Result<String> {
+    match metadata.crs.as_ref() {
+        None => Ok(UNSET_GEOSPATIAL_CRS.to_string()),
+        Some(serde_json::Value::String(crs)) => Ok(crs.clone()),
+        Some(serde_json::Value::Object(crs)) => {
+            let id = crs.get("id");
+            let authority = id
+                .and_then(|id| id.get("authority"))
+                .and_then(serde_json::Value::as_str)
+                .filter(|authority| !authority.is_empty());
+            let code = match id.and_then(|id| id.get("code")) {
+                Some(serde_json::Value::String(code)) => Some(code.clone()),
+                Some(serde_json::Value::Number(code)) => 
Some(code.to_string()),
+                _ => None,
+            };
+
+            match (authority, code) {
+                (Some(authority), Some(code)) => 
Ok(format!("{authority}:{code}")),
+                _ => Err(Error::new(
+                    ErrorKind::DataInvalid,
+                    "Cannot determine Iceberg geospatial type from PROJJSON 
CRS without an embedded authority/code",
+                )),
+            }
+        }
+        Some(_) => Err(Error::new(
+            ErrorKind::DataInvalid,
+            "Geospatial CRS metadata must be a string or PROJJSON object",
+        )),
+    }
+}
+
 /// A post order arrow schema visitor.
 ///
 /// For order of methods called, please refer to the internal `visit_schema` 
function.
@@ -413,7 +469,7 @@ impl ArrowSchemaConverter {
         let mut results = Vec::with_capacity(fields.len());
         for i in 0..fields.len() {
             let field = &fields[i];
-            let field_type = &field_results[i];
+            let field_type = self.apply_arrow_extension(field, 
field_results[i].clone())?;
             let id = self.get_field_id(field)?;
             let doc = get_field_doc(field);
             let nested_field = NestedField {
@@ -421,7 +477,7 @@ impl ArrowSchemaConverter {
                 doc,
                 name: field.name().clone(),
                 required: !field.is_nullable(),
-                field_type: Box::new(field_type.clone()),
+                field_type: Box::new(field_type),
                 initial_default: None,
                 write_default: None,
             };
@@ -429,6 +485,51 @@ impl ArrowSchemaConverter {
         }
         Ok(results)
     }
+
+    fn apply_arrow_extension(&self, arrow_field: &FieldRef, iceberg_type: 
Type) -> Result<Type> {
+        if arrow_field.extension_type_name() != Some(WkbType::NAME) {
+            return Ok(iceberg_type);
+        }
+
+        if !matches!(&iceberg_type, Type::Primitive(PrimitiveType::Binary)) {
+            return Err(Error::new(
+                ErrorKind::DataInvalid,
+                format!(
+                    "WKB extension type on field {} requires binary storage, 
got {iceberg_type}",
+                    arrow_field.name()
+                ),
+            ));
+        }
+
+        let wkb_type = 
arrow_field.try_extension_type::<WkbType>().map_err(|err| {
+            Error::new(
+                ErrorKind::DataInvalid,
+                format!(
+                    "Invalid geospatial Arrow extension metadata for field {}",
+                    arrow_field.name()
+                ),
+            )
+            .with_source(err)
+        })?;
+
+        let crs = iceberg_crs_from_wkb_metadata(wkb_type.metadata())?;
+
+        match wkb_type.metadata().type_hint() {
+            WkbTypeHint::Geometry => 
Ok(Type::Primitive(PrimitiveType::Geometry(
+                GeometryType::new(Some(crs))?,
+            ))),
+            WkbTypeHint::Geography => 
Ok(Type::Primitive(PrimitiveType::Geography(
+                GeographyType::new(
+                    Some(crs),
+                    wkb_type
+                        .metadata()
+                        .algorithm
+                        .unwrap_or(WkbEdges::Spherical)
+                        .into(),
+                )?,
+            ))),
+        }
+    }
 }
 
 impl ArrowSchemaVisitor for ArrowSchemaConverter {
@@ -459,6 +560,7 @@ impl ArrowSchemaVisitor for ArrowSchemaConverter {
             }
         };
 
+        let value = self.apply_arrow_extension(element_field, value)?;
         let id = self.get_field_id(element_field)?;
         let doc = get_field_doc(element_field);
         let mut element_field =
@@ -480,6 +582,8 @@ impl ArrowSchemaVisitor for ArrowSchemaConverter {
 
                     let key_field = &fields[0];
                     let value_field = &fields[1];
+                    let key_value = self.apply_arrow_extension(key_field, 
key_value)?;
+                    let value = self.apply_arrow_extension(value_field, 
value)?;
 
                     let key_id = self.get_field_id(key_field)?;
                     let key_doc = get_field_doc(key_field);
@@ -641,22 +745,32 @@ impl SchemaVisitor for ToArrowSchemaConverter {
         } else {
             HashMap::from([(PARQUET_FIELD_ID_META_KEY.to_string(), 
field.id.to_string())])
         };
-        let arrow_field =
+        let mut arrow_field =
             Field::new(field.name.clone(), ty, 
!field.required).with_metadata(metadata);
-        // A variant column's storage is a struct; tag the field with the 
canonical
-        // `arrow.parquet.variant` extension type so consumers read it as a 
Variant, not a struct.
-        let arrow_field = if field.field_type.is_variant() {
-            arrow_field.with_extension_type(VariantExtensionType)
-        } else if matches!(
-            field.field_type.as_ref(),
-            Type::Primitive(PrimitiveType::Uuid)
-        ) {
-            // A uuid column's storage is FixedSizeBinary(16); tag the field 
with the canonical
-            // `arrow.uuid` extension type so consumers read it as a UUID, not 
opaque bytes.
-            arrow_field.with_extension_type(UuidExtensionType)
-        } else {
-            arrow_field
-        };
+
+        match field.field_type.as_ref() {
+            Type::Variant(_) => {
+                // A variant column's storage is a struct; tag the field with 
the canonical
+                // `arrow.parquet.variant` extension type so consumers read it 
as a Variant, not a struct.
+                arrow_field.try_with_extension_type(VariantExtensionType)?;
+            }
+            Type::Primitive(PrimitiveType::Uuid) => {
+                // Preserve the canonical UUID extension on its 
FixedSizeBinary(16) storage.
+                arrow_field.try_with_extension_type(UuidExtensionType)?;
+            }
+            Type::Primitive(PrimitiveType::Geometry(geometry)) => {
+                let metadata = WkbMetadata::new(Some(geometry.crs()), None);
+                
arrow_field.try_with_extension_type(WkbType::new(Some(metadata)))?;
+            }
+            Type::Primitive(PrimitiveType::Geography(geography)) => {
+                let metadata = WkbMetadata::new(
+                    Some(geography.crs()),
+                    Some(to_arrow_wkb_edges(geography.algorithm())),
+                );
+                
arrow_field.try_with_extension_type(WkbType::new(Some(metadata)))?;
+            }
+            _ => {}
+        }
 
         Ok(ArrowSchemaOrFieldOrType::Field(arrow_field))
     }
@@ -776,11 +890,9 @@ impl SchemaVisitor for ToArrowSchemaConverter {
                     .map(DataType::FixedSizeBinary)
                     .unwrap_or(DataType::LargeBinary),
             )),
-            PrimitiveType::Binary => 
Ok(ArrowSchemaOrFieldOrType::Type(DataType::LargeBinary)),
-            PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => 
Err(Error::new(
-                ErrorKind::FeatureUnsupported,
-                format!("Converting {p} to an Arrow schema is not supported 
yet"),
-            )),
+            PrimitiveType::Binary | PrimitiveType::Geometry(_) | 
PrimitiveType::Geography(_) => {
+                Ok(ArrowSchemaOrFieldOrType::Type(DataType::LargeBinary))
+            }
         }
     }
 
@@ -1439,10 +1551,15 @@ mod tests {
     use std::collections::HashMap;
     use std::sync::Arc;
 
+    use arrow_schema::extension::ExtensionType;
     use arrow_schema::{DataType, Field, Schema as ArrowSchema, TimeUnit};
+    use parquet_geospatial::WkbEdges;
 
     use super::*;
     use crate::spec::decimal_utils::decimal_new;
+    use crate::spec::geospatial::{
+        DEFAULT_GEOSPATIAL_CRS, EdgeInterpolationAlgorithm as 
IcebergEdgeInterpolationAlgorithm,
+    };
     use crate::spec::{Literal, Schema};
 
     /// Create a simple field with metadata.
@@ -2379,21 +2496,183 @@ mod tests {
     }
 
     #[test]
-    fn test_geospatial_arrow_schema_conversion_is_unsupported() {
+    fn test_geospatial_arrow_schema_roundtrip() {
         let schema = Schema::builder()
+            .with_schema_id(1)
             .with_fields(vec![
-                NestedField::optional(
+                NestedField::required(
                     1,
                     "geom",
-                    
Type::Primitive(PrimitiveType::Geometry(Default::default())),
+                    Type::Primitive(PrimitiveType::Geometry(
+                        
GeometryType::new(Some("EPSG:3857".to_string())).unwrap(),
+                    )),
+                )
+                .into(),
+                NestedField::optional(
+                    2,
+                    "geog",
+                    Type::Primitive(PrimitiveType::Geography(
+                        GeographyType::new(
+                            Some("OGC:CRS27".to_string()),
+                            IcebergEdgeInterpolationAlgorithm::Karney,
+                        )
+                        .unwrap(),
+                    )),
+                )
+                .into(),
+                NestedField::optional(
+                    3,
+                    "geom_list",
+                    Type::List(ListType::new(
+                        NestedField::list_element(
+                            4,
+                            
Type::Primitive(PrimitiveType::Geometry(GeometryType::default())),
+                            true,
+                        )
+                        .into(),
+                    )),
                 )
                 .into(),
             ])
             .build()
             .unwrap();
 
-        let err = schema_to_arrow_schema(&schema).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+        let arrow_schema = schema_to_arrow_schema(&schema).unwrap();
+        let geom = arrow_schema.field(0);
+        assert_eq!(geom.data_type(), &DataType::LargeBinary);
+
+        let geog = arrow_schema.field(1);
+        let geog_wkb = geog.try_extension_type::<WkbType>().unwrap();
+        assert_eq!(geog_wkb.metadata().algorithm, Some(WkbEdges::Karney));
+        let geog_metadata: serde_json::Value =
+            
serde_json::from_str(&geog_wkb.serialize_metadata().unwrap()).unwrap();
+        assert_eq!(geog_metadata.get("edges").unwrap(), "karney");
+        assert!(geog_metadata.get("algorithm").is_none());
+
+        let list = arrow_schema.field(2);
+        let DataType::List(element) = list.data_type() else {
+            panic!("Expected list field");
+        };
+        assert!(
+            matches!(
+                element
+                    .try_extension_type::<WkbType>()
+                    .unwrap()
+                    .metadata()
+                    .type_hint(),
+                WkbTypeHint::Geometry
+            ),
+            "Expected list element to retain WKB extension metadata"
+        );
+        assert_eq!(
+            element
+                .try_extension_type::<WkbType>()
+                .unwrap()
+                .metadata()
+                .crs
+                .as_ref()
+                .and_then(serde_json::Value::as_str),
+            Some(DEFAULT_GEOSPATIAL_CRS)
+        );
+
+        let converted = arrow_schema_to_schema(&arrow_schema).unwrap();
+        assert_eq!(converted.as_struct().fields(), 
schema.as_struct().fields());
+    }
+
+    #[test]
+    fn test_geospatial_arrow_crs_import() {
+        let mut geom = simple_field("geom", DataType::LargeBinary, true, "1");
+        geom.try_with_extension_type(WkbType::new(Some(WkbMetadata::new(
+            Some(r#"{"id":{"authority":"EPSG","code":3857}}"#),
+            None,
+        ))))
+        .unwrap();
+        let mut unset_geom = simple_field("unset_geom", DataType::LargeBinary, 
true, "2");
+        unset_geom
+            .try_with_extension_type(WkbType::new(Some(WkbMetadata::new(None, 
None))))
+            .unwrap();
+
+        let arrow_schema = ArrowSchema::new(vec![geom, unset_geom]);
+        let schema = arrow_schema_to_schema(&arrow_schema).unwrap();
+
+        assert_eq!(
+            schema.field_by_id(1).unwrap().field_type.as_ref(),
+            &Type::Primitive(PrimitiveType::Geometry(
+                GeometryType::new(Some("EPSG:3857".to_string())).unwrap()
+            ))
+        );
+        assert_eq!(
+            schema.field_by_id(2).unwrap().field_type.as_ref(),
+            &Type::Primitive(PrimitiveType::Geometry(
+                
GeometryType::new(Some(UNSET_GEOSPATIAL_CRS.to_string())).unwrap()
+            ))
+        );
+    }
+
+    #[test]
+    fn test_geospatial_arrow_invalid_crs_is_rejected() {
+        for crs in [r#"{}"#, "42"] {
+            let mut field = simple_field("geom", DataType::LargeBinary, true, 
"1");
+            field
+                
.try_with_extension_type(WkbType::new(Some(WkbMetadata::new(Some(crs), None))))
+                .unwrap();
+
+            let err = 
arrow_schema_to_schema(&ArrowSchema::new(vec![field])).unwrap_err();
+            assert_eq!(err.kind(), ErrorKind::DataInvalid, "{err}");
+        }
+    }
+
+    #[test]
+    fn test_geospatial_arrow_extension_requires_binary_storage() {
+        use arrow_schema::extension::{EXTENSION_TYPE_METADATA_KEY, 
EXTENSION_TYPE_NAME_KEY};
+
+        let metadata = WkbType::new(Some(WkbMetadata::new(None, None)))
+            .serialize_metadata()
+            .unwrap();
+        let field =
+            simple_field("geom", DataType::Int32, true, 
"1").with_metadata(HashMap::from([
+                (PARQUET_FIELD_ID_META_KEY.to_string(), "1".to_string()),
+                (
+                    EXTENSION_TYPE_NAME_KEY.to_string(),
+                    WkbType::NAME.to_string(),
+                ),
+                (EXTENSION_TYPE_METADATA_KEY.to_string(), metadata),
+            ]));
+
+        let err = 
arrow_schema_to_schema(&ArrowSchema::new(vec![field])).unwrap_err();
+        assert_eq!(err.kind(), ErrorKind::DataInvalid, "{err}");
+        assert!(
+            err.message().contains("requires binary storage"),
+            "unexpected error: {err}"
+        );
+    }
+
+    #[test]
+    fn test_geospatial_parquet_statistics_are_ignored() {
+        use parquet::data_type::ByteArray;
+        use parquet::file::statistics::ValueStatistics;
+
+        let stats = Statistics::ByteArray(ValueStatistics::new(
+            Some(ByteArray::from(vec![1, 2, 3])),
+            Some(ByteArray::from(vec![4, 5, 6])),
+            None,
+            None,
+            false,
+        ));
+
+        for primitive_type in [
+            PrimitiveType::Geometry(GeometryType::default()),
+            PrimitiveType::Geography(GeographyType::default()),
+        ] {
+            assert_eq!(
+                get_parquet_stat_min_as_datum(&primitive_type, 
&stats).unwrap(),
+                None
+            );
+            assert_eq!(
+                get_parquet_stat_max_as_datum(&primitive_type, 
&stats).unwrap(),
+                None
+            );
+        }
     }
 
     #[test]
diff --git a/crates/iceberg/src/arrow/value.rs 
b/crates/iceberg/src/arrow/value.rs
index b3bfb3f8e..94546c297 100644
--- a/crates/iceberg/src/arrow/value.rs
+++ b/crates/iceberg/src/arrow/value.rs
@@ -18,10 +18,11 @@
 use std::sync::Arc;
 
 use arrow_array::{
-    Array, ArrayRef, BinaryArray, BooleanArray, Date32Array, Decimal128Array, 
FixedSizeBinaryArray,
-    FixedSizeListArray, Float32Array, Float64Array, Int32Array, Int64Array, 
LargeBinaryArray,
-    LargeListArray, LargeStringArray, ListArray, MapArray, StringArray, 
StructArray,
-    Time64MicrosecondArray, TimestampMicrosecondArray, 
TimestampNanosecondArray, new_null_array,
+    Array, ArrayRef, BinaryArray, BinaryViewArray, BooleanArray, Date32Array, 
Decimal128Array,
+    FixedSizeBinaryArray, FixedSizeListArray, Float32Array, Float64Array, 
Int32Array, Int64Array,
+    LargeBinaryArray, LargeListArray, LargeStringArray, ListArray, MapArray, 
StringArray,
+    StructArray, Time64MicrosecondArray, TimestampMicrosecondArray, 
TimestampNanosecondArray,
+    new_null_array,
 };
 use arrow_buffer::BooleanBuffer;
 use arrow_schema::{DataType, FieldRef, TimeUnit};
@@ -331,7 +332,7 @@ impl SchemaWithPartnerVisitor<ArrayRef> for 
ArrowArrayToIcebergStructConverter {
                     .map(|v| v.map(|v| Literal::fixed(v.iter().cloned())))
                     .collect())
             }
-            PrimitiveType::Binary => {
+            PrimitiveType::Binary | PrimitiveType::Geometry(_) | 
PrimitiveType::Geography(_) => {
                 if let Some(array) = 
partner.as_any().downcast_ref::<LargeBinaryArray>() {
                     Ok(array
                         .iter()
@@ -342,14 +343,15 @@ impl SchemaWithPartnerVisitor<ArrayRef> for 
ArrowArrayToIcebergStructConverter {
                         .iter()
                         .map(|v| v.map(|v| Literal::binary(v.to_vec())))
                         .collect())
+                } else if let Some(array) = 
partner.as_any().downcast_ref::<BinaryViewArray>() {
+                    Ok(array
+                        .iter()
+                        .map(|v| v.map(|v| Literal::binary(v.to_vec())))
+                        .collect())
                 } else {
                     Err(invalid_data!("The partner is not a binary array"))
                 }
             }
-            PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => 
Err(Error::new(
-                ErrorKind::FeatureUnsupported,
-                format!("Converting {p} Arrow array to an Iceberg literal is 
not supported yet"),
-            )),
         }
     }
 
@@ -575,6 +577,9 @@ pub(crate) fn create_primitive_array_single_element(
         (DataType::Binary, Some(PrimitiveLiteral::Binary(v))) => {
             Ok(Arc::new(BinaryArray::from_vec(vec![v.as_slice()])))
         }
+        (DataType::LargeBinary, Some(PrimitiveLiteral::Binary(v))) => {
+            Ok(Arc::new(LargeBinaryArray::from_vec(vec![v.as_slice()])))
+        }
         (DataType::Decimal128(precision, scale), 
Some(PrimitiveLiteral::Int128(v))) => {
             let array = Decimal128Array::from(vec![{ *v }])
                 .with_precision_and_scale(*precision, *scale)
@@ -1111,28 +1116,34 @@ mod test {
     }
 
     #[test]
-    fn test_arrow_geospatial_to_literal_is_unsupported() {
-        let values = 
Arc::new(LargeBinaryArray::from_vec(vec![b"wkb".as_slice()])) as ArrayRef;
-        let struct_array = Arc::new(StructArray::from(vec![(
-            Arc::new(
-                Field::new("geom", DataType::LargeBinary, 
false).with_metadata(HashMap::from([(
-                    PARQUET_FIELD_ID_META_KEY.to_string(),
-                    "1".to_string(),
-                )])),
-            ),
-            values,
-        )])) as ArrayRef;
-        let ty = StructType::new(vec![
-            NestedField::required(
-                1,
-                "geom",
-                Type::Primitive(PrimitiveType::Geometry(Default::default())),
-            )
-            .into(),
+    fn test_arrow_geospatial_to_literal() {
+        let values = Arc::new(LargeBinaryArray::from(vec![
+            Some(b"wkb-1".as_slice()),
+            None,
+            Some(b"wkb-2".as_slice()),
+        ])) as ArrayRef;
+        let mut converter = ArrowArrayToIcebergStructConverter;
+
+        let result = converter
+            .primitive(&PrimitiveType::Geometry(Default::default()), &values)
+            .unwrap();
+
+        assert_eq!(result, vec![
+            Some(Literal::binary(b"wkb-1".to_vec())),
+            None,
+            Some(Literal::binary(b"wkb-2".to_vec())),
         ]);
+    }
 
-        let err = arrow_struct_to_literal(&struct_array, &ty).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+    #[test]
+    fn test_arrow_geospatial_binary_view_to_literal() {
+        let wkb = parquet_geospatial::testing::wkb_point_xy(1.0, 2.0);
+        let values = Arc::new(BinaryViewArray::from(vec![Some(wkb.as_slice()), 
None])) as ArrayRef;
+        let ty = Type::Primitive(PrimitiveType::Geography(Default::default()));
+
+        let result = arrow_primitive_to_literal(&values, &ty).unwrap();
+
+        assert_eq!(result, vec![Some(Literal::binary(wkb)), None]);
     }
 
     #[test]
diff --git a/crates/iceberg/src/avro/schema.rs 
b/crates/iceberg/src/avro/schema.rs
index 9af68022a..3caf99bfe 100644
--- a/crates/iceberg/src/avro/schema.rs
+++ b/crates/iceberg/src/avro/schema.rs
@@ -275,12 +275,8 @@ impl SchemaVisitor for SchemaToAvroSchema {
             // already accepts both forms.
             PrimitiveType::Uuid => AvroSchema::Uuid(UuidSchema::String),
             PrimitiveType::Fixed(len) => 
self.define_once(avro_fixed_schema((*len) as usize)?),
-            PrimitiveType::Binary => AvroSchema::Bytes,
-            PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => {
-                return Err(Error::new(
-                    ErrorKind::FeatureUnsupported,
-                    format!("Converting {p} to an Avro schema is not supported 
yet"),
-                ));
+            PrimitiveType::Binary | PrimitiveType::Geometry(_) | 
PrimitiveType::Geography(_) => {
+                AvroSchema::Bytes
             }
             PrimitiveType::Decimal { precision, scale } => {
                 self.define_once(avro_decimal_schema(*precision as usize, 
*scale as usize)?)
@@ -1354,10 +1350,10 @@ mod tests {
     }
 
     #[test]
-    fn test_geospatial_to_avro_schema_is_unsupported() {
+    fn test_geospatial_to_avro_bytes_schema() {
         let schema = Schema::builder()
             .with_fields(vec![
-                NestedField::optional(
+                NestedField::required(
                     1,
                     "geom",
                     
Type::Primitive(PrimitiveType::Geometry(Default::default())),
@@ -1367,7 +1363,9 @@ mod tests {
             .build()
             .unwrap();
 
-        let err = schema_to_avro_schema("t", &schema).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+        let AvroSchema::Record(record) = schema_to_avro_schema("t", 
&schema).unwrap() else {
+            panic!("expected Avro record schema");
+        };
+        assert_eq!(record.fields[0].schema, AvroSchema::Bytes);
     }
 }
diff --git a/crates/iceberg/src/spec/values/datum.rs 
b/crates/iceberg/src/spec/values/datum.rs
index 5c4339d8b..71de11beb 100644
--- a/crates/iceberg/src/spec/values/datum.rs
+++ b/crates/iceberg/src/spec/values/datum.rs
@@ -384,12 +384,6 @@ impl Datum {
                     "Cannot create datum for unknown type from bytes",
                 ));
             }
-            PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => {
-                return Err(Error::new(
-                    ErrorKind::FeatureUnsupported,
-                    "Cannot create geospatial datum from bytes yet",
-                ));
-            }
             PrimitiveType::Boolean => {
                 if bytes.len() == 1 && bytes[0] == 0u8 {
                     PrimitiveLiteral::Boolean(false)
@@ -439,8 +433,10 @@ impl Datum {
             PrimitiveType::Uuid => {
                 
PrimitiveLiteral::UInt128(u128::from_be_bytes(bytes.try_into()?))
             }
-            PrimitiveType::Fixed(_) => 
PrimitiveLiteral::Binary(Vec::from(bytes)),
-            PrimitiveType::Binary => 
PrimitiveLiteral::Binary(Vec::from(bytes)),
+            PrimitiveType::Fixed(_)
+            | PrimitiveType::Binary
+            | PrimitiveType::Geometry(_)
+            | PrimitiveType::Geography(_) => 
PrimitiveLiteral::Binary(Vec::from(bytes)),
             PrimitiveType::Decimal { .. } => PrimitiveLiteral::Int128(
                 i128_from_be_bytes(bytes)
                     .ok_or_else(|| invalid_data!("Can't convert bytes to i128: 
{bytes:?}"))?,
diff --git a/crates/iceberg/src/spec/values/tests.rs 
b/crates/iceberg/src/spec/values/tests.rs
index 847fbd64c..2de6e2916 100644
--- a/crates/iceberg/src/spec/values/tests.rs
+++ b/crates/iceberg/src/spec/values/tests.rs
@@ -618,14 +618,14 @@ fn avro_bytes_string() {
 }
 
 #[test]
-fn geospatial_binary_deserialization_is_not_yet_supported() {
-    for primitive_type in [
-        PrimitiveType::Geometry(Default::default()),
-        PrimitiveType::Geography(Default::default()),
-    ] {
-        let err = Datum::try_from_bytes(&[1, 2, 3], 
primitive_type).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
-    }
+fn avro_bytes_geometry() {
+    let bytes = vec![1u8, 2u8, 3u8, 4u8];
+    let ty = PrimitiveType::Geometry(Default::default());
+    check_avro_bytes_serde(
+        bytes.clone(),
+        Datum::new(ty.clone(), PrimitiveLiteral::Binary(bytes)),
+        &ty,
+    );
 }
 
 #[test]

Reply via email to