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 87de4a722e [iceberg] Omit unknown bounds in Iceberg manifest partition
summary (#9152)
87de4a722e is described below
commit 87de4a722ec0053d1c40051af58e7daac15f3817
Author: Eunbin Son <[email protected]>
AuthorDate: Wed Aug 12 12:29:42 2026 +0900
[iceberg] Omit unknown bounds in Iceberg manifest partition summary (#9152)
---
.../paimon/iceberg/IcebergCommitCallback.java | 13 +++--
.../iceberg/manifest/IcebergManifestFile.java | 7 ++-
.../paimon/iceberg/IcebergCompatibilityTest.java | 67 ++++++++++++++++++++++
3 files changed, 81 insertions(+), 6 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
b/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
index 54180fe3e0..cfeb6acb1c 100644
---
a/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
+++
b/paimon-core/src/main/java/org/apache/paimon/iceberg/IcebergCommitCallback.java
@@ -1338,10 +1338,15 @@ public class IcebergCommitCallback implements
CommitCallback, TagCallback {
for (int i = 0; i < numFields; i++) {
IcebergPartitionSummary summary = fileMeta.partitions().get(i);
DataType fieldType = partitionType.getTypeAt(i);
- minValues.setField(
- i, IcebergConversions.toPaimonObject(fieldType,
summary.lowerBound()));
- maxValues.setField(
- i, IcebergConversions.toPaimonObject(fieldType,
summary.upperBound()));
+ // an omitted bound means the value is unknown; keep the slot
null
+ byte[] lowerBound = summary.lowerBound();
+ byte[] upperBound = summary.upperBound();
+ if (lowerBound != null) {
+ minValues.setField(i,
IcebergConversions.toPaimonObject(fieldType, lowerBound));
+ }
+ if (upperBound != null) {
+ maxValues.setField(i,
IcebergConversions.toPaimonObject(fieldType, upperBound));
+ }
// IcebergPartitionSummary only has `containsNull` field and
does not have the
// exact number of nulls.
nullCounts[i] = summary.containsNull() ? 1 : 0;
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 d5abb480a5..201b389570 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
@@ -266,12 +266,15 @@ public class IcebergManifestFile extends
ObjectsFile<IcebergManifestEntry> {
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,
- toByteBuffer(type, fieldStats.min()).array(),
- toByteBuffer(type, fieldStats.max()).array()));
+ min == null ? null : toByteBuffer(type,
min).array(),
+ max == null ? null : toByteBuffer(type,
max).array()));
}
return new IcebergManifestFileMeta(
path.toString(),
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 d8a22aaee4..6c6ceed190 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
@@ -1371,6 +1371,73 @@ public class IcebergCompatibilityTest {
assertThat(sawNanPartitionSummary).isTrue();
}
+ @Test
+ public void testNullPartitionValue() throws Exception {
+ RowType rowType =
+ RowType.of(
+ new DataType[] {DataTypes.VARCHAR(10),
DataTypes.INT()},
+ new String[] {"pt", "v"});
+ FileStoreTable table =
+ createPaimonTable(
+ rowType, Collections.singletonList("pt"),
Collections.emptyList(), -1);
+
+ String commitUser = UUID.randomUUID().toString();
+ TableWriteImpl<?> write = table.newWrite(commitUser);
+ TableCommitImpl commit = table.newCommit(commitUser);
+
+ write.write(GenericRow.of(BinaryString.fromString("a"), 1), 1);
+ commit.commit(1, write.prepareCommit(false, 1));
+
+ // every entry of this manifest has a null partition value, so its
partition
+ // statistics are unknown and the summary must omit both bounds
+ write.write(GenericRow.of(null, 2), 1);
+ commit.commit(2, write.prepareCommit(false, 2));
+
+ FileIO fileIO = table.fileIO();
+ IcebergMetadata metadata =
+ IcebergMetadata.fromPath(
+ fileIO, new Path(table.location(),
"metadata/v2.metadata.json"));
+ List<String> partitionSummaries = new ArrayList<>();
+ try (DataFileReader<GenericRecord> dataFileReader =
+ new DataFileReader<>(
+ new SeekableFileInput(new
File(metadata.currentSnapshot().manifestList())),
+ new GenericDatumReader<>())) {
+ while (dataFileReader.hasNext()) {
+
partitionSummaries.add(dataFileReader.next().get("partitions").toString());
+ }
+ }
+ assertThat(partitionSummaries)
+ .anySatisfy(
+ summary ->
+ assertThat(summary)
+ .contains("\"contains_null\": true")
+ .contains("\"lower_bound\": null")
+ .contains("\"upper_bound\": null"));
+ // known bounds are still recorded for non-null partition values
+ assertThat(partitionSummaries)
+ .anySatisfy(
+ summary ->
+ assertThat(summary)
+ .contains("\"lower_bound\": \"a\"")
+ .contains("\"upper_bound\": \"a\""));
+
+ write.write(GenericRow.of(BinaryString.fromString("b"), 3), 1);
+ commit.commit(3, write.prepareCommit(false, 3));
+
+ assertThat(getIcebergResult())
+ .containsExactlyInAnyOrder("Record(a, 1)", "Record(null, 2)",
"Record(b, 3)");
+
+ // a non add-only commit reads the omitted bounds back from the base
manifests
+ Map<String, String> partition = new HashMap<>();
+ partition.put("pt", "a");
+ commit.truncatePartitions(Collections.singletonList(partition));
+
+ assertThat(getIcebergResult()).containsExactlyInAnyOrder("Record(null,
2)", "Record(b, 3)");
+
+ write.close();
+ commit.close();
+ }
+
@Test
public void testStringPartitionNullPadding() throws Exception {
RowType rowType =