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 94f6ea8677 [core] Preserve partial-update sequence-group deletion 
state (#10300)
94f6ea8677 is described below

commit 94f6ea8677246762c9ca6a11bd044ef1c2eaae89
Author: kevin <[email protected]>
AuthorDate: Fri Oct 2 09:43:55 2026 +0800

    [core] Preserve partial-update sequence-group deletion state (#10300)
---
 .../merge-engine/partial-update.md                 |   5 +
 .../compact/PartialUpdateMergeFunction.java        |   6 +-
 .../LookupChangelogMergeFunctionWrapperTest.java   |  54 +++++++
 .../compact/PartialUpdateMergeFunctionTest.java    | 169 +++++++++++++++++++++
 4 files changed, 233 insertions(+), 1 deletion(-)

diff --git a/docs/docs/primary-key-table/merge-engine/partial-update.md 
b/docs/docs/primary-key-table/merge-engine/partial-update.md
index 2430eee293..2299de1c1d 100644
--- a/docs/docs/primary-key-table/merge-engine/partial-update.md
+++ b/docs/docs/primary-key-table/merge-engine/partial-update.md
@@ -236,5 +236,10 @@ Set `partial-update.remove-record-on-sequence-group` to a 
comma-separated list o
 field names from the groups whose deletes should remove the whole row. For the 
`profiles`
 schema above, using `profile_version` allows an accepted profile delete to 
remove the row.
 
+While merging a deleted row, subsequent retractions or updates to other groups 
do not cancel
+the whole-row deletion. An `INSERT` or `UPDATE_AFTER` for a configured 
deletion group can
+restore the row only when its group version passes the normal newer-or-equal 
comparison.
+An older update or a group with all-null ordering fields cannot restore the 
row.
+
 `partial-update.remove-record-on-delete` cannot be combined with sequence 
groups. Neither
 whole-row removal option can be combined with `ignore-delete`.
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunction.java
 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunction.java
index b97d7a29da..467524ee11 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunction.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunction.java
@@ -138,6 +138,7 @@ public class PartialUpdateMergeFunction implements 
MergeFunction<KeyValue> {
     @Override
     public void reset() {
         this.currentKey = null;
+        this.currentDeleteRow = false;
         this.meetInsert = false;
         this.notNullColumnFilled = false;
         this.row = new GenericRow(getters.length);
@@ -149,7 +150,6 @@ public class PartialUpdateMergeFunction implements 
MergeFunction<KeyValue> {
     public void add(KeyValue kv) {
         // refresh key object to avoid reference overwritten
         currentKey = kv.key();
-        currentDeleteRow = false;
         if (kv.valueKind().isRetract()) {
 
             if (!notNullColumnFilled) {
@@ -193,6 +193,7 @@ public class PartialUpdateMergeFunction implements 
MergeFunction<KeyValue> {
 
         latestSequenceNumber = kv.sequenceNumber();
         if (fieldSeqComparators.isEmpty()) {
+            currentDeleteRow = false;
             updateNonNullFields(kv);
         } else {
             updateWithSequenceGroup(kv);
@@ -257,6 +258,9 @@ public class PartialUpdateMergeFunction implements 
MergeFunction<KeyValue> {
                     if (Arrays.stream(seqComparator.compareFields())
                             .anyMatch(seqIndex -> seqIndex == index)) {
                         for (int fieldIndex : seqComparator.compareFields()) {
+                            if 
(sequenceGroupPartialDelete.contains(fieldIndex)) {
+                                currentDeleteRow = false;
+                            }
                             row.setField(
                                     fieldIndex, 
getters[fieldIndex].getFieldOrNull(kv.value()));
                             // Mark these sequence fields as processed
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupChangelogMergeFunctionWrapperTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupChangelogMergeFunctionWrapperTest.java
index db063462fb..d126eb6b86 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupChangelogMergeFunctionWrapperTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/LookupChangelogMergeFunctionWrapperTest.java
@@ -21,6 +21,7 @@ package org.apache.paimon.mergetree.compact;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.KeyValue;
 import org.apache.paimon.codegen.RecordEqualiser;
+import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.data.InternalRow.FieldGetter;
 import org.apache.paimon.lookup.LookupStrategy;
@@ -28,6 +29,7 @@ import 
org.apache.paimon.mergetree.compact.aggregate.AggregateMergeFunction;
 import org.apache.paimon.mergetree.compact.aggregate.FieldAggregator;
 import 
org.apache.paimon.mergetree.compact.aggregate.factory.FieldLastValueAggFactory;
 import 
org.apache.paimon.mergetree.compact.aggregate.factory.FieldSumAggFactory;
+import org.apache.paimon.options.Options;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
@@ -60,6 +62,58 @@ public class LookupChangelogMergeFunctionWrapperTest {
     private static final RecordEqualiser EQUALISER =
             (row1, row2) -> row1.getInt(0) == row2.getInt(0);
 
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    public void testSequenceGroupDeleteProducesDeleteChangelog(boolean 
lookupOldRow) {
+        RowType valueType =
+                RowType.of(
+                        DataTypes.INT().notNull(),
+                        DataTypes.INT(),
+                        DataTypes.INT(),
+                        DataTypes.INT(),
+                        DataTypes.INT());
+        Options options = new Options();
+        options.set("fields.f1.sequence-group", "f2");
+        options.set("fields.f3.sequence-group", "f4");
+        options.set("partial-update.remove-record-on-sequence-group", "f1");
+        KeyValue oldRow =
+                new KeyValue()
+                        .replace(row(1), 1, INSERT, GenericRow.of(1, 1, 10, 1, 
20))
+                        .setLevel(2);
+        LookupChangelogMergeFunctionWrapper function =
+                new LookupChangelogMergeFunctionWrapper(
+                        LookupMergeFunction.wrap(
+                                PartialUpdateMergeFunction.factory(
+                                        options, valueType, 
Collections.singletonList("f0")),
+                                null,
+                                null,
+                                null),
+                        key -> lookupOldRow ? oldRow : null,
+                        null,
+                        LookupStrategy.from(false, true, false, false),
+                        null,
+                        null);
+        function.reset();
+        if (!lookupOldRow) {
+            function.add(oldRow);
+        }
+        function.add(
+                new KeyValue()
+                        .replace(row(1), 2, DELETE, GenericRow.of(1, 2, 10, 
null, null))
+                        .setLevel(0));
+        function.add(
+                new KeyValue()
+                        .replace(row(1), 2, DELETE, GenericRow.of(1, null, 
null, 2, 20))
+                        .setLevel(0));
+
+        ChangelogResult result = function.getResult();
+        assertThat(result.result().valueKind()).isEqualTo(DELETE);
+        assertThat(result.result().value().getInt(1)).isEqualTo(2);
+        assertThat(result.changelogs()).hasSize(1);
+        assertThat(result.changelogs().get(0).valueKind()).isEqualTo(DELETE);
+        
assertThat(result.changelogs().get(0).value()).isEqualTo(oldRow.value());
+    }
+
     @ParameterizedTest
     @ValueSource(booleans = {false, true})
     public void testDeduplicate(boolean changelogRowDeduplicate) {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunctionTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunctionTest.java
index 3c7eca4e72..d8c70797a5 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunctionTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/PartialUpdateMergeFunctionTest.java
@@ -32,6 +32,7 @@ import 
org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
 import static org.apache.paimon.CoreOptions.FIELDS_DEFAULT_AGG_FUNC;
@@ -1154,6 +1155,174 @@ public class PartialUpdateMergeFunctionTest {
         validate(func, 1, 2, 2, null);
     }
 
+    @ParameterizedTest
+    @EnumSource(
+            value = RowKind.class,
+            names = {"DELETE", "UPDATE_BEFORE"})
+    public void testSequenceGroupDeleteSurvivesSubsequentRetractions(RowKind 
retractKind) {
+        MergeFunction<KeyValue> function = createSequenceGroupDeleteFunction();
+        function.reset();
+        add(function, 1, 1, 10, 1, 20);
+        add(function, RowKind.DELETE, 1, 2, 10, null, null);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+
+        add(function, retractKind, 1, null, null, 2, 20);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+        add(function, retractKind, 1, 1, 10, null, null);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+        add(function, retractKind, 1, null, null, null, null);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+        assertThat(function.getResult().value().getInt(1)).isEqualTo(2);
+    }
+
+    @ParameterizedTest
+    @EnumSource(
+            value = RowKind.class,
+            names = {"INSERT", "UPDATE_AFTER"})
+    public void 
testSequenceGroupDeleteOnlyRevivedByAcceptedGroupUpdate(RowKind updateKind) {
+        MergeFunction<KeyValue> function = createSequenceGroupDeleteFunction();
+        function.reset();
+        add(function, 1, 1, 10, 1, 20);
+        add(function, RowKind.DELETE, 1, 2, 10, null, null);
+
+        add(function, updateKind, 1, null, null, 3, 30);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+        add(function, updateKind, 1, 1, 99, null, null);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+        assertThat(function.getResult().value().getInt(1)).isEqualTo(2);
+
+        add(function, updateKind, 1, 3, 40, null, null);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.INSERT);
+        validate(function, 1, 3, 40, 3, 30);
+
+        add(function, RowKind.DELETE, 1, 2, 10, null, null);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.INSERT);
+    }
+
+    @Test
+    public void testSequenceGroupDeleteSurvivesMergeBoundaryAndReset() {
+        MergeFunction<KeyValue> function = createSequenceGroupDeleteFunction();
+        function.reset();
+        add(function, 1, 1, 10, 1, 20);
+        add(function, RowKind.DELETE, 1, 2, 10, null, null);
+        KeyValue result = function.getResult();
+        KeyValue deleted =
+                new KeyValue()
+                        .replace(
+                                result.key(),
+                                result.sequenceNumber(),
+                                result.valueKind(),
+                                result.value());
+
+        function.reset();
+        function.add(deleted);
+        add(function, 1, null, null, 3, 30);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+        assertThat(function.getResult().value().getInt(1)).isEqualTo(2);
+
+        function.reset();
+        function.add(
+                new KeyValue()
+                        .replace(
+                                GenericRow.of(2),
+                                sequence++,
+                                RowKind.INSERT,
+                                GenericRow.of(2, null, null, 1, 20)));
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.INSERT);
+        validate(function, 2, null, null, 1, 20);
+    }
+
+    @Test
+    public void testRemoveRecordOnDeleteIgnoresUpdateBeforeUntilInsert() {
+        Options options = new Options();
+        options.set("partial-update.remove-record-on-delete", "true");
+        MergeFunction<KeyValue> function =
+                PartialUpdateMergeFunction.factory(
+                                options,
+                                RowType.of(DataTypes.INT(), DataTypes.INT()),
+                                ImmutableList.of("f0"))
+                        .create();
+        function.reset();
+        add(function, 1, 10);
+        add(function, RowKind.DELETE, 1, 10);
+        add(function, RowKind.UPDATE_BEFORE, 1, 10);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.DELETE);
+
+        add(function, RowKind.INSERT, 1, 20);
+        assertThat(function.getResult().valueKind()).isEqualTo(RowKind.INSERT);
+        validate(function, 1, 20);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", "otherValue,id"})
+    public void 
testSequenceGroupDeleteStateWithProjectedCompositeSequence(String 
selectedFields) {
+        RowType rowType =
+                RowType.builder()
+                        .field("id", DataTypes.INT())
+                        .field("sequence", DataTypes.INT())
+                        .field("subSequence", DataTypes.INT())
+                        .field("value", DataTypes.INT())
+                        .field("otherSequence", DataTypes.INT())
+                        .field("otherValue", DataTypes.INT())
+                        .build();
+        Options options = new Options();
+        options.set("fields.sequence,subSequence.sequence-group", "value");
+        options.set("fields.otherSequence.sequence-group", "otherValue");
+        options.set("partial-update.remove-record-on-sequence-group", 
"sequence");
+        MergeFunctionFactory<KeyValue> factory =
+                PartialUpdateMergeFunction.factory(options, rowType, 
ImmutableList.of("id"));
+        RowType readType =
+                factory.adjustReadType(
+                        rowType.project(
+                                selectedFields.isEmpty()
+                                        ? new String[0]
+                                        : selectedFields.split(",")));
+        MergeFunction<KeyValue> function = factory.create(readType);
+        GenericRow[] records = {
+            GenericRow.of(1, 1, 1, 10, 1, 20),
+            GenericRow.of(1, 1, 2, 10, null, null),
+            GenericRow.of(1, null, null, null, 2, 20),
+            GenericRow.of(1, 1, 1, 99, null, null),
+            GenericRow.of(1, 1, 2, 30, null, null)
+        };
+        RowKind[] kinds = {
+            RowKind.INSERT, RowKind.DELETE, RowKind.DELETE, RowKind.INSERT, 
RowKind.UPDATE_AFTER
+        };
+        RowKind[] expected = {
+            RowKind.INSERT, RowKind.DELETE, RowKind.DELETE, RowKind.DELETE, 
RowKind.INSERT
+        };
+        function.reset();
+        for (int index = 0; index < records.length; index++) {
+            function.add(
+                    new KeyValue()
+                            .replace(
+                                    GenericRow.of(1),
+                                    sequence++,
+                                    kinds[index],
+                                    ProjectedRow.from(readType, rowType)
+                                            .replaceRow(records[index])));
+            assertThat(function.getResult().valueKind())
+                    .as("Projection %s, input %s", selectedFields, index)
+                    .isEqualTo(expected[index]);
+        }
+    }
+
+    private MergeFunction<KeyValue> createSequenceGroupDeleteFunction() {
+        Options options = new Options();
+        options.set("fields.f1.sequence-group", "f2");
+        options.set("fields.f3.sequence-group", "f4");
+        options.set("partial-update.remove-record-on-sequence-group", "f1");
+        RowType rowType =
+                RowType.of(
+                        DataTypes.INT().notNull(),
+                        DataTypes.INT(),
+                        DataTypes.INT(),
+                        DataTypes.INT(),
+                        DataTypes.INT());
+        return PartialUpdateMergeFunction.factory(options, rowType, 
ImmutableList.of("f0"))
+                .create();
+    }
+
     private void assertProjectedDelete(
             MergeFunction<KeyValue> function,
             ProjectedRow projection,

Reply via email to