Repository: apache/paimon (2026-07-10T01:54:29Z)

Issue:
[Bug] Internal map equality uses Java key lookup for binary keys
# [Bug] Internal map equality uses Java key lookup for binary keys

**Describe the bug**

`InternalRowUtils.equals` compares standalone `byte[]` values by content, but its `InternalMap` branch checks keys with Java `Map.containsKey` and `Map.get`.

For `MAP<BINARY, ...>`, two separate `byte[]` key objects with identical bytes are therefore treated as different keys.

**To Reproduce**

Compare two `GenericMap` instances with `DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())`; each map contains one key `{1, 2}` and value `1`, but the key arrays are different objects.

**Expected behavior**

The maps should compare equal because binary keys have the same logical bytes and values match.

**Actual behavior**

The maps compare unequal because Java arrays use reference equality for map lookup.

**Additional context**

The value comparison path already handles `byte[]` with `Arrays.equals`; map key comparison should use the same typed equality path.



Discussion:

Patch:
diff --git a/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowUtils.java b/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowUtils.java
index 668d1d21a014..1ff433f3e24d 100644
--- a/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowUtils.java
+++ b/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowUtils.java
@@ -109,10 +109,20 @@ public static boolean equals(Object data1, Object data2, DataType dataType) {
                                     mapType.getValueType());
                 }
                 InternalArray keyArray1 = map1.keyArray();
+                InternalArray keyArray2 = map2.keyArray();
+                InternalArray valueArray1 = map1.valueArray();
+                InternalArray valueArray2 = map2.valueArray();
+                boolean[] matched = new boolean[map2.size()];
                 for (int i = 0; i < map1.size(); i++) {
-                    Object key = get(keyArray1, i, mapType.getKeyType());
-                    if (!map2.contains(key)
-                            || !equals(map1.get(key), map2.get(key), mapType.getValueType())) {
+                    if (!hasEqualMapEntry(
+                            keyArray1,
+                            valueArray1,
+                            i,
+                            keyArray2,
+                            valueArray2,
+                            matched,
+                            mapType.getKeyType(),
+                            mapType.getValueType())) {
                         return false;
                     }
                 }
@@ -137,6 +147,33 @@ public static boolean equals(Object data1, Object data2, DataType dataType) {
         return true;
     }
 
+    private static boolean hasEqualMapEntry(
+            InternalArray keyArray1,
+            InternalArray valueArray1,
+            int pos1,
+            InternalArray keyArray2,
+            InternalArray valueArray2,
+            boolean[] matched,
+            DataType keyType,
+            DataType valueType) {
+        Object key1 = get(keyArray1, pos1, keyType);
+        Object value1 = get(valueArray1, pos1, valueType);
+        for (int j = 0; j < keyArray2.size(); j++) {
+            if (matched[j]) {
+                continue;
+            }
+            Object key2 = get(keyArray2, j, keyType);
+            if (equals(key1, key2, keyType)) {
+                Object value2 = get(valueArray2, j, valueType);
+                if (equals(value1, value2, valueType)) {
+                    matched[j] = true;
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     public static int hash(Object data, DataType dataType) {
         if (data == null) {
             return 0;
@@ -167,10 +204,12 @@ public static int hash(Object data, DataType dataType) {
                                 (InternalMap) data, mapType.getKeyType(), mapType.getValueType());
             }
             InternalArray keyArray = map.keyArray();
+            InternalArray valueArray = map.valueArray();
             for (int i = 0; i < map.size(); i++) {
                 Object key = get(keyArray, i, mapType.getKeyType());
-                result = 37 * result + hash(key, mapType.getKeyType());
-                result = 37 * result + hash(map.get(key), mapType.getValueType());
+                Object value = get(valueArray, i, mapType.getValueType());
+                result +=
+                        37 * hash(key, mapType.getKeyType()) + hash(value, mapType.getValueType());
             }
         } else if (data instanceof byte[]) {
             result = Arrays.hashCode((byte[]) data);


Test patch:
diff --git a/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowUtilsTest.java b/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowUtilsTest.java
index 821d52a540a5..c085147c4106 100644
--- a/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowUtilsTest.java
+++ b/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowUtilsTest.java
@@ -197,6 +197,60 @@ public void testEqualsAndHashCode() {
                 .isEqualTo(InternalRowUtils.hash(row2, rowType));
     }
 
+    @Test
+    public void testEqualsMapWithBinaryKeys() {
+        Map<byte[], Integer> map1 = new HashMap<>();
+        map1.put(new byte[] {1, 2}, 1);
+        map1.put(new byte[] {3, 4}, 2);
+        Map<byte[], Integer> map2 = new HashMap<>();
+        map2.put(new byte[] {3, 4}, 2);
+        map2.put(new byte[] {1, 2}, 1);
+
+        assertThat(
+                        InternalRowUtils.equals(
+                                new GenericMap(map1),
+                                new GenericMap(map2),
+                                DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())))
+                .isTrue();
+        assertThat(
+                        InternalRowUtils.hash(
+                                new GenericMap(map1),
+                                DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())))
+                .isEqualTo(
+                        InternalRowUtils.hash(
+                                new GenericMap(map2),
+                                DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())));
+
+        Map<byte[], Integer> map3 = new HashMap<>();
+        map3.put(new byte[] {1, 3}, 1);
+        map3.put(new byte[] {3, 4}, 2);
+        assertThat(
+                        InternalRowUtils.equals(
+                                new GenericMap(map1),
+                                new GenericMap(map3),
+                                DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())))
+                .isFalse();
+
+        Map<byte[], Integer> map4 = new HashMap<>();
+        map4.put(new byte[] {1, 2}, 1);
+        map4.put(new byte[] {1, 2}, 2);
+        Map<byte[], Integer> map5 = new HashMap<>();
+        map5.put(new byte[] {1, 2}, 1);
+        map5.put(new byte[] {1, 3}, 2);
+        assertThat(
+                        InternalRowUtils.equals(
+                                new GenericMap(map4),
+                                new GenericMap(map5),
+                                DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())))
+                .isFalse();
+        assertThat(
+                        InternalRowUtils.equals(
+                                new GenericMap(map5),
+                                new GenericMap(map4),
+                                DataTypes.MAP(DataTypes.BINARY(2), DataTypes.INT())))
+                .isFalse();
+    }
+
     @Test
     public void testCopyVector() {
         RowType rowType =