This is an automated email from the ASF dual-hosted git repository.

fhueske pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new d554162df59 [FLINK-40477][table] Fix constraint enforcer for partial 
deletes (#29067)
d554162df59 is described below

commit d554162df59e643b256ee1a2de0e12a8e045a317
Author: Fabian Hueske <[email protected]>
AuthorDate: Thu Sep 3 12:52:05 2026 +0200

    [FLINK-40477][table] Fix constraint enforcer for partial deletes (#29067)
    
    The sink NOT NULL enforcer inspected every row regardless of RowKind,
    so a by-key (partial) delete tombstone, whose non-key columns are 
legitimately null,
    was rejected (ERROR) or silently dropped (DROP), losing the delete.
    
    Relax NOT NULL for the non-key columns of DELETE rows in key-only-delete 
pipelines;
    key columns and all other row kinds stay enforced.
    
    Co-Generated: Claude Opus 4.8 (1M context)
---
 .../plan/nodes/exec/common/CommonExecSink.java     |  13 +-
 .../stream/ConstraintEnforcerSemanticTests.java    |   8 +-
 .../stream/ConstraintEnforcerTestPrograms.java     | 200 +++++++++++++++++++++
 .../constraint/ConstraintEnforcerExecutor.java     |  48 ++++-
 .../sink/constraint/NotNullConstraint.java         |  18 +-
 5 files changed, 278 insertions(+), 9 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/common/CommonExecSink.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/common/CommonExecSink.java
index 3738840ab27..ebfc58b91b6 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/common/CommonExecSink.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/common/CommonExecSink.java
@@ -198,7 +198,12 @@ public abstract class CommonExecSink extends 
ExecNodeBase<Object>
                 upsertMaterialize && (!inputInsertOnly || 
detectsDuplicateKeys(conflictStrategy));
 
         Transformation<RowData> sinkTransform =
-                applyConstraintValidations(inputTransform, config, 
persistedRowType);
+                applyConstraintValidations(
+                        inputTransform,
+                        config,
+                        persistedRowType,
+                        primaryKeys,
+                        inputChangelogMode.keyOnlyDeletes());
 
         if (hasPk) {
             sinkTransform =
@@ -270,10 +275,14 @@ public abstract class CommonExecSink extends 
ExecNodeBase<Object>
     private Transformation<RowData> applyConstraintValidations(
             Transformation<RowData> inputTransform,
             ExecNodeConfig config,
-            RowType physicalRowType) {
+            RowType physicalRowType,
+            int[] primaryKeys,
+            boolean keyOnlyDeletes) {
         final Optional<ConstraintEnforcerExecutor> enforcerExecutor =
                 ConstraintEnforcerExecutor.create(
                         physicalRowType,
+                        primaryKeys,
+                        keyOnlyDeletes,
                         
config.get(ExecutionConfigOptions.TABLE_EXEC_SINK_NOT_NULL_ENFORCER),
                         
config.get(ExecutionConfigOptions.TABLE_EXEC_SINK_TYPE_LENGTH_ENFORCER),
                         config.get(
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerSemanticTests.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerSemanticTests.java
index 5f08ddea816..7e0cac96748 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerSemanticTests.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerSemanticTests.java
@@ -44,6 +44,12 @@ public class ConstraintEnforcerSemanticTests extends 
SemanticTestBase {
                 
ConstraintEnforcerTestPrograms.CHAR_LENGTH_TRIM_PAD_WITH_NULLABLE_COLUMNS,
                 
ConstraintEnforcerTestPrograms.BINARY_LENGTH_TRIM_PAD_WITH_NULLABLE_COLUMNS,
                 
ConstraintEnforcerTestPrograms.CHAR_LENGTH_ERROR_WITH_NULLABLE_COLUMNS,
-                
ConstraintEnforcerTestPrograms.BINARY_LENGTH_ERROR_WITH_NULLABLE_COLUMNS);
+                
ConstraintEnforcerTestPrograms.BINARY_LENGTH_ERROR_WITH_NULLABLE_COLUMNS,
+                ConstraintEnforcerTestPrograms.NOT_NULL_ERROR_DELETE_BY_KEY,
+                ConstraintEnforcerTestPrograms.NOT_NULL_DROP_DELETE_BY_KEY,
+                
ConstraintEnforcerTestPrograms.NOT_NULL_ERROR_INSERT_IN_DELETE_BY_KEY,
+                
ConstraintEnforcerTestPrograms.NOT_NULL_ERROR_DELETE_BY_KEY_NULL_KEY,
+                
ConstraintEnforcerTestPrograms.NOT_NULL_ERROR_DELETE_BY_KEY_NESTED_ROW,
+                
ConstraintEnforcerTestPrograms.NOT_NULL_ERROR_DELETE_BY_KEY_COMPOSITE_PK);
     }
 }
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerTestPrograms.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerTestPrograms.java
index f777486c221..5f03fb5f815 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerTestPrograms.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ConstraintEnforcerTestPrograms.java
@@ -28,6 +28,7 @@ import org.apache.flink.table.test.program.SinkTestStep;
 import org.apache.flink.table.test.program.SourceTestStep;
 import org.apache.flink.table.test.program.TableTestProgram;
 import org.apache.flink.types.Row;
+import org.apache.flink.types.RowKind;
 
 import javax.annotation.Nullable;
 
@@ -937,6 +938,205 @@ public class ConstraintEnforcerTestPrograms {
                     .runSql("INSERT INTO sink_t SELECT * FROM source_t")
                     .build();
 
+    // 
------------------------------------------------------------------------------------------
+    // Delete-by-key: a by-key delete might carry null in its non-key columns, 
regardless
+    // of nullability constraints. The constraint enforcer should only check 
key columns for
+    // by-key delete messages and ignores all value columns.
+    // 
------------------------------------------------------------------------------------------
+
+    public static final String SCHEMA_DELETE_BY_KEY_NOT_NULL =
+            "id INT PRIMARY KEY NOT ENFORCED, v INT NOT NULL";
+
+    static final TableTestProgram NOT_NULL_ERROR_DELETE_BY_KEY =
+            TableTestProgram.of(
+                            "constraint-enforcer-error-not-null-delete-by-key",
+                            "validates that a by-key delete carrying null in a 
scalar NOT NULL"
+                                    + " non-key column is applied rather than 
rejected by the NOT"
+                                    + " NULL enforcer (ERROR strategy)")
+                    .setupConfig(TABLE_EXEC_SINK_NOT_NULL_ENFORCER, 
NotNullEnforcer.ERROR)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    
.addOption("source.produces-delete-by-key", "true")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, 1, 10),
+                                            Row.ofKind(RowKind.INSERT, 2, 20),
+                                            // tombstone: key only, NOT NULL 
value column is null
+                                            Row.ofKind(RowKind.DELETE, 1, 
null))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .addOption("sink.supports-delete-by-key", 
"true")
+                                    .testMaterializedData()
+                                    .consumedValues("+I[2, 20]")
+                                    .build())
+                    .runSql("INSERT INTO sink_t SELECT id, v FROM source_t")
+                    .build();
+
+    static final TableTestProgram NOT_NULL_DROP_DELETE_BY_KEY =
+            TableTestProgram.of(
+                            "constraint-enforcer-drop-not-null-delete-by-key",
+                            "validates that a by-key delete carrying null in a 
scalar NOT NULL"
+                                    + " non-key column is applied rather than 
silently dropped by the"
+                                    + " NOT NULL enforcer (DROP strategy), 
which would leave a stale"
+                                    + " row")
+                    .setupConfig(TABLE_EXEC_SINK_NOT_NULL_ENFORCER, 
NotNullEnforcer.DROP)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    
.addOption("source.produces-delete-by-key", "true")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, 1, 10),
+                                            Row.ofKind(RowKind.INSERT, 2, 20),
+                                            // tombstone: key only, NOT NULL 
value column is null
+                                            Row.ofKind(RowKind.DELETE, 1, 
null))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .addOption("sink.supports-delete-by-key", 
"true")
+                                    .testMaterializedData()
+                                    .consumedValues("+I[2, 20]")
+                                    .build())
+                    .runSql("INSERT INTO sink_t SELECT id, v FROM source_t")
+                    .build();
+
+    static final TableTestProgram NOT_NULL_ERROR_INSERT_IN_DELETE_BY_KEY =
+            TableTestProgram.of(
+                            
"constraint-enforcer-error-not-null-insert-in-delete-by-key",
+                            "validates that in a key-only-delete pipeline an 
INSERT carrying null in"
+                                    + " a NOT NULL column is still rejected by 
the NOT NULL enforcer")
+                    .setupConfig(TABLE_EXEC_SINK_NOT_NULL_ENFORCER, 
NotNullEnforcer.ERROR)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    
.addOption("source.produces-delete-by-key", "true")
+                                    .producedValues(
+                                            // genuine violation: an INSERT 
with a null NOT NULL
+                                            // value
+                                            Row.ofKind(RowKind.INSERT, 1, 
null))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .addOption("sink.supports-delete-by-key", 
"true")
+                                    .consumedValues(new Row[0])
+                                    .build())
+                    .runFailingSql(
+                            "INSERT INTO sink_t SELECT id, v FROM source_t",
+                            TableRuntimeException.class,
+                            "Column 'v' is NOT NULL, however, a null value is 
being written into it."
+                                    + " You can set job configuration"
+                                    + " 
'table.exec.sink.not-null-enforcer'='DROP' to suppress this"
+                                    + " exception and drop such records 
silently.")
+                    .build();
+
+    static final TableTestProgram NOT_NULL_ERROR_DELETE_BY_KEY_NULL_KEY =
+            TableTestProgram.of(
+                            
"constraint-enforcer-error-not-null-delete-by-key-null-key",
+                            "validates that a by-key delete carrying null in 
its NOT NULL key column"
+                                    + " is still rejected by the NOT NULL 
enforcer")
+                    .setupConfig(TABLE_EXEC_SINK_NOT_NULL_ENFORCER, 
NotNullEnforcer.ERROR)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    
.addOption("source.produces-delete-by-key", "true")
+                                    .producedValues(
+                                            // genuine violation: the key 
column is null on a delete
+                                            Row.ofKind(RowKind.DELETE, null, 
null))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    .addSchema(SCHEMA_DELETE_BY_KEY_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .addOption("sink.supports-delete-by-key", 
"true")
+                                    .consumedValues(new Row[0])
+                                    .build())
+                    .runFailingSql(
+                            "INSERT INTO sink_t SELECT id, v FROM source_t",
+                            TableRuntimeException.class,
+                            "Column 'id' is NOT NULL, however, a null value is 
being written into it."
+                                    + " You can set job configuration"
+                                    + " 
'table.exec.sink.not-null-enforcer'='DROP' to suppress this"
+                                    + " exception and drop such records 
silently.")
+                    .build();
+
+    public static final String SCHEMA_DELETE_BY_KEY_NESTED_NOT_NULL =
+            "id INT PRIMARY KEY NOT ENFORCED, v ROW<a INT NOT NULL, b INT> NOT 
NULL";
+
+    static final TableTestProgram NOT_NULL_ERROR_DELETE_BY_KEY_NESTED_ROW =
+            TableTestProgram.of(
+                            
"constraint-enforcer-error-not-null-delete-by-key-nested-row",
+                            "validates that a by-key delete carrying a null 
nested NOT NULL ROW"
+                                    + " non-key column is applied rather than 
rejected, with nested"
+                                    + " constraint checking enabled")
+                    .setupConfig(TABLE_EXEC_SINK_NESTED_CONSTRAINT_ENFORCER, 
NestedEnforcer.ROWS)
+                    .setupConfig(TABLE_EXEC_SINK_NOT_NULL_ENFORCER, 
NotNullEnforcer.ERROR)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    
.addSchema(SCHEMA_DELETE_BY_KEY_NESTED_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    
.addOption("source.produces-delete-by-key", "true")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, 1, 
Row.of(10, 100)),
+                                            Row.ofKind(RowKind.INSERT, 2, 
Row.of(20, 200)),
+                                            // tombstone: key only, the nested 
NOT NULL ROW is null
+                                            Row.ofKind(RowKind.DELETE, 1, 
null))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    
.addSchema(SCHEMA_DELETE_BY_KEY_NESTED_NOT_NULL)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .addOption("sink.supports-delete-by-key", 
"true")
+                                    .testMaterializedData()
+                                    .consumedValues("+I[2, +I[20, 200]]")
+                                    .build())
+                    .runSql("INSERT INTO sink_t SELECT id, v FROM source_t")
+                    .build();
+
+    // Multi-column, non-leading primary key: the key mask is index-based (not 
positional), so on a
+    // by-key delete the NOT NULL value columns on both sides of the key (v1, 
v2) are relaxed while
+    // both key columns (id1, id2) are still enforced. Guards against a 
positional regression.
+    public static final String SCHEMA_DELETE_BY_KEY_COMPOSITE_PK =
+            "v1 INT NOT NULL, id1 INT, id2 INT, v2 INT NOT NULL, PRIMARY KEY 
(id1, id2) NOT ENFORCED";
+
+    static final TableTestProgram NOT_NULL_ERROR_DELETE_BY_KEY_COMPOSITE_PK =
+            TableTestProgram.of(
+                            
"constraint-enforcer-error-not-null-delete-by-key-composite-pk",
+                            "validates that a by-key delete with a 
multi-column, non-leading primary"
+                                    + " key is applied: value columns around 
the key are relaxed"
+                                    + " while both key columns remain 
enforced")
+                    .setupConfig(TABLE_EXEC_SINK_NOT_NULL_ENFORCER, 
NotNullEnforcer.ERROR)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    
.addSchema(SCHEMA_DELETE_BY_KEY_COMPOSITE_PK)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    
.addOption("source.produces-delete-by-key", "true")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, 10, 1, 
2, 20),
+                                            Row.ofKind(RowKind.INSERT, 11, 3, 
4, 21),
+                                            // tombstone: only the key columns 
id1, id2 are set
+                                            Row.ofKind(RowKind.DELETE, null, 
1, 2, null))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    
.addSchema(SCHEMA_DELETE_BY_KEY_COMPOSITE_PK)
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .addOption("sink.supports-delete-by-key", 
"true")
+                                    .testMaterializedData()
+                                    .consumedValues("+I[11, 3, 4, 21]")
+                                    .build())
+                    .runSql("INSERT INTO sink_t SELECT v1, id1, id2, v2 FROM 
source_t")
+                    .build();
+
     private static Map<Long, Long> mapOfNullable(@Nullable Long key, @Nullable 
Long value) {
         final Map<Long, Long> map = new HashMap<>();
         map.put(key, value);
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/ConstraintEnforcerExecutor.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/ConstraintEnforcerExecutor.java
index b8377af468e..8c64b4a71b5 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/ConstraintEnforcerExecutor.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/ConstraintEnforcerExecutor.java
@@ -56,6 +56,7 @@ import java.util.stream.Stream;
 public class ConstraintEnforcerExecutor implements Serializable {
 
     private static final long serialVersionUID = 1L;
+    private static final int[] NO_PRIMARY_KEYS = new int[0];
     private final Constraint[] constraints;
 
     private ConstraintEnforcerExecutor(Constraint[] constraints) {
@@ -68,18 +69,44 @@ public class ConstraintEnforcerExecutor implements 
Serializable {
 
     public static Optional<ConstraintEnforcerExecutor> create(
             final RowType physicalType,
+            final int[] primaryKeys,
+            final boolean keyOnlyDeletes,
             final NotNullEnforcer notNullEnforcer,
             final TypeLengthEnforcer typeLengthEnforcer,
             final NestedEnforcer nestedConstraints) {
         final Constraint[] topLevelConstraints =
                 createConstraints(
-                        physicalType, notNullEnforcer, typeLengthEnforcer, 
nestedConstraints);
+                        physicalType,
+                        keyOnlyDeletes,
+                        primaryKeys,
+                        notNullEnforcer,
+                        typeLengthEnforcer,
+                        nestedConstraints);
 
         return create(topLevelConstraints);
     }
 
+    private static Constraint[] createNestedConstraints(
+            final RowType physicalType,
+            final NotNullEnforcer notNullEnforcer,
+            final TypeLengthEnforcer typeLengthEnforcer,
+            final NestedEnforcer nestedEnforcer) {
+        // Nested constraints are never relaxed for partial deletes: a by-key 
delete nulls the whole
+        // top-level field, so the nested constraint skips it (it only 
descends into non-null
+        // fields).
+        return createConstraints(
+                physicalType,
+                false,
+                NO_PRIMARY_KEYS,
+                notNullEnforcer,
+                typeLengthEnforcer,
+                nestedEnforcer);
+    }
+
     private static Constraint[] createConstraints(
             final RowType physicalType,
+            final boolean keyOnlyDeletes,
+            final int[] primaryKeys,
             final NotNullEnforcer notNullEnforcer,
             final TypeLengthEnforcer typeLengthEnforcer,
             final NestedEnforcer nestedEnforcer) {
@@ -94,11 +121,22 @@ public class ConstraintEnforcerExecutor implements 
Serializable {
                             .mapToObj(idx -> fieldNames[idx])
                             .toArray(String[]::new);
 
+            final BitSet primaryKeySet = new BitSet();
+            for (int pk : primaryKeys) {
+                primaryKeySet.set(pk);
+            }
+            final boolean[] keyField = new boolean[notNullFieldIndices.length];
+            for (int i = 0; i < notNullFieldIndices.length; i++) {
+                keyField[i] = primaryKeySet.get(notNullFieldIndices[i]);
+            }
+
             constraints.add(
                     new NotNullConstraint(
                             NotNullEnforcementStrategy.of(notNullEnforcer),
                             notNullFieldIndices,
-                            notNullFieldNames));
+                            notNullFieldNames,
+                            keyOnlyDeletes,
+                            keyField));
         }
 
         if (typeLengthEnforcer != TypeLengthEnforcer.IGNORE) {
@@ -144,7 +182,7 @@ public class ConstraintEnforcerExecutor implements 
Serializable {
                     nestedRowInfo.stream()
                             .map(
                                     r ->
-                                            createConstraints(
+                                            createNestedConstraints(
                                                     r.getFieldType(),
                                                     notNullEnforcer,
                                                     typeLengthEnforcer,
@@ -184,7 +222,7 @@ public class ConstraintEnforcerExecutor implements 
Serializable {
                     nestedArrayInfos.stream()
                             .map(
                                     r1 ->
-                                            createConstraints(
+                                            createNestedConstraints(
                                                     new RowType(
                                                             List.of(
                                                                     new 
RowType.RowField(
@@ -223,7 +261,7 @@ public class ConstraintEnforcerExecutor implements 
Serializable {
                     nestedMapInfos.stream()
                             .map(
                                     r1 ->
-                                            createConstraints(
+                                            createNestedConstraints(
                                                     new RowType(
                                                             List.of(
                                                                     new 
RowType.RowField(
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/NotNullConstraint.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/NotNullConstraint.java
index f2ae509c5b5..984273fc341 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/NotNullConstraint.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/constraint/NotNullConstraint.java
@@ -21,6 +21,7 @@ package 
org.apache.flink.table.runtime.operators.sink.constraint;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.table.api.config.ExecutionConfigOptions;
 import org.apache.flink.table.data.RowData;
+import org.apache.flink.types.RowKind;
 
 import javax.annotation.Nullable;
 
@@ -32,20 +33,35 @@ final class NotNullConstraint implements Constraint {
     private final NotNullEnforcementStrategy enforcementStrategy;
     private final int[] notNullFieldIndices;
     private final String[] notNullFieldNames;
+    // Whether the input is a key-only-delete pipeline, in which a DELETE 
tombstone carries only its
+    // key and sets all non-key fields to null regardless of nullability 
constraints.
+    private final boolean keyOnlyDeletes;
+    // Aligned with notNullFieldIndices: true if the field is part of the 
primary key.
+    private final boolean[] keyField;
 
     NotNullConstraint(
             NotNullEnforcementStrategy enforcementStrategy,
             int[] notNullFieldIndices,
-            String[] notNullFieldNames) {
+            String[] notNullFieldNames,
+            boolean keyOnlyDeletes,
+            boolean[] keyField) {
         this.enforcementStrategy = enforcementStrategy;
         this.notNullFieldIndices = notNullFieldIndices;
         this.notNullFieldNames = notNullFieldNames;
+        this.keyOnlyDeletes = keyOnlyDeletes;
+        this.keyField = keyField;
     }
 
     @Nullable
     @Override
     public RowData enforce(RowData input) {
+        // A key-only DELETE legitimately carries null in its non-key columns, 
so skip those.
+        // Key columns and all columns of other row kinds are always enforced.
+        final boolean partialDelete = keyOnlyDeletes && input.getRowKind() == 
RowKind.DELETE;
         for (int i = 0; i < notNullFieldIndices.length; i++) {
+            if (partialDelete && !keyField[i]) {
+                continue;
+            }
             final int index = notNullFieldIndices[i];
             if (input.isNullAt(index)) {
                 switch (enforcementStrategy) {

Reply via email to