Core: Read and write geometry and geography values in Avro#17119
Core: Read and write geometry and geography values in Avro#17119huan233usc wants to merge 2 commits into
Conversation
| } | ||
|
|
||
| @Override | ||
| protected boolean supportsGeospatial() { |
There was a problem hiding this comment.
TestAvroEncoderUtil extends DataTestBase and round-trips generic records through this same TypeToSchema + RandomAvroData path via AvroEncoderUtil.encode/decode, and on main it mirrors this test's type flags exactly (supportsUnknown, supportsTimestampNanos, supportsVariant). Enabling supportsGeospatial() there as well covers geo through the encoder entry point for free and keeps the two generic-model Avro tests in parity. szehon-ho raised the same coverage-parity point on the sibling Parquet PR #16982.
There was a problem hiding this comment.
Done — enabled supportsGeospatial() on TestAvroEncoderUtil in 67619a7, so geo now round-trips through the AvroEncoderUtil.encode/decode entry point too, keeping the two generic-model Avro tests in parity (good call on the #16982 coverage-parity precedent).
I also added explicit WKB round-trip tests while here:
- TestAvroDataWriter.testGeospatialWkbRoundTrip — geometry/geography through the Avro DataWriter → PlannedDataReader path, including nulls.
- TestParquet.testGeospatialWkbRoundTrip — through ParquetAvroWriter → ParquetAvroValueReaders,
500ea8d to
7b2cc6d
Compare
TypeToSchema mapped Iceberg types to Avro schema for every primitive except geometry and geography, so converting a schema with a geo column threw UnsupportedOperationException before any value could be read or written. Map both to an Avro bytes field carrying WKB, per the Avro type mapping in the spec (binary, geometry, and geography all use bytes). The value read/write paths dispatch on the Avro physical type, so once the schema is bytes the existing byteBuffers reader/writer handle geo unchanged in both the generic and internal object models (geo values are WKB ByteBuffers, like binary). Enable the shared DataTest geospatial coverage for the Avro object models (TestGenericAvro, TestInternalAvro, generic data TestGenericData) and add the GEOMETRY/GEOGRAPHY -> ByteBuffer handling to RandomAvroData and AvroTestHelpers that the generic Avro path needs, mirroring RandomInternalData/InternalTestHelpers.
7b2cc6d to
67619a7
Compare
| .isEqualTo(geoRecords); | ||
| } | ||
|
|
||
| private static ByteBuffer wkbPoint(double xCoord, double yCoord) { |
There was a problem hiding this comment.
RandomUtil.wkbPoint(double, double) in api/src/test/java/org/apache/iceberg/util/RandomUtil.java already encodes a little-endian WKB point, and it is on core's test classpath (RandomAvroData imports it). szehon-ho raised this exact duplication on the sibling Parquet PR #16982, and the merged TestParquetDataWriter.wkbPoint delegates: return ByteBuffer.wrap(RandomUtil.wkbPoint(xCoord, yCoord));. Doing the same here keeps WKB encoding in one place and drops the new java.nio.ByteOrder import.
| } | ||
|
|
||
| @Test | ||
| public void testGeospatialWkbRoundTrip() throws IOException { |
There was a problem hiding this comment.
The test plan in the PR body still describes the previous commit: it lists only the supportsGeospatial() flips plus the RandomAvroData/AvroTestHelpers cases, and does not mention TestAvroEncoderUtil, TestAvroDataWriter.testGeospatialWkbRoundTrip, or this test - so a PR titled Core: silently adds parquet-module coverage. Worth saying in the description (it becomes the squash commit message) that this closes the parquet-avro object model gap (ParquetAvroWriter / ParquetAvroValueReaders), which was unreachable for geo because AvroSchemaUtil.convert threw on geo types - the follow-up szehon-ho asked about on #16982.
There was a problem hiding this comment.
Updated the PR description: the test plan now lists TestAvroEncoderUtil, both testGeospatialWkbRoundTrip tests, and a Summary paragraph noting this closes the parquet-avro object model gap (ParquetAvroWriter/ParquetAvroValueReaders, unreachable for geo because AvroSchemaUtil.convert routed into the TypeToSchema that threw) — the #16982 follow-up. It'll carry into the squash message.
| optional(1, "geom", Types.GeometryType.crs84()), | ||
| optional(2, "geog", Types.GeographyType.crs84())); | ||
|
|
||
| ByteBuffer geomWkb = ByteBuffer.wrap(new byte[] {0x01, 0x02, 0x03}); |
There was a problem hiding this comment.
These bytes are not WKB. That is harmless in testGeospatialFooterMetricsSkipParquetBounds, which writes them through a Types.BinaryType schema, but this test writes them through the geometry/geography schema, so TypeToMessageType annotates the column with LogicalTypeAnnotation.geometryType(crs). Parquet then attaches a real GeospatialStatistics.Builder to that column (ColumnValueCollector), and its update(Binary) runs JTS WKBReader.read and catches ParseException with a "Failed to parse WKB geometry, omit it from stats" warning. So the test passes while writing a file whose geometry values are unparseable and whose geospatial statistics are empty. RandomUtil.wkbPoint(...) produces real WKB here too and makes the test name accurate.
There was a problem hiding this comment.
Great catch — fixed in bcbef59. testGeospatialWkbRoundTrip now uses RandomUtil.wkbPoint(30, 10) / wkbPoint(-5, 40), so the values are real WKB that the geospatial statistics builder parses successfully instead of silently omitting. The bytes in testGeospatialFooterMetricsSkipParquetBounds are left as-is since that test writes through a BinaryType schema (no WKB parsing), as you noted.
…l.wkbPoint TestParquet.testGeospatialWkbRoundTrip wrote arbitrary bytes through the geometry/geography schema, which carries a Parquet geospatial logical type: the writer parses each value with a WKB reader to build geospatial statistics, so the bogus bytes were silently dropped from stats and the round-trip never exercised a real WKB value. Use RandomUtil.wkbPoint for genuine WKB. Also delegate TestAvroDataWriter's local wkbPoint helper to RandomUtil.wkbPoint (already on the test classpath) to keep WKB encoding in one place, matching TestParquetDataWriter.
|
Thanks for the careful review @wombatu-kun! Addressed in bcbef59:
|
Summary
TypeToSchemamapped every Iceberg primitive to an Avro schema exceptgeometryandgeography,so converting a schema with a geo column threw
UnsupportedOperationException: Unsupported type ID: GEOMETRYbefore any value could be read or written through the Avro object model. This wires up thevalue path.
Per the Avro type mapping in the spec,
geometryandgeographyare stored as an Avrobytesfieldcarrying WKB — the same representation as
binary:binarybytesgeometrybytes(WKB)geographybytes(WKB)So
TypeToSchema.primitive()now maps both geo types to the existingBINARY_SCHEMA. The valueread/write paths dispatch on the Avro physical type, so once the schema is
bytesthe existingbyteBuffersreader/writer handle geo unchanged in both the generic and internal object models (geovalues are WKB
ByteBuffers, exactly likebinary) — no value-path code changes are needed.This also closes the parquet-avro object model gap:
ParquetAvroWriter/ParquetAvroValueReadersbuild their Avro schema through
AvroSchemaUtil.convert, which routed into the sameTypeToSchemathat threw on geo, so geo was unreachable through that path too. Fixing
TypeToSchemamakes it worktransitively, with no parquet-avro code change. This is the follow-up szehon-ho asked about on #16982
(which added the Parquet WKB value path and deliberately left the Avro object model as a follow-up).
Note on the reverse direction
SchemaToTypeintentionally still maps Avrobytes → binary. Plain Avrobytescarries no marker todistinguish geometry/geography from binary, and Iceberg resolves the real column type from the
expected/table schema, never by reverse-inferring from the file's Avro schema — the same way
binaryand other
bytes-backed types already behave.Test plan
Object-model coverage via the shared
DataTestgeo cases and the Avro encoder/writer entry points:supportsGeospatial()on the Avro object-model tests —TestGenericAvro,TestInternalAvro,the generic-data
TestGenericData, andTestAvroEncoderUtil(so geo also round-trips through theAvroEncoderUtil.encode/decodeentry point, keeping the two generic-model Avro tests in parity).This round-trips geometry and geography across multiple CRS and edge algorithms with randomly
generated WKB values.
GEOMETRY/GEOGRAPHY→ByteBufferhandling the generic Avro path needs inRandomAvroDataand
AvroTestHelpers, mirroring the existingRandomInternalData/InternalTestHelpershandling.TestAvroDataWriter.testGeospatialWkbRoundTrip— geometry/geography through the AvroDataWriter→
PlannedDataReaderpath, including nulls.TestParquet.testGeospatialWkbRoundTrip— throughParquetAvroWriter→ParquetAvroValueReaders,covering the parquet-avro object model, including nulls.
./gradlew :iceberg-core:test --tests org.apache.iceberg.avro.TestGenericAvro --tests org.apache.iceberg.avro.TestInternalAvro --tests org.apache.iceberg.avro.TestAvroEncoderUtil --tests org.apache.iceberg.avro.TestAvroDataWriter./gradlew :iceberg-data:test --tests org.apache.iceberg.data.avro.TestGenericData./gradlew :iceberg-parquet:test --tests org.apache.iceberg.parquet.TestParquet