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 28f54f9c19 [common] Collect min and max stats for binary columns 
(#8847)
28f54f9c19 is described below

commit 28f54f9c194ae9b5e154647d5e2a01bff9fda1cd
Author: Arnav Balyan <[email protected]>
AuthorDate: Tue Jul 28 18:42:58 2026 +0530

    [common] Collect min and max stats for binary columns (#8847)
---
 .../org/apache/paimon/format/SimpleColStats.java   | 16 +++++--
 .../statistics/FullSimpleColStatsCollector.java    | 13 ++++-
 .../TruncateSimpleColStatsCollector.java           | 48 ++++++++++++++++++-
 .../statistics/SimpleColStatsCollectorTest.java    | 55 ++++++++++++++++++++++
 .../apache/paimon/append/AppendOnlyWriterTest.java | 43 +++++++++++++++++
 .../paimon/table/ColumnTypeFileMetaTestBase.java   |  5 +-
 .../parquet/ParquetSimpleStatsExtractor.java       | 10 ++++
 .../format/orc/OrcSimpleStatsExtractorTest.java    | 12 +++++
 8 files changed, 191 insertions(+), 11 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/format/SimpleColStats.java 
b/paimon-common/src/main/java/org/apache/paimon/format/SimpleColStats.java
index 0b0062b756..29223db7bf 100644
--- a/paimon-common/src/main/java/org/apache/paimon/format/SimpleColStats.java
+++ b/paimon-common/src/main/java/org/apache/paimon/format/SimpleColStats.java
@@ -20,6 +20,7 @@ package org.apache.paimon.format;
 
 import javax.annotation.Nullable;
 
+import java.util.Arrays;
 import java.util.Objects;
 
 /**
@@ -70,18 +71,25 @@ public class SimpleColStats {
             return false;
         }
         SimpleColStats that = (SimpleColStats) o;
-        return Objects.equals(min, that.min)
-                && Objects.equals(max, that.max)
+        return Objects.deepEquals(min, that.min)
+                && Objects.deepEquals(max, that.max)
                 && Objects.equals(nullCount, that.nullCount);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(min, max, nullCount);
+        return Objects.hash(
+                min instanceof byte[] ? Arrays.hashCode((byte[]) min) : min,
+                max instanceof byte[] ? Arrays.hashCode((byte[]) max) : max,
+                nullCount);
     }
 
     @Override
     public String toString() {
-        return String.format("{%s, %s, %d}", min, max, nullCount);
+        return String.format(
+                "{%s, %s, %d}",
+                min instanceof byte[] ? Arrays.toString((byte[]) min) : min,
+                max instanceof byte[] ? Arrays.toString((byte[]) max) : max,
+                nullCount);
     }
 }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/statistics/FullSimpleColStatsCollector.java
 
b/paimon-common/src/main/java/org/apache/paimon/statistics/FullSimpleColStatsCollector.java
index a1e05eb2dc..20ce708d80 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/statistics/FullSimpleColStatsCollector.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/statistics/FullSimpleColStatsCollector.java
@@ -20,6 +20,7 @@ package org.apache.paimon.statistics;
 
 import org.apache.paimon.data.serializer.Serializer;
 import org.apache.paimon.format.SimpleColStats;
+import org.apache.paimon.utils.SortUtil;
 
 /** The full stats collector which will report null count, min value, max 
value if available. */
 public class FullSimpleColStatsCollector extends 
AbstractSimpleColStatsCollector {
@@ -31,7 +32,17 @@ public class FullSimpleColStatsCollector extends 
AbstractSimpleColStatsCollector
             return;
         }
 
-        // TODO use comparator for not comparable types and extract this logic 
to a util class
+        if (field instanceof byte[]) {
+            byte[] b = (byte[]) field;
+            if (minValue == null || SortUtil.compareBinary(b, (byte[]) 
minValue) < 0) {
+                minValue = fieldSerializer.copy(field);
+            }
+            if (maxValue == null || SortUtil.compareBinary(b, (byte[]) 
maxValue) > 0) {
+                maxValue = fieldSerializer.copy(field);
+            }
+            return;
+        }
+
         if (!(field instanceof Comparable)) {
             return;
         }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/statistics/TruncateSimpleColStatsCollector.java
 
b/paimon-common/src/main/java/org/apache/paimon/statistics/TruncateSimpleColStatsCollector.java
index cba942c790..fc8d9d8cc2 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/statistics/TruncateSimpleColStatsCollector.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/statistics/TruncateSimpleColStatsCollector.java
@@ -22,12 +22,14 @@ import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.serializer.Serializer;
 import org.apache.paimon.format.SimpleColStats;
 import org.apache.paimon.utils.Preconditions;
+import org.apache.paimon.utils.SortUtil;
 
+import java.util.Arrays;
 import java.util.regex.Pattern;
 
 /**
  * The truncate stats collector which will report null count, truncated 
min/max value. Currently,
- * truncation only performs on the {@link BinaryString} value.
+ * truncation only performs on the {@link BinaryString} and {@code byte[]} 
values.
  */
 public class TruncateSimpleColStatsCollector extends 
AbstractSimpleColStatsCollector {
 
@@ -58,7 +60,26 @@ public class TruncateSimpleColStatsCollector extends 
AbstractSimpleColStatsColle
             return;
         }
 
-        // TODO use comparator for not comparable types and extract this logic 
to a util class
+        if (field instanceof byte[]) {
+            byte[] bytes = (byte[]) field;
+            if (minValue == null || SortUtil.compareBinary(bytes, (byte[]) 
minValue) < 0) {
+                minValue = fieldSerializer.copy(truncateMin(field));
+            }
+            if (maxValue == null || SortUtil.compareBinary(bytes, (byte[]) 
maxValue) > 0) {
+                Object max = truncateMax(field);
+                // may fail
+                if (max != null) {
+                    if (max != field) {
+                        // copied in `truncateMax`
+                        maxValue = max;
+                    } else {
+                        maxValue = fieldSerializer.copy(max);
+                    }
+                }
+            }
+            return;
+        }
+
         if (!(field instanceof Comparable)) {
             return;
         }
@@ -106,6 +127,9 @@ public class TruncateSimpleColStatsCollector extends 
AbstractSimpleColStatsColle
         }
         if (field instanceof BinaryString) {
             return ((BinaryString) field).substring(0, length);
+        } else if (field instanceof byte[]) {
+            byte[] bytes = (byte[]) field;
+            return bytes.length <= length ? bytes : Arrays.copyOf(bytes, 
length);
         } else {
             return field;
         }
@@ -143,6 +167,26 @@ public class TruncateSimpleColStatsCollector extends 
AbstractSimpleColStatsColle
             }
             failed = true;
             return null; // Cannot find a valid upper bound
+        } else if (field instanceof byte[]) {
+            byte[] bytes = (byte[]) field;
+
+            // No need to increment if the input length is under the truncate 
length
+            if (bytes.length <= length) {
+                return field;
+            }
+
+            byte[] truncated = Arrays.copyOf(bytes, length);
+
+            // Try incrementing the bytes from the end
+            for (int i = length - 1; i >= 0; i--) {
+                // No overflow
+                if (truncated[i] != (byte) 0xFF) {
+                    truncated[i]++;
+                    return Arrays.copyOf(truncated, i + 1);
+                }
+            }
+            failed = true;
+            return null; // Cannot find a valid upper bound
         } else {
             return field;
         }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/statistics/SimpleColStatsCollectorTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/statistics/SimpleColStatsCollectorTest.java
index 4511ae26ee..a1be45b0ba 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/statistics/SimpleColStatsCollectorTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/statistics/SimpleColStatsCollectorTest.java
@@ -27,6 +27,7 @@ import org.apache.paimon.format.SimpleColStats;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.IntType;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.VarBinaryType;
 import org.apache.paimon.types.VarCharType;
 import org.apache.paimon.utils.StringUtils;
 
@@ -193,6 +194,60 @@ public class SimpleColStatsCollectorTest {
         assertThat(stats.max()).isNotSameAs(str);
     }
 
+    @Test
+    public void testFullBinaryMinMax() {
+        Serializer<Object> serializer =
+                (Serializer) InternalSerializers.create(new VarBinaryType());
+        FullSimpleColStatsCollector collector = new 
FullSimpleColStatsCollector();
+        collector.collect(new byte[] {1, 2, 3}, serializer);
+        collector.collect(new byte[] {(byte) 200}, serializer);
+        collector.collect(new byte[] {0}, serializer);
+        collector.collect(null, serializer);
+
+        SimpleColStats stats = collector.result();
+        assertThat((byte[]) stats.min()).isEqualTo(new byte[] {0});
+        assertThat((byte[]) stats.max()).isEqualTo(new byte[] {(byte) 200});
+        assertThat(stats.nullCount()).isEqualTo(1L);
+    }
+
+    @Test
+    public void testTruncateBinaryMinMax() {
+        Serializer<Object> serializer =
+                (Serializer) InternalSerializers.create(new VarBinaryType());
+        TruncateSimpleColStatsCollector collector = new 
TruncateSimpleColStatsCollector(2);
+        collector.collect(new byte[] {5, 7, 9, 11}, serializer);
+        collector.collect(new byte[] {5, 7, (byte) 200}, serializer);
+        collector.collect(null, serializer);
+
+        SimpleColStats stats = collector.result();
+        assertThat((byte[]) stats.min()).isEqualTo(new byte[] {5, 7});
+        assertThat((byte[]) stats.max()).isEqualTo(new byte[] {5, 8});
+        assertThat(stats.nullCount()).isEqualTo(1L);
+
+        stats =
+                collector.convert(
+                        new SimpleColStats(new byte[] {1, 2, 3}, new byte[] 
{1, 2, 3}, 0L));
+        assertThat((byte[]) stats.min()).isEqualTo(new byte[] {1, 2});
+        assertThat((byte[]) stats.max()).isEqualTo(new byte[] {1, 3});
+    }
+
+    @Test
+    public void testTruncateBinaryFail() {
+        TruncateSimpleColStatsCollector collector = new 
TruncateSimpleColStatsCollector(2);
+        Serializer<Object> serializer =
+                (Serializer) InternalSerializers.create(new VarBinaryType());
+        byte[] bytes = new byte[] {(byte) 0xFF, (byte) 0xFF, 7};
+
+        collector.collect(bytes, serializer);
+        SimpleColStats stats = collector.result();
+        assertThat(stats.min()).isNull();
+        assertThat(stats.max()).isNull();
+
+        stats = collector.convert(new SimpleColStats(bytes, bytes, 0L));
+        assertThat(stats.min()).isNull();
+        assertThat(stats.max()).isNull();
+    }
+
     @Test
     public void testTruncateFail() {
         TruncateSimpleColStatsCollector collector = new 
TruncateSimpleColStatsCollector(3);
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java 
b/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java
index ef56732447..fae027b7e0 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java
@@ -62,6 +62,7 @@ import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.IntType;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.VarBinaryType;
 import org.apache.paimon.types.VarCharType;
 import org.apache.paimon.utils.CommitIncrement;
 import org.apache.paimon.utils.ExecutorThreadFactory;
@@ -170,6 +171,48 @@ public class AppendOnlyWriterTest {
                 .isEqualTo(CoreOptions.FILE_FORMAT_AVRO);
     }
 
+    @Test
+    public void testBinaryColumnStatsRoundTrip() throws Exception {
+        RowType binarySchema =
+                RowType.builder()
+                        .fields(
+                                new DataType[] {new IntType(), new 
VarBinaryType()},
+                                new String[] {"id", "data"})
+                        .build();
+        Map<String, String> options = new HashMap<>();
+        options.put("metadata.stats-mode", "full");
+
+        AppendOnlyWriter writer =
+                createWriterBase(
+                                1024 * 1024L,
+                                null,
+                                binarySchema,
+                                false,
+                                true,
+                                true,
+                                true,
+                                Collections.emptyList(),
+                                compactBefore -> Collections.emptyList(),
+                                options)
+                        .getKey();
+
+        writer.write(GenericRow.of(1, new byte[] {1, 2, 3}));
+        writer.write(GenericRow.of(2, new byte[] {(byte) 200}));
+        writer.write(GenericRow.of(3, new byte[] {0}));
+        CommitIncrement increment = writer.prepareCommit(true);
+        writer.close();
+
+        DataFileMeta meta = increment.newFilesIncrement().newFiles().get(0);
+        
assertThat(LocalFileIO.create().exists(pathFactory.toPath(meta))).isTrue();
+
+        assertThat(meta.valueStats().minValues().isNullAt(1))
+                .as("binary column min must be present in file stats")
+                .isFalse();
+        assertThat(meta.valueStats().minValues().getBinary(1)).isEqualTo(new 
byte[] {0});
+        assertThat(meta.valueStats().maxValues().getBinary(1)).isEqualTo(new 
byte[] {(byte) 200});
+        assertThat(meta.valueStats().nullCounts().getLong(1)).isEqualTo(0L);
+    }
+
     @Test
     public void testMultipleCommits() throws Exception {
         RecordWriter<InternalRow> writer =
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/ColumnTypeFileMetaTestBase.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/ColumnTypeFileMetaTestBase.java
index e92ce9b49b..fa104b8df3 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/ColumnTypeFileMetaTestBase.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/ColumnTypeFileMetaTestBase.java
@@ -91,13 +91,10 @@ public abstract class ColumnTypeFileMetaTestBase extends 
SchemaEvolutionTableTes
         for (DataFileMeta fileMeta : fileMetaList) {
             SimpleStats stats = getTableValueStats(fileMeta);
             assertThat(stats.minValues().getFieldCount()).isEqualTo(12);
-            for (int i = 0; i < 11; i++) {
+            for (int i = 0; i < 12; i++) {
                 assertThat(stats.minValues().isNullAt(i)).isFalse();
                 assertThat(stats.maxValues().isNullAt(i)).isFalse();
             }
-            // Min and max value of binary type is null
-            assertThat(stats.minValues().isNullAt(11)).isTrue();
-            assertThat(stats.maxValues().isNullAt(11)).isTrue();
         }
     }
 
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSimpleStatsExtractor.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSimpleStatsExtractor.java
index f286cfce5d..fa91dbf289 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSimpleStatsExtractor.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSimpleStatsExtractor.java
@@ -142,6 +142,16 @@ public class ParquetSimpleStatsExtractor implements 
SimpleStatsExtractor {
                                 
BinaryString.fromString(stringStats.maxAsString()),
                                 nullCount);
                 break;
+            case BINARY:
+            case VARBINARY:
+                assertStatsClass(field, stats, BinaryStatistics.class);
+                BinaryStatistics binaryStats = (BinaryStatistics) stats;
+                fieldStats =
+                        new SimpleColStats(
+                                binaryStats.genericGetMin().getBytes(),
+                                binaryStats.genericGetMax().getBytes(),
+                                nullCount);
+                break;
             case BOOLEAN:
                 assertStatsClass(field, stats, BooleanStatistics.class);
                 BooleanStatistics boolStats = (BooleanStatistics) stats;
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/orc/OrcSimpleStatsExtractorTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/orc/OrcSimpleStatsExtractorTest.java
index 0ee3d665a0..e2c68ab60d 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/orc/OrcSimpleStatsExtractorTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/orc/OrcSimpleStatsExtractorTest.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.format.orc;
 
 import org.apache.paimon.format.FileFormat;
+import org.apache.paimon.format.SimpleColStats;
 import org.apache.paimon.format.SimpleColStatsExtractorTest;
 import org.apache.paimon.format.orc.filter.OrcSimpleStatsExtractor;
 import org.apache.paimon.options.Options;
@@ -27,6 +28,7 @@ import org.apache.paimon.types.BigIntType;
 import org.apache.paimon.types.BinaryType;
 import org.apache.paimon.types.BooleanType;
 import org.apache.paimon.types.CharType;
+import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DateType;
 import org.apache.paimon.types.DecimalType;
 import org.apache.paimon.types.DoubleType;
@@ -80,6 +82,16 @@ public class OrcSimpleStatsExtractorTest extends 
SimpleColStatsExtractorTest {
                 .build();
     }
 
+    @Override
+    protected SimpleColStats regenerate(SimpleColStats stats, DataType type) {
+        switch (type.getTypeRoot()) {
+            case BINARY:
+            case VARBINARY:
+                return new SimpleColStats(null, null, stats.nullCount());
+        }
+        return stats;
+    }
+
     @Override
     protected String fileCompression() {
         return "LZ4";

Reply via email to