This is an automated email from the ASF dual-hosted git repository.
snuyanzin 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 f8f70684560 [FLINK-40948][table] Maintain window properties for
different joins
f8f70684560 is described below
commit f8f70684560edc5bbd507fd2b76a74821a6e19d9
Author: Sergey Nuyanzin <[email protected]>
AuthorDate: Thu Oct 29 17:33:39 2026 +0100
[FLINK-40948][table] Maintain window properties for different joins
---
.../plan/metadata/FlinkRelMdWindowProperties.scala | 15 ++-
.../stream/WindowJoinSemanticTestPrograms.java | 150 +++++++++++++++++++++
.../nodes/exec/stream/WindowJoinSemanticTests.java | 37 +++++
.../planner/plan/stream/sql/WindowRankTest.xml | 62 +++++++++
.../plan/stream/sql/agg/WindowAggregateTest.xml | 98 ++++++++++++++
.../plan/stream/sql/join/WindowJoinTest.xml | 68 ++++++++++
.../planner/plan/stream/sql/WindowRankTest.scala | 26 ++++
.../plan/stream/sql/agg/WindowAggregateTest.scala | 32 +++++
.../plan/stream/sql/join/WindowJoinTest.scala | 23 ++++
9 files changed, 507 insertions(+), 4 deletions(-)
diff --git
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdWindowProperties.scala
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdWindowProperties.scala
index 80cce27a559..ca07a4fdc66 100644
---
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdWindowProperties.scala
+++
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdWindowProperties.scala
@@ -413,7 +413,7 @@ class FlinkRelMdWindowProperties private extends
MetadataHandler[FlinkMetadata.W
def getWindowProperties(rel: FlinkLogicalJoin, mq: RelMetadataQuery):
RelWindowProperties = {
if (satisfyWindowJoin(rel)) {
- getJoinWindowProperties(rel.getLeft, rel.getRight, mq)
+ getJoinWindowProperties(rel.getJoinType, rel.getLeft, rel.getRight, mq)
} else {
null
}
@@ -422,10 +422,11 @@ class FlinkRelMdWindowProperties private extends
MetadataHandler[FlinkMetadata.W
def getWindowProperties(
rel: StreamPhysicalWindowJoin,
mq: RelMetadataQuery): RelWindowProperties = {
- getJoinWindowProperties(rel.getLeft, rel.getRight, mq)
+ getJoinWindowProperties(rel.getJoinType, rel.getLeft, rel.getRight, mq)
}
private def getJoinWindowProperties(
+ joinType: JoinRelType,
left: RelNode,
right: RelNode,
mq: RelMetadataQuery): RelWindowProperties = {
@@ -435,10 +436,16 @@ class FlinkRelMdWindowProperties private extends
MetadataHandler[FlinkMetadata.W
def inferWindowPropertyAfterWindowJoin(
leftWindowProperty: ImmutableBitSet,
rightWindowProperty: ImmutableBitSet): ImmutableBitSet = {
+ // window columns on an outer join's null-generating side become
nullable, so they are no
+ // longer valid window properties
+ val preservedLeft =
+ if (joinType.generatesNullsOnLeft) ImmutableBitSet.of() else
leftWindowProperty
+ val preservedRight =
+ if (joinType.generatesNullsOnRight) ImmutableBitSet.of() else
rightWindowProperty
val fieldMapping = new JHashMap[Integer, Integer]()
(0 until rightFieldCnt).foreach(idx => fieldMapping.put(idx,
leftFieldCnt + idx))
- val rightWindowPropertyAfterWindowJoin =
rightWindowProperty.permute(fieldMapping)
- leftWindowProperty.union(rightWindowPropertyAfterWindowJoin)
+ val rightWindowPropertyAfterWindowJoin =
preservedRight.permute(fieldMapping)
+ preservedLeft.union(rightWindowPropertyAfterWindowJoin)
}
val fmq = FlinkRelMetadataQuery.reuseOrCreate(mq)
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/WindowJoinSemanticTestPrograms.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/WindowJoinSemanticTestPrograms.java
new file mode 100644
index 00000000000..e77af3bc7a2
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/WindowJoinSemanticTestPrograms.java
@@ -0,0 +1,150 @@
+/*
+ * 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.planner.plan.nodes.exec.stream;
+
+import org.apache.flink.table.test.program.SinkTestStep;
+import org.apache.flink.table.test.program.SourceTestStep;
+import org.apache.flink.table.test.program.TableTestProgram;
+import org.apache.flink.types.Row;
+
+/**
+ * Semantic {@link TableTestProgram} definitions for a window aggregate placed
on top of a {@link
+ * StreamExecWindowJoin}.
+ */
+public class WindowJoinSemanticTestPrograms {
+
+ // window W1 = [00:00, 00:15), two left rows, one right row -> matched on
both sides
+ // window W2 = [00:15, 00:30), two left rows, no right rows -> left-only
+ // window W3 = [00:30, 00:45), no left rows, two right rows -> right-only
+ private static final Row[] SOURCE_ONE_DATA = {
+ Row.of("2020-10-10 00:01:00"),
+ Row.of("2020-10-10 00:02:00"),
+ Row.of("2020-10-10 00:16:00"),
+ Row.of("2020-10-10 00:17:00")
+ };
+
+ private static final Row[] SOURCE_TWO_DATA = {
+ Row.of("2020-10-10 00:03:00"), Row.of("2020-10-10 00:31:00"),
Row.of("2020-10-10 00:32:00")
+ };
+
+ private static final String[] SOURCE_SCHEMA = {
+ "ts STRING",
+ "rowtime AS TO_TIMESTAMP(`ts`)",
+ "WATERMARK for `rowtime` AS `rowtime` - INTERVAL '1' SECOND"
+ };
+
+ private static final String[] SINK_SCHEMA = {
+ "window_start TIMESTAMP(3)", "window_end TIMESTAMP(3)", "cnt BIGINT"
+ };
+
+ private static final SourceTestStep SOURCE_ONE =
+ SourceTestStep.newBuilder("source_one_t")
+ .addSchema(SOURCE_SCHEMA)
+ .producedValues(SOURCE_ONE_DATA)
+ .build();
+
+ private static final SourceTestStep SOURCE_TWO =
+ SourceTestStep.newBuilder("source_two_t")
+ .addSchema(SOURCE_SCHEMA)
+ .producedValues(SOURCE_TWO_DATA)
+ .build();
+
+ private static String windowAggregateOnWindowJoin(String joinType, String
windowSide) {
+ return String.format(
+ "INSERT INTO sink_t\n"
+ + "SELECT %1$s.window_start, %1$s.window_end, COUNT(*)
AS cnt\n"
+ + "FROM TUMBLE(TABLE source_one_t,
DESCRIPTOR(rowtime), INTERVAL '15' MINUTE) t_left\n"
+ + "%2$s TUMBLE(TABLE source_two_t,
DESCRIPTOR(rowtime), INTERVAL '15' MINUTE) t_right\n"
+ + "ON t_left.window_start = t_right.window_start"
+ + " AND t_left.window_end = t_right.window_end\n"
+ + "GROUP BY %1$s.window_start, %1$s.window_end",
+ windowSide, joinType);
+ }
+
+ public static final TableTestProgram WINDOW_AGGREGATE_ON_INNER_WINDOW_JOIN
=
+ TableTestProgram.of(
+ "window-aggregate-on-inner-window-join",
+ "validates a window aggregate on top of an inner
window join keeps the"
+ + " window properties, so no row carries
null window columns")
+ .setupTableSource(SOURCE_ONE)
+ .setupTableSource(SOURCE_TWO)
+ .setupTableSink(
+ SinkTestStep.newBuilder("sink_t")
+ .addSchema(SINK_SCHEMA)
+ .testMaterializedData()
+ .consumedValues("+I[2020-10-10T00:00,
2020-10-10T00:15, 2]")
+ .build())
+ .runSql(windowAggregateOnWindowJoin("JOIN", "t_right"))
+ .build();
+
+ public static final TableTestProgram WINDOW_AGGREGATE_ON_LEFT_WINDOW_JOIN =
+ TableTestProgram.of(
+ "window-aggregate-on-left-window-join",
+ "validates a window aggregate on top of a left
window join groups the"
+ + " unmatched rows whose (right-side)
window columns are null")
+ .setupTableSource(SOURCE_ONE)
+ .setupTableSource(SOURCE_TWO)
+ .setupTableSink(
+ SinkTestStep.newBuilder("sink_t")
+ .addSchema(SINK_SCHEMA)
+ .testMaterializedData()
+ .consumedValues(
+ "+I[2020-10-10T00:00,
2020-10-10T00:15, 2]",
+ "+I[null, null, 2]")
+ .build())
+ .runSql(windowAggregateOnWindowJoin("LEFT JOIN",
"t_right"))
+ .build();
+
+ public static final TableTestProgram WINDOW_AGGREGATE_ON_RIGHT_WINDOW_JOIN
=
+ TableTestProgram.of(
+ "window-aggregate-on-right-window-join",
+ "validates a window aggregate on top of a right
window join groups the"
+ + " unmatched rows whose (left-side)
window columns are null")
+ .setupTableSource(SOURCE_ONE)
+ .setupTableSource(SOURCE_TWO)
+ .setupTableSink(
+ SinkTestStep.newBuilder("sink_t")
+ .addSchema(SINK_SCHEMA)
+ .testMaterializedData()
+ .consumedValues(
+ "+I[2020-10-10T00:00,
2020-10-10T00:15, 2]",
+ "+I[null, null, 2]")
+ .build())
+ .runSql(windowAggregateOnWindowJoin("RIGHT JOIN",
"t_left"))
+ .build();
+
+ public static final TableTestProgram WINDOW_AGGREGATE_ON_FULL_WINDOW_JOIN =
+ TableTestProgram.of(
+ "window-aggregate-on-full-window-join",
+ "validates a window aggregate on top of a full
window join groups the"
+ + " unmatched rows whose window columns
are null")
+ .setupTableSource(SOURCE_ONE)
+ .setupTableSource(SOURCE_TWO)
+ .setupTableSink(
+ SinkTestStep.newBuilder("sink_t")
+ .addSchema(SINK_SCHEMA)
+ .testMaterializedData()
+ .consumedValues(
+ "+I[2020-10-10T00:00,
2020-10-10T00:15, 2]",
+ "+I[2020-10-10T00:30,
2020-10-10T00:45, 2]",
+ "+I[null, null, 2]")
+ .build())
+ .runSql(windowAggregateOnWindowJoin("FULL JOIN",
"t_right"))
+ .build();
+}
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/WindowJoinSemanticTests.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/WindowJoinSemanticTests.java
new file mode 100644
index 00000000000..f432fcb7b1d
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/WindowJoinSemanticTests.java
@@ -0,0 +1,37 @@
+/*
+ * 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.planner.plan.nodes.exec.stream;
+
+import
org.apache.flink.table.planner.plan.nodes.exec.testutils.SemanticTestBase;
+import org.apache.flink.table.test.program.TableTestProgram;
+
+import java.util.List;
+
+/** Semantic tests for a window aggregate on top of a {@link
StreamExecWindowJoin}. */
+public class WindowJoinSemanticTests extends SemanticTestBase {
+
+ @Override
+ public List<TableTestProgram> programs() {
+ return List.of(
+
WindowJoinSemanticTestPrograms.WINDOW_AGGREGATE_ON_INNER_WINDOW_JOIN,
+
WindowJoinSemanticTestPrograms.WINDOW_AGGREGATE_ON_LEFT_WINDOW_JOIN,
+
WindowJoinSemanticTestPrograms.WINDOW_AGGREGATE_ON_RIGHT_WINDOW_JOIN,
+
WindowJoinSemanticTestPrograms.WINDOW_AGGREGATE_ON_FULL_WINDOW_JOIN);
+ }
+}
diff --git
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.xml
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.xml
index 07098bba996..b444868954f 100644
---
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.xml
+++
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.xml
@@ -502,6 +502,68 @@ Calc(select=[a, window_start, window_end, EXPR$3, EXPR$4,
EXPR$5])
+- Calc(select=[a, d, IS TRUE(>(b, 1000)) AS
$f5, b, e, c, rowtime])
+- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+-
TableSourceScan(table=[[default_catalog, default_database, MyTable]],
fields=[a, b, c, d, e, rowtime])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testWindowTopNOnNullSideFallsBackToRegularRank">
+ <Resource name="sql">
+ <![CDATA[
+SELECT *
+FROM (
+ SELECT *,
+ ROW_NUMBER() OVER (
+ PARTITION BY null_window_start, null_window_end ORDER BY a DESC) AS
rownum
+ FROM (
+ SELECT
+ null_side.window_start AS null_window_start,
+ null_side.window_end AS null_window_end,
+ preserved_side.a AS a
+ FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
preserved_side
+ LEFT JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
null_side
+ ON preserved_side.window_start = null_side.window_start
+ AND preserved_side.window_end = null_side.window_end
+ AND preserved_side.a = null_side.a
+ )
+)
+WHERE rownum <= 3
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalProject(null_window_start=[$0], null_window_end=[$1], a=[$2],
rownum=[$3])
++- LogicalFilter(condition=[<=($3, 3)])
+ +- LogicalProject(null_window_start=[$0], null_window_end=[$1], a=[$2],
rownum=[ROW_NUMBER() OVER (PARTITION BY $0, $1 ORDER BY $2 DESC NULLS LAST)])
+ +- LogicalProject(null_window_start=[$17], null_window_end=[$18], a=[$0])
+ +- LogicalJoin(condition=[AND(=($7, $17), =($8, $18), =($0, $10))],
joinType=[left])
+ :- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) c, DECIMAL(10, 3)
d, BIGINT e, TIMESTAMP(3) *ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME*
proctime, TIMESTAMP(3) window_start, TIMESTAMP(3) window_end, TIMESTAMP(3)
*ROWTIME* window_time)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[$6])
+ : +- LogicalWatermarkAssigner(rowtime=[rowtime],
watermark=[-($5, 1000:INTERVAL SECOND)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[PROCTIME()])
+ : +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+ +- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) c, DECIMAL(10, 3)
d, BIGINT e, TIMESTAMP(3) *ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME*
proctime, TIMESTAMP(3) window_start, TIMESTAMP(3) window_end, TIMESTAMP(3)
*ROWTIME* window_time)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[$6])
+ +- LogicalWatermarkAssigner(rowtime=[rowtime],
watermark=[-($5, 1000:INTERVAL SECOND)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[PROCTIME()])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+Calc(select=[window_start0 AS null_window_start, window_end0 AS
null_window_end, a, rownum])
++- Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER],
rankRange=[rankStart=1, rankEnd=3], partitionBy=[window_start0, window_end0],
orderBy=[a DESC], select=[a, window_start0, window_end0, rownum])
+ +- Exchange(distribution=[hash[window_start0, window_end0]])
+ +- Calc(select=[a, window_start0, window_end0])
+ +- WindowJoin(leftWindow=[TUMBLE(win_start=[window_start],
win_end=[window_end], size=[15 min])],
rightWindow=[TUMBLE(win_start=[window_start], win_end=[window_end], size=[15
min])], joinType=[LeftOuterJoin], where=[=(a, a0)], select=[a, b, c, d, e,
rowtime, proctime, window_start, window_end, window_time, a0, b0, c0, d0, e0,
rowtime0, proctime0, window_start0, window_end0, window_time0])
+ :- Exchange(distribution=[hash[a]])
+ : +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ : +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ : +- Calc(select=[a, b, c, d, e, rowtime, PROCTIME() AS
proctime])
+ : +- TableSourceScan(table=[[default_catalog,
default_database, MyTable]], fields=[a, b, c, d, e, rowtime])
+ +- Exchange(distribution=[hash[a]])
+ +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ +- Calc(select=[a, b, c, d, e, rowtime, PROCTIME() AS
proctime])
+ +- TableSourceScan(table=[[default_catalog,
default_database, MyTable]], fields=[a, b, c, d, e, rowtime])
]]>
</Resource>
</TestCase>
diff --git
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.xml
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.xml
index 68e4171ca14..ab610430755 100644
---
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.xml
+++
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.xml
@@ -2266,6 +2266,104 @@ Sink(table=[default_catalog.default_database.s1],
fields=[window_start, window_e
+- WatermarkAssigner(rowtime=[rowtime], watermark=[(rowtime -
1000:INTERVAL SECOND)])
+- Calc(select=[rowtime])
+- Reused(reference_id=[1])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testOnNullSideOfOuterWindowJoin[aggPhaseEnforcer=TWO_PHASE]">
+ <Resource name="sql">
+ <![CDATA[
+SELECT null_side.window_start, null_side.window_end, COUNT(*) AS cnt
+FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
preserved_side
+LEFT JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
null_side
+ON preserved_side.window_start = null_side.window_start
+ AND preserved_side.window_end = null_side.window_end
+ AND preserved_side.a = null_side.a
+GROUP BY null_side.window_start, null_side.window_end
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalAggregate(group=[{0, 1}], cnt=[COUNT()])
++- LogicalProject(window_start=[$17], window_end=[$18])
+ +- LogicalJoin(condition=[AND(=($7, $17), =($8, $18), =($0, $10))],
joinType=[left])
+ :- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) c, DECIMAL(10, 3)
d, BIGINT e, TIMESTAMP(3) *ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME*
proctime, TIMESTAMP(3) window_start, TIMESTAMP(3) window_end, TIMESTAMP(3)
*ROWTIME* window_time)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[$6])
+ : +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($5,
1000:INTERVAL SECOND)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[PROCTIME()])
+ : +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+ +- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) c, DECIMAL(10, 3)
d, BIGINT e, TIMESTAMP(3) *ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME*
proctime, TIMESTAMP(3) window_start, TIMESTAMP(3) window_end, TIMESTAMP(3)
*ROWTIME* window_time)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[$6])
+ +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($5,
1000:INTERVAL SECOND)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[PROCTIME()])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+GroupAggregate(groupBy=[window_start, window_end], select=[window_start,
window_end, COUNT(*) AS cnt])
++- Exchange(distribution=[hash[window_start, window_end]])
+ +- Calc(select=[window_start0 AS window_start, window_end0 AS window_end])
+ +- WindowJoin(leftWindow=[TUMBLE(win_start=[window_start],
win_end=[window_end], size=[15 min])],
rightWindow=[TUMBLE(win_start=[window_start], win_end=[window_end], size=[15
min])], joinType=[LeftOuterJoin], where=[=(a, a0)], select=[a, window_start,
window_end, a0, window_start0, window_end0])
+ :- Exchange(distribution=[hash[a]])
+ : +- Calc(select=[a, window_start, window_end])
+ : +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ : +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ : +- TableSourceScan(table=[[default_catalog,
default_database, MyTable, project=[a, rowtime]]], fields=[a, rowtime])
+ +- Exchange(distribution=[hash[a]])
+ +- Calc(select=[a, window_start, window_end])
+ +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ +- TableSourceScan(table=[[default_catalog,
default_database, MyTable, project=[a, rowtime]]], fields=[a, rowtime])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase
name="testOnPreservedSideOfOuterWindowJoin[aggPhaseEnforcer=TWO_PHASE]">
+ <Resource name="sql">
+ <![CDATA[
+SELECT preserved_side.window_start, preserved_side.window_end, COUNT(*) AS cnt
+FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
preserved_side
+LEFT JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
null_side
+ON preserved_side.window_start = null_side.window_start
+ AND preserved_side.window_end = null_side.window_end
+ AND preserved_side.a = null_side.a
+GROUP BY preserved_side.window_start, preserved_side.window_end
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalAggregate(group=[{0, 1}], cnt=[COUNT()])
++- LogicalProject(window_start=[$7], window_end=[$8])
+ +- LogicalJoin(condition=[AND(=($7, $17), =($8, $18), =($0, $10))],
joinType=[left])
+ :- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) c, DECIMAL(10, 3)
d, BIGINT e, TIMESTAMP(3) *ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME*
proctime, TIMESTAMP(3) window_start, TIMESTAMP(3) window_end, TIMESTAMP(3)
*ROWTIME* window_time)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[$6])
+ : +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($5,
1000:INTERVAL SECOND)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[PROCTIME()])
+ : +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+ +- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) c, DECIMAL(10, 3)
d, BIGINT e, TIMESTAMP(3) *ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME*
proctime, TIMESTAMP(3) window_start, TIMESTAMP(3) window_end, TIMESTAMP(3)
*ROWTIME* window_time)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[$6])
+ +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($5,
1000:INTERVAL SECOND)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], d=[$3], e=[$4],
rowtime=[$5], proctime=[PROCTIME()])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+Calc(select=[window_start, window_end, cnt])
++- GlobalWindowAggregate(window=[TUMBLE(win_end=[$window_end], size=[15
min])], select=[COUNT(count1$0) AS cnt, start('w$) AS window_start, end('w$) AS
window_end])
+ +- Exchange(distribution=[single])
+ +- LocalWindowAggregate(window=[TUMBLE(win_start=[window_start],
win_end=[window_end], size=[15 min])], select=[COUNT(*) AS count1$0,
slice_end('w$) AS $window_end])
+ +- Calc(select=[window_start, window_end])
+ +- WindowJoin(leftWindow=[TUMBLE(win_start=[window_start],
win_end=[window_end], size=[15 min])],
rightWindow=[TUMBLE(win_start=[window_start], win_end=[window_end], size=[15
min])], joinType=[LeftOuterJoin], where=[=(a, a0)], select=[a, window_start,
window_end, a0, window_start0, window_end0])
+ :- Exchange(distribution=[hash[a]])
+ : +- Calc(select=[a, window_start, window_end])
+ : +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ : +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ : +- TableSourceScan(table=[[default_catalog,
default_database, MyTable, project=[a, rowtime]]], fields=[a, rowtime])
+ +- Exchange(distribution=[hash[a]])
+ +- Calc(select=[a, window_start, window_end])
+ +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ +- TableSourceScan(table=[[default_catalog,
default_database, MyTable, project=[a, rowtime]]], fields=[a, rowtime])
]]>
</Resource>
</TestCase>
diff --git
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.xml
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.xml
index ca0cd46423c..80983fc90fa 100644
---
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.xml
+++
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.xml
@@ -2288,6 +2288,74 @@ Calc(select=[rowtime, a, l_cnt, l_uv, r_cnt, r_uv, a0,
b, c, rowtime0, PROCTIME_
+- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime,
1000:INTERVAL SECOND)])
+- Calc(select=[a, b, c, rowtime, PROCTIME() AS proctime])
+- TableSourceScan(table=[[default_catalog, default_database,
MyTable4]], fields=[a, b, c, rowtime])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testWindowJoinOnNullSideFallsBackToRegularJoin">
+ <Resource name="sql">
+ <![CDATA[
+SELECT outer_join.null_window_start, outer_join.null_window_end, probe_side.a
+FROM (
+ SELECT
+ null_side.window_start AS null_window_start,
+ null_side.window_end AS null_window_end,
+ preserved_side.a AS a
+ FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
preserved_side
+ LEFT JOIN TUMBLE(TABLE MyTable2, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
null_side
+ ON preserved_side.window_start = null_side.window_start
+ AND preserved_side.window_end = null_side.window_end
+ AND preserved_side.a = null_side.a
+) outer_join
+JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
probe_side
+ON outer_join.null_window_start = probe_side.window_start
+ AND outer_join.null_window_end = probe_side.window_end
+ ]]>
+ </Resource>
+ <Resource name="ast">
+ <![CDATA[
+LogicalProject(null_window_start=[$0], null_window_end=[$1], a=[$3])
++- LogicalJoin(condition=[AND(=($0, $8), =($1, $9))], joinType=[inner])
+ :- LogicalProject(null_window_start=[$13], null_window_end=[$14], a=[$0])
+ : +- LogicalJoin(condition=[AND(=($5, $13), =($6, $14), =($0, $8))],
joinType=[left])
+ : :- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, VARCHAR(2147483647) b, BIGINT c, TIMESTAMP(3)
*ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME* proctime, TIMESTAMP(3)
window_start, TIMESTAMP(3) window_end, TIMESTAMP(3) *ROWTIME* window_time)])
+ : : +- LogicalProject(a=[$0], b=[$1], c=[$2], rowtime=[$3],
proctime=[$4])
+ : : +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($3,
1000:INTERVAL SECOND)])
+ : : +- LogicalProject(a=[$0], b=[$1], c=[$2], rowtime=[$3],
proctime=[PROCTIME()])
+ : : +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable]])
+ : +- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, VARCHAR(2147483647) b, BIGINT c, TIMESTAMP(3)
*ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME* proctime, TIMESTAMP(3)
window_start, TIMESTAMP(3) window_end, TIMESTAMP(3) *ROWTIME* window_time)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], rowtime=[$3],
proctime=[$4])
+ : +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($3,
1000:INTERVAL SECOND)])
+ : +- LogicalProject(a=[$0], b=[$1], c=[$2], rowtime=[$3],
proctime=[PROCTIME()])
+ : +- LogicalTableScan(table=[[default_catalog,
default_database, MyTable2]])
+ +- LogicalTableFunctionScan(invocation=[TUMBLE(TABLE(#0),
DESCRIPTOR(_UTF-16LE'rowtime'), 900000:INTERVAL MINUTE)],
rowType=[RecordType(INTEGER a, VARCHAR(2147483647) b, BIGINT c, TIMESTAMP(3)
*ROWTIME* rowtime, TIMESTAMP_LTZ(3) *PROCTIME* proctime, TIMESTAMP(3)
window_start, TIMESTAMP(3) window_end, TIMESTAMP(3) *ROWTIME* window_time)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], rowtime=[$3], proctime=[$4])
+ +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($3,
1000:INTERVAL SECOND)])
+ +- LogicalProject(a=[$0], b=[$1], c=[$2], rowtime=[$3],
proctime=[PROCTIME()])
+ +- LogicalTableScan(table=[[default_catalog, default_database,
MyTable]])
+]]>
+ </Resource>
+ <Resource name="optimized rel plan">
+ <![CDATA[
+Calc(select=[null_window_start, null_window_end, a])
++- Join(joinType=[InnerJoin], where=[AND(=(null_window_start, window_start),
=(null_window_end, window_end))], select=[null_window_start, null_window_end,
a, window_start, window_end], leftInputSpec=[NoUniqueKey],
rightInputSpec=[NoUniqueKey])
+ :- Exchange(distribution=[hash[null_window_start, null_window_end]])
+ : +- Calc(select=[window_start0 AS null_window_start, window_end0 AS
null_window_end])
+ : +- WindowJoin(leftWindow=[TUMBLE(win_start=[window_start],
win_end=[window_end], size=[15 min])],
rightWindow=[TUMBLE(win_start=[window_start], win_end=[window_end], size=[15
min])], joinType=[LeftOuterJoin], where=[=(a, a0)], select=[a, window_start,
window_end, a0, window_start0, window_end0])
+ : :- Exchange(distribution=[hash[a]])
+ : : +- Calc(select=[a, window_start, window_end])
+ : : +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ : : +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ : : +- TableSourceScan(table=[[default_catalog,
default_database, MyTable, project=[a, rowtime]]], fields=[a, rowtime])
+ : +- Exchange(distribution=[hash[a]])
+ : +- Calc(select=[a, window_start, window_end])
+ : +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime],
size=[15 min])])
+ : +- WatermarkAssigner(rowtime=[rowtime],
watermark=[-(rowtime, 1000:INTERVAL SECOND)])
+ : +- TableSourceScan(table=[[default_catalog,
default_database, MyTable2, project=[a, rowtime]]], fields=[a, rowtime])
+ +- Exchange(distribution=[hash[window_start, window_end]])
+ +- Calc(select=[a, window_start, window_end])
+ +- WindowTableFunction(window=[TUMBLE(time_col=[rowtime], size=[15
min])])
+ +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime,
1000:INTERVAL SECOND)])
+ +- TableSourceScan(table=[[default_catalog, default_database,
MyTable, project=[a, rowtime]]], fields=[a, rowtime])
]]>
</Resource>
</TestCase>
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.scala
index fde6fbd50d6..704dd574a4e 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/WindowRankTest.scala
@@ -557,4 +557,30 @@ class WindowRankTest extends TableTestBase {
"Rank strategy rankEnd=max_b is not supported on window rank
currently.")
.isInstanceOf[TableException]
}
+
+ @Test
+ def testWindowTopNOnNullSideFallsBackToRegularRank(): Unit = {
+ val sql =
+ """
+ |SELECT *
+ |FROM (
+ | SELECT *,
+ | ROW_NUMBER() OVER (
+ | PARTITION BY null_window_start, null_window_end ORDER BY a
DESC) AS rownum
+ | FROM (
+ | SELECT
+ | null_side.window_start AS null_window_start,
+ | null_side.window_end AS null_window_end,
+ | preserved_side.a AS a
+ | FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15'
MINUTE) preserved_side
+ | LEFT JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL
'15' MINUTE) null_side
+ | ON preserved_side.window_start = null_side.window_start
+ | AND preserved_side.window_end = null_side.window_end
+ | AND preserved_side.a = null_side.a
+ | )
+ |)
+ |WHERE rownum <= 3
+ """.stripMargin
+ util.verifyRelPlan(sql)
+ }
}
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.scala
index 56987570274..8a89d76fc3f 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/WindowAggregateTest.scala
@@ -1753,6 +1753,38 @@ class WindowAggregateTest(aggPhaseEnforcer:
AggregatePhaseStrategy) extends Tabl
|group by window_start, window_end, c, window_time
|""".stripMargin)
}
+
+ @TestTemplate
+ def testOnPreservedSideOfOuterWindowJoin(): Unit = {
+ assumeThat(isTwoPhase).isTrue
+ val sql =
+ """
+ |SELECT preserved_side.window_start, preserved_side.window_end,
COUNT(*) AS cnt
+ |FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
preserved_side
+ |LEFT JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15'
MINUTE) null_side
+ |ON preserved_side.window_start = null_side.window_start
+ | AND preserved_side.window_end = null_side.window_end
+ | AND preserved_side.a = null_side.a
+ |GROUP BY preserved_side.window_start, preserved_side.window_end
+ """.stripMargin
+ util.verifyRelPlan(sql)
+ }
+
+ @TestTemplate
+ def testOnNullSideOfOuterWindowJoin(): Unit = {
+ assumeThat(isTwoPhase).isTrue
+ val sql =
+ """
+ |SELECT null_side.window_start, null_side.window_end, COUNT(*) AS cnt
+ |FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
preserved_side
+ |LEFT JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15'
MINUTE) null_side
+ |ON preserved_side.window_start = null_side.window_start
+ | AND preserved_side.window_end = null_side.window_end
+ | AND preserved_side.a = null_side.a
+ |GROUP BY null_side.window_start, null_side.window_end
+ """.stripMargin
+ util.verifyRelPlan(sql)
+ }
}
object WindowAggregateTest {
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.scala
index 8ad6d45cdc8..59247a047ed 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/join/WindowJoinTest.scala
@@ -1348,4 +1348,27 @@ class WindowJoinTest extends TableTestBase {
statementSet.addInsertSql("INSERT INTO sink2 SELECT * FROM food_view")
util.verifyRelPlan(statementSet)
}
+
+ @Test
+ def testWindowJoinOnNullSideFallsBackToRegularJoin(): Unit = {
+ val sql =
+ """
+ |SELECT outer_join.null_window_start, outer_join.null_window_end,
probe_side.a
+ |FROM (
+ | SELECT
+ | null_side.window_start AS null_window_start,
+ | null_side.window_end AS null_window_end,
+ | preserved_side.a AS a
+ | FROM TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15'
MINUTE) preserved_side
+ | LEFT JOIN TUMBLE(TABLE MyTable2, DESCRIPTOR(rowtime), INTERVAL '15'
MINUTE) null_side
+ | ON preserved_side.window_start = null_side.window_start
+ | AND preserved_side.window_end = null_side.window_end
+ | AND preserved_side.a = null_side.a
+ |) outer_join
+ |JOIN TUMBLE(TABLE MyTable, DESCRIPTOR(rowtime), INTERVAL '15' MINUTE)
probe_side
+ |ON outer_join.null_window_start = probe_side.window_start
+ | AND outer_join.null_window_end = probe_side.window_end
+ """.stripMargin
+ util.verifyRelPlan(sql)
+ }
}