Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/src/main/java/org/apache/iceberg/avro/TypeToSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ public Schema primitive(Type.PrimitiveType primitive) {
case BINARY:
primitiveSchema = BINARY_SCHEMA;
break;
case GEOMETRY:
case GEOGRAPHY:
// geometry and geography values are stored as WKB in an Avro bytes field
primitiveSchema = BINARY_SCHEMA;
break;
case DECIMAL:
Types.DecimalType decimal = (Types.DecimalType) primitive;
primitiveSchema =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ private static void assertEquals(Type type, Object expected, Object actual) {
case FIXED:
case BINARY:
case DECIMAL:
case GEOMETRY:
case GEOGRAPHY:
assertThat(actual).as("Primitive value should be equal to expected").isEqualTo(expected);
break;
case VARIANT:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public Object primitive(Type.PrimitiveType primitive) {
case FIXED:
return new GenericData.Fixed(typeToSchema.get(primitive), (byte[]) result);
case BINARY:
case GEOMETRY:
case GEOGRAPHY:
return ByteBuffer.wrap((byte[]) result);
case UUID:
return UUID.nameUUIDFromBytes((byte[]) result);
Expand Down
62 changes: 62 additions & 0 deletions core/src/test/java/org/apache/iceberg/avro/TestAvroDataWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Path;
import java.util.List;
import org.apache.iceberg.DataFile;
Expand Down Expand Up @@ -110,4 +112,64 @@ public void testDataWriter() throws IOException {

assertThat(writtenRecords).as("Written records should match").isEqualTo(records);
}

@Test
public void testGeospatialWkbRoundTrip() throws IOException {
Schema schema =
new Schema(
Types.NestedField.required(1, "id", Types.LongType.get()),
Types.NestedField.optional(2, "geom", Types.GeometryType.crs84()),
Types.NestedField.optional(3, "geog", Types.GeographyType.crs84()));

GenericRecord record = GenericRecord.create(schema);
List<Record> geoRecords =
ImmutableList.of(
record.copy(
ImmutableMap.of("id", 1L, "geom", wkbPoint(30, 10), "geog", wkbPoint(-5, 40))),
// geog is left null
record.copy(ImmutableMap.of("id", 2L, "geom", wkbPoint(0, 0))),
// both geo columns are left null
record.copy(ImmutableMap.of("id", 3L)));

OutputFile file = Files.localOutput(temp.toFile());
DataWriter<Record> dataWriter =
Avro.writeData(file)
.schema(schema)
.createWriterFunc(org.apache.iceberg.data.avro.DataWriter::create)
.overwrite()
.withSpec(PartitionSpec.unpartitioned())
.build();
try (dataWriter) {
for (Record geoRecord : geoRecords) {
dataWriter.write(geoRecord);
}
}

assertThat(dataWriter.toDataFile().recordCount()).isEqualTo(geoRecords.size());

List<Record> writtenRecords;
try (AvroIterable<Record> reader =
Avro.read(file.toInputFile())
.project(schema)
.createResolvingReader(PlannedDataReader::create)
.build()) {
writtenRecords = Lists.newArrayList(reader);
}

assertThat(writtenRecords)
.as("Geometry and geography WKB should round-trip through Avro")
.isEqualTo(geoRecords);
}

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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ protected boolean supportsVariant() {
return true;
}

@Override
protected boolean supportsGeospatial() {
return true;
}

@Override
protected void writeAndValidate(org.apache.iceberg.Schema schema) throws IOException {
List<GenericData.Record> expected = RandomAvroData.generate(schema, 100, 1990L);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ protected boolean supportsVariant() {
return true;
}

@Override
protected boolean supportsGeospatial() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

return true;
}

@Override
protected void writeAndValidate(Schema schema) throws IOException {
List<Record> expected = RandomAvroData.generate(schema, 100, 0L);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ protected boolean supportsVariant() {
return true;
}

@Override
protected boolean supportsGeospatial() {
return true;
}

@Override
protected void writeAndValidate(Schema schema) throws IOException {
List<Record> expected = RandomInternalData.generate(schema, 100, 42L);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,9 @@ protected boolean supportsTimestampNanos() {
protected boolean supportsVariant() {
return true;
}

@Override
protected boolean supportsGeospatial() {
return true;
}
}
33 changes: 33 additions & 0 deletions parquet/src/test/java/org/apache/iceberg/parquet/TestParquet.java
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,39 @@ public void testGeospatialFooterMetricsSkipParquetBounds() throws IOException {
}
}

@Test
public void testGeospatialWkbRoundTrip() throws IOException {
Schema schema =
new Schema(
optional(1, "geom", Types.GeometryType.crs84()),
optional(2, "geog", Types.GeographyType.crs84()));

ByteBuffer geomWkb = ByteBuffer.wrap(new byte[] {0x01, 0x02, 0x03});
ByteBuffer geogWkb = ByteBuffer.wrap(new byte[] {0x04, 0x05, 0x06});

org.apache.avro.Schema avroSchema = AvroSchemaUtil.convert(schema.asStruct());
GenericData.Record record = new GenericData.Record(avroSchema);
record.put("geom", geomWkb);
record.put("geog", geogWkb);
GenericData.Record nulls = new GenericData.Record(avroSchema);

File file = createTempFile(temp);
write(file, schema, Collections.emptyMap(), ParquetAvroWriter::buildWriter, record, nulls);

try (CloseableIterable<GenericData.Record> reader =
Parquet.read(Files.localInput(file))
.project(schema)
.createReaderFunc(fileSchema -> ParquetAvroValueReaders.buildReader(schema, fileSchema))
.build()) {
List<GenericData.Record> rows = Lists.newArrayList(reader);
assertThat(rows).hasSize(2);
assertThat(rows.get(0).get("geom")).isEqualTo(geomWkb);
assertThat(rows.get(0).get("geog")).isEqualTo(geogWkb);
assertThat(rows.get(1).get("geom")).isNull();
assertThat(rows.get(1).get("geog")).isNull();
}
}

@Test
public void testPerColumnDictionaryEncoding() throws Exception {
Schema schema =
Expand Down
Loading