From 7d94520c84c4a0f85c059c461477e5e14b9e7e62 Mon Sep 17 00:00:00 2001 From: Xin Huang Date: Thu, 9 Jul 2026 08:18:29 -0700 Subject: [PATCH] Core: Test geometry and geography metrics keep counts without bounds Reland of #17131 (reverted in #17141 for conflicting with the ParquetMetrics FieldStats refactor #17116). Rewritten on top of #17116's simplified metrics test API, which passes Metrics directly to assertCounts/assertBounds instead of the removed MetricsWithStats wrapper. TestMetrics pins per-type metrics behavior but had no geometry/geography coverage. Add testMetricsForGeospatialTypes asserting that geo columns keep value/null counts while intentionally producing no lower/upper bounds (lexicographic WKB min/max is not meaningful; spatial bounds are a separate follow-up). Geo values are only written by the Parquet path today, so the test is skipped for other formats. --- .../java/org/apache/iceberg/TestMetrics.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/src/test/java/org/apache/iceberg/TestMetrics.java b/core/src/test/java/org/apache/iceberg/TestMetrics.java index feffc184770e..095feb29382f 100644 --- a/core/src/test/java/org/apache/iceberg/TestMetrics.java +++ b/core/src/test/java/org/apache/iceberg/TestMetrics.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -257,6 +258,37 @@ public void testMetricsForTopLevelFields() throws IOException { } } + @TestTemplate + public void testMetricsForGeospatialTypes() throws IOException { + // Geometry and geography are only written by the Parquet path; other formats do not support + // the geo types yet. + assumeThat(fileFormat()).isEqualTo(FileFormat.PARQUET); + + Schema schema = + new Schema( + required(1, "id", LongType.get()), + optional(2, "geom", Types.GeometryType.crs84()), + optional(3, "geog", Types.GeographyType.crs84())); + + Record first = GenericRecord.create(schema); + first.setField("id", 1L); + first.setField("geom", wkbPoint(30, 10)); + first.setField("geog", wkbPoint(-5, 40)); + Record second = GenericRecord.create(schema); + second.setField("id", 2L); + // both geo columns are left null + + Metrics metrics = getMetrics(schema, first, second); + assertThat(metrics.recordCount()).isEqualTo(2L); + + // geometry and geography keep value/null counts but no bounds: lexicographic WKB min/max is not + // meaningful, so bounds are intentionally skipped (spatial bounds are a separate follow-up). + assertCounts(2, 2L, 1L, metrics); + assertBounds(2, Types.GeometryType.crs84(), null, null, metrics); + assertCounts(3, 2L, 1L, metrics); + assertBounds(3, Types.GeographyType.crs84(), null, null, metrics); + } + @TestTemplate public void testMetricsForDecimals() throws IOException { Schema schema = @@ -761,6 +793,18 @@ public void testMetricsForSortedNestedStructFields() throws IOException { assertBounds(5, LongType.get(), Long.MAX_VALUE, Long.MAX_VALUE, metrics); } + private static ByteBuffer wkbPoint(double xCoord, double yCoord) { + // little-endian WKB encoding of a point + return ByteBuffer.wrap( + ByteBuffer.allocate(21) + .order(ByteOrder.LITTLE_ENDIAN) + .put((byte) 1) // byte order: little endian + .putInt(1) // WKB geometry type: Point + .putDouble(xCoord) + .putDouble(yCoord) + .array()); + } + protected void assertCounts(int fieldId, Long valueCount, Long nullValueCount, Metrics metrics) { assertCounts(fieldId, valueCount, nullValueCount, null, metrics); }