Repository: apache/paimon (2026-07-27T20:19:34Z)

Issue:
[Bug] SliceComparator throws ClassCastException on BINARY/VARBINARY primary-key fields
### Search before asking

- [x] I searched in the [issues](https://github.com/apache/paimon/issues) and found nothing similar.

### Paimon version

master (also reproduces on 1.4.2)

### Compute Engine

any (reproduces with the table API alone)

### Minimal reproduce step

Create a primary-key table whose primary key contains a `BYTES`/`VARBINARY` field, enable a lookup strategy (`deletion-vectors.enabled = true`, or `changelog-producer = lookup`), write enough that lookup compaction must probe a higher level for an overwritten key (e.g. restore-style workloads where level-1+ files exist before new level-0 writes). The first sorted-lookup-file seek throws:

```
java.lang.ClassCastException: class [B cannot be cast to class java.lang.Comparable
    at org.apache.paimon.data.serializer.RowCompactedSerializer$SliceComparator.compare(RowCompactedSerializer.java)
    at org.apache.paimon.sst.BlockIterator.seekTo(BlockIterator.java:74)
    at org.apache.paimon.lookup.sort.SortLookupStoreReader.lookup(SortLookupStoreReader.java:66)
    at org.apache.paimon.mergetree.LookupLevels.lookup(...)
```

### What doesn't meet your expectations?

Binary primary keys are legal: `SchemaValidation` admits them and `TypeCheckUtils.isComparable` returns true for BINARY/VARBINARY, so `createSliceComparator`'s guard passes. But `SliceComparator.compare` casts every field value to `Comparable`, and BINARY/VARBINARY fields read back as `byte[]`, which is not. The comparator should order binary fields the way `BinaryRow` does (unsigned lexicographic, `SortUtil.compareBinary`).

The bug stays hidden on small tables because compactions there tend to pick full merges (no lookup); it surfaces exactly when lookup compaction starts probing higher levels.

### Anything else?

Fix incoming as a PR.

### Are you willing to submit a PR?

- [x] I'm willing to submit a PR!


Discussion:

Patch:
diff --git a/paimon-common/src/main/java/org/apache/paimon/data/serializer/RowCompactedSerializer.java b/paimon-common/src/main/java/org/apache/paimon/data/serializer/RowCompactedSerializer.java
index 4ceffc8f5f65..c7fb58417afa 100644
--- a/paimon-common/src/main/java/org/apache/paimon/data/serializer/RowCompactedSerializer.java
+++ b/paimon-common/src/main/java/org/apache/paimon/data/serializer/RowCompactedSerializer.java
@@ -40,6 +40,7 @@
 import org.apache.paimon.types.RowKind;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.VectorType;
+import org.apache.paimon.utils.SortUtil;
 import org.apache.paimon.utils.TypeCheckUtils;
 import org.apache.paimon.utils.VarLengthIntUtils;
 
@@ -760,8 +761,16 @@ public int compare(MemorySlice slice1, MemorySlice slice2) {
                         FieldReader fieldReader = fieldReaders[i];
                         Object o1 = fieldReader.readField(reader1, i);
                         Object o2 = fieldReader.readField(reader2, i);
-                        @SuppressWarnings({"unchecked", "rawtypes"})
-                        int comp = ((Comparable) o1).compareTo(o2);
+                        int comp;
+                        if (o1 instanceof byte[]) {
+                            // BINARY / VARBINARY fields read back as byte[], which does not
+                            // implement Comparable; order them like BinaryRow does.
+                            comp = SortUtil.compareBinary((byte[]) o1, (byte[]) o2);
+                        } else {
+                            @SuppressWarnings({"unchecked", "rawtypes"})
+                            int comparableComp = ((Comparable) o1).compareTo(o2);
+                            comp = comparableComp;
+                        }
                         if (comp != 0) {
                             return comp;
                         }


Test patch:
diff --git a/paimon-common/src/test/java/org/apache/paimon/data/serializer/RowCompactedSerializerTest.java b/paimon-common/src/test/java/org/apache/paimon/data/serializer/RowCompactedSerializerTest.java
index 9b55fe2261a4..833a38a6f532 100644
--- a/paimon-common/src/test/java/org/apache/paimon/data/serializer/RowCompactedSerializerTest.java
+++ b/paimon-common/src/test/java/org/apache/paimon/data/serializer/RowCompactedSerializerTest.java
@@ -23,17 +23,21 @@
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.data.variant.GenericVariant;
+import org.apache.paimon.memory.MemorySlice;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
 
 import org.junit.jupiter.api.Test;
 
+import java.util.Comparator;
+
 import static org.apache.paimon.data.BinaryString.fromString;
 import static org.apache.paimon.data.serializer.InternalRowSerializerTest.createArray;
 import static org.apache.paimon.data.serializer.InternalRowSerializerTest.createMap;
 import static org.apache.paimon.data.serializer.InternalRowSerializerTest.createRow;
 import static org.apache.paimon.data.serializer.InternalRowSerializerTest.deepEqualsInternalRow;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Test for {@link RowCompactedSerializer}. */
@@ -202,6 +206,44 @@ private static RowCompactedSerializer getRowSerializer() {
         }
     }
 
+    static final class BinaryFieldTest extends RowCompactedSerializerTest {
+        public BinaryFieldTest() {
+            super(getRowSerializer(), getData());
+        }
+
+        private static InternalRow[] getData() {
+            return new GenericRow[] {
+                GenericRow.of(1, new byte[] {1, 2, 3}),
+                GenericRow.of(1, new byte[] {1, 2, 4}),
+                GenericRow.of(2, new byte[] {(byte) 0xFF})
+            };
+        }
+
+        private static RowCompactedSerializer getRowSerializer() {
+            return new RowCompactedSerializer(RowType.of(DataTypes.INT(), DataTypes.BYTES()));
+        }
+
+        @Test
+        public void testSliceComparatorOnBinaryFields() {
+            RowCompactedSerializer serializer = getRowSerializer();
+            Comparator<MemorySlice> comparator = serializer.createSliceComparator();
+            MemorySlice small =
+                    MemorySlice.wrap(
+                            serializer.serializeToBytes(GenericRow.of(1, new byte[] {1, 2, 3})));
+            MemorySlice large =
+                    MemorySlice.wrap(
+                            serializer.serializeToBytes(GenericRow.of(1, new byte[] {1, 2, 4})));
+            MemorySlice unsigned =
+                    MemorySlice.wrap(
+                            serializer.serializeToBytes(
+                                    GenericRow.of(1, new byte[] {(byte) 0xFF})));
+            assertThat(comparator.compare(small, large)).isLessThan(0);
+            assertThat(comparator.compare(large, small)).isGreaterThan(0);
+            assertThat(comparator.compare(small, small)).isEqualTo(0);
+            assertThat(comparator.compare(unsigned, small)).isGreaterThan(0);
+        }
+    }
+
     static final class VectorTypesTest extends RowCompactedSerializerTest {
         public VectorTypesTest() {
             super(getRowSerializer(), getData());