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

twalthr 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 52af72b713a [FLINK-40528][table-planner] Address additional feedback 
for partial-delete StreamExecCalc
52af72b713a is described below

commit 52af72b713a61b1f4fc24a3fd2d0f43f27b80b2e
Author: Timo Walther <[email protected]>
AuthorDate: Wed Sep 16 13:19:58 2026 +0200

    [FLINK-40528][table-planner] Address additional feedback for partial-delete 
StreamExecCalc
    
    This closes #29192.
---
 .../plan/nodes/physical/stream/StreamPhysicalCalc.scala   | 10 +++++++---
 .../program/FlinkChangelogModeInferenceProgram.scala      | 15 ++++++++++++++-
 .../plan/nodes/exec/stream/DeletesByKeyPrograms.java      |  2 +-
 3 files changed, 22 insertions(+), 5 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalCalc.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalCalc.scala
index a13938bf786..79060d396af 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalCalc.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalCalc.scala
@@ -87,9 +87,13 @@ class StreamPhysicalCalc(
     }
 
     // Every column in every candidate is, by construction of
-    // FlinkRelMdUniqueKeys.getProjectUniqueKeys, guaranteed to be a trivial
-    // pass-through of an input field - never a risky expression to evaluate.
-    val keyIndices = outputUpsertKeys.flatMap(bitSet => 
bitSet.map(_.intValue())).toSet.toArray
+    // FlinkRelMdUniqueKeys.getProjectUniqueKeys, guaranteed to be a 
pass-through
+    // of an input field or an injective expression.
+    val keyIndices = outputUpsertKeys
+      .flatMap(bitSet => bitSet.map(_.intValue()))
+      .toSet
+      .toArray
+      .sorted
     if (keyIndices.nonEmpty) {
       keyIndices
     } else {
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkChangelogModeInferenceProgram.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkChangelogModeInferenceProgram.scala
index 300691a6982..e72d00e1aab 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkChangelogModeInferenceProgram.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkChangelogModeInferenceProgram.scala
@@ -1436,7 +1436,7 @@ class FlinkChangelogModeInferenceProgram extends 
FlinkOptimizeProgram[StreamOpti
         case calc: StreamPhysicalCalc =>
           if (
             requiredTrait == DeleteKindTrait.DELETE_BY_KEY &&
-            isNonUpsertKeyCondition(calc)
+            (isNonUpsertKeyCondition(calc) || !hasOutputUpsertKey(calc))
           ) {
             None
           } else {
@@ -1697,6 +1697,19 @@ class FlinkChangelogModeInferenceProgram extends 
FlinkOptimizeProgram[StreamOpti
     }
   }
 
+  /**
+   * Whether this calc's own output still has an upsert key after its 
projection. A DELETE_BY_KEY
+   * tombstone passed through this calc must still carry a key in its output, 
otherwise nothing
+   * downstream would know what to delete.
+   *
+   * This method is an extra safety net, in case downstream consumers don't 
require an upsert key.
+   */
+  private def hasOutputUpsertKey(calc: StreamPhysicalCalcBase): Boolean = {
+    val fmq = 
FlinkRelMetadataQuery.reuseOrCreate(calc.getCluster.getMetadataQuery)
+    val upsertKeys = fmq.getUpsertKeys(calc)
+    upsertKeys != null && upsertKeys.exists(!_.isEmpty)
+  }
+
   private def isNonUpsertKeyCondition(calc: StreamPhysicalCalcBase): Boolean = 
{
     val program = calc.getProgram
     if (program.getCondition == null) {
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/DeletesByKeyPrograms.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/DeletesByKeyPrograms.java
index c230b2fbfb1..d9b9b334437 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/DeletesByKeyPrograms.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/DeletesByKeyPrograms.java
@@ -300,7 +300,7 @@ public final class DeletesByKeyPrograms {
             TableTestProgram.of(
                             "delete-by-key-delete-by-key-with-expression",
                             "NOT NULL constraints have no effect. The row 
constructor expression"
-                                    + "is not evaluated for partial deletion.")
+                                    + " is not evaluated for partial 
deletion.")
                     .setupTableSource(
                             SourceTestStep.newBuilder("source_t")
                                     .addSchema(

Reply via email to