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 8d01380ff5b [FLINK-41034][table] Don't store rows whose join key can 
never match in streaming joins
8d01380ff5b is described below

commit 8d01380ff5bf9892462ef9808b1b56fd9723f54d
Author: Rion Williams <[email protected]>
AuthorDate: Thu Oct 22 05:49:23 2026 -0500

    [FLINK-41034][table] Don't store rows whose join key can never match in 
streaming joins
    
    Fixed in #29532.
---
 .../join/JoinConditionWithNullFilters.java         |  24 ++-
 .../join/stream/StreamingJoinOperator.java         |  13 ++
 .../join/stream/StreamingSemiAntiJoinOperator.java |  15 +-
 .../AsyncStateStreamingJoinOperator.java           |   8 +
 .../operators/join/stream/utils/JoinHelper.java    |   1 +
 .../join/JoinConditionWithNullFiltersTest.java     |  86 ++++++++++
 .../join/stream/StreamingJoinOperatorTest.java     | 174 +++++++++++++++++++++
 .../join/stream/StreamingJoinOperatorTestBase.java |   6 +-
 .../stream/StreamingMiniBatchJoinOperatorTest.java |  23 ++-
 .../stream/StreamingSemiAntiJoinOperatorTest.java  |  38 +++++
 .../flink/table/utils/HandwrittenSelectorUtil.java |   2 +-
 11 files changed, 373 insertions(+), 17 deletions(-)

diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFilters.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFilters.java
index 120dc9e5c97..8990e6e94de 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFilters.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFilters.java
@@ -21,7 +21,7 @@ package org.apache.flink.table.runtime.operators.join;
 import org.apache.flink.api.common.functions.WrappingFunction;
 import org.apache.flink.streaming.api.operators.KeyContext;
 import org.apache.flink.table.data.RowData;
-import org.apache.flink.table.data.binary.NullAwareGetters;
+import org.apache.flink.table.data.binary.BinaryRowData;
 import org.apache.flink.table.runtime.generated.JoinCondition;
 
 /** Utility to take null filters into consideration when apply join condition. 
*/
@@ -52,15 +52,23 @@ public class JoinConditionWithNullFilters extends 
WrappingFunction<JoinCondition
 
     @Override
     public boolean apply(RowData left, RowData right) {
-        if (!nullSafe) { // is not null safe, return false if any null exists
-            // key is always BinaryRowData
-            NullAwareGetters joinKey = (NullAwareGetters) 
keyContext.getCurrentKey();
-            if (filterAllNulls ? joinKey.anyNull() : 
joinKey.anyNull(nullFilterKeys)) {
-                // find null present, return false directly
-                return false;
-            }
+        if (currentKeyCannotMatch()) {
+            return false;
         }
         // test condition
         return wrappedFunction.apply(left, right);
     }
+
+    /**
+     * Returns whether no record of the current join key can ever match, 
because the key has a null
+     * in a field that is not null-safe. The current key must be set.
+     */
+    public boolean currentKeyCannotMatch() {
+        if (nullSafe) {
+            return false;
+        }
+        // key is always BinaryRowData
+        return NullAwareJoinHelper.shouldFilter(
+                false, filterAllNulls, nullFilterKeys, (BinaryRowData) 
keyContext.getCurrentKey());
+    }
 }
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperator.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperator.java
index 83bf57f4f80..16b879ee9e3 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperator.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperator.java
@@ -179,6 +179,10 @@ public class StreamingJoinOperator extends 
AbstractStreamingJoinOperator {
      *
      * <pre>
      * if input record is accumulate
+     * |  if the join key can never match
+     * |  |  if input side is outer, send +I[record+null]
+     * |  |  return without storing the record
+     * |  endif
      * |  if record replaces a stored record (upsert) and the condition is 
non-equi
      * |  |  for each other that the replaced record matches but the record 
does not
      * |  |  |  send -D[replaced+other]
@@ -260,6 +264,15 @@ public class StreamingJoinOperator extends 
AbstractStreamingJoinOperator {
         input.setRowKind(RowKind.INSERT); // erase RowKind for later state 
updating
 
         if (isAccumulateMsg) { // record is accumulate
+            if (joinCondition.currentKeyCannotMatch()) {
+                // do not store a record that can never match
+                if (inputIsOuter) {
+                    // send +I[record+null]
+                    outRow.setRowKind(RowKind.INSERT);
+                    outputNullPadding(input, inputIsLeft);
+                }
+                return;
+            }
             final RowData replacedRecord =
                     replacedRecord(input, inputSideStateView, inputIsLeft, 
isSuppress);
             boolean replacedRecordHadNoMatches = false;
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java
index 2d7e15bbdb4..cc770771bb4 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperator.java
@@ -101,7 +101,8 @@ public class StreamingSemiAntiJoinOperator extends 
AbstractStreamingJoinOperator
      *   if anti join, send input record
      * if there are matched rows on the other side
      *   if semi join, send input record
-     * if the input record is accumulate, state.add(record, matched size)
+     * if the input record is accumulate, state.add(record, matched size), 
unless the join key can
+     * never match
      * if the input record is retract, state.retract(record)
      * </pre>
      */
@@ -123,7 +124,10 @@ public class StreamingSemiAntiJoinOperator extends 
AbstractStreamingJoinOperator
         if (RowDataUtil.isAccumulateMsg(input)) {
             // erase RowKind for state updating
             input.setRowKind(RowKind.INSERT);
-            leftRecordStateView.addRecord(input, 
Iterators.size(associatedRecords));
+            // do not store a record that can never match
+            if (!joinCondition.currentKeyCannotMatch()) {
+                leftRecordStateView.addRecord(input, 
Iterators.size(associatedRecords));
+            }
         } else { // input is retract
             // erase RowKind for state updating
             input.setRowKind(RowKind.INSERT);
@@ -142,7 +146,7 @@ public class StreamingSemiAntiJoinOperator extends 
AbstractStreamingJoinOperator
      *
      * <pre>
      * if input record is accumulate
-     * | state.add(record)
+     * | state.add(record), unless the join key can never match
      * | if there is no matched rows on the other side, skip
      * | if there are matched rows on the other side
      * | | if the matched num in the matched rows == 0
@@ -177,7 +181,10 @@ public class StreamingSemiAntiJoinOperator extends 
AbstractStreamingJoinOperator
                 AbstractStreamingJoinOperator.iterator(
                         input, false, leftRecordStateView, joinCondition);
         if (isAccumulateMsg) { // record is accumulate
-            rightRecordStateView.addRecord(input);
+            // do not store a record that can never match
+            if (!joinCondition.currentKeyCannotMatch()) {
+                rightRecordStateView.addRecord(input);
+            }
             while (associatedRecords.hasNext()) {
                 OuterRecord outerRecord = associatedRecords.next();
                 RowData other = outerRecord.record;
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/asyncprocessing/AsyncStateStreamingJoinOperator.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/asyncprocessing/AsyncStateStreamingJoinOperator.java
index 5c92d7a6ab2..8e127653d55 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/asyncprocessing/AsyncStateStreamingJoinOperator.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/asyncprocessing/AsyncStateStreamingJoinOperator.java
@@ -185,6 +185,10 @@ public class AsyncStateStreamingJoinOperator extends 
AbstractAsyncStateStreaming
 
         @Override
         public void addRecord(JoinRecordAsyncStateView stateView, RowData 
record) throws Exception {
+            // do not store a record that can never match
+            if (joinCondition.currentKeyCannotMatch()) {
+                return;
+            }
             // no need to wait for the future
             stateView.addRecord(record);
         }
@@ -200,6 +204,10 @@ public class AsyncStateStreamingJoinOperator extends 
AbstractAsyncStateStreaming
         public void addRecordInOuterSide(
                 OuterJoinRecordAsyncStateView stateView, RowData record, int 
numOfAssociations)
                 throws Exception {
+            // do not store a record that can never match
+            if (joinCondition.currentKeyCannotMatch()) {
+                return;
+            }
             // no need to wait for the future
             stateView.addRecord(record, numOfAssociations);
         }
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/utils/JoinHelper.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/utils/JoinHelper.java
index 885a1ff25e9..50c3fa04a7e 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/utils/JoinHelper.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/utils/JoinHelper.java
@@ -68,6 +68,7 @@ public abstract class JoinHelper<STATE_VIEW, OUTER_STATE_VIEW 
extends STATE_VIEW
      *
      * <pre>
      * if input record is accumulate
+     * |  skip every state.add below if the join key can never match
      * |  if input side is outer
      * |  |  if there is no matched rows on the other side, send 
+I[record+null], state.add(record, 0)
      * |  |  if there are matched rows on the other side
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFiltersTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFiltersTest.java
new file mode 100644
index 00000000000..a08ff6d0216
--- /dev/null
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/JoinConditionWithNullFiltersTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.table.runtime.operators.join;
+
+import org.apache.flink.streaming.api.operators.KeyContext;
+import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.writer.BinaryRowWriter;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link JoinConditionWithNullFilters}. */
+class JoinConditionWithNullFiltersTest {
+
+    private final TestKeyContext keyContext = new TestKeyContext();
+
+    /** The first key field is compared with {@code =}, the second with IS NOT 
DISTINCT FROM. */
+    @Test
+    void testMixedKeyCannotMatchOnlyWithNullInFilteredField() {
+        final JoinConditionWithNullFilters condition =
+                new JoinConditionWithNullFilters(null, new boolean[] {true, 
false}, keyContext);
+
+        keyContext.setCurrentKey(key(null, 1));
+        assertThat(condition.currentKeyCannotMatch()).isTrue();
+        keyContext.setCurrentKey(key(1, null));
+        assertThat(condition.currentKeyCannotMatch()).isFalse();
+        keyContext.setCurrentKey(key(1, 1));
+        assertThat(condition.currentKeyCannotMatch()).isFalse();
+    }
+
+    @Test
+    void testNullSafeKeyCanAlwaysMatch() {
+        final JoinConditionWithNullFilters condition =
+                new JoinConditionWithNullFilters(null, new boolean[] {false, 
false}, keyContext);
+
+        keyContext.setCurrentKey(key(null, null));
+        assertThat(condition.currentKeyCannotMatch()).isFalse();
+    }
+
+    private static BinaryRowData key(Integer first, Integer second) {
+        final BinaryRowData key = new BinaryRowData(2);
+        final BinaryRowWriter writer = new BinaryRowWriter(key);
+        final Integer[] fields = {first, second};
+        for (int i = 0; i < fields.length; i++) {
+            if (fields[i] == null) {
+                writer.setNullAt(i);
+            } else {
+                writer.writeInt(i, fields[i]);
+            }
+        }
+        writer.complete();
+        return key;
+    }
+
+    private static class TestKeyContext implements KeyContext {
+
+        private Object currentKey;
+
+        @Override
+        public void setCurrentKey(Object key) {
+            currentKey = key;
+        }
+
+        @Override
+        public Object getCurrentKey() {
+            return currentKey;
+        }
+    }
+}
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTest.java
index b12f6a26dab..3a5b3083b69 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTest.java
@@ -18,9 +18,11 @@
 
 package org.apache.flink.table.runtime.operators.join.stream;
 
+import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
 import org.apache.flink.streaming.api.operators.TwoInputStreamOperator;
 import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
 import org.apache.flink.streaming.util.KeyedTwoInputStreamOperatorTestHarness;
+import 
org.apache.flink.streaming.util.asyncprocessing.AsyncKeyedTwoInputStreamOperatorTestHarness;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.runtime.generated.GeneratedJoinCondition;
 import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
@@ -48,6 +50,7 @@ import java.util.List;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
+import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.binaryRecord;
 import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.deleteRecord;
 import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
 import static org.apache.flink.table.runtime.util.StreamRecordUtils.rowOfKind;
@@ -1123,6 +1126,177 @@ class StreamingJoinOperatorTest extends 
StreamingJoinOperatorTestBase {
         return enableAsyncState;
     }
 
+    @TestTemplate
+    void testInnerJoinSkipsNullKeys() throws Exception {
+        useNullKeyJoinOperator(
+                JoinInputSideSpec.withoutUniqueKey(),
+                JoinInputSideSpec.withoutUniqueKey(),
+                false,
+                false,
+                true,
+                null);
+        testHarness.processElement1(insertRecord("Ord#1", null, ADDRESS));
+        testHarness.processElement2(insertRecord(null, "AIR"));
+        testHarness.processElement2(deleteRecord(null, "AIR"));
+        assertor.shouldEmitNothing(testHarness);
+        assertStoresOnlyNonNullKeys();
+    }
+
+    @TestTemplate
+    void testLeftOuterJoinSkipsNullKeys() throws Exception {
+        useNullKeyJoinOperator(leftHasUniqueKeySpec(), rightInputSpec, true, 
false, true, null);
+        testHarness.processElement1(insertRecord("Ord#1", null, ADDRESS));
+        assertor.shouldEmit(
+                testHarness, rowOfKind(RowKind.INSERT, "Ord#1", null, ADDRESS, 
null, null));
+        assertThat(numStateEntries()).isZero();
+        testHarness.processElement1(updateAfterRecord("Ord#1", null, 
NEW_ADDRESS));
+        assertor.shouldEmit(
+                testHarness, rowOfKind(RowKind.INSERT, "Ord#1", null, 
NEW_ADDRESS, null, null));
+        testHarness.processElement1(deleteRecord("Ord#1", null, NEW_ADDRESS));
+        assertor.shouldEmit(
+                testHarness, rowOfKind(RowKind.DELETE, "Ord#1", null, 
NEW_ADDRESS, null, null));
+        assertStoresOnlyNonNullKeys();
+    }
+
+    @TestTemplate
+    void testRightOuterJoinSkipsNullKeys() throws Exception {
+        useNullKeyJoinOperator(leftInputSpec, rightInputSpec, false, true, 
true, null);
+        testHarness.processElement2(insertRecord(null, "AIR"));
+        assertor.shouldEmit(testHarness, rowOfKind(RowKind.INSERT, null, null, 
null, null, "AIR"));
+        testHarness.processElement1(insertRecord("Ord#1", null, ADDRESS));
+        testHarness.processElement2(deleteRecord(null, "AIR"));
+        assertor.shouldEmit(testHarness, rowOfKind(RowKind.DELETE, null, null, 
null, null, "AIR"));
+        assertStoresOnlyNonNullKeys();
+    }
+
+    @TestTemplate
+    void testFullOuterJoinSkipsNullKeys() throws Exception {
+        useNullKeyJoinOperator(
+                JoinInputSideSpec.withoutUniqueKey(),
+                JoinInputSideSpec.withoutUniqueKey(),
+                true,
+                true,
+                true,
+                null);
+        testHarness.processElement1(insertRecord("Ord#1", null, ADDRESS));
+        testHarness.processElement2(insertRecord(null, "AIR"));
+        testHarness.processElement1(deleteRecord("Ord#1", null, ADDRESS));
+        assertor.shouldEmit(
+                testHarness,
+                rowOfKind(RowKind.INSERT, "Ord#1", null, ADDRESS, null, null),
+                rowOfKind(RowKind.INSERT, null, null, null, null, "AIR"),
+                rowOfKind(RowKind.DELETE, "Ord#1", null, ADDRESS, null, null));
+        assertStoresOnlyNonNullKeys();
+    }
+
+    @TestTemplate
+    void testNullSafeInnerJoinMatchesNullKeys() throws Exception {
+        useNullKeyJoinOperator(
+                JoinInputSideSpec.withoutUniqueKey(),
+                JoinInputSideSpec.withoutUniqueKey(),
+                false,
+                false,
+                false,
+                null);
+        testHarness.processElement1(insertRecord("Ord#1", null, ADDRESS));
+        testHarness.processElement2(insertRecord(null, "AIR"));
+        assertor.shouldEmit(
+                testHarness, rowOfKind(RowKind.INSERT, "Ord#1", null, ADDRESS, 
null, "AIR"));
+        assertThat(numStateEntries()).isEqualTo(2);
+    }
+
+    /**
+     * A row with a null key that a null-safe join stored is still removed by 
its retraction. Binary
+     * rows, because restored records are binary and compare unequal to 
generic ones.
+     */
+    @TestTemplate
+    void testLeftOuterJoinRetractsRestoredNullKeys() throws Exception {
+        final JoinInputSideSpec spec = JoinInputSideSpec.withoutUniqueKey();
+        useNullKeyJoinOperator(spec, spec, true, false, false, null);
+        testHarness.processElement1(binaryRecord(RowKind.INSERT, "Ord#1", 
null, ADDRESS));
+        assertor.shouldEmit(
+                testHarness, rowOfKind(RowKind.INSERT, "Ord#1", null, ADDRESS, 
null, null));
+        assertThat(numStateEntries()).isOne();
+        final OperatorSubtaskState snapshot = testHarness.snapshot(0L, 0L);
+
+        useNullKeyJoinOperator(spec, spec, true, false, true, snapshot);
+        assertThat(numStateEntries()).isOne();
+        testHarness.processElement1(binaryRecord(RowKind.DELETE, "Ord#1", 
null, ADDRESS));
+        assertor.shouldEmit(
+                testHarness, rowOfKind(RowKind.DELETE, "Ord#1", null, ADDRESS, 
null, null));
+        assertStoresOnlyNonNullKeys();
+    }
+
+    /** Asserts that nothing is stored, and that a row with a non-null key is. 
*/
+    private void assertStoresOnlyNonNullKeys() throws Exception {
+        assertThat(numStateEntries()).isZero();
+        testHarness.processElement1(insertRecord("Ord#2", "LineOrd#2", 
ADDRESS));
+        assertThat(numStateEntries()).isOne();
+    }
+
+    private int numStateEntries() throws Exception {
+        if (testHarness instanceof 
AsyncKeyedTwoInputStreamOperatorTestHarness) {
+            ((AsyncKeyedTwoInputStreamOperatorTestHarness<?, ?, ?, ?>) 
testHarness)
+                    .drainStateRequests();
+        }
+        return testHarness.numKeyedStateEntries();
+    }
+
+    /** Uses a join of the given shape with the state API of the test, 
restored if given a state. */
+    private void useNullKeyJoinOperator(
+            JoinInputSideSpec leftSpec,
+            JoinInputSideSpec rightSpec,
+            boolean leftIsOuter,
+            boolean rightIsOuter,
+            boolean filterNullKeys,
+            @Nullable OperatorSubtaskState restoredState)
+            throws Exception {
+        final boolean[] filterNulls = {filterNullKeys};
+        testHarness.close();
+        if (enableAsyncState) {
+            testHarness =
+                    AsyncKeyedTwoInputStreamOperatorTestHarness.create(
+                            new AsyncStateStreamingJoinOperator(
+                                    leftTypeInfo,
+                                    rightTypeInfo,
+                                    joinCondition,
+                                    leftSpec,
+                                    rightSpec,
+                                    leftIsOuter,
+                                    rightIsOuter,
+                                    filterNulls,
+                                    0L,
+                                    0L),
+                            leftKeySelector,
+                            rightKeySelector,
+                            leftKeySelector.getProducedType());
+        } else {
+            testHarness =
+                    new KeyedTwoInputStreamOperatorTestHarness<>(
+                            new StreamingJoinOperator(
+                                    leftTypeInfo,
+                                    rightTypeInfo,
+                                    joinCondition,
+                                    leftSpec,
+                                    rightSpec,
+                                    leftIsOuter,
+                                    rightIsOuter,
+                                    null,
+                                    null,
+                                    false,
+                                    filterNulls,
+                                    0L,
+                                    0L),
+                            leftKeySelector,
+                            rightKeySelector,
+                            leftKeySelector.getProducedType());
+        }
+        if (restoredState != null) {
+            testHarness.initializeState(restoredState);
+        }
+        testHarness.open();
+    }
+
     private void useStreamingJoinOperator(
             JoinInputSideSpec leftSpec,
             JoinInputSideSpec rightSpec,
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTestBase.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTestBase.java
index 9002706e742..71f00ea758a 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTestBase.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingJoinOperatorTestBase.java
@@ -51,7 +51,7 @@ public abstract class StreamingJoinOperatorTestBase {
                     RowType.of(
                             new LogicalType[] {
                                 new CharType(false, 20),
-                                new CharType(false, 20),
+                                new CharType(true, 20),
                                 VarCharType.STRING_TYPE
                             },
                             new String[] {"order_id", "line_order_id", 
"shipping_address"}));
@@ -59,7 +59,7 @@ public abstract class StreamingJoinOperatorTestBase {
     protected InternalTypeInfo<RowData> rightTypeInfo =
             InternalTypeInfo.of(
                     RowType.of(
-                            new LogicalType[] {new CharType(false, 20), new 
CharType(true, 10)},
+                            new LogicalType[] {new CharType(true, 20), new 
CharType(true, 10)},
                             new String[] {"line_order_id0", 
"line_order_ship_mode"}));
 
     protected RowDataKeySelector leftKeySelector =
@@ -77,7 +77,7 @@ public abstract class StreamingJoinOperatorTestBase {
             JoinInputSideSpec.withUniqueKeyContainedByJoinKey(rightTypeInfo, 
rightKeySelector);
 
     protected final InternalTypeInfo<RowData> joinKeyTypeInfo =
-            InternalTypeInfo.of(new CharType(false, 20));
+            InternalTypeInfo.of(new CharType(true, 20));
 
     protected final String funcCode =
             "public class ConditionFunction extends 
org.apache.flink.api.common.functions.AbstractRichFunction "
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingMiniBatchJoinOperatorTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingMiniBatchJoinOperatorTest.java
index fde7a318c51..d2f38446958 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingMiniBatchJoinOperatorTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingMiniBatchJoinOperatorTest.java
@@ -48,6 +48,7 @@ import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord
 import static org.apache.flink.table.runtime.util.StreamRecordUtils.rowOfKind;
 import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
 import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.updateBeforeRecord;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /** Test for StreamingMiniBatchJoinOperatorTest. */
 final class StreamingMiniBatchJoinOperatorTest extends 
StreamingJoinOperatorTestBase {
@@ -55,6 +56,26 @@ final class StreamingMiniBatchJoinOperatorTest extends 
StreamingJoinOperatorTest
     private RowDataKeySelector leftUniqueKeySelector;
     private RowDataKeySelector rightUniqueKeySelector;
 
+    @Tag("miniBatchSize=3")
+    @Test
+    void testLeftJoinSkipsNullKeys() throws Exception {
+        testHarness.processElement1(insertRecord("Ord#1", null, "3 Bellevue 
Drive"));
+        testHarness.processElement2(insertRecord("Ord#X", null, "AIR"));
+        testHarness.processElement1(insertRecord("Ord#2", "LineOrd#2", "4 
Bellevue Drive"));
+        assertor.shouldEmitAll(
+                testHarness,
+                rowOfKind(RowKind.INSERT, "Ord#1", null, "3 Bellevue Drive", 
null, null, null),
+                rowOfKind(
+                        RowKind.INSERT,
+                        "Ord#2",
+                        "LineOrd#2",
+                        "4 Bellevue Drive",
+                        null,
+                        null,
+                        null));
+        assertThat(testHarness.numKeyedStateEntries()).isOne();
+    }
+
     @Tag("miniBatchSize=3")
     @Test
     void testLeftJoinWithLeftArriveFirst() throws Exception {
@@ -2091,7 +2112,7 @@ final class StreamingMiniBatchJoinOperatorTest extends 
StreamingJoinOperatorTest
                             RowType.of(
                                     new LogicalType[] {
                                         new CharType(false, 20),
-                                        new CharType(false, 20),
+                                        new CharType(true, 20),
                                         new CharType(true, 10)
                                     },
                                     new String[] {
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperatorTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperatorTest.java
index af5201f023d..f9d82da1616 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperatorTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/stream/StreamingSemiAntiJoinOperatorTest.java
@@ -31,6 +31,7 @@ import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.deleteRecord
 import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.insertRecord;
 import static org.apache.flink.table.runtime.util.StreamRecordUtils.rowOfKind;
 import static 
org.apache.flink.table.runtime.util.StreamRecordUtils.updateAfterRecord;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /** Test for {@link StreamingSemiAntiJoinOperator}. */
 class StreamingSemiAntiJoinOperatorTest extends StreamingJoinOperatorTestBase {
@@ -289,6 +290,43 @@ class StreamingSemiAntiJoinOperatorTest extends 
StreamingJoinOperatorTestBase {
         assertor.shouldEmitNothing(testHarness);
     }
 
+    @Test
+    void testLeftSemiJoinSkipsNullKeys() throws Exception {
+        assertSkipsNullKeys(false);
+    }
+
+    /** An anti join emits and retracts a left row with a null key without 
storing it. */
+    @Test
+    void testLeftAntiJoinSkipsNullKeys() throws Exception {
+        assertSkipsNullKeys(true);
+    }
+
+    private void assertSkipsNullKeys(boolean isAntiJoin) throws Exception {
+        String address = "3 Bellevue Drive, Pottstown, PA 19464";
+        testHarness.processElement1(insertRecord("Ord#1", null, address));
+        if (isAntiJoin) {
+            assertor.shouldEmit(testHarness, rowOfKind(RowKind.INSERT, 
"Ord#1", null, address));
+        } else {
+            assertor.shouldEmitNothing(testHarness);
+        }
+        testHarness.processElement2(insertRecord(null, "AIR"));
+        assertor.shouldEmitNothing(testHarness);
+        assertThat(testHarness.numKeyedStateEntries()).isZero();
+
+        testHarness.processElement1(deleteRecord("Ord#1", null, address));
+        if (isAntiJoin) {
+            assertor.shouldEmit(testHarness, rowOfKind(RowKind.DELETE, 
"Ord#1", null, address));
+        } else {
+            assertor.shouldEmitNothing(testHarness);
+        }
+        testHarness.processElement2(deleteRecord(null, "AIR"));
+        assertor.shouldEmitNothing(testHarness);
+        assertThat(testHarness.numKeyedStateEntries()).isZero();
+
+        testHarness.processElement2(insertRecord("LineOrd#1", "AIR"));
+        assertThat(testHarness.numKeyedStateEntries()).isOne();
+    }
+
     private static final Predicate<String> ANTI_JOIN_CHECKER =
             (testDisplayName) -> testDisplayName.contains("Anti");
 }
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/utils/HandwrittenSelectorUtil.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/utils/HandwrittenSelectorUtil.java
index 73d0799c2e8..321992da0b7 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/utils/HandwrittenSelectorUtil.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/utils/HandwrittenSelectorUtil.java
@@ -72,7 +72,7 @@ public class HandwrittenSelectorUtil {
             BinaryRowData ret = new BinaryRowData(keyFields.length);
             BinaryRowWriter writer = new BinaryRowWriter(ret);
             for (int i = 0; i < keyFields.length; i++) {
-                if (value.isNullAt(i)) {
+                if (value.isNullAt(keyFields[i])) {
                     writer.setNullAt(i);
                 } else {
                     BinaryWriter.write(

Reply via email to