You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by gi...@apache.org on 2026/10/16 22:19:24 UTC
(iceberg-rust) branch main updated: feat: recognize geometry and geography types [1/3] (#2933)
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 697abcee7 feat: recognize geometry and geography types [1/3] (#2933)
697abcee7 is described below
commit 697abcee75b503fc106f445dae8bcfc401524dcc
Author: Xuanyi Li <sh...@outlook.com>
AuthorDate: Fri Oct 16 22:12:57 2026 +0000
feat: recognize geometry and geography types [1/3] (#2933)
feat: recognize geometry and geography types
Co-authored-by: Danny C Jones <49...@users.noreply.github.com>
Co-authored-by: Shawn Chang <42...@users.noreply.github.com>
---
crates/catalog/glue/src/schema.rs | 31 ++++-
crates/catalog/hms/src/schema.rs | 26 +++-
crates/iceberg/public-api.txt | 108 +++++++++++++++
crates/iceberg/src/arrow/schema.rs | 25 ++++
crates/iceberg/src/arrow/value.rs | 29 ++++
crates/iceberg/src/avro/schema.rs | 24 ++++
crates/iceberg/src/spec/datatypes.rs | 107 ++++++++++++++-
crates/iceberg/src/spec/geospatial.rs | 215 ++++++++++++++++++++++++++++++
crates/iceberg/src/spec/mod.rs | 2 +
crates/iceberg/src/spec/schema/mod.rs | 18 ++-
crates/iceberg/src/spec/sort.rs | 51 ++++++-
crates/iceberg/src/spec/transform.rs | 10 +-
crates/iceberg/src/spec/values/datum.rs | 12 ++
crates/iceberg/src/spec/values/literal.rs | 16 ++-
crates/iceberg/src/spec/values/tests.rs | 59 ++++++++
crates/iceberg/src/transform/bucket.rs | 5 +-
crates/iceberg/src/transform/identity.rs | 5 +-
crates/iceberg/src/transform/truncate.rs | 7 +-
18 files changed, 735 insertions(+), 15 deletions(-)
diff --git a/crates/catalog/glue/src/schema.rs b/crates/catalog/glue/src/schema.rs
index 6116fc224..95c1d8bab 100644
--- a/crates/catalog/glue/src/schema.rs
+++ b/crates/catalog/glue/src/schema.rs
@@ -148,6 +148,10 @@ impl SchemaVisitor for GlueSchemaBuilder {
}
fn primitive(&mut self, p: &PrimitiveType) -> Result<Self::T> {
+ // Glue column types are informational for Iceberg tables; readers use Iceberg metadata as
+ // the source of truth. Keep the existing Glue-compatible spellings and use the Iceberg
+ // display representation for geospatial types. A non-failing fallback for all other types
+ // is tracked in https://github.com/apache/iceberg-rust/issues/3375.
let glue_type = match p {
PrimitiveType::Unknown => {
return Err(Error::new(
@@ -173,6 +177,7 @@ impl SchemaVisitor for GlueSchemaBuilder {
"string".to_string()
}
PrimitiveType::Binary | PrimitiveType::Fixed(_) => "binary".to_string(),
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => p.to_string(),
PrimitiveType::Decimal { precision, scale } => {
format!("decimal({precision},{scale})")
}
@@ -189,7 +194,9 @@ impl SchemaVisitor for GlueSchemaBuilder {
#[cfg(test)]
mod tests {
use iceberg::TableCreation;
- use iceberg::spec::{FormatVersion, Schema, TableMetadataBuilder};
+ use iceberg::spec::{
+ FormatVersion, GeometryType, NestedField, PrimitiveType, Schema, TableMetadataBuilder, Type,
+ };
use super::*;
@@ -560,4 +567,26 @@ mod tests {
assert_eq!(result, expected);
Ok(())
}
+
+ #[test]
+ fn test_schema_with_geospatial_type() {
+ let schema = Schema::builder()
+ .with_fields(vec![
+ NestedField::required(
+ 1,
+ "geom",
+ Type::Primitive(PrimitiveType::Geometry(GeometryType::default())),
+ )
+ .into(),
+ ])
+ .build()
+ .unwrap();
+ let metadata = create_metadata_with_format_version(schema, FormatVersion::V3).unwrap();
+
+ let result = GlueSchemaBuilder::from_iceberg(&metadata).unwrap().build();
+
+ assert_eq!(result, vec![
+ create_column("geom", "geometry(OGC:CRS84)", "1", false,).unwrap()
+ ]);
+ }
}
diff --git a/crates/catalog/hms/src/schema.rs b/crates/catalog/hms/src/schema.rs
index f04ee11c1..5909454bf 100644
--- a/crates/catalog/hms/src/schema.rs
+++ b/crates/catalog/hms/src/schema.rs
@@ -124,6 +124,12 @@ impl SchemaVisitor for HiveSchemaBuilder {
"string".to_string()
}
PrimitiveType::Binary | PrimitiveType::Fixed(_) => "binary".to_string(),
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => {
+ return Err(Error::new(
+ ErrorKind::FeatureUnsupported,
+ format!("Conversion from {p:?} is not supported"),
+ ));
+ }
PrimitiveType::Decimal { precision, scale } => {
format!("decimal({precision},{scale})")
}
@@ -142,7 +148,7 @@ impl SchemaVisitor for HiveSchemaBuilder {
#[cfg(test)]
mod tests {
use iceberg::Result;
- use iceberg::spec::Schema;
+ use iceberg::spec::{GeometryType, NestedField, PrimitiveType, Schema, Type};
use super::*;
@@ -486,4 +492,22 @@ mod tests {
Ok(())
}
+
+ #[test]
+ fn test_schema_with_geospatial_type_is_unsupported() {
+ let schema = Schema::builder()
+ .with_fields(vec![
+ NestedField::required(
+ 1,
+ "geom",
+ Type::Primitive(PrimitiveType::Geometry(GeometryType::default())),
+ )
+ .into(),
+ ])
+ .build()
+ .unwrap();
+
+ let err = HiveSchemaBuilder::from_iceberg(&schema).unwrap_err();
+ assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+ }
}
diff --git a/crates/iceberg/public-api.txt b/crates/iceberg/public-api.txt
index ad1af4389..b86e48c77 100644
--- a/crates/iceberg/public-api.txt
+++ b/crates/iceberg/public-api.txt
@@ -1490,6 +1490,71 @@ pub fn iceberg::sensitive::SensitiveString::fmt(&self, f: &mut core::fmt::Format
impl core::marker::StructuralPartialEq for iceberg::sensitive::SensitiveString
pub mod iceberg::spec
pub use iceberg::spec::ByteBuf
+pub mod iceberg::spec::geospatial
+pub enum iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub iceberg::spec::geospatial::EdgeInterpolationAlgorithm::Andoyer
+pub iceberg::spec::geospatial::EdgeInterpolationAlgorithm::Karney
+pub iceberg::spec::geospatial::EdgeInterpolationAlgorithm::Spherical
+pub iceberg::spec::geospatial::EdgeInterpolationAlgorithm::Thomas
+pub iceberg::spec::geospatial::EdgeInterpolationAlgorithm::Vincenty
+impl core::clone::Clone for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::clone(&self) -> iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+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::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
+pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
+impl core::marker::Copy for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+impl core::marker::StructuralPartialEq for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+impl serde_core::ser::Serialize for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
+impl<'de> serde_core::de::Deserialize<'de> for iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::EdgeInterpolationAlgorithm::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
+pub struct iceberg::spec::geospatial::GeographyType
+impl iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::algorithm(&self) -> iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::GeographyType::crs(&self) -> &str
+pub fn iceberg::spec::geospatial::GeographyType::new(crs: core::option::Option<alloc::string::String>, algorithm: iceberg::spec::geospatial::EdgeInterpolationAlgorithm) -> iceberg::Result<Self>
+impl core::clone::Clone for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::clone(&self) -> iceberg::spec::geospatial::GeographyType
+impl core::cmp::Eq for iceberg::spec::geospatial::GeographyType
+impl core::cmp::PartialEq for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::eq(&self, other: &iceberg::spec::geospatial::GeographyType) -> bool
+impl core::default::Default for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::default() -> Self
+impl core::fmt::Debug for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
+impl core::marker::StructuralPartialEq for iceberg::spec::geospatial::GeographyType
+impl serde_core::ser::Serialize for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
+impl<'de> serde_core::de::Deserialize<'de> for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
+pub struct iceberg::spec::geospatial::GeometryType
+impl iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::crs(&self) -> &str
+pub fn iceberg::spec::geospatial::GeometryType::new(crs: core::option::Option<alloc::string::String>) -> iceberg::Result<Self>
+impl core::clone::Clone for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::clone(&self) -> iceberg::spec::geospatial::GeometryType
+impl core::cmp::Eq for iceberg::spec::geospatial::GeometryType
+impl core::cmp::PartialEq for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::eq(&self, other: &iceberg::spec::geospatial::GeometryType) -> bool
+impl core::default::Default for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::default() -> iceberg::spec::geospatial::GeometryType
+impl core::fmt::Debug for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
+impl core::marker::StructuralPartialEq for iceberg::spec::geospatial::GeometryType
+impl serde_core::ser::Serialize for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
+impl<'de> serde_core::de::Deserialize<'de> for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
pub enum iceberg::spec::DataContentType
pub iceberg::spec::DataContentType::Data = 0
pub iceberg::spec::DataContentType::EqualityDeletes = 2
@@ -1735,6 +1800,8 @@ pub iceberg::spec::PrimitiveType::Decimal::scale: u32
pub iceberg::spec::PrimitiveType::Double
pub iceberg::spec::PrimitiveType::Fixed(u64)
pub iceberg::spec::PrimitiveType::Float
+pub iceberg::spec::PrimitiveType::Geography(iceberg::spec::geospatial::GeographyType)
+pub iceberg::spec::PrimitiveType::Geometry(iceberg::spec::geospatial::GeometryType)
pub iceberg::spec::PrimitiveType::Int
pub iceberg::spec::PrimitiveType::Long
pub iceberg::spec::PrimitiveType::String
@@ -2112,6 +2179,47 @@ impl serde_core::ser::Serialize for iceberg::spec::FieldSummary
pub fn iceberg::spec::FieldSummary::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
impl<'de> serde_core::de::Deserialize<'de> for iceberg::spec::FieldSummary
pub fn iceberg::spec::FieldSummary::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
+pub struct iceberg::spec::GeographyType
+impl iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::algorithm(&self) -> iceberg::spec::geospatial::EdgeInterpolationAlgorithm
+pub fn iceberg::spec::geospatial::GeographyType::crs(&self) -> &str
+pub fn iceberg::spec::geospatial::GeographyType::new(crs: core::option::Option<alloc::string::String>, algorithm: iceberg::spec::geospatial::EdgeInterpolationAlgorithm) -> iceberg::Result<Self>
+impl core::clone::Clone for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::clone(&self) -> iceberg::spec::geospatial::GeographyType
+impl core::cmp::Eq for iceberg::spec::geospatial::GeographyType
+impl core::cmp::PartialEq for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::eq(&self, other: &iceberg::spec::geospatial::GeographyType) -> bool
+impl core::default::Default for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::default() -> Self
+impl core::fmt::Debug for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
+impl core::marker::StructuralPartialEq for iceberg::spec::geospatial::GeographyType
+impl serde_core::ser::Serialize for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
+impl<'de> serde_core::de::Deserialize<'de> for iceberg::spec::geospatial::GeographyType
+pub fn iceberg::spec::geospatial::GeographyType::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
+pub struct iceberg::spec::GeometryType
+impl iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::crs(&self) -> &str
+pub fn iceberg::spec::geospatial::GeometryType::new(crs: core::option::Option<alloc::string::String>) -> iceberg::Result<Self>
+impl core::clone::Clone for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::clone(&self) -> iceberg::spec::geospatial::GeometryType
+impl core::cmp::Eq for iceberg::spec::geospatial::GeometryType
+impl core::cmp::PartialEq for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::eq(&self, other: &iceberg::spec::geospatial::GeometryType) -> bool
+impl core::default::Default for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::default() -> iceberg::spec::geospatial::GeometryType
+impl core::fmt::Debug for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
+impl core::marker::StructuralPartialEq for iceberg::spec::geospatial::GeometryType
+impl serde_core::ser::Serialize for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
+impl<'de> serde_core::de::Deserialize<'de> for iceberg::spec::geospatial::GeometryType
+pub fn iceberg::spec::geospatial::GeometryType::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
pub struct iceberg::spec::ListType
pub iceberg::spec::ListType::element_field: iceberg::spec::NestedFieldRef
impl iceberg::spec::ListType
diff --git a/crates/iceberg/src/arrow/schema.rs b/crates/iceberg/src/arrow/schema.rs
index 748b97b3a..34eb597d4 100644
--- a/crates/iceberg/src/arrow/schema.rs
+++ b/crates/iceberg/src/arrow/schema.rs
@@ -777,6 +777,10 @@ impl SchemaVisitor for ToArrowSchemaConverter {
.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"),
+ )),
}
}
@@ -1246,6 +1250,9 @@ pub(crate) fn primitive_type_to_arrow_type_with_ree(primitive_type: &PrimitiveTy
PrimitiveType::Uuid => make_ree(DataType::Binary),
PrimitiveType::Fixed(_) => make_ree(DataType::Binary),
PrimitiveType::Binary => make_ree(DataType::Binary),
+ // Match the LargeBinary storage used by schema_to_arrow_schema in the read layer.
+ // Its 64-bit offsets cover the array's total WKB bytes, not only one geometry's size.
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => make_ree(DataType::LargeBinary),
PrimitiveType::Decimal { precision, scale } => {
make_ree(DataType::Decimal128(*precision as u8, *scale as i8))
}
@@ -2371,6 +2378,24 @@ mod tests {
);
}
+ #[test]
+ fn test_geospatial_arrow_schema_conversion_is_unsupported() {
+ let schema = Schema::builder()
+ .with_fields(vec![
+ NestedField::optional(
+ 1,
+ "geom",
+ Type::Primitive(PrimitiveType::Geometry(Default::default())),
+ )
+ .into(),
+ ])
+ .build()
+ .unwrap();
+
+ let err = schema_to_arrow_schema(&schema).unwrap_err();
+ assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+ }
+
#[test]
fn test_type_conversion() {
// test primitive type
diff --git a/crates/iceberg/src/arrow/value.rs b/crates/iceberg/src/arrow/value.rs
index 7cbebf68c..b3bfb3f8e 100644
--- a/crates/iceberg/src/arrow/value.rs
+++ b/crates/iceberg/src/arrow/value.rs
@@ -346,6 +346,10 @@ impl SchemaWithPartnerVisitor<ArrayRef> for ArrowArrayToIcebergStructConverter {
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"),
+ )),
}
}
@@ -1106,6 +1110,31 @@ 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(),
+ ]);
+
+ let err = arrow_struct_to_literal(&struct_array, &ty).unwrap_err();
+ assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+ }
+
#[test]
fn test_find_field_by_id() {
// Create Arrow arrays for the nested structure
diff --git a/crates/iceberg/src/avro/schema.rs b/crates/iceberg/src/avro/schema.rs
index 47254317e..8bdd67c50 100644
--- a/crates/iceberg/src/avro/schema.rs
+++ b/crates/iceberg/src/avro/schema.rs
@@ -276,6 +276,12 @@ impl SchemaVisitor for SchemaToAvroSchema {
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::Decimal { precision, scale } => {
self.define_once(avro_decimal_schema(*precision as usize, *scale as usize)?)
}
@@ -1404,4 +1410,22 @@ mod tests {
let err = schema_to_avro_schema("t", &schema).unwrap_err();
assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
}
+
+ #[test]
+ fn test_geospatial_to_avro_schema_is_unsupported() {
+ let schema = Schema::builder()
+ .with_fields(vec![
+ NestedField::optional(
+ 1,
+ "geom",
+ Type::Primitive(PrimitiveType::Geometry(Default::default())),
+ )
+ .into(),
+ ])
+ .build()
+ .unwrap();
+
+ let err = schema_to_avro_schema("t", &schema).unwrap_err();
+ assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+ }
}
diff --git a/crates/iceberg/src/spec/datatypes.rs b/crates/iceberg/src/spec/datatypes.rs
index bfee52023..a5ea6ee87 100644
--- a/crates/iceberg/src/spec/datatypes.rs
+++ b/crates/iceberg/src/spec/datatypes.rs
@@ -28,6 +28,7 @@ use serde::de::{Error, IntoDeserializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value as JsonValue;
+use super::geospatial::{GeographyType, GeometryType, parse_geography, parse_geometry};
use super::values::Literal;
use crate::ensure_data_valid;
use crate::error::Result;
@@ -133,7 +134,7 @@ impl Type {
/// Minimum [`FormatVersion`] required to support this type, **without** taking
/// nested field types into account.
///
- /// `Unknown` / `TimestampNs` / `TimestamptzNs` / `Variant` require
+ /// `Unknown` / `TimestampNs` / `TimestamptzNs` / `Geometry` / `Geography` / `Variant` require
/// [`FormatVersion::V3`]; every other type is valid from [`FormatVersion::V1`]. Mirrors Java's
/// `Schema.MIN_FORMAT_VERSIONS` (a shallow lookup keyed by type id), so it
/// intentionally does not recurse: callers needing the floor for a whole schema
@@ -143,7 +144,11 @@ impl Type {
pub(crate) fn min_format_version(&self) -> FormatVersion {
match self {
Type::Primitive(
- PrimitiveType::Unknown | PrimitiveType::TimestampNs | PrimitiveType::TimestamptzNs,
+ PrimitiveType::Unknown
+ | PrimitiveType::TimestampNs
+ | PrimitiveType::TimestamptzNs
+ | PrimitiveType::Geometry(_)
+ | PrimitiveType::Geography(_),
)
| Type::Variant(_) => FormatVersion::V3,
_ => FormatVersion::V1,
@@ -274,6 +279,10 @@ pub enum PrimitiveType {
Binary,
/// Default / null column type used when a more specific type is not known.
Unknown,
+ /// Geometry values encoded as well-known binary.
+ Geometry(GeometryType),
+ /// Geography values encoded as well-known binary.
+ Geography(GeographyType),
}
impl PrimitiveType {
@@ -297,6 +306,8 @@ impl PrimitiveType {
| (PrimitiveType::Uuid, PrimitiveLiteral::UInt128(_))
| (PrimitiveType::Fixed(_), PrimitiveLiteral::Binary(_))
| (PrimitiveType::Binary, PrimitiveLiteral::Binary(_))
+ | (PrimitiveType::Geometry(_), PrimitiveLiteral::Binary(_))
+ | (PrimitiveType::Geography(_), PrimitiveLiteral::Binary(_))
)
}
}
@@ -325,6 +336,16 @@ impl<'de> Deserialize<'de> for PrimitiveType {
deserialize_decimal(s.into_deserializer())
} else if s.starts_with("fixed") {
deserialize_fixed(s.into_deserializer())
+ } else if s
+ .get(..8)
+ .is_some_and(|prefix| prefix.eq_ignore_ascii_case("geometry"))
+ {
+ parse_geometry(&s).map_err(D::Error::custom)
+ } else if s
+ .get(..9)
+ .is_some_and(|prefix| prefix.eq_ignore_ascii_case("geography"))
+ {
+ parse_geography(&s).map_err(D::Error::custom)
} else {
PrimitiveType::deserialize(s.into_deserializer())
}
@@ -339,6 +360,9 @@ impl Serialize for PrimitiveType {
serialize_decimal(precision, scale, serializer)
}
PrimitiveType::Fixed(l) => serialize_fixed(l, serializer),
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => {
+ serializer.serialize_str(&self.to_string())
+ }
_ => PrimitiveType::serialize(self, serializer),
}
}
@@ -435,6 +459,13 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Uuid => write!(f, "uuid"),
PrimitiveType::Fixed(size) => write!(f, "fixed({size})"),
PrimitiveType::Binary => write!(f, "binary"),
+ PrimitiveType::Geometry(geometry) => write!(f, "geometry({})", geometry.crs()),
+ PrimitiveType::Geography(geography) => write!(
+ f,
+ "geography({}, {})",
+ geography.crs(),
+ geography.algorithm().as_str()
+ ),
}
}
}
@@ -944,10 +975,13 @@ impl<'de> Deserialize<'de> for VariantType {
#[cfg(test)]
mod tests {
+ use std::hash::{Hash, Hasher};
+
use pretty_assertions::assert_eq;
use uuid::Uuid;
use super::*;
+ use crate::spec::geospatial::{DEFAULT_GEOSPATIAL_CRS, EdgeInterpolationAlgorithm};
use crate::spec::values::PrimitiveLiteral;
fn check_type_serde(json: &str, expected_type: Type) {
@@ -1076,6 +1110,67 @@ mod tests {
)
}
+ #[test]
+ fn primitive_type_geospatial() {
+ let cases = vec![
+ (
+ r#""geometry""#,
+ PrimitiveType::Geometry(GeometryType::default()),
+ "geometry(OGC:CRS84)",
+ ),
+ (
+ r#""GeOmEtRy ( EPSG:3857 )""#,
+ PrimitiveType::Geometry(GeometryType::new(Some("EPSG:3857".to_string())).unwrap()),
+ "geometry(EPSG:3857)",
+ ),
+ (
+ r#""GEOGRAPHY""#,
+ PrimitiveType::Geography(GeographyType::default()),
+ "geography(OGC:CRS84, spherical)",
+ ),
+ (
+ r#""GeOgRaPhY ( OGC:CRS27 , KARNEY )""#,
+ PrimitiveType::Geography(
+ GeographyType::new(
+ Some("OGC:CRS27".to_string()),
+ EdgeInterpolationAlgorithm::Karney,
+ )
+ .unwrap(),
+ ),
+ "geography(OGC:CRS27, karney)",
+ ),
+ ];
+
+ for (json, expected, display) in cases {
+ let actual: PrimitiveType = serde_json::from_str(json).unwrap();
+ assert_eq!(
+ actual, expected,
+ "parsed primitive type did not match expectation"
+ );
+ assert_eq!(actual.to_string(), display, "display impl did not match");
+ assert_eq!(
+ serde_json::to_string(&actual).unwrap(),
+ format!(r#""{display}""#),
+ "JSON serialization did not match expectation"
+ );
+ }
+
+ let equivalent_default = GeometryType::new(Some("EPSG:4326".to_string())).unwrap();
+ assert_eq!(equivalent_default.crs(), "EPSG:4326");
+ assert_ne!(equivalent_default, GeometryType::default());
+ let lowercase_default =
+ GeometryType::new(Some(DEFAULT_GEOSPATIAL_CRS.to_ascii_lowercase())).unwrap();
+ assert_eq!(lowercase_default, GeometryType::default());
+ let mut lowercase_hash = std::collections::hash_map::DefaultHasher::new();
+ lowercase_default.hash(&mut lowercase_hash);
+ let mut default_hash = std::collections::hash_map::DefaultHasher::new();
+ GeometryType::default().hash(&mut default_hash);
+ assert_eq!(lowercase_hash.finish(), default_hash.finish());
+ assert!(
+ serde_json::from_str::<PrimitiveType>(r#""geography(OGC:CRS27,unknown)""#).is_err()
+ );
+ }
+
#[test]
fn struct_type() {
let record = r#"
@@ -1551,6 +1646,14 @@ mod tests {
),
(PrimitiveType::Fixed(8), PrimitiveLiteral::Binary(vec![1])),
(PrimitiveType::Binary, PrimitiveLiteral::Binary(vec![1])),
+ (
+ PrimitiveType::Geometry(GeometryType::default()),
+ PrimitiveLiteral::Binary(vec![1]),
+ ),
+ (
+ PrimitiveType::Geography(GeographyType::default()),
+ PrimitiveLiteral::Binary(vec![1]),
+ ),
];
for (ty, literal) in pairs {
assert!(ty.compatible(&literal));
diff --git a/crates/iceberg/src/spec/geospatial.rs b/crates/iceberg/src/spec/geospatial.rs
new file mode 100644
index 000000000..df2ea5a2e
--- /dev/null
+++ b/crates/iceberg/src/spec/geospatial.rs
@@ -0,0 +1,215 @@
+// 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.
+
+//! Geospatial types defined by the Iceberg specification.
+
+use std::hash::{Hash, Hasher};
+
+use serde::{Deserialize, Serialize};
+
+use super::PrimitiveType;
+use crate::error::{Result, invalid_data};
+
+pub(crate) const DEFAULT_GEOSPATIAL_CRS: &str = "OGC:CRS84";
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct Crs(String);
+
+impl Crs {
+ fn new(crs: Option<String>) -> Result<Self> {
+ let crs = crs.unwrap_or_else(|| DEFAULT_GEOSPATIAL_CRS.to_string());
+ let crs = crs.trim().to_string();
+ if crs.is_empty() || crs.contains([',', ')']) {
+ return Err(crate::Error::new(
+ crate::ErrorKind::DataInvalid,
+ "Geospatial CRS must be non-empty and must not contain ',' or ')'",
+ ));
+ }
+
+ Ok(Self(crs))
+ }
+}
+
+impl Default for Crs {
+ fn default() -> Self {
+ Self(DEFAULT_GEOSPATIAL_CRS.to_string())
+ }
+}
+
+impl PartialEq for Crs {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.eq_ignore_ascii_case(&other.0)
+ }
+}
+
+impl Eq for Crs {}
+
+impl Hash for Crs {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ for byte in self.0.bytes() {
+ state.write_u8(byte.to_ascii_lowercase());
+ }
+ }
+}
+
+/// Iceberg geometry type.
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash, Default)]
+pub struct GeometryType {
+ crs: Crs,
+}
+
+impl GeometryType {
+ /// Creates a geometry type with an optional coordinate reference system.
+ pub fn new(crs: Option<String>) -> Result<Self> {
+ Ok(Self {
+ crs: Crs::new(crs)?,
+ })
+ }
+
+ /// Returns the coordinate reference system.
+ pub fn crs(&self) -> &str {
+ &self.crs.0
+ }
+}
+
+/// Iceberg geography edge interpolation algorithm.
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, Default)]
+#[serde(rename_all = "lowercase")]
+pub enum EdgeInterpolationAlgorithm {
+ /// Spherical edge interpolation.
+ #[default]
+ Spherical,
+ /// Vincenty edge interpolation.
+ Vincenty,
+ /// Thomas edge interpolation.
+ Thomas,
+ /// Andoyer edge interpolation.
+ Andoyer,
+ /// Karney edge interpolation.
+ Karney,
+}
+
+impl EdgeInterpolationAlgorithm {
+ pub(super) fn as_str(self) -> &'static str {
+ match self {
+ Self::Spherical => "spherical",
+ Self::Vincenty => "vincenty",
+ Self::Thomas => "thomas",
+ Self::Andoyer => "andoyer",
+ Self::Karney => "karney",
+ }
+ }
+
+ fn parse(value: &str) -> Result<Self> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "spherical" => Ok(Self::Spherical),
+ "vincenty" => Ok(Self::Vincenty),
+ "thomas" => Ok(Self::Thomas),
+ "andoyer" => Ok(Self::Andoyer),
+ "karney" => Ok(Self::Karney),
+ _ => Err(invalid_data!(
+ "Unknown geography edge interpolation algorithm: {value}"
+ )),
+ }
+ }
+}
+
+/// Iceberg geography type.
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
+pub struct GeographyType {
+ crs: Crs,
+ algorithm: EdgeInterpolationAlgorithm,
+}
+
+impl Default for GeographyType {
+ fn default() -> Self {
+ Self {
+ crs: Crs::default(),
+ algorithm: EdgeInterpolationAlgorithm::Spherical,
+ }
+ }
+}
+
+impl GeographyType {
+ /// Creates a geography type with an optional coordinate reference system and edge interpolation algorithm.
+ pub fn new(crs: Option<String>, algorithm: EdgeInterpolationAlgorithm) -> Result<Self> {
+ Ok(Self {
+ crs: Crs::new(crs)?,
+ algorithm,
+ })
+ }
+
+ /// Returns the coordinate reference system.
+ pub fn crs(&self) -> &str {
+ &self.crs.0
+ }
+
+ /// Returns the edge interpolation algorithm.
+ pub fn algorithm(&self) -> EdgeInterpolationAlgorithm {
+ self.algorithm
+ }
+}
+
+fn parse_geospatial_params<'a>(value: &'a str, type_name: &str) -> Result<Vec<&'a str>> {
+ if value.eq_ignore_ascii_case(type_name) {
+ return Ok(vec![]);
+ }
+
+ let params = value
+ .get(..type_name.len())
+ .filter(|prefix| prefix.eq_ignore_ascii_case(type_name))
+ .map(|_| &value[type_name.len()..])
+ .map(str::trim_start)
+ .and_then(|s| s.strip_prefix('('))
+ .and_then(|s| s.strip_suffix(')'))
+ .ok_or_else(|| invalid_data!("Invalid {type_name} type: {value}"))?;
+
+ if params.trim().is_empty() {
+ return Err(invalid_data!("{type_name} requires a non-empty CRS"));
+ }
+
+ Ok(params.split(',').map(str::trim).collect())
+}
+
+pub(super) fn parse_geometry(value: &str) -> Result<PrimitiveType> {
+ let params = parse_geospatial_params(value.trim(), "geometry")?;
+ let geometry = match params.as_slice() {
+ [] => GeometryType::default(),
+ [crs] if !crs.is_empty() => GeometryType::new(Some((*crs).to_string()))?,
+ _ => return Err(invalid_data!("Invalid geometry type: {value}")),
+ };
+
+ Ok(PrimitiveType::Geometry(geometry))
+}
+
+pub(super) fn parse_geography(value: &str) -> Result<PrimitiveType> {
+ let params = parse_geospatial_params(value.trim(), "geography")?;
+ let geography = match params.as_slice() {
+ [] => GeographyType::default(),
+ [crs] if !crs.is_empty() => GeographyType::new(
+ Some((*crs).to_string()),
+ EdgeInterpolationAlgorithm::Spherical,
+ )?,
+ [crs, algorithm] if !crs.is_empty() && !algorithm.is_empty() => GeographyType::new(
+ Some((*crs).to_string()),
+ EdgeInterpolationAlgorithm::parse(algorithm)?,
+ )?,
+ _ => return Err(invalid_data!("Invalid geography type: {value}")),
+ };
+
+ Ok(PrimitiveType::Geography(geography))
+}
diff --git a/crates/iceberg/src/spec/mod.rs b/crates/iceberg/src/spec/mod.rs
index 707ebbb63..7bbb37c79 100644
--- a/crates/iceberg/src/spec/mod.rs
+++ b/crates/iceberg/src/spec/mod.rs
@@ -19,6 +19,7 @@
mod datatypes;
mod encrypted_key;
+pub mod geospatial;
mod manifest;
mod manifest_list;
mod name_mapping;
@@ -39,6 +40,7 @@ mod view_version;
pub use datatypes::*;
pub use encrypted_key::*;
+pub use geospatial::{GeographyType, GeometryType};
pub use manifest::*;
pub use manifest_list::*;
pub use name_mapping::*;
diff --git a/crates/iceberg/src/spec/schema/mod.rs b/crates/iceberg/src/spec/schema/mod.rs
index d55d0889d..4d539b36c 100644
--- a/crates/iceberg/src/spec/schema/mod.rs
+++ b/crates/iceberg/src/spec/schema/mod.rs
@@ -662,7 +662,7 @@ mod tests {
#[test]
fn test_calc_min_compatible_format() {
- use crate::spec::{FormatVersion, VariantType};
+ use crate::spec::{FormatVersion, GeometryType, VariantType};
fn schema_with(fields: Vec<NestedFieldRef>) -> Schema {
Schema::builder().with_fields(fields).build().unwrap()
@@ -692,6 +692,22 @@ mod tests {
.is_err()
);
+ // Geospatial types are v3-only.
+ let geometry = schema_with(vec![
+ NestedField::optional(
+ 1,
+ "geom",
+ Primitive(PrimitiveType::Geometry(GeometryType::default())),
+ )
+ .into(),
+ ]);
+ assert_eq!(geometry.calc_min_compatible_format(), FormatVersion::V3);
+ assert!(
+ geometry
+ .check_format_compatibility(FormatVersion::V2)
+ .is_err()
+ );
+
// A v3-only type nested inside a list inside a struct → V3 (flattened fields).
let nested = schema_with(vec![
NestedField::required(
diff --git a/crates/iceberg/src/spec/sort.rs b/crates/iceberg/src/spec/sort.rs
index e40aaa65d..c1c9fdbad 100644
--- a/crates/iceberg/src/spec/sort.rs
+++ b/crates/iceberg/src/spec/sort.rs
@@ -27,7 +27,7 @@ use typed_builder::TypedBuilder;
use super::transform::Transform;
use crate::error::Result;
-use crate::spec::Schema;
+use crate::spec::{PrimitiveType, Schema, Type};
use crate::{Error, ErrorKind};
/// Reference to [`SortOrder`].
@@ -194,6 +194,19 @@ impl SortOrderBuilder {
));
}
+ if matches!(
+ source_type,
+ Type::Primitive(PrimitiveType::Geometry(_) | PrimitiveType::Geography(_))
+ ) {
+ return Err(Error::new(
+ ErrorKind::DataInvalid,
+ format!(
+ "Cannot sort by geospatial source field: {}",
+ source_field.name
+ ),
+ ));
+ }
+
let field_transform = sort_field.transform;
field_transform.result_type(source_type)?;
}
@@ -207,7 +220,7 @@ impl SortOrderBuilder {
#[cfg(test)]
mod tests {
use super::*;
- use crate::spec::{ListType, NestedField, PrimitiveType, Type};
+ use crate::spec::{GeometryType, ListType, NestedField, PrimitiveType, Type};
#[test]
fn test_sort_field() {
@@ -474,6 +487,40 @@ mod tests {
);
}
+ #[test]
+ fn test_build_should_reject_geospatial_source_fields() {
+ let schema = Schema::builder()
+ .with_schema_id(1)
+ .with_fields(vec![
+ NestedField::required(
+ 1,
+ "geom",
+ Type::Primitive(PrimitiveType::Geometry(GeometryType::default())),
+ )
+ .into(),
+ ])
+ .build()
+ .unwrap();
+
+ let err = SortOrder::builder()
+ .with_sort_field(
+ SortField::builder()
+ .source_id(1)
+ .direction(SortDirection::Ascending)
+ .null_order(NullOrder::First)
+ .transform(Transform::Void)
+ .build(),
+ )
+ .build(&schema)
+ .unwrap_err();
+
+ assert_eq!(err.kind(), ErrorKind::DataInvalid);
+ assert_eq!(
+ err.message(),
+ "Cannot sort by geospatial source field: geom"
+ );
+ }
+
#[test]
fn test_build_should_return_err_if_source_field_type_is_not_supported_by_transform() {
let schema = Schema::builder()
diff --git a/crates/iceberg/src/spec/transform.rs b/crates/iceberg/src/spec/transform.rs
index c28dc28c1..0d2b61e73 100644
--- a/crates/iceberg/src/spec/transform.rs
+++ b/crates/iceberg/src/spec/transform.rs
@@ -278,7 +278,15 @@ impl Transform {
pub fn result_type(&self, input_type: &Type) -> Result<Type> {
match self {
Transform::Identity => {
- if matches!(input_type, Type::Primitive(_)) {
+ if matches!(
+ input_type,
+ Type::Primitive(PrimitiveType::Geometry(_) | PrimitiveType::Geography(_))
+ ) {
+ Err(Error::new(
+ ErrorKind::DataInvalid,
+ format!("{input_type} is not a valid input type of identity transform",),
+ ))
+ } else if matches!(input_type, Type::Primitive(_)) {
Ok(input_type.clone())
} else {
Err(invalid_data!(
diff --git a/crates/iceberg/src/spec/values/datum.rs b/crates/iceberg/src/spec/values/datum.rs
index 2a7bd3f44..09c1e8831 100644
--- a/crates/iceberg/src/spec/values/datum.rs
+++ b/crates/iceberg/src/spec/values/datum.rs
@@ -269,6 +269,12 @@ impl PartialOrd for Datum {
PrimitiveType::Binary,
PrimitiveType::Binary,
) => val.partial_cmp(other_val),
+ (
+ PrimitiveLiteral::Binary(_),
+ PrimitiveLiteral::Binary(_),
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_),
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_),
+ ) => None,
(
PrimitiveLiteral::Int128(val),
PrimitiveLiteral::Int128(other_val),
@@ -378,6 +384,12 @@ 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)
diff --git a/crates/iceberg/src/spec/values/literal.rs b/crates/iceberg/src/spec/values/literal.rs
index 1d27ae995..52e4a5c73 100644
--- a/crates/iceberg/src/spec/values/literal.rs
+++ b/crates/iceberg/src/spec/values/literal.rs
@@ -32,7 +32,7 @@ use super::decimal_utils::{
use super::primitive::PrimitiveLiteral;
use super::struct_value::Struct;
use super::temporal::{date, time, timestamp, timestamptz};
-use crate::error::{Result, invalid_data};
+use crate::error::{Error, ErrorKind, Result, invalid_data};
use crate::spec::datatypes::{PrimitiveType, Type};
/// Values present in iceberg type
@@ -508,6 +508,13 @@ impl Literal {
(PrimitiveType::Binary, JsonValue::String(s)) => Ok(Some(Literal::Primitive(
PrimitiveLiteral::Binary(decode_hex_bytes(&s)?),
))),
+ (
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_),
+ JsonValue::String(_),
+ ) => Err(Error::new(
+ ErrorKind::FeatureUnsupported,
+ "WKT deserialization for geospatial types is not yet supported",
+ )),
(
PrimitiveType::Decimal {
precision: _,
@@ -676,6 +683,13 @@ impl Literal {
(PrimitiveType::Binary, PrimitiveLiteral::Binary(val)) => {
Ok(JsonValue::String(encode_hex_bytes(&val)))
}
+ (
+ PrimitiveType::Geometry(_) | PrimitiveType::Geography(_),
+ PrimitiveLiteral::Binary(_),
+ ) => Err(Error::new(
+ ErrorKind::FeatureUnsupported,
+ "WKT serialization for geospatial types is not yet supported",
+ )),
(_, PrimitiveLiteral::Int128(val)) => match r#type {
Type::Primitive(PrimitiveType::Decimal {
precision: _precision,
diff --git a/crates/iceberg/src/spec/values/tests.rs b/crates/iceberg/src/spec/values/tests.rs
index f21e78ef3..a6945015b 100644
--- a/crates/iceberg/src/spec/values/tests.rs
+++ b/crates/iceberg/src/spec/values/tests.rs
@@ -548,6 +548,65 @@ fn avro_bytes_string() {
check_avro_bytes_serde(bytes, Datum::string("iceberg"), &PrimitiveType::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}");
+ }
+}
+
+#[test]
+fn geospatial_wkt_json_is_not_yet_supported() {
+ for primitive_type in [
+ PrimitiveType::Geometry(Default::default()),
+ PrimitiveType::Geography(Default::default()),
+ ] {
+ let err = Literal::try_from_json(
+ serde_json::json!("POINT (1 2)"),
+ &Primitive(primitive_type.clone()),
+ )
+ .unwrap_err();
+
+ assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+ assert_eq!(
+ err.message(),
+ "WKT deserialization for geospatial types is not yet supported"
+ );
+
+ let err = Literal::Primitive(PrimitiveLiteral::Binary(vec![1, 2, 3]))
+ .try_into_json(&Primitive(primitive_type))
+ .unwrap_err();
+ assert_eq!(err.kind(), ErrorKind::FeatureUnsupported, "{err}");
+ assert_eq!(
+ err.message(),
+ "WKT serialization for geospatial types is not yet supported"
+ );
+ }
+}
+
+#[test]
+fn geospatial_datums_are_not_orderable() {
+ let geometry_type = PrimitiveType::Geometry(Default::default());
+ let geography_type = PrimitiveType::Geography(Default::default());
+ let geometry = Datum::new(
+ geometry_type.clone(),
+ PrimitiveLiteral::Binary(vec![1, 2, 3]),
+ );
+ let other_geometry = Datum::new(geometry_type, PrimitiveLiteral::Binary(vec![4, 5, 6]));
+ let geography = Datum::new(
+ geography_type.clone(),
+ PrimitiveLiteral::Binary(vec![1, 2, 3]),
+ );
+ let other_geography = Datum::new(geography_type, PrimitiveLiteral::Binary(vec![4, 5, 6]));
+
+ assert_eq!(geometry.partial_cmp(&other_geometry), None);
+ assert_eq!(geography.partial_cmp(&other_geography), None);
+}
+
#[test]
fn avro_bytes_decimal() {
// (input_bytes, decimal_num, expect_scale, expect_precision)
diff --git a/crates/iceberg/src/transform/bucket.rs b/crates/iceberg/src/transform/bucket.rs
index 128fb1728..33911eb07 100644
--- a/crates/iceberg/src/transform/bucket.rs
+++ b/crates/iceberg/src/transform/bucket.rs
@@ -297,12 +297,12 @@ mod test {
use crate::Result;
use crate::expr::PredicateOperator;
use crate::spec::PrimitiveType::{
- Binary, Date, Decimal, Fixed, Int, Long, String as StringType, Time, Timestamp,
+ Binary, Date, Decimal, Fixed, Geometry, Int, Long, String as StringType, Time, Timestamp,
TimestampNs, Timestamptz, TimestamptzNs, Uuid,
};
use crate::spec::Type::{Primitive, Struct};
use crate::spec::decimal_utils::decimal_new;
- use crate::spec::{Datum, NestedField, PrimitiveType, StructType, Transform};
+ use crate::spec::{Datum, GeometryType, NestedField, PrimitiveType, StructType, Transform};
use crate::transform::TransformFunction;
use crate::transform::test::{TestProjectionFixture, TestTransformFixture};
@@ -341,6 +341,7 @@ mod test {
(Primitive(Timestamptz), Some(Primitive(Int))),
(Primitive(TimestampNs), Some(Primitive(Int))),
(Primitive(TimestamptzNs), Some(Primitive(Int))),
+ (Primitive(Geometry(GeometryType::default())), None),
(
Struct(StructType::new(vec![
NestedField::optional(1, "a", Primitive(Timestamp)).into(),
diff --git a/crates/iceberg/src/transform/identity.rs b/crates/iceberg/src/transform/identity.rs
index dd096337a..d2d9cc476 100644
--- a/crates/iceberg/src/transform/identity.rs
+++ b/crates/iceberg/src/transform/identity.rs
@@ -37,11 +37,11 @@ impl TransformFunction for Identity {
#[cfg(test)]
mod test {
use crate::spec::PrimitiveType::{
- Binary, Date, Decimal, Fixed, Int, Long, String as StringType, Time, Timestamp,
+ Binary, Date, Decimal, Fixed, Geometry, Int, Long, String as StringType, Time, Timestamp,
TimestampNs, Timestamptz, TimestamptzNs, Uuid,
};
use crate::spec::Type::{Primitive, Struct};
- use crate::spec::{NestedField, StructType, Transform};
+ use crate::spec::{GeometryType, NestedField, StructType, Transform};
use crate::transform::test::TestTransformFixture;
#[test]
@@ -83,6 +83,7 @@ mod test {
(Primitive(Timestamptz), Some(Primitive(Timestamptz))),
(Primitive(TimestampNs), Some(Primitive(TimestampNs))),
(Primitive(TimestamptzNs), Some(Primitive(TimestamptzNs))),
+ (Primitive(Geometry(GeometryType::default())), None),
(
Struct(StructType::new(vec![
NestedField::optional(1, "a", Primitive(Timestamp)).into(),
diff --git a/crates/iceberg/src/transform/truncate.rs b/crates/iceberg/src/transform/truncate.rs
index 2bb7fdf22..923650298 100644
--- a/crates/iceberg/src/transform/truncate.rs
+++ b/crates/iceberg/src/transform/truncate.rs
@@ -194,11 +194,13 @@ mod test {
use crate::Result;
use crate::expr::PredicateOperator;
use crate::spec::PrimitiveType::{
- Binary, Date, Decimal, Fixed, Int, Long, String as StringType, Time, Timestamp,
+ Binary, Date, Decimal, Fixed, Geometry, Int, Long, String as StringType, Time, Timestamp,
TimestampNs, Timestamptz, TimestamptzNs, Uuid,
};
use crate::spec::Type::{Primitive, Struct};
- use crate::spec::{Datum, NestedField, PrimitiveLiteral, PrimitiveType, StructType, Transform};
+ use crate::spec::{
+ Datum, GeometryType, NestedField, PrimitiveLiteral, PrimitiveType, StructType, Transform,
+ };
use crate::transform::TransformFunction;
use crate::transform::test::{TestProjectionFixture, TestTransformFixture};
@@ -241,6 +243,7 @@ mod test {
(Primitive(Timestamptz), None),
(Primitive(TimestampNs), None),
(Primitive(TimestamptzNs), None),
+ (Primitive(Geometry(GeometryType::default())), None),
(
Struct(StructType::new(vec![
NestedField::optional(1, "a", Primitive(Timestamp)).into(),