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 eead57e7a07 [FLINK-41067][table] Don't store rows whose join key can
never match in window and interval joins
eead57e7a07 is described below
commit eead57e7a073c6d69e3fa48e0baebcd7f597fad5
Author: Rion Williams <[email protected]>
AuthorDate: Tue Oct 27 11:58:24 2026 -0500
[FLINK-41067][table] Don't store rows whose join key can never match in
window and interval joins
This fixes #29558.
---
.../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) {