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 4d54b5a86d [core] Match the lookup high level record by position, not 
by identity (#9279)
4d54b5a86d is described below

commit 4d54b5a86dfbfa7c1189d8e8ce0b780d818e55c1
Author: ZIHAN DAI <[email protected]>
AuthorDate: Thu Aug 20 12:02:16 2026 +1000

    [core] Match the lookup high level record by position, not by identity 
(#9279)
---
 .../mergetree/compact/LookupMergeFunction.java     | 30 +++++++---
 .../mergetree/compact/LookupMergeFunctionTest.java | 66 ++++++++++++++++++++++
 2 files changed, 87 insertions(+), 9 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/LookupMergeFunction.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/LookupMergeFunction.java
index 309d97a903..a98baacf59 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/LookupMergeFunction.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/LookupMergeFunction.java
@@ -42,6 +42,9 @@ public class LookupMergeFunction implements 
MergeFunction<KeyValue> {
     private boolean containLevel0;
     private InternalRow currentKey;
 
+    /** Position of the record {@link #pickHighLevel} chose, -1 when there is 
none. */
+    private int highLevelIndex = -1;
+
     public LookupMergeFunction(
             MergeFunction<KeyValue> mergeFunction,
             CoreOptions options,
@@ -57,6 +60,7 @@ public class LookupMergeFunction implements 
MergeFunction<KeyValue> {
         candidates.reset();
         currentKey = null;
         containLevel0 = false;
+        highLevelIndex = -1;
     }
 
     @Override
@@ -75,19 +79,22 @@ public class LookupMergeFunction implements 
MergeFunction<KeyValue> {
     @Nullable
     public KeyValue pickHighLevel() {
         KeyValue highLevel = null;
+        highLevelIndex = -1;
+        int index = 0;
         try (CloseableIterator<KeyValue> iterator = candidates.iterator()) {
             while (iterator.hasNext()) {
                 KeyValue kv = iterator.next();
                 // records that has not been stored on the disk yet, such as 
the data in the write
                 // buffer being at level -1
-                if (kv.level() <= 0) {
-                    continue;
-                }
-                // For high-level comparison logic (not involving Level 0), 
only the value of the
-                // minimum Level should be selected
-                if (highLevel == null || kv.level() < highLevel.level()) {
-                    highLevel = kv;
+                if (kv.level() > 0) {
+                    // For high-level comparison logic (not involving Level 
0), only the value of
+                    // the minimum Level should be selected
+                    if (highLevel == null || kv.level() < highLevel.level()) {
+                        highLevel = kv;
+                        highLevelIndex = index;
+                    }
                 }
+                index++;
             }
         } catch (Exception e) {
             throw new RuntimeException(e);
@@ -106,15 +113,20 @@ public class LookupMergeFunction implements 
MergeFunction<KeyValue> {
     @Override
     public KeyValue getResult() {
         mergeFunction.reset();
-        KeyValue highLevel = pickHighLevel();
+        // match the high level record by its position: once the candidates 
have spilled, every
+        // iteration deserializes fresh KeyValue instances, so the one picked 
above is never the
+        // same object as the one seen here
+        pickHighLevel();
+        int index = 0;
         try (CloseableIterator<KeyValue> iterator = candidates.iterator()) {
             while (iterator.hasNext()) {
                 KeyValue kv = iterator.next();
                 // records that has not been stored on the disk yet, such as 
the data in the write
                 // buffer being at level -1
-                if (kv.level() <= 0 || kv == highLevel) {
+                if (kv.level() <= 0 || index == highLevelIndex) {
                     mergeFunction.add(kv);
                 }
+                index++;
             }
         } catch (Exception e) {
             throw new RuntimeException(e);
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupMergeFunctionTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupMergeFunctionTest.java
index 71ac78259d..2087eb6656 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupMergeFunctionTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupMergeFunctionTest.java
@@ -18,9 +18,19 @@
 
 package org.apache.paimon.mergetree.compact;
 
+import org.apache.paimon.CoreOptions;
 import org.apache.paimon.KeyValue;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.annotation.Nullable;
+
+import java.nio.file.Path;
 
 import static org.apache.paimon.io.DataFileTestUtils.row;
 import static org.apache.paimon.types.RowKind.INSERT;
@@ -57,4 +67,60 @@ class LookupMergeFunctionTest {
         assertThat(kv).isNotNull();
         assertThat(kv.value().getInt(0)).isEqualTo(1);
     }
+
+    @TempDir Path tempDir;
+
+    /**
+     * Same scenario as {@link #testKeepLowestHighLevel()}, but with the 
candidates spilled: every
+     * iteration over a spilled buffer deserializes fresh instances, so an 
identity check against
+     * the record picked by a previous iteration can never match.
+     */
+    @Test
+    public void testKeepLowestHighLevelWhenCandidatesHaveSpilled() {
+        for (boolean withIoManager : new boolean[] {false, true}) {
+            LookupMergeFunction function = spillingFunction(withIoManager);
+            function.reset();
+            function.add(new KeyValue().replace(row(1), 1, INSERT, 
row(2)).setLevel(1));
+            function.add(new KeyValue().replace(row(1), 1, INSERT, 
row(1)).setLevel(2));
+            KeyValue kv = function.getResult();
+            assertThat(kv).as("spilled, ioManager=%s", 
withIoManager).isNotNull();
+            assertThat(kv.value().getInt(0)).isEqualTo(2);
+        }
+    }
+
+    /**
+     * The lowest high level record is not simply the first or the last one, 
so this also covers the
+     * position bookkeeping rather than only "some high level record was 
merged".
+     */
+    @Test
+    public void 
testPicksTheLowestHighLevelFromTheMiddleWhenCandidatesHaveSpilled() {
+        for (boolean withIoManager : new boolean[] {false, true}) {
+            LookupMergeFunction function = spillingFunction(withIoManager);
+            function.reset();
+            function.add(new KeyValue().replace(row(1), 1, INSERT, 
row(30)).setLevel(3));
+            function.add(new KeyValue().replace(row(1), 2, INSERT, 
row(10)).setLevel(1));
+            function.add(new KeyValue().replace(row(1), 3, INSERT, 
row(20)).setLevel(2));
+            KeyValue kv = function.getResult();
+            assertThat(kv).as("spilled, ioManager=%s", 
withIoManager).isNotNull();
+            assertThat(kv.value().getInt(0)).isEqualTo(10);
+        }
+    }
+
+    private LookupMergeFunction spillingFunction(boolean withIoManager) {
+        Options options = new Options();
+        // spill as soon as there is more than one candidate for the key
+        options.set(CoreOptions.LOOKUP_MERGE_RECORDS_THRESHOLD, 1);
+        RowType keyType = RowType.builder().field("k", 
DataTypes.INT()).build();
+        RowType valueType = RowType.builder().field("v", 
DataTypes.INT()).build();
+        LookupMergeFunction.Factory factory =
+                (LookupMergeFunction.Factory)
+                        LookupMergeFunction.wrap(
+                                DeduplicateMergeFunction.factory(),
+                                new CoreOptions(options),
+                                keyType,
+                                valueType);
+        @Nullable IOManager ioManager = withIoManager ? 
IOManager.create(tempDir.toString()) : null;
+        factory.withIOManager(ioManager);
+        return (LookupMergeFunction) factory.create();
+    }
 }

Reply via email to