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

gustavodemorais 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 e9001f6a85c [FLINK-38233][table] Adds support to 
StreamNonDeterministicUpdatePlanVisitor for PTFs
e9001f6a85c is described below

commit e9001f6a85ca1c4f4a7cc8a275d8dcbaf7f98e77
Author: Alan Sheinberg <[email protected]>
AuthorDate: Wed Jul 1 01:20:26 2026 -0700

    [FLINK-38233][table] Adds support to 
StreamNonDeterministicUpdatePlanVisitor for PTFs
    
    This closes #28061.
---
 .../StreamNonDeterministicUpdatePlanVisitor.java   |  88 ++++++++
 .../table/api/QueryOperationTestPrograms.java      |  16 --
 .../stream/ProcessTableFunctionSemanticTests.java  |  12 -
 .../exec/stream/ProcessTableFunctionTestUtils.java |  21 ++
 .../plan/stream/sql/NonDeterministicDagTest.scala  | 244 +++++++++++++++++++++
 5 files changed, 353 insertions(+), 28 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
index 99016be46fa..344352b958f 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
@@ -26,6 +26,7 @@ import 
org.apache.flink.table.connector.source.abilities.SupportsReadingMetadata
 import org.apache.flink.table.legacy.api.TableSchema;
 import org.apache.flink.table.legacy.api.constraints.UniqueConstraint;
 import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
+import org.apache.flink.table.planner.calcite.RexTableArgCall;
 import org.apache.flink.table.planner.connectors.DynamicSourceUtils;
 import org.apache.flink.table.planner.plan.metadata.FlinkRelMetadataQuery;
 import org.apache.flink.table.planner.plan.nodes.exec.spec.OverSpec;
@@ -48,6 +49,7 @@ import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalM
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMiniBatchAssigner;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMultiJoin;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalOverAggregateBase;
+import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalProcessTableFunction;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalRank;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalRel;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalSink;
@@ -75,12 +77,16 @@ import org.apache.flink.table.planner.utils.ShortcutUtils;
 import org.apache.flink.table.runtime.operators.join.FlinkJoinType;
 import 
org.apache.flink.table.runtime.operators.join.stream.utils.JoinInputSideSpec;
 import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.types.inference.StaticArgument;
+import org.apache.flink.table.types.inference.StaticArgumentTrait;
 import org.apache.flink.types.RowKind;
 
+import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.sql.SqlExplainLevel;
@@ -218,6 +224,8 @@ public class StreamNonDeterministicUpdatePlanVisitor {
             return transmitDeterminismRequirement(rel, requireDeterminism);
         } else if (rel instanceof StreamPhysicalMatch) {
             return visitMatch((StreamPhysicalMatch) rel, requireDeterminism);
+        } else if (rel instanceof StreamPhysicalProcessTableFunction) {
+            return visitPtf((StreamPhysicalProcessTableFunction) rel, 
requireDeterminism);
         } else {
             throw new UnsupportedOperationException(
                     String.format(
@@ -958,6 +966,86 @@ public class StreamNonDeterministicUpdatePlanVisitor {
         return transmitDeterminismRequirement(rel, inputRequireDeterminism);
     }
 
+    private StreamPhysicalRel visitPtf(
+            final StreamPhysicalProcessTableFunction ptf,
+            final ImmutableBitSet requireDeterminism) {
+        final RexCall call = ptf.getCall();
+
+        // Concern 1: PTF function itself is non-deterministic and downstream 
nodes
+        // require determinism.
+        if (!requireDeterminism.isEmpty()) {
+            final Optional<String> ndCall = 
FlinkRexUtil.getNonDeterministicCallName(call);
+            if (ndCall.isPresent()) {
+                throwNonDeterministicColumnsError(
+                        requireDeterminism.toList(), ptf.getRowType(), ptf, 
null, ndCall);
+            }
+        }
+
+        if (inputInsertOnly(ptf)) {
+            // No retracts arrive at the PTF input, so input-column 
determinism does
+            // not affect retract correctness
+            return transmitDeterminismRequirement(ptf, 
NO_REQUIRED_DETERMINISM);
+        }
+
+        // Concern 2: non-deterministic input columns.
+        // The PTF is a black box: there is no way to project downstream 
column requirements
+        // (requireDeterminism) back through the PTF's internal computation to 
determine which
+        // input columns need to be deterministic. requireDeterminism is 
therefore ignored here.
+        //
+        // A stricter alternative would be to require all input columns to be 
deterministic
+        // whenever any output column downstream requires it. That avoids the 
gap described
+        // below but rejects legitimate queries where the PTF does not 
actually consume the
+        // non-deterministic column, so we keep the lenient behavior.
+        //
+        // Known gap: cases like the one below are not covered today and may 
produce wrong
+        // results on failover, since the requirement on {@code nb} at the 
retract sink never
+        // reaches {@code ndFunc(b)} upstream of the PTF.
+        //
+        // <pre>{@code
+        // CREATE VIEW v AS SELECT a, ndFunc(b) AS nb FROM upsert_src;
+        // INSERT INTO retract_sink SELECT * FROM rowPtf(TABLE v);            
-- row-semantic
+        // INSERT INTO retract_sink SELECT * FROM setPtf(TABLE v PARTITION BY 
a); -- set-semantic
+        // }</pre>
+        //
+        // Note: the physical changelog the PTF receives is not solely 
determined by the
+        // REQUIRE_UPDATE_BEFORE trait. Per the SUPPORT_UPDATES contract, the 
function receives
+        // {+I,+U,-D} only when the input is upserting on the same key as the 
partition key;
+        // otherwise it receives full retractions {+I,-U,+U,-D}, including UB 
messages, even
+        // without REQUIRE_UPDATE_BEFORE. UBs may therefore arrive regardless 
of the trait.
+        //
+        // What the trait controls is which messages the PTF is contractually 
allowed to
+        // consume for state management, and that is what drives the 
determinism requirement:
+        //   - No REQUIRE_UPDATE_BEFORE: the PTF does not consume UBs; retract 
handling is
+        //     keyed by partition key, so only partition key columns must be 
deterministic.
+        //     Any non-key columns on an incidentally-delivered UB are not 
used by the PTF.
+        //   - Has REQUIRE_UPDATE_BEFORE: the PTF explicitly opts in to 
consuming UB to
+        //     reconstruct the previously processed row; that row must match 
exactly, so all
+        //     input columns must be deterministic.
+        final List<Ord<StaticArgument>> providedInputArgs =
+                StreamPhysicalProcessTableFunction.getProvidedInputArgs(call);
+        final List<RexNode> operands = call.getOperands();
+
+        final List<RelNode> newInputs = new ArrayList<>();
+        for (int i = 0; i < ptf.getInputs().size(); i++) {
+            final StreamPhysicalRel input = (StreamPhysicalRel) 
ptf.getInput(i);
+            final StaticArgument staticArg = providedInputArgs.get(i).e;
+            final RexTableArgCall tableArgCall =
+                    (RexTableArgCall) operands.get(providedInputArgs.get(i).i);
+            final ImmutableBitSet inputReq;
+            if (staticArg.is(StaticArgumentTrait.REQUIRE_UPDATE_BEFORE)) {
+                // The PTF consumes UB to reconstruct the previously processed 
row, which must
+                // match exactly — all input columns must be deterministic.
+                inputReq = 
ImmutableBitSet.range(input.getRowType().getFieldCount());
+            } else {
+                // The PTF does not consume UB; retract handling is keyed by 
partition key, so
+                // only partition key columns must be deterministic.
+                inputReq = ImmutableBitSet.of(tableArgCall.getPartitionKeys());
+            }
+            newInputs.add(visit(input, 
requireDeterminismExcludeUpsertKey(input, inputReq)));
+        }
+        return (StreamPhysicalRel) ptf.copy(ptf.getTraitSet(), newInputs);
+    }
+
     private void checkNonDeterministicRexProgram(
             final ImmutableBitSet requireDeterminism,
             final RexProgram program,
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/api/QueryOperationTestPrograms.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/api/QueryOperationTestPrograms.java
index 03b8a73ed98..e8671f0da73 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/api/QueryOperationTestPrograms.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/api/QueryOperationTestPrograms.java
@@ -21,7 +21,6 @@ package org.apache.flink.table.api;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.table.api.config.ExecutionConfigOptions;
 import 
org.apache.flink.table.api.config.ExecutionConfigOptions.AsyncOutputMode;
-import org.apache.flink.table.api.config.OptimizerConfigOptions;
 import org.apache.flink.table.functions.ScalarFunction;
 import org.apache.flink.table.operations.QueryOperation;
 import org.apache.flink.table.planner.factories.TestValuesModelFactory;
@@ -1059,11 +1058,6 @@ public class QueryOperationTestPrograms {
 
     public static final TableTestProgram PTF_ROW_SEMANTIC_TABLE =
             TableTestProgram.of("ptf-row-semantic-table", "table with row 
semantics")
-                    // TODO [FLINK-38233]: Remove this config when PTF support 
in
-                    //  StreamNonDeterministicUpdatePlanVisitor is added.
-                    .setupConfig(
-                            
OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
-                            
OptimizerConfigOptions.NonDeterministicUpdateStrategy.IGNORE)
                     .setupTemporarySystemFunction("f", 
RowSemanticTableFunction.class)
                     .setupSql(BASIC_VALUES)
                     .setupTableSink(
@@ -1089,11 +1083,6 @@ public class QueryOperationTestPrograms {
 
     static final TableTestProgram PTF_SET_SEMANTIC_TABLE =
             TableTestProgram.of("ptf-set-semantic-table", "verifies SQL 
serialization")
-                    // TODO [FLINK-38233]: Remove this config when PTF support 
in
-                    //  StreamNonDeterministicUpdatePlanVisitor is added.
-                    .setupConfig(
-                            
OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
-                            
OptimizerConfigOptions.NonDeterministicUpdateStrategy.IGNORE)
                     .setupTemporarySystemFunction("f1", 
ChainedSendingFunction.class)
                     .setupTemporarySystemFunction("f2", 
ChainedReceivingFunction.class)
                     .setupTableSource(TIMED_SOURCE)
@@ -1321,11 +1310,6 @@ public class QueryOperationTestPrograms {
 
     static final TableTestProgram PTF_ORDER_BY =
             TableTestProgram.of("ptf-order-by", "verifies SQL serialization 
with ORDER BY clause")
-                    // TODO [FLINK-38233]: Remove this config when PTF support 
in
-                    //  StreamNonDeterministicUpdatePlanVisitor is added.
-                    .setupConfig(
-                            
OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
-                            
OptimizerConfigOptions.NonDeterministicUpdateStrategy.IGNORE)
                     .setupTemporarySystemFunction("f", 
SetSemanticTableFunction.class)
                     .setupTableSource(TIMED_SOURCE)
                     .setupTableSink(
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
index 0cdc6d9c243..39131ff4ed3 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionSemanticTests.java
@@ -18,8 +18,6 @@
 
 package org.apache.flink.table.planner.plan.nodes.exec.stream;
 
-import org.apache.flink.table.api.TableConfig;
-import org.apache.flink.table.api.config.OptimizerConfigOptions;
 import 
org.apache.flink.table.planner.plan.nodes.exec.testutils.SemanticTestBase;
 import org.apache.flink.table.test.program.TableTestProgram;
 
@@ -28,16 +26,6 @@ import java.util.List;
 /** Semantic tests for {@link StreamExecProcessTableFunction}. */
 public class ProcessTableFunctionSemanticTests extends SemanticTestBase {
 
-    // TODO [FLINK-38233]: Remove this override when PTF support in
-    //  StreamNonDeterministicUpdatePlanVisitor is added.
-    @Override
-    protected void applyDefaultEnvironmentOptions(TableConfig config) {
-        super.applyDefaultEnvironmentOptions(config);
-        config.set(
-                
OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
-                OptimizerConfigOptions.NonDeterministicUpdateStrategy.IGNORE);
-    }
-
     @Override
     public List<TableTestProgram> programs() {
         return List.of(
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
index aaf1e71185a..89fc127a413 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/ProcessTableFunctionTestUtils.java
@@ -952,6 +952,18 @@ public class ProcessTableFunctionTestUtils {
         }
     }
 
+    /**
+     * Testing function that is itself non-deterministic (isDeterministic() = 
false). Used to verify
+     * that Concern 1 (PTF own non-determinism) is caught by the NDU visitor 
when downstream
+     * requires deterministic output columns.
+     */
+    public static class NonDeterministicUpdatingRetractFunction extends 
UpdatingRetractFunction {
+        @Override
+        public boolean isDeterministic() {
+            return false;
+        }
+    }
+
     /** Testing function. */
     public static class UpdatingUpsertFullDeletesFunction
             extends ChangelogProcessTableFunctionBase {
@@ -994,6 +1006,15 @@ public class ProcessTableFunctionTestUtils {
         }
     }
 
+    /** Row-semantic counterpart of {@link 
NonDeterministicUpdatingRetractFunction}. */
+    public static class NonDeterministicUpdatingRetractRowSemanticFunction
+            extends UpdatingRetractRowSemanticFunction {
+        @Override
+        public boolean isDeterministic() {
+            return false;
+        }
+    }
+
     /** Testing function. */
     public static class InvalidRowKindFunction extends 
AppendProcessTableFunctionBase {
         public void eval(@ArgumentHint(ROW_SEMANTIC_TABLE) Row r) {
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/NonDeterministicDagTest.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/NonDeterministicDagTest.scala
index aa53283add5..24efc06a7f5 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/NonDeterministicDagTest.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/NonDeterministicDagTest.scala
@@ -28,6 +28,7 @@ import org.apache.flink.table.data.RowData
 import org.apache.flink.table.legacy.api.TableSchema
 import org.apache.flink.table.planner.JBoolean
 import 
org.apache.flink.table.planner.expressions.utils.{TestNonDeterministicUdaf, 
TestNonDeterministicUdf, TestNonDeterministicUdtf}
+import 
org.apache.flink.table.planner.plan.nodes.exec.stream.ProcessTableFunctionTestUtils.{NonDeterministicUpdatingRetractFunction,
 NonDeterministicUpdatingRetractRowSemanticFunction, UpdatingJoinFunction, 
UpdatingRetractFunction, UpdatingRetractRowSemanticFunction, 
UpdatingUpsertFunction}
 import 
org.apache.flink.table.planner.runtime.utils.JavaUserDefinedTableFunctions.StringSplit
 import org.apache.flink.table.planner.utils.{StreamTableTestUtil, 
TableTestBase}
 import org.apache.flink.table.runtime.typeutils.InternalTypeInfo
@@ -231,6 +232,101 @@ class 
NonDeterministicDagTest(nonDeterministicUpdateStrategy: NonDeterministicUp
     util.tableEnv.createTemporaryFunction("ndAggFunc", new 
TestNonDeterministicUdaf)
     // deterministic table function
     util.tableEnv.createTemporaryFunction("str_split", new StringSplit())
+
+    // PTF functions for NDU changelog tests
+    util.tableEnv.createTemporaryFunction("updatingUpsertFunc", new 
UpdatingUpsertFunction)
+    util.tableEnv.createTemporaryFunction("updatingRetractFunc", new 
UpdatingRetractFunction)
+    util.tableEnv.createTemporaryFunction(
+      "nonDeterministicRetractFunc",
+      new NonDeterministicUpdatingRetractFunction)
+    // PTF taking two table arguments (multi-table case)
+    util.tableEnv.createTemporaryFunction("updatingJoinFunc", new 
UpdatingJoinFunction)
+    // Row-semantic PTFs
+    util.tableEnv.createTemporaryFunction(
+      "updatingRetractRowSemanticFunc",
+      new UpdatingRetractRowSemanticFunction)
+    util.tableEnv.createTemporaryFunction(
+      "nonDeterministicRetractRowSemanticFunc",
+      new NonDeterministicUpdatingRetractRowSemanticFunction)
+
+    // View with ndFunc applied to the partition key column (INT -> INT)
+    util.tableEnv.executeSql("""CREATE VIEW nd_partition_key_src AS
+                               |SELECT ndFunc(a) AS nd_a, b, c, d FROM 
upsert_src""".stripMargin)
+
+    // View with ndFunc applied to a non-key column (BIGINT -> BIGINT)
+    util.tableEnv.executeSql("""CREATE VIEW nd_col_src AS
+                               |SELECT a, ndFunc(b) AS nd_b, c, d FROM 
cdc""".stripMargin)
+
+    // Upsert sink: 4 columns (INT, INT, BIGINT, STRING) matching PTF upsert 
output over INT sources
+    util.tableEnv.executeSql("""CREATE TABLE ptf_upsert_sink (
+                               |  f0 INT, f1 STRING, f2 BIGINT, f3 STRING,
+                               |  PRIMARY KEY (f0) NOT ENFORCED
+                               |) WITH (
+                               |  'connector' = 'values',
+                               |  'sink-insert-only' = 'false',
+                               |  'sink-changelog-mode-enforced' = 'I,UA,D'
+                               |)""".stripMargin)
+
+    // Retract sink: same column types, no primary key
+    util.tableEnv.executeSql("""CREATE TABLE ptf_retract_sink (
+                               |  f0 INT, f1 STRING, f2 BIGINT, f3 STRING
+                               |) WITH (
+                               |  'connector' = 'values',
+                               |  'sink-insert-only' = 'false'
+                               |)""".stripMargin)
+
+    // Two upsert sources for the multi-table (join) PTF, both keyed by id.
+    util.tableEnv.executeSql("""CREATE TABLE ptf_score_src (
+                               |  id INT,
+                               |  score INT,
+                               |  PRIMARY KEY (id) NOT ENFORCED
+                               |) WITH (
+                               |  'connector' = 'values',
+                               |  'changelog-mode' = 'I,UA,D',
+                               |  'source.produces-delete-by-key' = 'true'
+                               |)""".stripMargin)
+    util.tableEnv.executeSql("""CREATE TABLE ptf_city_src (
+                               |  id INT,
+                               |  city STRING,
+                               |  PRIMARY KEY (id) NOT ENFORCED
+                               |) WITH (
+                               |  'connector' = 'values',
+                               |  'changelog-mode' = 'I,UA,D',
+                               |  'source.produces-delete-by-key' = 'true'
+                               |)""".stripMargin)
+
+    // Views applying ndFunc to each source's key column, so its upsert key no 
longer matches the
+    // partition key used by the PTF.
+    util.tableEnv.executeSql("""CREATE VIEW nd_score_src AS
+                               |SELECT ndFunc(id) AS id, score FROM 
ptf_score_src""".stripMargin)
+    util.tableEnv.executeSql("""CREATE VIEW nd_city_src AS
+                               |SELECT ndFunc(id) AS id, city FROM 
ptf_city_src""".stripMargin)
+
+    // Upsert sink for the join PTF output: the two table arguments are 
co-partitioned on the same
+    // key, so a single partition key (id) is prepended to the PTF output.
+    util.tableEnv.executeSql("""CREATE TABLE ptf_join_sink (
+                               |  id INT,
+                               |  `out` STRING,
+                               |  PRIMARY KEY (id) NOT ENFORCED
+                               |) WITH (
+                               |  'connector' = 'values',
+                               |  'sink-insert-only' = 'false',
+                               |  'sink-changelog-mode-enforced' = 'I,UA,D',
+                               |  'sink.supports-delete-by-key' = 'true'
+                               |)""".stripMargin)
+
+    // Retract sink matching the row-semantic changelog PTF output ROW<name, 
count, mode>, no PK.
+    util.tableEnv.executeSql("""CREATE TABLE ptf_row_retract_sink (
+                               |  f0 STRING, f1 BIGINT, f2 STRING
+                               |) WITH (
+                               |  'connector' = 'values',
+                               |  'sink-insert-only' = 'false'
+                               |)""".stripMargin)
+
+    // Insert-only source with a non-deterministic column. Feeding this to a 
row-semantic PTF
+    // exercises the inputInsertOnly short-circuit in visitPtf.
+    util.tableEnv.executeSql("""CREATE VIEW nd_insert_only_src AS
+                               |SELECT a, ndFunc(b) AS nd_b, c, d FROM 
src""".stripMargin)
   }
 
   @TestTemplate
@@ -1962,6 +2058,154 @@ class 
NonDeterministicDagTest(nonDeterministicUpdateStrategy: NonDeterministicUp
       .isInstanceOf[TableException]
   }
 
+  // 
---------------------------------------------------------------------------
+  // PTF changelog NDU tests
+  // 
---------------------------------------------------------------------------
+
+  /** Partition key equals the source upsert key — requirement excluded, no 
NDU error. */
+  @TestTemplate
+  def testPtfUpsertOutputPartitionKeyMatchesUpsertKey(): Unit = {
+    assertThatCode(
+      () =>
+        util.tableEnv.explainSql(
+          "INSERT INTO ptf_upsert_sink" +
+            " SELECT * FROM updatingUpsertFunc(TABLE upsert_src PARTITION BY 
a)"))
+      .doesNotThrowAnyException()
+  }
+
+  /**
+   * Non-deterministic function on the partition key column — the partition 
key of the PTF input
+   * must be deterministic for upsert output correctness; expect error only 
under TRY_RESOLVE.
+   */
+  @TestTemplate
+  def testPtfUpsertOutputNonDeterministicPartitionKey(): Unit = {
+    val callable: ThrowingCallable = () =>
+      util.tableEnv.explainSql(
+        "INSERT INTO ptf_upsert_sink" +
+          " SELECT * FROM updatingUpsertFunc(TABLE nd_partition_key_src 
PARTITION BY nd_a)")
+    if (tryResolve) {
+      assertThatThrownBy(callable)
+        .hasMessageContaining("non-deterministic function: ndFunc")
+        .isInstanceOf[TableException]
+    } else {
+      assertThatCode(callable).doesNotThrowAnyException()
+    }
+  }
+
+  /** All CDC input columns are deterministic — full-retract output requires 
no NDU error. */
+  @TestTemplate
+  def testPtfRetractOutputDeterministicInput(): Unit = {
+    assertThatCode(
+      () =>
+        util.tableEnv.explainSql(
+          "INSERT INTO ptf_retract_sink" +
+            " SELECT * FROM updatingRetractFunc(TABLE cdc PARTITION BY a)"))
+      .doesNotThrowAnyException()
+  }
+
+  /**
+   * Non-deterministic function on a non-key input column — full-retract 
output requires all columns
+   * to be deterministic; expect error only under TRY_RESOLVE.
+   */
+  @TestTemplate
+  def testPtfRetractOutputNonDeterministicInputColumn(): Unit = {
+    val callable: ThrowingCallable = () =>
+      util.tableEnv.explainSql(
+        "INSERT INTO ptf_retract_sink" +
+          " SELECT * FROM updatingRetractFunc(TABLE nd_col_src PARTITION BY 
a)")
+    if (tryResolve) {
+      assertThatThrownBy(callable)
+        .hasMessageContaining("non-deterministic function: ndFunc")
+        .isInstanceOf[TableException]
+    } else {
+      assertThatCode(callable).doesNotThrowAnyException()
+    }
+  }
+
+  /**
+   * A non-deterministic PTF writing to a retract sink (no PK): the retract 
sink requires all PTF
+   * output columns to be deterministic, and the retract PTF has no upsert key 
so nothing is zeroed
+   * by requireDeterminismExcludeUpsertKey — requireDeterminism is fully 
non-empty when it reaches
+   * visitPtf. The PTF claims isDeterministic() == false (Concern 1), so the 
NDU visitor must flag
+   * it.
+   *
+   * <p>Inputs are deterministic (cdc with no ndFunc), so Concern 2 
(non-deterministic input
+   * columns) does not trigger.
+   */
+  @TestTemplate
+  def testPtfNonDeterministicFunctionWithRequiredOutputDeterminism(): Unit = {
+    val callable: ThrowingCallable = () =>
+      util.tableEnv.explainSql(
+        "INSERT INTO ptf_retract_sink" +
+          " SELECT * FROM nonDeterministicRetractFunc(TABLE cdc PARTITION BY 
a)")
+    if (tryResolve) {
+      assertThatThrownBy(callable)
+        .hasMessageContaining("non-deterministic function")
+        .isInstanceOf[TableException]
+    } else {
+      assertThatCode(callable).doesNotThrowAnyException()
+    }
+  }
+
+  /** Multi-table PTF: both table arguments are partitioned by their 
respective upsert keys. */
+  @TestTemplate
+  def testPtfMultiInputUpsertOutputPartitionKeyMatchesUpsertKey(): Unit = {
+    assertThatCode(
+      () =>
+        util.tableEnv.explainSql(
+          "INSERT INTO ptf_join_sink SELECT id, `out` FROM updatingJoinFunc(" +
+            "scoreTable => TABLE ptf_score_src PARTITION BY id, " +
+            "cityTable => TABLE ptf_city_src PARTITION BY id)"))
+      .doesNotThrowAnyException()
+  }
+
+  /**
+   * Multi-table (join) PTF where one table argument is partitioned by a 
non-deterministic column
+   * (ndFunc(id)) that no longer matches the source's upsert key.
+   */
+  @TestTemplate
+  def testPtfMultiInputUpsertOutputNonDeterministicPartitionKey(): Unit = {
+    val callable: ThrowingCallable = () =>
+      util.tableEnv.explainSql(
+        "INSERT INTO ptf_join_sink SELECT id, `out` FROM updatingJoinFunc(" +
+          "scoreTable => TABLE nd_score_src PARTITION BY id, " +
+          "cityTable => TABLE ptf_city_src PARTITION BY id)")
+    if (tryResolve) {
+      assertThatThrownBy(callable)
+        .hasMessageContaining("non-deterministic function: ndFunc")
+        .isInstanceOf[TableException]
+    } else {
+      assertThatCode(callable).doesNotThrowAnyException()
+    }
+  }
+
+  /** Row-semantic PTF that is itself non-deterministic, writing to a retract 
sink. */
+  @TestTemplate
+  def testPtfRowSemanticNonDeterministicFunctionWithRetractSink(): Unit = {
+    val callable: ThrowingCallable = () =>
+      util.tableEnv.explainSql(
+        "INSERT INTO ptf_row_retract_sink" +
+          " SELECT * FROM nonDeterministicRetractRowSemanticFunc(TABLE cdc)")
+    if (tryResolve) {
+      assertThatThrownBy(callable)
+        .hasMessageContaining("non-deterministic function")
+        .isInstanceOf[TableException]
+    } else {
+      assertThatCode(callable).doesNotThrowAnyException()
+    }
+  }
+
+  /** Row-semantic PTF reading an insert-only input that carries a 
non-deterministic column. */
+  @TestTemplate
+  def testPtfRowSemanticInsertOnlyInputWithRetractSink(): Unit = {
+    assertThatCode(
+      () =>
+        util.tableEnv.explainSql(
+          "INSERT INTO ptf_row_retract_sink" +
+            " SELECT * FROM updatingRetractRowSemanticFunc(TABLE 
nd_insert_only_src)"))
+      .doesNotThrowAnyException()
+  }
+
   /**
    * This upsert test sink does support getting primary key from table schema. 
We defined a similar
    * test sink here not using existing {@link TestingUpsertTableSink} in 
{@link StreamTestSink}

Reply via email to