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 8c479701c2e [FLINK-40890][table] Require deterministic build columns 
for LATERAL SNAPSHOT join (#29380)
8c479701c2e is described below

commit 8c479701c2e4a57f2e2a6787595c821051d83e12
Author: Fabian Hueske <[email protected]>
AuthorDate: Mon Oct 5 16:39:05 2026 +0200

    [FLINK-40890][table] Require deterministic build columns for LATERAL 
SNAPSHOT join (#29380)
    
    Generated-by: Claude Code (Claude Opus 4.8)
---
 .../StreamNonDeterministicUpdatePlanVisitor.java   | 20 +++++--
 .../stream/sql/join/LateralSnapshotJoinTest.java   | 69 ++++++++++++++++++++++
 2 files changed, 85 insertions(+), 4 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 344352b958f..8a7c1e04cdf 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
@@ -40,6 +40,7 @@ import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalD
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalExchange;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalExpand;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalGroupAggregateBase;
+import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalLateralSnapshotJoin;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalLegacySink;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalLegacyTableSourceScan;
 import 
org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalLimit;
@@ -630,6 +631,10 @@ public class StreamNonDeterministicUpdatePlanVisitor {
             throwNonDeterministicConditionError(
                     ndCall.get(), join.getCondition(), (StreamPhysicalRel) 
join);
         }
+        // The LATERAL SNAPSHOT join keys its build (right) state by the 
complete build row, so
+        // build retractions are matched by exact row equality. A unique key 
offers no protection
+        // here: every build column must be deterministic whenever the build 
side can update.
+        boolean forceRightWholeRowDeterminism = join instanceof 
StreamPhysicalLateralSnapshotJoin;
         int leftFieldCnt = leftRel.getRowType().getFieldCount();
         StreamPhysicalRel newLeft =
                 visitJoinChild(
@@ -641,7 +646,8 @@ public class StreamNonDeterministicUpdatePlanVisitor {
                         join.joinSpec().getLeftKeys(),
                         // TODO remove this conversion when scala-free was 
total done.
                         scala.collection.JavaConverters.seqAsJavaList(
-                                join.getUpsertKeys(leftRel, 
join.joinSpec().getLeftKeys())));
+                                join.getUpsertKeys(leftRel, 
join.joinSpec().getLeftKeys())),
+                        false);
         StreamPhysicalRel newRight =
                 visitJoinChild(
                         requireDeterminism,
@@ -652,7 +658,8 @@ public class StreamNonDeterministicUpdatePlanVisitor {
                         join.joinSpec().getRightKeys(),
                         // TODO remove this conversion when scala-free was 
total done.
                         scala.collection.JavaConverters.seqAsJavaList(
-                                join.getUpsertKeys(rightRel, 
join.joinSpec().getRightKeys())));
+                                join.getUpsertKeys(rightRel, 
join.joinSpec().getRightKeys())),
+                        forceRightWholeRowDeterminism);
 
         return (StreamPhysicalRel)
                 join.copy(
@@ -930,7 +937,8 @@ public class StreamNonDeterministicUpdatePlanVisitor {
             final int leftFieldCnt,
             final boolean isLeft,
             final int[] joinKeys,
-            final List<int[]> inputUniqueKeys) {
+            final List<int[]> inputUniqueKeys,
+            final boolean forceWholeRowDeterminism) {
         JoinInputSideSpec joinInputSideSpec =
                 JoinUtil.analyzeJoinInput(
                         ShortcutUtils.unwrapClassLoader(rel),
@@ -939,7 +947,11 @@ public class StreamNonDeterministicUpdatePlanVisitor {
                         inputUniqueKeys);
         ImmutableBitSet inputRequireDeterminism;
         if (inputHasUpdate) {
-            if (joinInputSideSpec.hasUniqueKey() || 
joinInputSideSpec.joinKeyContainsUniqueKey()) {
+            if (forceWholeRowDeterminism) {
+                // the operator retracts by the whole input row regardless of 
any unique key
+                inputRequireDeterminism = 
ImmutableBitSet.range(rel.getRowType().getFieldCount());
+            } else if (joinInputSideSpec.hasUniqueKey()
+                    || joinInputSideSpec.joinKeyContainsUniqueKey()) {
                 // join hasUniqueKey or joinKeyContainsUniqueKey, then 
transmit corresponding
                 // requirement to input
                 if (isLeft) {
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/join/LateralSnapshotJoinTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/join/LateralSnapshotJoinTest.java
index 81ac5c8ed80..aa5ee760490 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/join/LateralSnapshotJoinTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/join/LateralSnapshotJoinTest.java
@@ -23,8 +23,11 @@ import 
org.apache.flink.streaming.api.operators.SimpleOperatorFactory;
 import org.apache.flink.streaming.api.transformations.TwoInputTransformation;
 import org.apache.flink.table.api.CompiledPlan;
 import org.apache.flink.table.api.TableConfig;
+import org.apache.flink.table.api.TableException;
 import org.apache.flink.table.api.ValidationException;
 import org.apache.flink.table.api.config.ExecutionConfigOptions;
+import org.apache.flink.table.api.config.OptimizerConfigOptions;
+import 
org.apache.flink.table.api.config.OptimizerConfigOptions.NonDeterministicUpdateStrategy;
 import org.apache.flink.table.api.internal.CompiledPlanUtils;
 import org.apache.flink.table.planner.utils.TableTestBase;
 import org.apache.flink.table.planner.utils.TableTestUtil;
@@ -42,6 +45,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Plan tests for the {@code LATERAL SNAPSHOT} processing-time temporal table 
join. */
@@ -259,6 +263,47 @@ public class LateralSnapshotJoinTest extends TableTestBase 
{
         assertThat(plan).doesNotContain("DropUpdateBefore");
     }
 
+    @Test
+    void testRejectNonDeterministicUpdatingBuildColumnWithPrimaryKey() {
+        // The LSJ operator keys its state by the whole build row, so 
retractions are matched by
+        // exact row equality. A non-deterministic build column therefore 
breaks retraction even
+        // when the build side has a unique key (FLINK-40890). TRY_RESOLVE 
must reject it.
+        enableTryResolve();
+        createUpdatingPkBuildSource();
+        util.tableEnv()
+                .executeSql(
+                        "CREATE VIEW b_view AS "
+                                + "SELECT bk, CAST(NOW() AS STRING) AS bn, bts 
FROM b_pk");
+
+        final String sql =
+                "SELECT probe.pk, s.bk, s.bn FROM probe JOIN LATERAL SNAPSHOT("
+                        + "input => TABLE b_view, on_time => DESCRIPTOR(bts), "
+                        + "load_completed_time => CAST(TIMESTAMP '2026-07-01 
00:00:00' AS TIMESTAMP_LTZ(3))"
+                        + ") AS s ON probe.pk = s.bk";
+
+        assertThatThrownBy(() -> util.tableEnv().explainSql(sql))
+                .isInstanceOf(TableException.class)
+                .hasMessageContaining("can not satisfy the determinism 
requirement");
+    }
+
+    @Test
+    void testAcceptDeterministicUpdatingBuildColumnWithPrimaryKey() {
+        // Counterpart to the rejection test: a deterministic build column 
over the same
+        // primary-keyed, updating source must still pass TRY_RESOLVE (no 
over-rejection).
+        enableTryResolve();
+        createUpdatingPkBuildSource();
+        util.tableEnv()
+                .executeSql("CREATE VIEW b_view AS SELECT bk, UPPER(bk) AS bn, 
bts FROM b_pk");
+
+        final String sql =
+                "SELECT probe.pk, s.bk, s.bn FROM probe JOIN LATERAL SNAPSHOT("
+                        + "input => TABLE b_view, on_time => DESCRIPTOR(bts), "
+                        + "load_completed_time => CAST(TIMESTAMP '2026-07-01 
00:00:00' AS TIMESTAMP_LTZ(3))"
+                        + ") AS s ON probe.pk = s.bk";
+
+        assertThatCode(() -> 
util.tableEnv().explainSql(sql)).doesNotThrowAnyException();
+    }
+
     @Test
     void testNonEquiConditionCompilesEndToEnd() {
         final String sql =
@@ -681,4 +726,28 @@ public class LateralSnapshotJoinTest extends TableTestBase 
{
                                 new AssertionError(
                                         "No LateralSnapshotJoinOperator found 
in the plan."));
     }
+
+    private void enableTryResolve() {
+        util.tableEnv()
+                .getConfig()
+                .set(
+                        
OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
+                        NonDeterministicUpdateStrategy.TRY_RESOLVE);
+    }
+
+    private void createUpdatingPkBuildSource() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE b_pk ("
+                                + "  bk STRING,"
+                                + "  bv INT,"
+                                + "  bts TIMESTAMP(3),"
+                                + "  WATERMARK FOR bts AS bts,"
+                                + "  PRIMARY KEY (bk) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'bounded' = 'false',"
+                                + "  'changelog-mode' = 'I,UA,D'"
+                                + ")");
+    }
 }

Reply via email to