-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Core: Fix geo bounds in v4 legacy stat maps #17493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
4bc33e8
d0c91c8
dfaaac0
c169efb
ac69057
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| import java.nio.ByteBuffer; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.apache.iceberg.geospatial.GeospatialBound; | ||
| import org.apache.iceberg.types.Conversions; | ||
| import org.apache.iceberg.types.Types; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
@@ -99,6 +100,132 @@ public void testUpperBounds() { | |
| Map.entry(3, Conversions.toByteBuffer(Types.DoubleType.get(), 9.0))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testGeoBoundsUseSinglePointEncoding() { | ||
| Schema geoSchema = | ||
| new Schema( | ||
| required(1, "id", Types.LongType.get()), | ||
| optional(10, "geom", Types.GeometryType.crs84()), | ||
| optional(11, "geog", Types.GeographyType.crs84())); | ||
| Types.StructType statsType = StatsUtil.statsReadSchema(geoSchema, List.of(1, 10, 11)); | ||
|
|
||
| ContentStatsStruct stats = new ContentStatsStruct(statsType); | ||
| stats.setStats( | ||
| 1, | ||
| StatsTestUtil.mockFieldStats( | ||
| statsType.field("id").type().asStructType(), 1, 1L, 5L, 26L, null, null)); | ||
| stats.setStats( | ||
| 10, | ||
| StatsTestUtil.mockFieldStats( | ||
| statsType.field("geom").type().asStructType(), | ||
| 10, | ||
| TestHelpers.Row.of(1.0, 2.0, null, null), | ||
| TestHelpers.Row.of(5.0, 6.0, null, null), | ||
| 26L, | ||
| 2L, | ||
| null)); | ||
| stats.setStats( | ||
| 11, | ||
| StatsTestUtil.mockFieldStats( | ||
| statsType.field("geog").type().asStructType(), | ||
| 11, | ||
| TestHelpers.Row.of(-1.0, -2.0, 3.0, 4.0), | ||
| TestHelpers.Row.of(7.0, 8.0, 9.0, 10.0), | ||
| 26L, | ||
| 0L, | ||
| null)); | ||
|
|
||
| // geometry and geography bounds are stored as bounding-box structs (x, y, z, m) but must be | ||
| // presented in the legacy maps using the spec's single-point encoding | ||
| Map<Integer, ByteBuffer> lower = ContentStatsBackedMap.lowerBounds(stats); | ||
| assertThat(lower) | ||
| .containsOnly( | ||
| Map.entry(1, Conversions.toByteBuffer(Types.LongType.get(), 1L)), | ||
| Map.entry(10, GeospatialBound.createXY(1.0, 2.0).toByteBuffer()), | ||
| Map.entry(11, GeospatialBound.createXYZM(-1.0, -2.0, 3.0, 4.0).toByteBuffer())); | ||
|
|
||
| Map<Integer, ByteBuffer> upper = ContentStatsBackedMap.upperBounds(stats); | ||
| assertThat(upper) | ||
| .containsOnly( | ||
| Map.entry(1, Conversions.toByteBuffer(Types.LongType.get(), 5L)), | ||
| Map.entry(10, GeospatialBound.createXY(5.0, 6.0).toByteBuffer()), | ||
| Map.entry(11, GeospatialBound.createXYZM(7.0, 8.0, 9.0, 10.0).toByteBuffer())); | ||
|
|
||
| // the encoding must round-trip through the geo conversion used by legacy readers | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Conversions.fromByteBuffer round-trip assertion covers only the XY (geometry lower) and XYZM (geography upper) variants; the XYZ (24-byte) and XYM (32-byte, NaN z-slot) encodings are asserted only at raw ByteBuffer byte-equality via .isEqualTo(GeospatialBound.createXYZ(...).toByteBuffer()), not round-tripped back through Conversions.fromByteBuffer. GeospatialBound.toByteBuffer/fromByteBuffer are separately tested so the risk is low, but XYM is the encoding most likely to harbor a NaN-slot/endianness edge case and is the exact integration point the fix depends on; adding fromByteBuffer round-trips for XYZ and XYM would close the integration coverage gap. This goes for testGeoBoundsUseSinglePointEncoding, testGeoBoundWithZOnlyAndMOnly, etc.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also cc @szehon-ho |
||
| GeospatialBound geomLower = | ||
| Conversions.fromByteBuffer(Types.GeometryType.crs84(), lower.get(10)); | ||
| assertThat(geomLower).isEqualTo(GeospatialBound.createXY(1.0, 2.0)); | ||
| GeospatialBound geogUpper = | ||
| Conversions.fromByteBuffer(Types.GeographyType.crs84(), upper.get(11)); | ||
| assertThat(geogUpper).isEqualTo(GeospatialBound.createXYZM(7.0, 8.0, 9.0, 10.0)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testGeoBoundWithZOnlyAndMOnly() { | ||
| Schema geoSchema = new Schema(optional(10, "geom", Types.GeometryType.crs84())); | ||
| Types.StructType statsType = StatsUtil.statsReadSchema(geoSchema, List.of(10)); | ||
|
|
||
| ContentStatsStruct stats = new ContentStatsStruct(statsType); | ||
| stats.setStats( | ||
| 10, | ||
| StatsTestUtil.mockFieldStats( | ||
| statsType.field("geom").type().asStructType(), | ||
| 10, | ||
| TestHelpers.Row.of(1.0, 2.0, 3.0, null), | ||
| TestHelpers.Row.of(5.0, 6.0, null, 7.0), | ||
| 26L, | ||
| 2L, | ||
| null)); | ||
|
|
||
| Map<Integer, ByteBuffer> lower = ContentStatsBackedMap.lowerBounds(stats); | ||
| assertThat(lower.get(10)).isEqualTo(GeospatialBound.createXYZ(1.0, 2.0, 3.0).toByteBuffer()); | ||
|
|
||
| Map<Integer, ByteBuffer> upper = ContentStatsBackedMap.upperBounds(stats); | ||
| assertThat(upper.get(10)).isEqualTo(GeospatialBound.createXYM(5.0, 6.0, 7.0).toByteBuffer()); | ||
|
|
||
| // the encoding must round-trip through the geo conversion used by legacy readers | ||
| GeospatialBound geomLower = | ||
| Conversions.fromByteBuffer(Types.GeometryType.crs84(), lower.get(10)); | ||
| assertThat(geomLower).isEqualTo(GeospatialBound.createXYZ(1.0, 2.0, 3.0)); | ||
| GeospatialBound geomUpper = | ||
| Conversions.fromByteBuffer(Types.GeometryType.crs84(), upper.get(10)); | ||
| assertThat(geomUpper).isEqualTo(GeospatialBound.createXYM(5.0, 6.0, 7.0)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testGeoFieldWithoutLowerBoundIsAbsentFromView() { | ||
| Schema geoSchema = | ||
| new Schema( | ||
| required(1, "id", Types.LongType.get()), | ||
| optional(10, "geom", Types.GeometryType.crs84())); | ||
| Types.StructType statsType = StatsUtil.statsReadSchema(geoSchema, List.of(1, 10)); | ||
|
|
||
| ContentStatsStruct stats = new ContentStatsStruct(statsType); | ||
| stats.setStats( | ||
| 1, | ||
| StatsTestUtil.mockFieldStats( | ||
| statsType.field("id").type().asStructType(), 1, 1L, 5L, 26L, null, null)); | ||
| stats.setStats( | ||
| 10, | ||
| StatsTestUtil.mockFieldStats( | ||
| statsType.field("geom").type().asStructType(), | ||
| 10, | ||
| null, | ||
| TestHelpers.Row.of(5.0, 6.0, null, null), | ||
| 26L, | ||
| 2L, | ||
| null)); | ||
|
|
||
| Map<Integer, ByteBuffer> lower = ContentStatsBackedMap.lowerBounds(stats); | ||
| assertThat(lower.get(10)).isNull(); | ||
| assertThat(lower.containsKey(10)).isFalse(); | ||
| assertThat(lower) | ||
| .containsOnly(Map.entry(1, Conversions.toByteBuffer(Types.LongType.get(), 1L))); | ||
|
|
||
| Map<Integer, ByteBuffer> upper = ContentStatsBackedMap.upperBounds(stats); | ||
| assertThat(upper.get(10)).isEqualTo(GeospatialBound.createXY(5.0, 6.0).toByteBuffer()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testGetReturnsNullForMissingKey() { | ||
| Map<Integer, Long> map = ContentStatsBackedMap.valueCounts(POPULATED_STATS); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: rather than treating any struct bound as geo via
isStructType(), can we check that this is a geometry/geography bound explicitly? That works today only becauseStatsUtiluses struct bounds solely for geo types, and an explicit check would avoid silently taking this path if another type ever gains struct bounds.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review @szehon-ho, Added StatsUtil.isGeoBoundType which checks the bounding-box shape explicitly with a direct test in TestStatsUtil.
So, a non-geo struct bound now falls through to Conversions.toByteBuffer and fails loudly instead of silently encoding as geo.