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 7d6d146bba [core] Fix lookup merge buffer serialization for projected 
reads (#10549)
7d6d146bba is described below

commit 7d6d146bbaf33a349dc83c3caf71334bd551e3d5
Author: reswqa <[email protected]>
AuthorDate: Fri Oct 16 08:54:12 2026 +0800

    [core] Fix lookup merge buffer serialization for projected reads (#10549)
---
 .../mergetree/compact/LookupMergeFunction.java     |  24 ++++-
 .../mergetree/compact/LookupMergeFunctionTest.java | 103 ++++++++++++++++++++-
 .../paimon/table/PrimaryKeySimpleTableTest.java    |  46 +++++++++
 3 files changed, 170 insertions(+), 3 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 a98baacf59..dd71c41ffb 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
@@ -22,12 +22,15 @@ import org.apache.paimon.CoreOptions;
 import org.apache.paimon.KeyValue;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.CloseableIterator;
 
 import javax.annotation.Nullable;
 
+import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.List;
 
 /**
  * A {@link MergeFunction} for lookup, this wrapper only considers the latest 
high level record,
@@ -185,8 +188,27 @@ public class LookupMergeFunction implements 
MergeFunction<KeyValue> {
 
         @Override
         public MergeFunction<KeyValue> create(@Nullable RowType readType) {
+            RowType bufferType = valueType;
+            if (readType != null) {
+                // PR #9978(https://github.com/apache/paimon/pull/9978) makes 
some fields nullable
+                // in valueType so deletes can omit their values,
+                // while readType may still mark them NOT NULL. Keep the read 
layout, but preserve
+                // this nullability so buffered deletes can be serialized 
safely when spilling.
+                List<DataField> bufferFields = new ArrayList<>();
+                for (DataField readField : readType.getFields()) {
+                    bufferFields.add(
+                            valueType.containsField(readField.id())
+                                            && valueType
+                                                    .getField(readField.id())
+                                                    .type()
+                                                    .isNullable()
+                                    ? readField.copy(true)
+                                    : readField);
+                }
+                bufferType = readType.copy(bufferFields);
+            }
             return new LookupMergeFunction(
-                    wrapped.create(readType), options, keyType, valueType, 
ioManager);
+                    wrapped.create(readType), options, keyType, bufferType, 
ioManager);
         }
 
         @Override
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 2087eb6656..1e743b0046 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
@@ -20,9 +20,15 @@ package org.apache.paimon.mergetree.compact;
 
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.KeyValue;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.disk.IOManagerImpl;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowKind;
 import org.apache.paimon.types.RowType;
 
 import org.junit.jupiter.api.Test;
@@ -33,11 +39,17 @@ import javax.annotation.Nullable;
 import java.nio.file.Path;
 
 import static org.apache.paimon.io.DataFileTestUtils.row;
+import static org.apache.paimon.types.RowKind.DELETE;
 import static org.apache.paimon.types.RowKind.INSERT;
 import static org.assertj.core.api.Assertions.assertThat;
 
 class LookupMergeFunctionTest {
 
+    private static final RowType KEY_TYPE =
+            DataTypes.ROW(DataTypes.FIELD(0, "_KEY_id", DataTypes.INT()));
+
+    @TempDir Path tempDir;
+
     @Test
     public void testKeepLowestHighLevel() {
         LookupMergeFunction function =
@@ -68,8 +80,6 @@ class LookupMergeFunctionTest {
         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
@@ -123,4 +133,93 @@ class LookupMergeFunctionTest {
         factory.withIOManager(ioManager);
         return (LookupMergeFunction) factory.create();
     }
+
+    @Test
+    public void testSpillWithProjectedReadType() throws Exception {
+        RowType valueType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(1, "payload", DataTypes.INT()),
+                        DataTypes.FIELD(2, "items", 
DataTypes.ARRAY(DataTypes.INT())));
+        InternalRow result =
+                mergeWithSpill(
+                        valueType,
+                        valueType.project("items", "id"),
+                        GenericRow.of(new GenericArray(new int[] {10, 20}), 1),
+                        INSERT);
+
+        assertThat(result.getFieldCount()).isEqualTo(2);
+        assertThat(result.getArray(0).toIntArray()).containsExactly(10, 20);
+        assertThat(result.getInt(1)).isEqualTo(1);
+    }
+
+    @Test
+    public void testSpillWithNotNullReadType() throws Exception {
+        RowType valueType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT().notNull()),
+                        DataTypes.FIELD(1, "payload", DataTypes.INT()));
+        RowType readType = DataTypes.ROW(valueType.getField(0), 
valueType.getField(1).copy(false));
+
+        InternalRow result = mergeWithSpill(valueType, readType, 
GenericRow.of(1, null), DELETE);
+
+        assertThat(result.getFieldCount()).isEqualTo(2);
+        assertThat(result.getInt(0)).isEqualTo(1);
+        assertThat(result.isNullAt(1)).isTrue();
+        assertThat(readType.getTypeAt(1).isNullable()).isFalse();
+    }
+
+    @Test
+    public void testSpillWithNestedReadType() throws Exception {
+        RowType nestedType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(2, "items", 
DataTypes.ARRAY(DataTypes.INT())),
+                        DataTypes.FIELD(3, "payload", 
DataTypes.INT().notNull()));
+        RowType valueType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(1, "nested", nestedType));
+        RowType readType =
+                DataTypes.ROW(
+                        valueType.getField(0),
+                        DataTypes.FIELD(1, "nested", 
nestedType.project("payload").notNull()));
+
+        InternalRow result =
+                mergeWithSpill(valueType, readType, GenericRow.of(1, 
GenericRow.of(42)), INSERT);
+
+        assertThat(result.getFieldCount()).isEqualTo(2);
+        assertThat(result.getInt(0)).isEqualTo(1);
+        assertThat(result.getRow(1, 1).getInt(0)).isEqualTo(42);
+    }
+
+    private InternalRow mergeWithSpill(
+            RowType valueType, RowType readType, InternalRow value, RowKind 
valueKind)
+            throws Exception {
+        Options options = new Options();
+        options.set(CoreOptions.LOOKUP_MERGE_RECORDS_THRESHOLD, 1);
+        LookupMergeFunction.Factory factory =
+                (LookupMergeFunction.Factory)
+                        LookupMergeFunction.wrap(
+                                DeduplicateMergeFunction.factory(),
+                                new CoreOptions(options),
+                                KEY_TYPE,
+                                valueType);
+        try (IOManager ioManager = new IOManagerImpl(tempDir.toString())) {
+            factory.withIOManager(ioManager);
+            MergeFunction<KeyValue> function = factory.create(readType);
+            function.reset();
+            try {
+                function.add(new KeyValue().replace(row(1), 1, valueKind, 
value).setLevel(0));
+                function.add(new KeyValue().replace(row(1), 2, valueKind, 
value).setLevel(0));
+                KeyValue result = function.getResult();
+                assertThat(result).isNotNull();
+                assertThat(result.key().getInt(0)).isEqualTo(1);
+                assertThat(result.sequenceNumber()).isEqualTo(2);
+                assertThat(result.valueKind()).isEqualTo(valueKind);
+                return new 
InternalRowSerializer(readType).copy(result.value());
+            } finally {
+                function.reset();
+            }
+        }
+    }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
index 140f549d50..f6bc8fcc42 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
@@ -24,6 +24,7 @@ import org.apache.paimon.KeyValue;
 import org.apache.paimon.Snapshot;
 import org.apache.paimon.catalog.TableQueryAuthResult;
 import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericArray;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.disk.IOManager;
@@ -643,6 +644,51 @@ public class PrimaryKeySimpleTableTest extends 
SimpleTableTestBase {
                 .isEqualTo(Arrays.asList("20001|21", "202|22"));
     }
 
+    @Test
+    public void testBatchProjectionWithLookupSpill() throws Exception {
+        RowType rowType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "pt", DataTypes.INT()),
+                        DataTypes.FIELD(1, "a", DataTypes.INT()),
+                        DataTypes.FIELD(2, "payload", DataTypes.INT()),
+                        DataTypes.FIELD(3, "items", 
DataTypes.ARRAY(DataTypes.INT())));
+        FileStoreTable table =
+                createFileStoreTable(
+                        options -> {
+                            options.set(FILE_FORMAT, "avro");
+                            options.set(CHANGELOG_PRODUCER, LOOKUP);
+                            options.set(CoreOptions.WRITE_ONLY, true);
+                            
options.set(CoreOptions.LOOKUP_MERGE_RECORDS_THRESHOLD, 1);
+                        },
+                        rowType);
+
+        try (IOManager ioManager = new IOManagerImpl(tempDir.toString())) {
+            try (StreamTableWrite write = 
table.newWrite(commitUser).withIOManager(ioManager);
+                    StreamTableCommit commit = table.newCommit(commitUser)) {
+                write.write(GenericRow.of(1, 10, 100, new GenericArray(new 
int[] {1, 2})));
+                commit.commit(0, write.prepareCommit(true, 0));
+                write.write(GenericRow.of(1, 10, 200, new GenericArray(new 
int[] {3, 4})));
+                commit.commit(1, write.prepareCommit(true, 1));
+            }
+
+            ReadBuilder readBuilder =
+                    table.newReadBuilder().withReadType(rowType.project("pt", 
"a", "payload"));
+            List<Split> splits = readBuilder.newScan().plan().splits();
+            assertThat(splits).hasSize(1);
+            assertThat(((DataSplit) splits.get(0)).dataFiles()).hasSize(2);
+
+            List<String> result = new ArrayList<>();
+            try (RecordReader<InternalRow> reader =
+                    
readBuilder.newRead().withIOManager(ioManager).createReader(splits.get(0))) {
+                reader.forEachRemaining(
+                        row ->
+                                result.add(
+                                        row.getInt(0) + "|" + row.getInt(1) + 
"|" + row.getInt(2)));
+            }
+            assertThat(result).containsExactly("1|10|200");
+        }
+    }
+
     @ParameterizedTest
     @ValueSource(booleans = {true, false})
     public void testBatchFilter(boolean statsDenseStore) throws Exception {

Reply via email to