This is an automated email from the ASF dual-hosted git repository.
ferenc-csaky 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 f9d81b2e11e [FLINK-39583][table-planner] Normalize Calcite correl
variables for more effective sub-plan digest reuse (#27959)
f9d81b2e11e is described below
commit f9d81b2e11e5cff7469b523cc73ce34b1d28e5ab
Author: Ferenc Csaky <[email protected]>
AuthorDate: Thu Jul 9 13:49:09 2026 +0200
[FLINK-39583][table-planner] Normalize Calcite correl variables for more
effective sub-plan digest reuse (#27959)
---
.../optimize/CorrelVariableNormalizerShuttle.java | 201 +++++++++++++++++++++
.../table/planner/plan/optimize/RelNodeBlock.scala | 20 +-
.../CorrelVariableNormalizerShuttleTest.java | 94 ++++++++++
.../planner/plan/stream/sql/SubplanReuseTest.xml | 119 ++++++++++++
.../planner/plan/stream/sql/SubplanReuseTest.scala | 92 ++++++++++
tools/maven/suppressions.xml | 2 +-
6 files changed, 525 insertions(+), 3 deletions(-)
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/CorrelVariableNormalizerShuttle.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/CorrelVariableNormalizerShuttle.java
new file mode 100644
index 00000000000..ccfc789fde5
--- /dev/null
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/CorrelVariableNormalizerShuttle.java
@@ -0,0 +1,201 @@
+/*
+ * 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.optimize;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelShuttleImpl;
+import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.logical.LogicalCorrelate;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalTableFunctionScan;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCorrelVariable;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexSubQuery;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Normalizes correlation variable ids in a RelNode tree to make equivalent
subplans digest-match.
+ */
+public final class CorrelVariableNormalizerShuttle extends RelShuttleImpl {
+
+ private final Map<Integer, Integer> idMap = new LinkedHashMap<>();
+
+ private final RexBuilder rexBuilder;
+ private final RexShuttle rexCorrelNormalizer;
+
+ public CorrelVariableNormalizerShuttle(RexBuilder rexBuilder) {
+ this.rexBuilder = rexBuilder;
+ rexCorrelNormalizer = new RexCorrelNormalizer();
+ }
+
+ @Override
+ public RelNode visit(LogicalCorrelate correlate) {
+ var adjustedId = adjustCorrelationId(correlate.getCorrelationId());
+ if (adjustedId.isPresent()) {
+ var left = correlate.getLeft().accept(this);
+ var right = correlate.getRight().accept(this);
+ return correlate.copy(
+ correlate.getTraitSet(),
+ left,
+ right,
+ adjustedId.get(),
+ correlate.getRequiredColumns(),
+ correlate.getJoinType());
+ }
+
+ return super.visit(correlate);
+ }
+
+ @Override
+ public RelNode visit(RelNode relNode) {
+ if (relNode instanceof LogicalTableFunctionScan &&
relNode.getInputs().isEmpty()) {
+ // visitChild applies the RexShuttle while walking RelNode inputs.
A zero-input table
+ // function scan is a leaf, but unlike a regular TableScan it can
still contain RexNodes
+ // (e.g., UNNEST over a correl variable), so rewrite it explicitly.
+ return relNode.accept(rexCorrelNormalizer);
+ }
+
+ return super.visit(relNode);
+ }
+
+ @Override
+ protected RelNode visitChild(RelNode parent, int i, RelNode child) {
+ if (i == 0) {
+ parent = parent.accept(rexCorrelNormalizer);
+ parent = remapVariablesSet(parent);
+ }
+
+ return super.visitChild(parent, i, child);
+ }
+
+ /**
+ * Filter, Project, and Join carry a {@link CorrelationId} set alongside
their RexNodes. {@code
+ * RelNode.accept(RexShuttle)} only rewrites the RexNodes and preserves
the old {@code
+ * variablesSet} via {@code copy()}, so ids we just adjusted in the
condition/projects are still
+ * advertised under their old names. To overcome that, we need to rebuild
that variable with the
+ * adjusted ids set as well.
+ */
+ private RelNode remapVariablesSet(RelNode relNode) {
+ var oldSet = relNode.getVariablesSet();
+ if (oldSet.isEmpty()) {
+ return relNode;
+ }
+
+ var builder = ImmutableSet.<CorrelationId>builder();
+ boolean changed = false;
+ for (var id : oldSet) {
+ var adjusted = adjustCorrelationId(id);
+ if (adjusted.isPresent()) {
+ builder.add(adjusted.get());
+ changed = true;
+ } else {
+ builder.add(id);
+ }
+ }
+
+ if (!changed) {
+ return relNode;
+ }
+
+ var newSet = builder.build();
+ if (relNode instanceof LogicalFilter) {
+ var filter = (LogicalFilter) relNode;
+ return new LogicalFilter(
+ filter.getCluster(),
+ filter.getTraitSet(),
+ filter.getHints(),
+ filter.getInput(),
+ filter.getCondition(),
+ newSet);
+ }
+
+ if (relNode instanceof LogicalProject) {
+ var project = (LogicalProject) relNode;
+ return new LogicalProject(
+ project.getCluster(),
+ project.getTraitSet(),
+ project.getHints(),
+ project.getInput(),
+ project.getProjects(),
+ project.getRowType(),
+ newSet);
+ }
+
+ if (relNode instanceof LogicalJoin) {
+ var join = (LogicalJoin) relNode;
+ return new LogicalJoin(
+ join.getCluster(),
+ join.getTraitSet(),
+ join.getHints(),
+ join.getLeft(),
+ join.getRight(),
+ join.getCondition(),
+ newSet,
+ join.getJoinType(),
+ join.isSemiJoinDone(),
+ ImmutableList.copyOf(join.getSystemFieldList()));
+ }
+
+ return relNode;
+ }
+
+ private Optional<CorrelationId> adjustCorrelationId(CorrelationId
correlationId) {
+ if (correlationId.getName().startsWith(CorrelationId.CORREL_PREFIX)) {
+ int oldId = correlationId.getId();
+ int newId = idMap.computeIfAbsent(oldId, k -> idMap.size() + 1);
+ if (newId != oldId) {
+ return Optional.of(new CorrelationId(newId));
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private final class RexCorrelNormalizer extends RexShuttle {
+
+ @Override
+ public RexNode visitCorrelVariable(RexCorrelVariable variable) {
+ var adjustedId = adjustCorrelationId(variable.id);
+ if (adjustedId.isPresent()) {
+ return rexBuilder.makeCorrel(variable.getType(),
adjustedId.get());
+ } else {
+ return super.visitCorrelVariable(variable);
+ }
+ }
+
+ @Override
+ public RexNode visitSubQuery(RexSubQuery subQuery) {
+ // Let the base shuttle rewrite the RexSubQuery's operands first,
so any
+ // RexCorrelVariables they carry (e.g., the LHS of IN/SOME) are
also adjusted.
+ var withOperands = (RexSubQuery) super.visitSubQuery(subQuery);
+ var rewritten =
withOperands.rel.accept(CorrelVariableNormalizerShuttle.this);
+
+ return rewritten == withOperands.rel ? withOperands :
withOperands.clone(rewritten);
+ }
+ }
+}
diff --git
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/RelNodeBlock.scala
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/RelNodeBlock.scala
index 0a61c191f39..3d4f771245f 100644
---
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/RelNodeBlock.scala
+++
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/RelNodeBlock.scala
@@ -412,10 +412,26 @@ object RelNodeBlockPlanBuilder {
return relNodes
}
- // reuse sub-plan with same digest in input RelNode trees.
+ // The reuse lookup uses the original trees, while the rewrite runs on
normalized
+ // trees. This keeps existing reuse unchanged: normalization
+ // does not change subtrees without correlation variables, so they still
reuse as before. Subtrees with
+ // correlation variables (e.g., CROSS JOIN UNNEST or decorrelated
sub-queries)
+ // used to have different digests in each view expansion, so they were not
reused.
+ //
+ // Reusing those newly matching correlated subtrees is not safe yet. If
such a
+ // subtree is shared, it can become a separate RelNodeBlock and be
optimized without
+ // seeing its parent operators. During that local optimization, ROWTIME
output fields
+ // may be converted to regular TIMESTAMP_LTZ fields. The parents still
refer to the
+ // old ROWTIME-typed fields, so replacing the child can fail validation
with a
+ // ROWTIME/plain timestamp mismatch.
val context = new SubplanReuseContext(true, relNodes: _*)
val reuseShuttle = new SubplanReuseShuttle(context)
- relNodes.map(_.accept(reuseShuttle))
+
+ relNodes
+ // Normalize correlation variable ids per node so structurally equivalent
+ // subplans will share digests.
+ .map(n => n.accept(new
CorrelVariableNormalizerShuttle(n.getCluster.getRexBuilder)))
+ .map(_.accept(reuseShuttle))
}
}
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/optimize/CorrelVariableNormalizerShuttleTest.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/optimize/CorrelVariableNormalizerShuttleTest.java
new file mode 100644
index 00000000000..37194619e99
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/optimize/CorrelVariableNormalizerShuttleTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.optimize;
+
+import org.apache.flink.table.planner.calcite.FlinkRelBuilder;
+import org.apache.flink.table.planner.utils.PlannerMocks;
+
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.calcite.rex.RexBuilder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link CorrelVariableNormalizerShuttle}. */
+class CorrelVariableNormalizerShuttleTest {
+
+ private RexBuilder rexBuilder;
+ private RelOptCluster cluster;
+
+ @BeforeEach
+ void before() {
+ FlinkRelBuilder relBuilder =
PlannerMocks.create().getPlannerContext().createRelBuilder();
+ rexBuilder = relBuilder.getRexBuilder();
+ cluster = relBuilder.getCluster();
+ }
+
+ @Test
+ void testRemapsVariablesSetForFilterProjectAndJoin() {
+ CorrelationId oldId = new CorrelationId(5);
+ CorrelationId newId = new CorrelationId(1);
+ RelNode input = oneRow();
+
+ RelNode filter =
+ LogicalFilter.create(
+ input,
+ rexBuilder.makeLiteral(true),
+ com.google.common.collect.ImmutableSet.of(oldId));
+ assertThat(normalize(filter).getVariablesSet()).containsExactly(newId);
+
+ RelNode project =
+ LogicalProject.create(
+ input,
+ List.of(),
+ List.of(rexBuilder.makeInputRef(input, 0)),
+ List.of("ZERO"),
+ Set.of(oldId));
+
assertThat(normalize(project).getVariablesSet()).containsExactly(newId);
+
+ RelNode join =
+ LogicalJoin.create(
+ oneRow(),
+ oneRow(),
+ List.of(),
+ rexBuilder.makeLiteral(true),
+ Set.of(oldId),
+ JoinRelType.INNER);
+ assertThat(normalize(join).getVariablesSet()).containsExactly(newId);
+ }
+
+ private RelNode normalize(RelNode relNode) {
+ return relNode.accept(new CorrelVariableNormalizerShuttle(rexBuilder));
+ }
+
+ private RelNode oneRow() {
+ return LogicalValues.createOneRow(cluster);
+ }
+}
diff --git
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.xml
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.xml
index 5bf4359078a..08678375c8c 100644
---
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.xml
+++
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.xml
@@ -902,6 +902,125 @@ Union(all=[true], union=[a, b])
: +- TableSourceScan(table=[[default_catalog, default_database,
x]], fields=[a, b, c])
+- Calc(select=[a, (b * 2) AS b], where=[(b < 10)])
+- Reused(reference_id=[1])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testSubplanReuseOnTemporalJoinWithUnnest">
+ <Resource name="ast">
+ <![CDATA[
+LogicalSink(table=[default_catalog.default_database.Sink1], fields=[k, v])
++- LogicalProject(k=[$0], v=[$1])
+ +- LogicalProject(k=[$0], v=[$3])
+ +- LogicalProject(k=[$0], p_ts=[$2], x=[$3], v=[$5], d_ts=[$6])
+ +- LogicalCorrelate(correlation=[$cor7], joinType=[inner],
requiredColumns=[{0, 2}])
+ :- LogicalCorrelate(correlation=[$cor6], joinType=[inner],
requiredColumns=[{1}])
+ : :- LogicalWatermarkAssigner(rowtime=[ts], watermark=[$2])
+ : : +- LogicalTableScan(table=[[default_catalog,
default_database, Probe]])
+ : +- Uncollect
+ : +- LogicalProject(arr=[$cor6.arr])
+ : +- LogicalValues(tuples=[[{ 0 }]])
+ +- LogicalFilter(condition=[=($cor7.k, $0)])
+ +- LogicalSnapshot(period=[$cor7.ts])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2])
+ +- LogicalFilter(condition=[=($3, 1)])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2],
rn=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
+ +- LogicalWatermarkAssigner(rowtime=[ts],
watermark=[$2])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, Versioned]])
+
+LogicalSink(table=[default_catalog.default_database.Sink2], fields=[x, v])
++- LogicalProject(x=[$0], v=[$1])
+ +- LogicalProject(x=[$2], v=[$3])
+ +- LogicalProject(k=[$0], p_ts=[$2], x=[$3], v=[$5], d_ts=[$6])
+ +- LogicalCorrelate(correlation=[$cor9], joinType=[inner],
requiredColumns=[{0, 2}])
+ :- LogicalCorrelate(correlation=[$cor8], joinType=[inner],
requiredColumns=[{1}])
+ : :- LogicalWatermarkAssigner(rowtime=[ts], watermark=[$2])
+ : : +- LogicalTableScan(table=[[default_catalog,
default_database, Probe]])
+ : +- Uncollect
+ : +- LogicalProject(arr=[$cor8.arr])
+ : +- LogicalValues(tuples=[[{ 0 }]])
+ +- LogicalFilter(condition=[=($cor9.k, $0)])
+ +- LogicalSnapshot(period=[$cor9.ts])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2])
+ +- LogicalFilter(condition=[=($3, 1)])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2],
rn=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
+ +- LogicalWatermarkAssigner(rowtime=[ts],
watermark=[$2])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, Versioned]])
+
+LogicalSink(table=[default_catalog.default_database.Sink3], fields=[k, EXPR$1,
x, v, EXPR$4])
++- LogicalProject(k=[$0], EXPR$1=[CAST($1):TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)
NOT NULL], x=[$2], v=[$3], EXPR$4=[CAST($4):TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)
NOT NULL])
+ +- LogicalProject(k=[$0], p_ts=[$2], x=[$3], v=[$5], d_ts=[$6])
+ +- LogicalCorrelate(correlation=[$cor11], joinType=[inner],
requiredColumns=[{0, 2}])
+ :- LogicalCorrelate(correlation=[$cor10], joinType=[inner],
requiredColumns=[{1}])
+ : :- LogicalWatermarkAssigner(rowtime=[ts], watermark=[$2])
+ : : +- LogicalTableScan(table=[[default_catalog, default_database,
Probe]])
+ : +- Uncollect
+ : +- LogicalProject(arr=[$cor10.arr])
+ : +- LogicalValues(tuples=[[{ 0 }]])
+ +- LogicalFilter(condition=[=($cor11.k, $0)])
+ +- LogicalSnapshot(period=[$cor11.ts])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2])
+ +- LogicalFilter(condition=[=($3, 1)])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2],
rn=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
+ +- LogicalWatermarkAssigner(rowtime=[ts],
watermark=[$2])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, Versioned]])
+
+LogicalSink(table=[default_catalog.default_database.Sink4], fields=[k, EXPR$1,
x, v, EXPR$4])
++- LogicalProject(k=[$0], EXPR$1=[CAST($1):TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)
NOT NULL], x=[$2], v=[$3], EXPR$4=[CAST($4):TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)
NOT NULL])
+ +- LogicalProject(k=[$0], p_ts=[$2], x=[$3], v=[$5], d_ts=[$6])
+ +- LogicalCorrelate(correlation=[$cor13], joinType=[inner],
requiredColumns=[{0, 2}])
+ :- LogicalCorrelate(correlation=[$cor12], joinType=[inner],
requiredColumns=[{1}])
+ : :- LogicalWatermarkAssigner(rowtime=[ts], watermark=[$2])
+ : : +- LogicalTableScan(table=[[default_catalog, default_database,
Probe]])
+ : +- Uncollect
+ : +- LogicalProject(arr=[$cor12.arr])
+ : +- LogicalValues(tuples=[[{ 0 }]])
+ +- LogicalFilter(condition=[=($cor13.k, $0)])
+ +- LogicalSnapshot(period=[$cor13.ts])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2])
+ +- LogicalFilter(condition=[=($3, 1)])
+ +- LogicalProject(k=[$0], v=[$1], ts=[$2],
rn=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
+ +- LogicalWatermarkAssigner(rowtime=[ts],
watermark=[$2])
+ +- LogicalTableScan(table=[[default_catalog,
default_database, Versioned]])
+]]>
+ </Resource>
+ <Resource name="optimized exec plan">
+ <![CDATA[
+Correlate(invocation=[$UNNEST_ROWS$1($cor2.arr)],
correlate=[table($UNNEST_ROWS$1($cor2.arr))], select=[k,arr,ts,x],
rowType=[RecordType(BIGINT k, RecordType:peek_no_expand(BIGINT x) ARRAY arr,
TIMESTAMP_LTZ(3) *ROWTIME* ts, BIGINT x)], joinType=[INNER])(reuse_id=[1])
++- WatermarkAssigner(rowtime=[ts], watermark=[ts])
+ +- TableSourceScan(table=[[default_catalog, default_database, Probe]],
fields=[k, arr, ts])
+
+Exchange(distribution=[hash[k]])(reuse_id=[2])
++- Deduplicate(keep=[LastRow], key=[k], order=[ROWTIME],
outputInsertOnly=[false])
+ +- Exchange(distribution=[hash[k]])
+ +- WatermarkAssigner(rowtime=[ts], watermark=[ts])
+ +- TableSourceScan(table=[[default_catalog, default_database,
Versioned]], fields=[k, v, ts])
+
+Sink(table=[default_catalog.default_database.Sink1], fields=[k, v])
++- Calc(select=[k, v])
+ +- TemporalJoin(joinType=[InnerJoin], where=[((k = k0) AND
__TEMPORAL_JOIN_CONDITION(ts, ts0, __TEMPORAL_JOIN_CONDITION_PRIMARY_KEY(k0),
__TEMPORAL_JOIN_LEFT_KEY(k), __TEMPORAL_JOIN_RIGHT_KEY(k0)))], select=[k, ts,
k0, v, ts0])
+ :- Exchange(distribution=[hash[k]])
+ : +- Calc(select=[k, ts])
+ : +- Reused(reference_id=[1])
+ +- Reused(reference_id=[2])
+
+TemporalJoin(joinType=[InnerJoin], where=[((k = k0) AND
__TEMPORAL_JOIN_CONDITION(ts, ts0, __TEMPORAL_JOIN_CONDITION_PRIMARY_KEY(k0),
__TEMPORAL_JOIN_LEFT_KEY(k), __TEMPORAL_JOIN_RIGHT_KEY(k0)))], select=[k, ts,
x, k0, v, ts0])(reuse_id=[3])
+:- Exchange(distribution=[hash[k]])
+: +- Calc(select=[k, ts, x])
+: +- Reused(reference_id=[1])
++- Reused(reference_id=[2])
+
+Sink(table=[default_catalog.default_database.Sink2], fields=[x, v])
++- Calc(select=[x, v])
+ +- Reused(reference_id=[3])
+
+Calc(select=[k, CAST(ts AS TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)) AS EXPR$1, x, v,
CAST(ts0 AS TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)) AS EXPR$4])(reuse_id=[4])
++- Reused(reference_id=[3])
+
+Sink(table=[default_catalog.default_database.Sink3], fields=[k, EXPR$1, x, v,
EXPR$4])
++- Reused(reference_id=[4])
+
+Sink(table=[default_catalog.default_database.Sink4], fields=[k, EXPR$1, x, v,
EXPR$4])
++- Reused(reference_id=[4])
]]>
</Resource>
</TestCase>
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.scala
index 02e3255e68f..2c7c66d462b 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/SubplanReuseTest.scala
@@ -442,6 +442,98 @@ class SubplanReuseTest extends TableTestBase {
util.verifyExecPlan(stmtSet)
}
+ @Test
+ def testSubplanReuseOnTemporalJoinWithUnnest(): Unit = {
+ util.tableEnv.getConfig.set(
+
OptimizerConfigOptions.TABLE_OPTIMIZER_REUSE_OPTIMIZE_BLOCK_WITH_DIGEST_ENABLED,
+ Boolean.box(true))
+
+ util.addTable("""
+ |CREATE TABLE Versioned (
+ | k BIGINT NOT NULL,
+ | v BIGINT NOT NULL,
+ | ts TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL,
+ | WATERMARK FOR ts AS ts
+ |) WITH ('connector' = 'values')
+ """.stripMargin)
+
+ util.addTable("""
+ |CREATE TABLE Probe (
+ | k BIGINT NOT NULL,
+ | arr ARRAY<ROW<x BIGINT NOT NULL> NOT NULL> NOT NULL,
+ | ts TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL,
+ | WATERMARK FOR ts AS ts
+ |) WITH ('connector' = 'values')
+ """.stripMargin)
+
+ util.addTable(
+ """
+ |CREATE VIEW Dedup AS
+ |SELECT k, v, ts FROM (
+ | SELECT *, ROW_NUMBER() OVER (PARTITION BY k ORDER BY ts DESC) AS rn
FROM Versioned
+ |) WHERE rn = 1
+ """.stripMargin)
+
+ util.addTable("""
+ |CREATE VIEW Joined AS
+ |SELECT p.k, p.ts AS p_ts, e.x, d.v, d.ts AS d_ts
+ |FROM Probe AS p
+ | CROSS JOIN UNNEST(p.arr) AS e(x)
+ | INNER JOIN Dedup FOR SYSTEM_TIME AS OF p.ts AS d ON p.k
= d.k
+ """.stripMargin)
+
+ util.addTable("""
+ |CREATE VIEW Out1 AS SELECT k, v FROM Joined
+ """.stripMargin)
+ util.addTable("""
+ |CREATE VIEW Out2 AS SELECT x, v FROM Joined
+ """.stripMargin)
+
+ util.addTable("""
+ |CREATE TABLE Sink1 (k BIGINT, v BIGINT) WITH ('connector'
= 'values')
+ """.stripMargin)
+ util.addTable("""
+ |CREATE TABLE Sink2 (x BIGINT, v BIGINT) WITH ('connector'
= 'values')
+ """.stripMargin)
+ util.addTable("""
+ |CREATE TABLE Sink3 (
+ | k BIGINT,
+ | p_ts TIMESTAMP(3) WITH LOCAL TIME ZONE,
+ | x BIGINT,
+ | v BIGINT,
+ | d_ts TIMESTAMP(3) WITH LOCAL TIME ZONE
+ |) WITH ('connector' = 'values')
+ """.stripMargin)
+ util.addTable("""
+ |CREATE TABLE Sink4 (
+ | k BIGINT,
+ | p_ts TIMESTAMP(3) WITH LOCAL TIME ZONE,
+ | x BIGINT,
+ | v BIGINT,
+ | d_ts TIMESTAMP(3) WITH LOCAL TIME ZONE
+ |) WITH ('connector' = 'values')
+ """.stripMargin)
+
+ val stmtSet = util.tableEnv.createStatementSet()
+ stmtSet.addInsertSql("INSERT INTO Sink1 SELECT * FROM Out1")
+ stmtSet.addInsertSql("INSERT INTO Sink2 SELECT * FROM Out2")
+ stmtSet.addInsertSql("""
+ |INSERT INTO Sink3 SELECT k,
+ | CAST(p_ts AS TIMESTAMP(3) WITH LOCAL TIME
ZONE),
+ | x, v,
+ | CAST(d_ts AS TIMESTAMP(3) WITH LOCAL TIME
ZONE)
+ |FROM Joined
+ """.stripMargin)
+ stmtSet.addInsertSql("""
+ |INSERT INTO Sink4 SELECT k,
+ | CAST(p_ts AS TIMESTAMP(3) WITH LOCAL TIME
ZONE),
+ | x, v,
+ | CAST(d_ts AS TIMESTAMP(3) WITH LOCAL TIME
ZONE)
+ |FROM Joined
+ """.stripMargin)
+ util.verifyExecPlan(stmtSet)
+ }
+
@Test
def testSourceReuseWithEmptyFilterCondAndIgnoreEmptyFilter(): Unit = {
util.addTable(s"""
diff --git a/tools/maven/suppressions.xml b/tools/maven/suppressions.xml
index 84cbbdd619d..1e4287b1e72 100644
--- a/tools/maven/suppressions.xml
+++ b/tools/maven/suppressions.xml
@@ -57,7 +57,7 @@ under the License.
<!-- Have to use guava directly -->
<suppress
-
files="OverConvertRule.java|InConverter.java|SymbolUtil.java|RexNodeJsonDeserializer.java|RexNodeJsonSerializer.java|RexNodeJsonSerdeTest.java|FlinkAggregateProjectMergeRule.java|BatchPhysicalConstantTableFunctionScanRule.java|StreamPhysicalConstantTableFunctionScanRule.java|AggregateReduceGroupingRule.java|SplitAggregateRule.java"
+
files="OverConvertRule.java|InConverter.java|SymbolUtil.java|RexNodeJsonDeserializer.java|RexNodeJsonSerializer.java|RexNodeJsonSerdeTest.java|FlinkAggregateProjectMergeRule.java|BatchPhysicalConstantTableFunctionScanRule.java|StreamPhysicalConstantTableFunctionScanRule.java|AggregateReduceGroupingRule.java|SplitAggregateRule.java|CorrelVariableNormalizerShuttle.java"
checks="IllegalImport"/>
<!-- Classes copied from AWS -->
<suppress