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

RocMarshal pushed a commit to branch release-2.4
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/release-2.4 by this push:
     new 1fa0b04b8ac [FLINK-41067][table] Don't store rows whose join key can 
never match in window and interval joins (#29646)
1fa0b04b8ac is described below

commit 1fa0b04b8ac46f3b02d82f45a3731cc7861a39d6
Author: Chan hae OH <[email protected]>
AuthorDate: Thu Oct 29 14:08:15 2026 +0900

    [FLINK-41067][table] Don't store rows whose join key can never match in 
window and interval joins (#29646)
    
    * [FLINK-41067][table] Don't store rows whose join key can never match in 
window and interval joins
    
    Like the streaming joins before FLINK-41034, the window join and the
    interval join stored rows whose join key has a NULL in a field that is
    not null-safe, although such rows can never match. All of them share one
    key, so they ended up in the state of a single subtask until the window
    fired or the interval expired.
    
    WindowJoinOperator, AsyncStateWindowJoinOperator and TimeIntervalJoin no
    longer store these rows on an input whose unmatched rows are never
    emitted. An input whose unmatched rows are emitted, such as the preserved
    side of an outer join, still stores them, so they are emitted at the same
    time as before.
    
    Generated-by: Claude Code (Claude Opus 5.5)
    
    * [FLINK-41067][table] Reuse NullAwareJoinHelper in IntervalJoinFunction 
and test a partly null-safe key
    
    IntervalJoinFunction#currentKeyCannotMatch repeated the body of
    NullAwareJoinHelper#shouldFilter, so delegate to it like
    JoinConditionWithNullFilters does.
    
    Also cover a composite key that is only partly null-safe: a row with a
    NULL in the field compared with = is not cached, while a row with a NULL
    in the null-safe field is cached and matches.
    
    Generated-by: Claude Code (Claude Opus 5.5)
    
    * [FLINK-41067][table] Let NullAwareJoinHelper#shouldFilter take 
NullAwareGetters
    
    shouldFilter only needs anyNull, so it can take a NullAwareGetters.
    IntervalJoinFunction then keeps its join key as NullAwareGetters, as
    before, and the existing callers pass BinaryRowData unchanged.
    
    Generated-by: Claude Code (Claude Opus 5.5)
    
    ---------
    
    Co-authored-by: Rion Williams <[email protected]>
---
 .../operators/join/NullAwareJoinHelper.java        |   4 +-
 .../join/interval/IntervalJoinFunction.java        |  15 +-
 .../operators/join/interval/TimeIntervalJoin.java  |  12 +-
 .../operators/join/window/WindowJoinOperator.java  |   2 +
 .../AsyncStateWindowJoinOperator.java              |   2 +
 .../join/window/utils/WindowJoinHelper.java        |  13 ++
 .../join/interval/ProcTimeIntervalJoinTest.java    |  42 ++++++
 .../join/interval/RowTimeIntervalJoinTest.java     | 156 +++++++++++++++++++++
 .../interval/TimeIntervalStreamJoinTestBase.java   |   6 +-
 .../join/window/WindowJoinOperatorTest.java        |  85 ++++++++++-
 10 files changed, 324 insertions(+), 13 deletions(-)

diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/NullAwareJoinHelper.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/NullAwareJoinHelper.java
index 9a1d87d3710..09f117e1a75 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/NullAwareJoinHelper.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/NullAwareJoinHelper.java
@@ -18,7 +18,7 @@
 
 package org.apache.flink.table.runtime.operators.join;
 
-import org.apache.flink.table.data.binary.BinaryRowData;
+import org.apache.flink.table.data.binary.NullAwareGetters;
 
 import org.apache.commons.lang3.ArrayUtils;
 
@@ -42,7 +42,7 @@ public class NullAwareJoinHelper {
     }
 
     public static boolean shouldFilter(
-            boolean nullSafe, boolean filterAllNulls, int[] nullFilterKeys, 
BinaryRowData key) {
+            boolean nullSafe, boolean filterAllNulls, int[] nullFilterKeys, 
NullAwareGetters key) {
         return !nullSafe && (filterAllNulls ? key.anyNull() : 
key.anyNull(nullFilterKeys));
     }
 }
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/IntervalJoinFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/IntervalJoinFunction.java
index f293feacbf2..2bdfc5ede43 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/IntervalJoinFunction.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/IntervalJoinFunction.java
@@ -71,11 +71,8 @@ public class IntervalJoinFunction extends 
RichFlatJoinFunction<RowData, RowData,
 
     @Override
     public void join(RowData first, RowData second, Collector<RowData> out) 
throws Exception {
-        if (!nullSafe) { // is not null safe, return false if any null exists
-            if (filterAllNulls ? joinKey.anyNull() : 
joinKey.anyNull(nullFilterKeys)) {
-                // find null present, return false directly
-                return;
-            }
+        if (currentKeyCannotMatch()) {
+            return;
         }
         if (joinCondition.apply(first, second)) {
             out.collect(reusedJoinRowData.replace(first, second));
@@ -90,4 +87,12 @@ public class IntervalJoinFunction extends 
RichFlatJoinFunction<RowData, RowData,
     public void setJoinKey(RowData currentKey) {
         this.joinKey = (NullAwareGetters) currentKey;
     }
+
+    /**
+     * 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 join key must be set.
+     */
+    public boolean currentKeyCannotMatch() {
+        return NullAwareJoinHelper.shouldFilter(nullSafe, filterAllNulls, 
nullFilterKeys, joinKey);
+    }
 }
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalJoin.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalJoin.java
index e699dfb7d9a..d5384013953 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalJoin.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalJoin.java
@@ -321,8 +321,10 @@ abstract class TimeIntervalJoin extends 
KeyedCoProcessFunction<RowData, RowData,
                 // ordered keys.
             }
         }
-        // Check if we need to cache the current row.
-        if (rightOperatorTime < rightQualifiedUpperBound) {
+        // Check if we need to cache the current row. A row that can never 
match is only cached to
+        // pad it with nulls once its window has closed.
+        if (rightOperatorTime < rightQualifiedUpperBound
+                && (joinType.isLeftOuter() || 
!joinFunction.currentKeyCannotMatch())) {
             // Operator time of right stream has not exceeded the upper window 
bound of the current
             // row. Put it into the left cache, since later coming records 
from the right stream are
             // expected to be joined with it.
@@ -437,8 +439,10 @@ abstract class TimeIntervalJoin extends 
KeyedCoProcessFunction<RowData, RowData,
                 // ordered keys.
             }
         }
-        // Check if we need to cache the current row.
-        if (leftOperatorTime < leftQualifiedUpperBound) {
+        // Check if we need to cache the current row. A row that can never 
match is only cached to
+        // pad it with nulls once its window has closed.
+        if (leftOperatorTime < leftQualifiedUpperBound
+                && (joinType.isRightOuter() || 
!joinFunction.currentKeyCannotMatch())) {
             // Operator time of left stream has not exceeded the upper window 
bound of the current
             // row. Put it into the right cache, since later coming records 
from the left stream are
             // expected to be joined with it.
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperator.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperator.java
index a087cfdada7..660969311b0 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperator.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperator.java
@@ -160,6 +160,7 @@ public class WindowJoinOperator extends 
TableStreamOperator<RowData>
     public void processElement1(StreamRecord<RowData> element) throws 
Exception {
         helper.processElement(
                 element,
+                true,
                 leftWindowEndIndex,
                 helper.getLeftLateRecordsDroppedRate(),
                 (windowEnd, rowData) -> leftWindowState.add(windowEnd, 
rowData));
@@ -169,6 +170,7 @@ public class WindowJoinOperator extends 
TableStreamOperator<RowData>
     public void processElement2(StreamRecord<RowData> element) throws 
Exception {
         helper.processElement(
                 element,
+                false,
                 rightWindowEndIndex,
                 helper.getRightLateRecordsDroppedRate(),
                 (windowEnd, rowData) -> rightWindowState.add(windowEnd, 
rowData));
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/asyncprocessing/AsyncStateWindowJoinOperator.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/asyncprocessing/AsyncStateWindowJoinOperator.java
index 50d2f1a0033..69e7ed50283 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/asyncprocessing/AsyncStateWindowJoinOperator.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/asyncprocessing/AsyncStateWindowJoinOperator.java
@@ -177,6 +177,7 @@ public class AsyncStateWindowJoinOperator extends 
AsyncStateTableStreamOperator<
     public void processElement1(StreamRecord<RowData> element) throws 
Exception {
         helper.processElement(
                 element,
+                true,
                 leftWindowEndIndex,
                 helper.getLeftLateRecordsDroppedRate(),
                 (windowEnd, rowData) -> leftWindowState.asyncAdd(windowEnd, 
rowData));
@@ -186,6 +187,7 @@ public class AsyncStateWindowJoinOperator extends 
AsyncStateTableStreamOperator<
     public void processElement2(StreamRecord<RowData> element) throws 
Exception {
         helper.processElement(
                 element,
+                false,
                 rightWindowEndIndex,
                 helper.getRightLateRecordsDroppedRate(),
                 (windowEnd, rowData) -> rightWindowState.asyncAdd(windowEnd, 
rowData));
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/utils/WindowJoinHelper.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/utils/WindowJoinHelper.java
index 27dc5c4dbb2..cc977af0ba0 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/utils/WindowJoinHelper.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/window/utils/WindowJoinHelper.java
@@ -75,6 +75,11 @@ public abstract class WindowJoinHelper {
 
     private final WindowJoinProcessor windowJoinProcessor;
 
+    // Whether unmatched records of the left or right input are emitted when 
the window fires. If
+    // not, a record of that input that can never match is not stored.
+    private final boolean leftUnmatchedIsEmitted;
+    private final boolean rightUnmatchedIsEmitted;
+
     // ------------------------------------------------------------------------
     // Metrics
     // ------------------------------------------------------------------------
@@ -98,6 +103,8 @@ public abstract class WindowJoinHelper {
         this.collector = collector;
 
         this.windowJoinProcessor = getWindowJoinProcessor(joinType);
+        this.leftUnmatchedIsEmitted = joinType.isLeftOuter() || joinType == 
FlinkJoinType.ANTI;
+        this.rightUnmatchedIsEmitted = joinType.isRightOuter();
     }
 
     public void registerMetric(OperatorMetricGroup metrics) {
@@ -127,6 +134,7 @@ public abstract class WindowJoinHelper {
 
     public void processElement(
             StreamRecord<RowData> element,
+            boolean inputIsLeft,
             int windowEndIndex,
             Meter lateRecordsDroppedRate,
             BiConsumerWithException<Long, RowData, Exception> accStateConsumer)
@@ -139,6 +147,11 @@ public abstract class WindowJoinHelper {
             return;
         }
         if (RowDataUtil.isAccumulateMsg(inputRow)) {
+            final boolean unmatchedIsEmitted =
+                    inputIsLeft ? leftUnmatchedIsEmitted : 
rightUnmatchedIsEmitted;
+            if (!unmatchedIsEmitted && joinCondition.currentKeyCannotMatch()) {
+                return;
+            }
             accStateConsumer.accept(windowEnd, inputRow);
         } else {
             // Window join could not handle retraction input stream
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/ProcTimeIntervalJoinTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/ProcTimeIntervalJoinTest.java
index 42a2e1dacbd..3b16ad4a371 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/ProcTimeIntervalJoinTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/ProcTimeIntervalJoinTest.java
@@ -248,6 +248,48 @@ class ProcTimeIntervalJoinTest extends 
TimeIntervalStreamJoinTestBase {
         testHarness.close();
     }
 
+    @Test
+    void testDoesNotCacheRowsThatCanNeverMatch() throws Exception {
+        // Only rows of an outer input are cached, to pad them with nulls when 
their window closes
+        assertNullKeyRows(FlinkJoinType.INNER, 0);
+        assertNullKeyRows(FlinkJoinType.LEFT, 2, insertRecord(null, "a", null, 
null));
+        assertNullKeyRows(FlinkJoinType.RIGHT, 2, insertRecord(null, null, 
null, "b"));
+        assertNullKeyRows(
+                FlinkJoinType.FULL,
+                4,
+                insertRecord(null, "a", null, null),
+                insertRecord(null, null, null, "b"));
+    }
+
+    /**
+     * Processes a row with a null key on each input and checks the number of 
state entries and the
+     * output once the windows of the rows have closed.
+     */
+    private void assertNullKeyRows(
+            FlinkJoinType joinType, int expectedStateEntries, Object... 
expectedRows)
+            throws Exception {
+        ProcTimeIntervalJoin joinProcessFunc =
+                new ProcTimeIntervalJoin(
+                        joinType, -5, 9, 0, rowType, rowType, 
newJoinFunction(), -1L);
+        KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData> testHarness =
+                createTestHarness(joinProcessFunc);
+        testHarness.open();
+
+        testHarness.setProcessingTime(1);
+        testHarness.processElement1(insertRecord(null, "a"));
+        testHarness.processElement2(insertRecord(null, "b"));
+        assertThat(testHarness.numKeyedStateEntries())
+                .as("state entries for %s", joinType)
+                .isEqualTo(expectedStateEntries);
+
+        testHarness.setProcessingTime(100);
+
+        assertor.assertOutputEqualsSorted(
+                "output wrong for " + joinType, List.of(expectedRows), 
testHarness.getOutput());
+        assertThat(testHarness.numKeyedStateEntries()).isZero();
+        testHarness.close();
+    }
+
     private KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData>
             createTestHarness(ProcTimeIntervalJoin intervalJoinFunc) throws 
Exception {
         KeyedCoProcessOperator<RowData, RowData, RowData, RowData> operator =
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/RowTimeIntervalJoinTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/RowTimeIntervalJoinTest.java
index 76efcfaad80..53e8bc93e87 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/RowTimeIntervalJoinTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/RowTimeIntervalJoinTest.java
@@ -32,6 +32,10 @@ import 
org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
 import org.apache.flink.table.runtime.operators.join.FlinkJoinType;
 import 
org.apache.flink.table.runtime.operators.join.KeyedCoProcessOperatorWithWatermarkDelay;
 import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
+import org.apache.flink.table.runtime.util.RowDataHarnessAssertor;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.VarCharType;
 import org.apache.flink.table.utils.HandwrittenSelectorUtil;
 
 import org.junit.jupiter.api.Test;
@@ -491,6 +495,158 @@ class RowTimeIntervalJoinTest extends 
TimeIntervalStreamJoinTestBase {
         testHarness.close();
     }
 
+    @Test
+    void testDoesNotCacheRowsThatCanNeverMatch() throws Exception {
+        // Only rows of an outer input are cached, to pad them with nulls when 
their window closes
+        assertNullKeyRows(FlinkJoinType.INNER, 0);
+        assertNullKeyRows(FlinkJoinType.LEFT, 2, insertRecord(1L, null, null, 
null));
+        assertNullKeyRows(FlinkJoinType.RIGHT, 2, insertRecord(null, null, 1L, 
null));
+        assertNullKeyRows(
+                FlinkJoinType.FULL,
+                4,
+                insertRecord(1L, null, null, null),
+                insertRecord(null, null, 1L, null));
+    }
+
+    /**
+     * Processes a row with a null key on each input and checks the number of 
state entries and the
+     * output once the windows of the rows have closed.
+     */
+    private void assertNullKeyRows(
+            FlinkJoinType joinType, int expectedStateEntries, Object... 
expectedRows)
+            throws Exception {
+        RowTimeIntervalJoin joinProcessFunc =
+                new RowTimeIntervalJoin(
+                        joinType,
+                        -5,
+                        9,
+                        0,
+                        7,
+                        rowType,
+                        rowType,
+                        newJoinFunction(),
+                        0,
+                        0,
+                        -1L,
+                        false);
+        KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData> testHarness =
+                createTestHarness(joinProcessFunc);
+        testHarness.open();
+
+        testHarness.processElement1(insertRecord(1L, null));
+        testHarness.processElement2(insertRecord(1L, null));
+        assertThat(testHarness.numKeyedStateEntries())
+                .as("state entries for %s", joinType)
+                .isEqualTo(expectedStateEntries);
+
+        testHarness.processWatermark1(new Watermark(100));
+        testHarness.processWatermark2(new Watermark(100));
+
+        List<Object> expectedOutput = new ArrayList<>(List.of(expectedRows));
+        expectedOutput.add(new Watermark(91));
+        assertor.assertOutputEqualsSorted(
+                "output wrong for " + joinType, expectedOutput, 
testHarness.getOutput());
+        assertThat(testHarness.numKeyedStateEntries()).isZero();
+        testHarness.close();
+    }
+
+    @Test
+    void testCachesRowsWithNullSafeKeys() throws Exception {
+        // A null key can match if it is compared null-safely, as with IS NOT 
DISTINCT FROM
+        RowTimeIntervalJoin joinProcessFunc =
+                new RowTimeIntervalJoin(
+                        FlinkJoinType.INNER,
+                        -5,
+                        9,
+                        0,
+                        7,
+                        rowType,
+                        rowType,
+                        newJoinFunction(new boolean[] {false}),
+                        0,
+                        0,
+                        -1L,
+                        false);
+        KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData> testHarness =
+                createTestHarness(joinProcessFunc);
+        testHarness.open();
+
+        testHarness.processElement1(insertRecord(1L, null));
+        testHarness.processElement2(insertRecord(1L, null));
+        assertThat(testHarness.numKeyedStateEntries()).isEqualTo(4);
+
+        testHarness.processWatermark1(new Watermark(100));
+        testHarness.processWatermark2(new Watermark(100));
+
+        List<Object> expectedOutput = new ArrayList<>();
+        expectedOutput.add(insertRecord(1L, null, 1L, null));
+        expectedOutput.add(new Watermark(91));
+        assertor.assertOutputEqualsSorted("output wrong.", expectedOutput, 
testHarness.getOutput());
+        testHarness.close();
+    }
+
+    @Test
+    void testCachesRowsWithNullsOnlyInNullSafeKeyFields() throws Exception {
+        // The key (a, b) compares a with = and b with IS NOT DISTINCT FROM
+        final InternalTypeInfo<RowData> compositeKeyRowType =
+                InternalTypeInfo.ofFields(
+                        new BigIntType(), VarCharType.STRING_TYPE, 
VarCharType.STRING_TYPE);
+        final RowDataKeySelector compositeKeySelector =
+                HandwrittenSelectorUtil.getRowDataSelector(
+                        new int[] {1, 2}, 
compositeKeyRowType.toRowFieldTypes());
+        final RowTimeIntervalJoin joinProcessFunc =
+                new RowTimeIntervalJoin(
+                        FlinkJoinType.INNER,
+                        -5,
+                        9,
+                        0,
+                        7,
+                        compositeKeyRowType,
+                        compositeKeyRowType,
+                        newJoinFunction(new boolean[] {true, false}),
+                        0,
+                        0,
+                        -1L,
+                        false);
+        final KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, 
RowData, RowData>
+                testHarness =
+                        new KeyedTwoInputStreamOperatorTestHarness<>(
+                                new KeyedCoProcessOperatorWithWatermarkDelay<>(
+                                        joinProcessFunc, 
joinProcessFunc.getMaxOutputDelay()),
+                                compositeKeySelector,
+                                compositeKeySelector,
+                                keyType);
+        testHarness.open();
+
+        testHarness.processElement1(insertRecord(1L, null, "x"));
+        testHarness.processElement2(insertRecord(1L, null, "x"));
+        assertThat(testHarness.numKeyedStateEntries()).isZero();
+
+        testHarness.processElement1(insertRecord(1L, "a", null));
+        testHarness.processElement2(insertRecord(1L, "a", null));
+        assertThat(testHarness.numKeyedStateEntries()).isEqualTo(4);
+
+        testHarness.processWatermark1(new Watermark(100));
+        testHarness.processWatermark2(new Watermark(100));
+
+        final RowDataHarnessAssertor compositeKeyAssertor =
+                new RowDataHarnessAssertor(
+                        new LogicalType[] {
+                            new BigIntType(),
+                            VarCharType.STRING_TYPE,
+                            VarCharType.STRING_TYPE,
+                            new BigIntType(),
+                            VarCharType.STRING_TYPE,
+                            VarCharType.STRING_TYPE
+                        });
+        List<Object> expectedOutput = new ArrayList<>();
+        expectedOutput.add(insertRecord(1L, "a", null, 1L, "a", null));
+        expectedOutput.add(new Watermark(91));
+        compositeKeyAssertor.assertOutputEqualsSorted(
+                "output wrong.", expectedOutput, testHarness.getOutput());
+        testHarness.close();
+    }
+
     @Test
     public void testInterruptibleTimers() throws Exception {
         final boolean[] isMailProcessed = new boolean[1]; // e.g. checkpoint 
mail
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalStreamJoinTestBase.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalStreamJoinTestBase.java
index 1a1ea06847e..6d94fafba0c 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalStreamJoinTestBase.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalStreamJoinTestBase.java
@@ -56,9 +56,13 @@ abstract class TimeIntervalStreamJoinTestBase {
     // instance cannot be opened twice. Restore tests that open a second 
operator must build a fresh
     // function for the restored harness.
     protected IntervalJoinFunction newJoinFunction() {
+        return newJoinFunction(new boolean[] {true});
+    }
+
+    protected IntervalJoinFunction newJoinFunction(boolean[] filterNulls) {
         return new IntervalJoinFunction(
                 new GeneratedJoinCondition("TestIntervalJoinCondition", 
funcCode, new Object[0]),
                 outputRowType,
-                new boolean[] {true});
+                filterNulls);
     }
 }
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperatorTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperatorTest.java
index b1fe275087c..8f5fd251e8c 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperatorTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/window/WindowJoinOperatorTest.java
@@ -504,8 +504,91 @@ class WindowJoinOperatorTest {
         testHarness.close();
     }
 
+    @TestTemplate
+    void testDoesNotStoreRecordsThatCanNeverMatch() throws Exception {
+        final long time = toUtcTimestampMills(3L, shiftTimeZone);
+        // Only records of an input whose unmatched records are emitted are 
stored
+        assertNullKeyRecords(FlinkJoinType.INNER, 0);
+        assertNullKeyRecords(FlinkJoinType.SEMI, 0);
+        assertNullKeyRecords(FlinkJoinType.ANTI, 1, insertRecord(time, null));
+        assertNullKeyRecords(FlinkJoinType.LEFT, 1, insertRecord(time, null, 
null, null));
+        assertNullKeyRecords(FlinkJoinType.RIGHT, 1, insertRecord(null, null, 
time, null));
+        assertNullKeyRecords(
+                FlinkJoinType.FULL,
+                2,
+                insertRecord(time, null, null, null),
+                insertRecord(null, null, time, null));
+    }
+
+    /**
+     * Processes a record with a null key on each input and checks the number 
of stored records and
+     * the output when the window fires.
+     */
+    private void assertNullKeyRecords(
+            FlinkJoinType joinType, int expectedStateEntries, Object... 
expectedRecords)
+            throws Exception {
+        KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData> testHarness =
+                createTestHarness(joinType);
+        testHarness.open();
+        testHarness.processWatermark1(new Watermark(1));
+        testHarness.processWatermark2(new Watermark(1));
+
+        final long time = toUtcTimestampMills(3L, shiftTimeZone);
+        testHarness.processElement1(insertRecord(time, null));
+        testHarness.processElement2(insertRecord(time, null));
+        assertThat(testHarness.numKeyedStateEntries())
+                .as("stored records for %s", joinType)
+                .isEqualTo(expectedStateEntries);
+
+        testHarness.processWatermark1(new Watermark(10));
+        testHarness.processWatermark2(new Watermark(10));
+
+        List<Object> expectedOutput = new ArrayList<>();
+        expectedOutput.add(new Watermark(1));
+        expectedOutput.addAll(Arrays.asList(expectedRecords));
+        expectedOutput.add(new Watermark(10));
+        RowDataHarnessAssertor assertor =
+                joinType == FlinkJoinType.SEMI || joinType == 
FlinkJoinType.ANTI
+                        ? SEMI_ANTI_JOIN_ASSERTER
+                        : ASSERTER;
+        assertor.assertOutputEqualsSorted(
+                "output wrong for " + joinType, expectedOutput, 
testHarness.getOutput());
+        assertThat(testHarness.numKeyedStateEntries()).isZero();
+        testHarness.close();
+    }
+
+    @TestTemplate
+    void testStoresRecordsWithNullSafeKeys() throws Exception {
+        // A null key can match if it is compared null-safely, as with IS NOT 
DISTINCT FROM
+        final long time = toUtcTimestampMills(3L, shiftTimeZone);
+        KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData> testHarness =
+                createTestHarness(FlinkJoinType.INNER, new boolean[] {false});
+        testHarness.open();
+        testHarness.processWatermark1(new Watermark(1));
+        testHarness.processWatermark2(new Watermark(1));
+
+        testHarness.processElement1(insertRecord(time, null));
+        testHarness.processElement2(insertRecord(time, null));
+        assertThat(testHarness.numKeyedStateEntries()).isEqualTo(2);
+
+        testHarness.processWatermark1(new Watermark(10));
+        testHarness.processWatermark2(new Watermark(10));
+
+        List<Object> expectedOutput = new ArrayList<>();
+        expectedOutput.add(new Watermark(1));
+        expectedOutput.add(insertRecord(time, null, time, null));
+        expectedOutput.add(new Watermark(10));
+        ASSERTER.assertOutputEqualsSorted("output wrong.", expectedOutput, 
testHarness.getOutput());
+        testHarness.close();
+    }
+
     private KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData>
             createTestHarness(FlinkJoinType joinType) throws Exception {
+        return createTestHarness(joinType, new boolean[] {true});
+    }
+
+    private KeyedTwoInputStreamOperatorTestHarness<RowData, RowData, RowData, 
RowData>
+            createTestHarness(FlinkJoinType joinType, boolean[] 
filterNullKeys) throws Exception {
         String funcCode =
                 "public class TestWindowJoinCondition extends 
org.apache.flink.api.common.functions.AbstractRichFunction "
                         + "implements 
org.apache.flink.table.runtime.generated.JoinCondition {\n"
@@ -533,7 +616,7 @@ class WindowJoinOperatorTest {
                         .generatedJoinCondition(joinFunction)
                         .leftWindowEndIndex(0)
                         .rightWindowEndIndex(0)
-                        .filterNullKeys(new boolean[] {true})
+                        .filterNullKeys(filterNullKeys)
                         .joinType(joinType)
                         .withShiftTimezone(shiftTimeZone);
         if (enableAsyncState) {

Reply via email to