This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new a398e4e533 [iceberg] Exclude NaN from manifest partition bounds (#9188)
a398e4e533 is described below
commit a398e4e5332b6a41cab5743019e41ea4f556a3b0
Author: Arnav Balyan <[email protected]>
AuthorDate: Thu Aug 13 19:29:57 2026 +0530
[iceberg] Exclude NaN from manifest partition bounds (#9188)
---
.../iceberg/manifest/IcebergManifestFile.java | 55 ++++++++++++------
.../paimon/iceberg/IcebergCompatibilityTest.java | 67 +++++++++++++++-------
2 files changed, 81 insertions(+), 41 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergManifestFile.java
b/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergManifestFile.java
index 201b389570..f2f11a697c 100644
---
a/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergManifestFile.java
+++
b/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergManifestFile.java
@@ -20,6 +20,7 @@ package org.apache.paimon.iceberg.manifest;
import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.serializer.Serializer;
import org.apache.paimon.format.FileFormat;
import org.apache.paimon.format.FormatReaderFactory;
import org.apache.paimon.format.FormatWriterFactory;
@@ -36,6 +37,8 @@ import org.apache.paimon.io.SingleFileWriter;
import org.apache.paimon.manifest.ManifestEntry;
import org.apache.paimon.options.MemorySize;
import org.apache.paimon.options.Options;
+import org.apache.paimon.statistics.FullSimpleColStatsCollector;
+import org.apache.paimon.statistics.SimpleColStatsCollector;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.RowType;
@@ -194,6 +197,7 @@ public class IcebergManifestFile extends
ObjectsFile<IcebergManifestEntry> {
extends SingleFileWriter<IcebergManifestEntry,
IcebergManifestFileMeta> {
private final SimpleStatsCollector partitionStatsCollector;
+ private final IcebergPartitionStatsCollector[]
partitionStatsCollectors;
private final long sequenceNumber;
private int addedFilesCount = 0;
@@ -219,7 +223,21 @@ public class IcebergManifestFile extends
ObjectsFile<IcebergManifestEntry> {
serializer::toRow,
fileCompression,
false);
- this.partitionStatsCollector = new
SimpleStatsCollector(partitionType);
+ this.partitionStatsCollectors =
+ new
IcebergPartitionStatsCollector[partitionType.getFieldCount()];
+ SimpleColStatsCollector.Factory[] statsFactories =
+ new
SimpleColStatsCollector.Factory[partitionType.getFieldCount()];
+ for (int i = 0; i < partitionType.getFieldCount(); i++) {
+ int position = i;
+ statsFactories[i] =
+ () -> {
+ IcebergPartitionStatsCollector collector =
+ new IcebergPartitionStatsCollector();
+ partitionStatsCollectors[position] = collector;
+ return collector;
+ };
+ }
+ this.partitionStatsCollector = new
SimpleStatsCollector(partitionType, statsFactories);
this.sequenceNumber = sequenceNumber;
this.content = content;
}
@@ -257,22 +275,12 @@ public class IcebergManifestFile extends
ObjectsFile<IcebergManifestEntry> {
for (int i = 0; i < stats.length; i++) {
SimpleColStats fieldStats = stats[i];
DataType type = partitionType.getTypeAt(i);
- boolean containsNan = false;
- switch (type.getTypeRoot()) {
- case FLOAT:
- case DOUBLE:
- containsNan = isNaN(fieldStats.min()) ||
isNaN(fieldStats.max());
- break;
- default:
- // contains_nan is only meaningful for FLOAT/DOUBLE
per the Iceberg spec
- }
- // an unknown bound must be omitted, not published as a value
Object min = fieldStats.min();
Object max = fieldStats.max();
partitionSummaries.add(
new IcebergPartitionSummary(
Objects.requireNonNull(fieldStats.nullCount())
> 0,
- containsNan,
+ partitionStatsCollectors[i].containsNan(),
min == null ? null : toByteBuffer(type,
min).array(),
max == null ? null : toByteBuffer(type,
max).array()));
}
@@ -292,15 +300,24 @@ public class IcebergManifestFile extends
ObjectsFile<IcebergManifestEntry> {
deletedRowsCount,
partitionSummaries);
}
+ }
- private boolean isNaN(@Nullable Object value) {
- if (value instanceof Float) {
- return Float.isNaN((Float) value);
- }
- if (value instanceof Double) {
- return Double.isNaN((Double) value);
+ private static class IcebergPartitionStatsCollector extends
FullSimpleColStatsCollector {
+
+ private boolean containsNan;
+
+ @Override
+ public void collect(Object field, Serializer<Object> fieldSerializer) {
+ if ((field instanceof Float && Float.isNaN((Float) field))
+ || (field instanceof Double && Double.isNaN((Double)
field))) {
+ containsNan = true;
+ return;
}
- return false;
+ super.collect(field, fieldSerializer);
+ }
+
+ private boolean containsNan() {
+ return containsNan;
}
}
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
b/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
index 6c6ceed190..6d1d9fc886 100644
---
a/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
@@ -82,6 +82,7 @@ import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -1344,31 +1345,27 @@ public class IcebergCompatibilityTest {
write.write(GenericRow.of(2.0, 200), 1);
write.write(GenericRow.of(Double.NaN, 300), 1);
commit.commit(1, write.prepareCommit(false, 1));
- write.close();
- commit.close();
- FileIO fileIO = table.fileIO();
- IcebergMetadata metadata =
- IcebergMetadata.fromPath(
- fileIO, new Path(table.location(),
"metadata/v1.metadata.json"));
-
- String currentSnapshotManifest =
metadata.currentSnapshot().manifestList();
- File snapShotAvroFile = new File(currentSnapshotManifest);
+ assertThat(readPartitionSummaries(table, 1))
+ .anySatisfy(
+ summary -> {
+
assertThat(summary.get("contains_nan")).isEqualTo(true);
+ assertThat(readDoubleBound(summary,
"lower_bound")).isEqualTo(1.0);
+ assertThat(readDoubleBound(summary,
"upper_bound")).isEqualTo(2.0);
+ });
- boolean sawNanPartitionSummary = false;
- try (DataFileReader<GenericRecord> dataFileReader =
- new DataFileReader<>(
- new SeekableFileInput(snapShotAvroFile), new
GenericDatumReader<>())) {
- while (dataFileReader.hasNext()) {
- GenericRecord record = dataFileReader.next();
- String partitionSummary = record.get("partitions").toString();
- if (partitionSummary.contains("contains_nan\": true")) {
- sawNanPartitionSummary = true;
- }
- }
- }
+ write.write(GenericRow.of(Double.NaN, 400), 1);
+ commit.commit(2, write.prepareCommit(false, 2));
+ write.close();
+ commit.close();
- assertThat(sawNanPartitionSummary).isTrue();
+ assertThat(readPartitionSummaries(table, 2))
+ .anySatisfy(
+ summary -> {
+
assertThat(summary.get("contains_nan")).isEqualTo(true);
+ assertThat(summary.get("lower_bound")).isNull();
+ assertThat(summary.get("upper_bound")).isNull();
+ });
}
@Test
@@ -2650,6 +2647,32 @@ public class IcebergCompatibilityTest {
icebergTable -> IcebergGenerics.read(icebergTable).build(),
Record::toString);
}
+ private List<GenericRecord> readPartitionSummaries(FileStoreTable table,
long version)
+ throws Exception {
+ IcebergMetadata metadata =
+ IcebergMetadata.fromPath(
+ table.fileIO(),
+ new Path(table.location(), "metadata/v" + version +
".metadata.json"));
+ List<GenericRecord> partitionSummaries = new ArrayList<>();
+ try (DataFileReader<GenericRecord> dataFileReader =
+ new DataFileReader<>(
+ new SeekableFileInput(new
File(metadata.currentSnapshot().manifestList())),
+ new GenericDatumReader<>())) {
+ while (dataFileReader.hasNext()) {
+ GenericRecord record = dataFileReader.next();
+ partitionSummaries.add((GenericRecord) ((List<?>)
record.get("partitions")).get(0));
+ }
+ }
+ return partitionSummaries;
+ }
+
+ private double readDoubleBound(GenericRecord partitionSummary, String
field) {
+ return ((ByteBuffer) partitionSummary.get(field))
+ .duplicate()
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .getDouble();
+ }
+
private org.apache.iceberg.Table getIcebergTable() {
HadoopCatalog icebergCatalog = new HadoopCatalog(new Configuration(),
tempDir.toString());
TableIdentifier icebergIdentifier = TableIdentifier.of("mydb.db", "t");