From 5bc590c9698ca1bda96364fcc2f4b82528c29ba6 Mon Sep 17 00:00:00 2001 From: Xin Huang Date: Wed, 10 Jun 2026 17:09:32 -0700 Subject: [PATCH] API: Treat default edge algorithm as equal to an omitted one in GeographyType GeographyType used null to mean "default" for both crs and algorithm, but equals/hashCode/toString compared the raw nullable fields, so GeographyType.of(crs, EdgeAlgorithm.SPHERICAL) was not equal to the equivalent GeographyType.of(crs) even though SPHERICAL is the documented default. A plain geography therefore did not round-trip through format conversions that omit default values (e.g. Parquet, where an unset algorithm defaults to SPHERICAL). Normalize at the comparison/render boundary instead of in the constructor: equals, hashCode, and toString now use the resolved getters crs() / algorithm() (which already apply the defaults), so an explicit default and an omitted one compare equal and render the same. The constructor is unchanged. CRS already behaved this way; this brings the algorithm in line. Co-authored-by: Isaac --- .../java/org/apache/iceberg/types/Types.java | 9 ++++---- .../org/apache/iceberg/types/TestTypes.java | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/apache/iceberg/types/Types.java b/api/src/main/java/org/apache/iceberg/types/Types.java index ec6076b04fa0..24077b13e892 100644 --- a/api/src/main/java/org/apache/iceberg/types/Types.java +++ b/api/src/main/java/org/apache/iceberg/types/Types.java @@ -680,18 +680,19 @@ public boolean equals(Object o) { } GeographyType that = (GeographyType) o; - return Objects.equals(crs, that.crs) && Objects.equals(algorithm, that.algorithm); + // compare the resolved CRS and algorithm so an explicit default is equal to an omitted one + return crs().equals(that.crs()) && algorithm() == that.algorithm(); } @Override public int hashCode() { - return Objects.hash(GeographyType.class, crs, algorithm); + return Objects.hash(GeographyType.class, crs(), algorithm()); } @Override public String toString() { - if (algorithm != null) { - return String.format("geography(%s, %s)", crs != null ? crs : DEFAULT_CRS, algorithm); + if (algorithm() != DEFAULT_ALGORITHM) { + return String.format("geography(%s, %s)", crs(), algorithm()); } else if (crs != null) { return String.format("geography(%s)", crs); } else { diff --git a/api/src/test/java/org/apache/iceberg/types/TestTypes.java b/api/src/test/java/org/apache/iceberg/types/TestTypes.java index fa5ed4304d3c..9e6907210477 100644 --- a/api/src/test/java/org/apache/iceberg/types/TestTypes.java +++ b/api/src/test/java/org/apache/iceberg/types/TestTypes.java @@ -172,6 +172,27 @@ public void testGeospatialTypeToString() { .isEqualTo("geography(srid:4326, karney)"); assertThat(Types.GeographyType.of(null, EdgeAlgorithm.KARNEY).toString()) .isEqualTo("geography(OGC:CRS84, karney)"); + assertThat(Types.GeographyType.of("OGC:CRS84", EdgeAlgorithm.SPHERICAL).toString()) + .isEqualTo("geography"); + assertThat(Types.GeographyType.of("srid:4326", EdgeAlgorithm.SPHERICAL).toString()) + .isEqualTo("geography(srid:4326)"); + } + + @Test + public void testGeospatialTypeDefaultNormalization() { + // the default CRS and edge algorithm normalize so that equivalent type specs are equal + assertThat(Types.GeometryType.of("OGC:CRS84")).isEqualTo(Types.GeometryType.crs84()); + assertThat(Types.GeographyType.of("OGC:CRS84")).isEqualTo(Types.GeographyType.crs84()); + assertThat(Types.GeographyType.of("OGC:CRS84", EdgeAlgorithm.SPHERICAL)) + .isEqualTo(Types.GeographyType.crs84()) + .hasSameHashCodeAs(Types.GeographyType.crs84()); + assertThat(Types.GeographyType.of("srid:4326", EdgeAlgorithm.SPHERICAL)) + .isEqualTo(Types.GeographyType.of("srid:4326")) + .hasSameHashCodeAs(Types.GeographyType.of("srid:4326")); + assertThat(Types.GeographyType.of("srid:4326", EdgeAlgorithm.SPHERICAL).algorithm()) + .isEqualTo(EdgeAlgorithm.SPHERICAL); + assertThat(Types.GeographyType.of("srid:4326", EdgeAlgorithm.KARNEY)) + .isNotEqualTo(Types.GeographyType.of("srid:4326")); } @Test