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 f8a28cdef6 [core] Allow primary-key-only deletes for deduplicate 
tables (#9978)
f8a28cdef6 is described below

commit f8a28cdef6a82c5d7fef4ee581969213ab01d002
Author: Akash Reddy Jammula <[email protected]>
AuthorDate: Sat Sep 19 22:47:20 2026 -0700

    [core] Allow primary-key-only deletes for deduplicate tables (#9978)
---
 .../paimon/table/PrimaryKeyFileStoreTable.java     |  35 +++-
 .../apache/paimon/table/sink/TableWriteImpl.java   |  62 ++++--
 .../apache/paimon/table/sink/TableWriteTest.java   | 210 +++++++++++++++++++++
 3 files changed, 290 insertions(+), 17 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/PrimaryKeyFileStoreTable.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/PrimaryKeyFileStoreTable.java
index f521f2e9b1..a478d17bfc 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/PrimaryKeyFileStoreTable.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/PrimaryKeyFileStoreTable.java
@@ -45,8 +45,10 @@ import org.apache.paimon.utils.RowKindFilter;
 
 import javax.annotation.Nullable;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 
 import static org.apache.paimon.predicate.PredicateBuilder.and;
 import static 
org.apache.paimon.predicate.PredicateBuilder.pickTransformFieldMapping;
@@ -81,11 +83,24 @@ public class PrimaryKeyFileStoreTable extends 
AbstractFileStoreTable {
                     PrimaryKeyTableUtils.PrimaryKeyFieldsExtractor.EXTRACTOR;
 
             RowType keyType = new RowType(extractor.keyFields(tableSchema));
+            List<String> deleteNotNullFields = deleteNotNullFields(options);
+            RowType valueType =
+                    deleteNotNullFields == null
+                            ? rowType
+                            : rowType.copy(
+                                    rowType.getFields().stream()
+                                            .map(
+                                                    field ->
+                                                            
deleteNotNullFields.contains(
+                                                                            
field.name())
+                                                                    ? field
+                                                                    : 
field.copy(true))
+                                            .collect(Collectors.toList()));
 
             MergeFunctionFactory<KeyValue> mfFactory =
                     
PrimaryKeyTableUtils.createMergeFunctionFactory(tableSchema);
             if (options.needLookup()) {
-                mfFactory = LookupMergeFunction.wrap(mfFactory, options, 
keyType, rowType);
+                mfFactory = LookupMergeFunction.wrap(mfFactory, options, 
keyType, valueType);
             }
 
             lazyStore =
@@ -97,7 +112,7 @@ public class PrimaryKeyFileStoreTable extends 
AbstractFileStoreTable {
                             options,
                             tableSchema.logicalPartitionType(),
                             keyType,
-                            rowType,
+                            valueType,
                             extractor,
                             mfFactory,
                             name(),
@@ -189,6 +204,7 @@ public class PrimaryKeyFileStoreTable extends 
AbstractFileStoreTable {
 
     private TableWriteImpl<KeyValue> newWrite(AbstractFileStoreWrite<KeyValue> 
storeWrite) {
         KeyValue kv = new KeyValue();
+        List<String> deleteNotNullFields = deleteNotNullFields(coreOptions());
         return new TableWriteImpl<>(
                 rowType(),
                 storeWrite,
@@ -200,7 +216,20 @@ public class PrimaryKeyFileStoreTable extends 
AbstractFileStoreTable {
                                 rowKind,
                                 record.row()),
                 rowKindGenerator(),
-                RowKindFilter.of(coreOptions()));
+                RowKindFilter.of(coreOptions()),
+                deleteNotNullFields);
+    }
+
+    @Nullable
+    private List<String> deleteNotNullFields(CoreOptions options) {
+        if (options.mergeEngine() != CoreOptions.MergeEngine.DEDUPLICATE
+                || schema().crossPartitionUpdate()) {
+            return null;
+        }
+
+        List<String> fields = new ArrayList<>(schema().primaryKeys());
+        fields.addAll(options.sequenceField());
+        return fields;
     }
 
     @Override
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableWriteImpl.java 
b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableWriteImpl.java
index cb26d47110..3d728aeed7 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/sink/TableWriteImpl.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/sink/TableWriteImpl.java
@@ -41,7 +41,9 @@ import org.apache.paimon.utils.RowKindFilter;
 
 import javax.annotation.Nullable;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.stream.Collectors;
 
@@ -63,8 +65,10 @@ public class TableWriteImpl<T> implements InnerTableWrite, 
Restorable<List<State
     private boolean batchCommitted = false;
     private RowType writeType;
     private int[] notNullFieldIndex;
+    private int[] deleteNotNullFieldIndex;
 
     private final @Nullable DefaultValueRow defaultValueRow;
+    private final @Nullable Set<String> deleteNotNullFieldNames;
 
     public TableWriteImpl(
             RowType rowType,
@@ -73,19 +77,33 @@ public class TableWriteImpl<T> implements InnerTableWrite, 
Restorable<List<State
             RecordExtractor<T> recordExtractor,
             @Nullable RowKindGenerator rowKindGenerator,
             @Nullable RowKindFilter rowKindFilter) {
+        this(
+                rowType,
+                write,
+                keyAndBucketExtractor,
+                recordExtractor,
+                rowKindGenerator,
+                rowKindFilter,
+                null);
+    }
+
+    public TableWriteImpl(
+            RowType rowType,
+            FileStoreWrite<T> write,
+            KeyAndBucketExtractor<InternalRow> keyAndBucketExtractor,
+            RecordExtractor<T> recordExtractor,
+            @Nullable RowKindGenerator rowKindGenerator,
+            @Nullable RowKindFilter rowKindFilter,
+            @Nullable List<String> deleteNotNullFieldNames) {
         this.writeType = rowType;
         this.write = write;
         this.keyAndBucketExtractor = keyAndBucketExtractor;
         this.recordExtractor = recordExtractor;
         this.rowKindGenerator = rowKindGenerator;
         this.rowKindFilter = rowKindFilter;
-
-        List<String> notNullColumnNames =
-                rowType.getFields().stream()
-                        .filter(field -> !field.type().isNullable())
-                        .map(DataField::name)
-                        .collect(Collectors.toList());
-        this.notNullFieldIndex = rowType.getFieldIndices(notNullColumnNames);
+        this.deleteNotNullFieldNames =
+                deleteNotNullFieldNames == null ? null : new 
HashSet<>(deleteNotNullFieldNames);
+        updateNotNullFieldIndexes();
         this.defaultValueRow = DefaultValueRow.create(rowType);
     }
 
@@ -115,13 +133,26 @@ public class TableWriteImpl<T> implements 
InnerTableWrite, Restorable<List<State
     public TableWriteImpl<T> withWriteType(RowType writeType) {
         write.withWriteType(writeType);
         this.writeType = writeType;
+        updateNotNullFieldIndexes();
+        return this;
+    }
+
+    private void updateNotNullFieldIndexes() {
         List<String> notNullColumnNames =
                 writeType.getFields().stream()
                         .filter(field -> !field.type().isNullable())
                         .map(DataField::name)
                         .collect(Collectors.toList());
         this.notNullFieldIndex = writeType.getFieldIndices(notNullColumnNames);
-        return this;
+        if (deleteNotNullFieldNames == null) {
+            this.deleteNotNullFieldIndex = notNullFieldIndex;
+        } else {
+            List<String> deleteNotNullColumnNames =
+                    notNullColumnNames.stream()
+                            .filter(deleteNotNullFieldNames::contains)
+                            .collect(Collectors.toList());
+            this.deleteNotNullFieldIndex = 
writeType.getFieldIndices(deleteNotNullColumnNames);
+        }
     }
 
     @Override
@@ -203,13 +234,14 @@ public class TableWriteImpl<T> implements 
InnerTableWrite, Restorable<List<State
     @Nullable
     private SinkRecord writeAndReturn(InternalRow row, int bucket, @Nullable 
Integer totalBuckets)
             throws Exception {
-        checkNullability(row);
-        row = wrapDefaultValue(row);
-        RowKind rowKind = RowKindGenerator.getRowKind(rowKindGenerator, row);
+        InternalRow wrappedRow = wrapDefaultValue(row);
+        RowKind rowKind = RowKindGenerator.getRowKind(rowKindGenerator, 
wrappedRow);
+        checkNullability(row, rowKind);
         if (rowKindFilter != null && !rowKindFilter.test(rowKind)) {
             return null;
         }
-        SinkRecord record = bucket == -1 ? toSinkRecord(row) : 
toSinkRecord(row, bucket);
+        SinkRecord record =
+                bucket == -1 ? toSinkRecord(wrappedRow) : 
toSinkRecord(wrappedRow, bucket);
         T extracted = recordExtractor.extract(record, rowKind);
         if (totalBuckets == null) {
             write.write(record.partition(), record.bucket(), extracted);
@@ -219,8 +251,10 @@ public class TableWriteImpl<T> implements InnerTableWrite, 
Restorable<List<State
         return record;
     }
 
-    private void checkNullability(InternalRow row) {
-        for (int idx : notNullFieldIndex) {
+    private void checkNullability(InternalRow row, RowKind rowKind) {
+        int[] fieldIndexes =
+                rowKind == RowKind.DELETE ? deleteNotNullFieldIndex : 
notNullFieldIndex;
+        for (int idx : fieldIndexes) {
             if (row.isNullAt(idx)) {
                 String columnName = writeType.getFields().get(idx).name();
                 throw new RuntimeException(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java 
b/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java
index afef435380..3441dadecf 100644
--- a/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java
@@ -21,6 +21,7 @@ package org.apache.paimon.table.sink;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.disk.IOManagerImpl;
@@ -316,6 +317,173 @@ public class TableWriteTest {
         write.close();
     }
 
+    @Test
+    public void testDeduplicateDeleteWithPrimaryKeyOnly() throws Exception {
+        FileStoreTable table = createNotNullFileStoreTable(new Options());
+        TableWriteImpl<?> write = table.newWrite(commitUser);
+        StreamTableCommit commit = table.newCommit(commitUser);
+
+        write.write(GenericRow.of(1, 1, 10L));
+        commit.commit(0, write.prepareCommit(false, 0));
+
+        assertThatThrownBy(() -> write.write(GenericRow.ofKind(RowKind.DELETE, 
1, null, null)))
+                .hasMessageContaining("non-null column(k)");
+        assertThatThrownBy(() -> write.write(GenericRow.of(1, 1, null)))
+                .hasMessageContaining("non-null column(v)");
+        assertThatThrownBy(() -> 
write.write(GenericRow.ofKind(RowKind.UPDATE_BEFORE, 1, 1, null)))
+                .hasMessageContaining("non-null column(v)");
+        assertThatThrownBy(() -> 
write.write(GenericRow.ofKind(RowKind.UPDATE_AFTER, 1, 1, null)))
+                .hasMessageContaining("non-null column(v)");
+
+        write.write(GenericRow.ofKind(RowKind.DELETE, 1, 1, null));
+        commit.commit(1, write.prepareCommit(false, 1));
+
+        assertThat(readRows(table)).isEmpty();
+        write.close();
+        commit.close();
+    }
+
+    @Test
+    public void testLookupDeleteWithPrimaryKeyOnly() throws Exception {
+        Options options = new Options();
+        options.set(CoreOptions.CHANGELOG_PRODUCER, 
CoreOptions.ChangelogProducer.LOOKUP);
+        options.set(CoreOptions.LOOKUP_MERGE_RECORDS_THRESHOLD, 1);
+        FileStoreTable table = createNotNullFileStoreTable(options);
+        TableWriteImpl<?> write =
+                table.newWrite(commitUser)
+                        .withIOManager(
+                                new IOManagerImpl(
+                                        
tempDir.resolve(UUID.randomUUID().toString()).toString()));
+        StreamTableCommit commit = table.newCommit(commitUser);
+
+        write.write(GenericRow.of(1, 1, 10L));
+        commit.commit(0, write.prepareCommit(false, 0));
+        write.write(GenericRow.ofKind(RowKind.DELETE, 1, 1, null));
+        commit.commit(1, write.prepareCommit(false, 1));
+
+        assertThat(readRows(table)).isEmpty();
+        write.close();
+        commit.close();
+    }
+
+    @Test
+    public void testPartialUpdateDeleteRequiresNonKeyFields() throws Exception 
{
+        Options options = new Options();
+        options.set(CoreOptions.MERGE_ENGINE, 
CoreOptions.MergeEngine.PARTIAL_UPDATE);
+        FileStoreTable table = createNotNullFileStoreTable(options);
+        TableWriteImpl<?> write = table.newWrite(commitUser);
+
+        assertThatThrownBy(() -> write.write(GenericRow.ofKind(RowKind.DELETE, 
1, 1, null)))
+                .hasMessageContaining("non-null column(v)");
+
+        write.close();
+    }
+
+    @Test
+    public void testDeleteRequiresSequenceField() throws Exception {
+        Options options = new Options();
+        options.set(CoreOptions.SEQUENCE_FIELD, "seq");
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.INT().notNull(),
+                            DataTypes.INT().notNull(),
+                            DataTypes.INT().notNull(),
+                            DataTypes.BIGINT().notNull()
+                        },
+                        new String[] {"pt", "k", "seq", "v"});
+        FileStoreTable table =
+                createFileStoreTable(
+                        rowType,
+                        Collections.singletonList("pt"),
+                        Arrays.asList("pt", "k"),
+                        options);
+        TableWriteImpl<?> write = table.newWrite(commitUser);
+        StreamTableCommit commit = table.newCommit(commitUser);
+
+        write.write(GenericRow.of(1, 1, 10, 100L));
+        commit.commit(0, write.prepareCommit(false, 0));
+
+        assertThatThrownBy(() -> write.write(GenericRow.ofKind(RowKind.DELETE, 
1, 1, null, null)))
+                .hasMessageContaining("non-null column(seq)");
+
+        write.write(GenericRow.ofKind(RowKind.DELETE, 1, 1, 9, null));
+        commit.commit(1, write.prepareCommit(false, 1));
+        List<InternalRow> rows = readRows(table);
+        assertThat(rows).hasSize(1);
+        assertThat(rows.get(0).getInt(2)).isEqualTo(10);
+        assertThat(rows.get(0).getLong(3)).isEqualTo(100L);
+
+        write.write(GenericRow.ofKind(RowKind.DELETE, 1, 1, 11, null));
+        commit.commit(2, write.prepareCommit(false, 2));
+        assertThat(readRows(table)).isEmpty();
+
+        write.close();
+        commit.close();
+    }
+
+    @Test
+    public void testGeneratedDeleteWithPrimaryKeyOnly() throws Exception {
+        Options options = new Options();
+        options.set(CoreOptions.ROWKIND_FIELD, "kind");
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.INT().notNull(),
+                            DataTypes.INT().notNull(),
+                            DataTypes.BIGINT().notNull(),
+                            DataTypes.STRING().notNull()
+                        },
+                        new String[] {"pt", "k", "v", "kind"});
+        FileStoreTable table =
+                createFileStoreTable(
+                        rowType,
+                        Collections.singletonList("pt"),
+                        Arrays.asList("pt", "k"),
+                        options);
+        TableWriteImpl<?> write = table.newWrite(commitUser);
+        StreamTableCommit commit = table.newCommit(commitUser);
+
+        write.write(GenericRow.of(1, 1, 10L, BinaryString.fromString("+I")));
+        commit.commit(0, write.prepareCommit(false, 0));
+        assertThatThrownBy(
+                        () -> write.write(GenericRow.of(1, 1, null, 
BinaryString.fromString("-U"))))
+                .hasMessageContaining("non-null column(v)");
+        assertThatThrownBy(
+                        () -> write.write(GenericRow.of(1, 1, null, 
BinaryString.fromString("+U"))))
+                .hasMessageContaining("non-null column(v)");
+        write.write(GenericRow.of(1, 1, null, BinaryString.fromString("-D")));
+        commit.commit(1, write.prepareCommit(false, 1));
+
+        assertThat(readRows(table)).isEmpty();
+        write.close();
+        commit.close();
+    }
+
+    @Test
+    public void testCrossPartitionDeleteRequiresPartitionField() throws 
Exception {
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.INT().notNull(),
+                            DataTypes.INT().notNull(),
+                            DataTypes.BIGINT().notNull()
+                        },
+                        new String[] {"pt", "k", "v"});
+        FileStoreTable table =
+                createFileStoreTable(
+                        rowType,
+                        Collections.singletonList("pt"),
+                        Collections.singletonList("k"),
+                        new Options());
+        TableWriteImpl<?> write = table.newWrite(commitUser);
+
+        assertThatThrownBy(() -> write.write(GenericRow.ofKind(RowKind.DELETE, 
null, 1, null)))
+                .hasMessageContaining("non-null column(pt)");
+
+        write.close();
+    }
+
     private BinaryRow partition(int x) {
         BinaryRow partition = new BinaryRow(1);
         BinaryRowWriter writer = new BinaryRowWriter(partition);
@@ -335,6 +503,48 @@ public class TableWriteTest {
         return actual;
     }
 
+    private List<InternalRow> readRows(FileStoreTable table) throws Exception {
+        List<InternalRow> rows = new ArrayList<>();
+        TableScan.Plan plan = table.newScan().plan();
+        try (RecordReaderIterator<InternalRow> iterator =
+                new 
RecordReaderIterator<>(table.newRead().createReader(plan))) {
+            while (iterator.hasNext()) {
+                rows.add(iterator.next());
+            }
+        }
+        return rows;
+    }
+
+    private FileStoreTable createNotNullFileStoreTable(Options options) throws 
Exception {
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.INT().notNull(),
+                            DataTypes.INT().notNull(),
+                            DataTypes.BIGINT().notNull()
+                        },
+                        new String[] {"pt", "k", "v"});
+        return createFileStoreTable(
+                rowType, Collections.singletonList("pt"), Arrays.asList("pt", 
"k"), options);
+    }
+
+    private FileStoreTable createFileStoreTable(
+            RowType rowType, List<String> partitionKeys, List<String> 
primaryKeys, Options options)
+            throws Exception {
+        options.set(CoreOptions.BUCKET, 1);
+        Path path = new 
Path(tempDir.resolve(UUID.randomUUID().toString()).toUri());
+        TableSchema tableSchema =
+                SchemaUtils.forceCommit(
+                        new FileSystemSchemaManager(LocalFileIO.create(), 
path),
+                        new Schema(
+                                rowType.getFields(),
+                                partitionKeys,
+                                primaryKeys,
+                                options.toMap(),
+                                ""));
+        return FileStoreTableFactory.create(LocalFileIO.create(), path, 
tableSchema);
+    }
+
     private FileStoreTable createFileStoreTable(Options conf) throws Exception 
{
         TableSchema tableSchema =
                 SchemaUtils.forceCommit(

Reply via email to