From 4ef2914565c3bfff8d5ef044677009d2c8ea710c Mon Sep 17 00:00:00 2001 From: Fabian Hueske Date: Tue, 7 Jul 2026 11:54:05 +0200 Subject: [PATCH 1/3] [FLINK-39784][table] Forbid SNAPSHOT outside a LATERAL context Adds ForbidSnapshotOutsideLateralRule, which rejects any SNAPSHOT scan that survives the LATERAL SNAPSHOT rewrite with a clear message instead of failing later in the generic PTF translation. Generated-By: Claude Opus 4.8 (1M context) --- .../ForbidSnapshotOutsideLateralRule.java | 85 +++++++++++++++++++ .../plan/rules/FlinkStreamRuleSets.scala | 3 + .../stream/sql/SnapshotTableFunctionTest.java | 12 +-- 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/ForbidSnapshotOutsideLateralRule.java diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/ForbidSnapshotOutsideLateralRule.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/ForbidSnapshotOutsideLateralRule.java new file mode 100644 index 00000000000000..95db7221528f9b --- /dev/null +++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/ForbidSnapshotOutsideLateralRule.java @@ -0,0 +1,85 @@ +/* + * 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.rules.logical; + +import org.apache.flink.table.api.ValidationException; +import org.apache.flink.table.planner.plan.nodes.logical.FlinkLogicalTableFunctionScan; +import org.apache.flink.table.planner.plan.utils.LateralSnapshotJoinUtil; + +import org.apache.calcite.plan.RelOptRuleCall; +import org.apache.calcite.plan.RelRule; +import org.apache.calcite.rex.RexCall; +import org.immutables.value.Value; + +/** + * Rejects any {@link FlinkLogicalTableFunctionScan} that is still backed by the built-in {@code + * SNAPSHOT} function, with a clear error message. + * + *

{@code SNAPSHOT} is a planner placeholder that is only valid as the build side of a {@code + * LATERAL} join, where {@link LogicalJoinToLateralSnapshotJoinRule} rewrites the surrounding join + * into a dedicated {@link + * org.apache.flink.table.planner.plan.nodes.logical.FlinkLogicalLateralSnapshotJoin} and removes + * the SNAPSHOT scan. This rule must therefore run after that rewrite (see {@code + * FlinkStreamRuleSets.LOGICAL_REWRITE}). + */ +@Value.Enclosing +public class ForbidSnapshotOutsideLateralRule + extends RelRule { + + public static final ForbidSnapshotOutsideLateralRule INSTANCE = + ForbidSnapshotOutsideLateralRule.ForbidSnapshotOutsideLateralRuleConfig.DEFAULT + .toRule(); + + private ForbidSnapshotOutsideLateralRule(ForbidSnapshotOutsideLateralRuleConfig config) { + super(config); + } + + @Override + public boolean matches(RelOptRuleCall call) { + final FlinkLogicalTableFunctionScan scan = call.rel(0); + return scan.getCall() instanceof RexCall + && LateralSnapshotJoinUtil.isSnapshotCall((RexCall) scan.getCall()); + } + + @Override + public void onMatch(RelOptRuleCall call) { + throw new ValidationException( + "The SNAPSHOT function can only be used as the build side (right-hand side) of a " + + "LATERAL join. It cannot be used as a standalone table function or " + + "outside of a LATERAL context."); + } + + /** Rule configuration. */ + @Value.Immutable(singleton = false) + public interface ForbidSnapshotOutsideLateralRuleConfig extends RelRule.Config { + + ForbidSnapshotOutsideLateralRule.ForbidSnapshotOutsideLateralRuleConfig DEFAULT = + ImmutableForbidSnapshotOutsideLateralRule.ForbidSnapshotOutsideLateralRuleConfig + .builder() + .build() + .withOperandSupplier( + b0 -> b0.operand(FlinkLogicalTableFunctionScan.class).anyInputs()) + .withDescription("ForbidSnapshotOutsideLateralRule"); + + @Override + default ForbidSnapshotOutsideLateralRule toRule() { + return new ForbidSnapshotOutsideLateralRule(this); + } + } +} diff --git a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala index 65e6adbf772049..284b1dbc056dbb 100644 --- a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala +++ b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/rules/FlinkStreamRuleSets.scala @@ -405,6 +405,9 @@ object FlinkStreamRuleSets { // Rewrites a join over a SNAPSHOT table function call into a dedicated // FlinkLogicalLateralSnapshotJoin for the LATERAL SNAPSHOT operator. LogicalJoinToLateralSnapshotJoinRule.INSTANCE, + // Rejects SNAPSHOT scans that survived the rewrite above, i.e. SNAPSHOT calls used outside a + // LATERAL context. Must run after LogicalJoinToLateralSnapshotJoinRule. + ForbidSnapshotOutsideLateralRule.INSTANCE, // Avoids accessing a field from the result (condition). PythonCalcSplitRule.SPLIT_CONDITION_REX_FIELD, // Avoids accessing a field from the result (projection). diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/SnapshotTableFunctionTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/SnapshotTableFunctionTest.java index 12511c4ae49361..6d254b220b86f8 100644 --- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/SnapshotTableFunctionTest.java +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/SnapshotTableFunctionTest.java @@ -19,6 +19,7 @@ package org.apache.flink.table.planner.plan.stream.sql; import org.apache.flink.table.api.TableConfig; +import org.apache.flink.table.api.ValidationException; import org.apache.flink.table.planner.utils.TableTestBase; import org.apache.flink.table.planner.utils.TableTestUtil; @@ -153,13 +154,14 @@ void testPartitionByNotAllowed() { @Test void testFromContextRejectedOutsideLateral() { - // SNAPSHOT used outside a LATERAL context is not rewritten by the LATERAL SNAPSHOT rule and - // reaches the regular PTF physical rule. Because SNAPSHOT disables system arguments, that - // rule rejects it. FLINK-39784 will replace this with a clearer message - // ("SNAPSHOT can only be used inside a LATERAL clause"). + // SNAPSHOT used outside a LATERAL context is not rewritten by the LATERAL SNAPSHOT rule. + // ForbidSnapshotOutsideLateralRule intercepts the surviving SNAPSHOT scan and rejects it + // with a clear message before it reaches the generic PTF translation. assertThatThrownBy(() -> util.verifyRelPlan("SELECT * FROM SNAPSHOT(input => TABLE Rates)")) .satisfies( anyCauseMatches( - "Disabling system arguments is not supported for user-defined PTF.")); + ValidationException.class, + "The SNAPSHOT function can only be used as the build side " + + "(right-hand side) of a LATERAL join")); } } From 22a8771ed8f56b81482036567fef57646d55efff Mon Sep 17 00:00:00 2001 From: Fabian Hueske Date: Wed, 8 Jul 2026 20:14:01 +0200 Subject: [PATCH 2/3] [FLINK-39785][table] Honor source.sleep-* in TestValues watermark-push-down source Wire the existing source.sleep-after-elements / source.sleep-time options into TestValuesScanTableSourceWithWatermarkPushDown Generated-By: Claude Opus 4.8 (1M context) --- .../factories/TestValuesRuntimeFunctions.java | 18 +++++++++++++- .../factories/TestValuesTableFactory.java | 24 +++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesRuntimeFunctions.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesRuntimeFunctions.java index df439bd2c9e7f3..851275d4ef804a 100644 --- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesRuntimeFunctions.java +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesRuntimeFunctions.java @@ -257,15 +257,24 @@ public static class FromElementSourceFunctionWithWatermark private final TerminatingLogic terminating; + /** Sleep for {@link #sleepTimeMillis} after emitting every {@code sleepAfterElements}. */ + private final int sleepAfterElements; + + private final long sleepTimeMillis; + public FromElementSourceFunctionWithWatermark( String tableName, TypeSerializer serializer, Iterable elements, WatermarkStrategy watermarkStrategy, - TerminatingLogic terminating) + TerminatingLogic terminating, + int sleepAfterElements, + long sleepTimeMillis) throws IOException { this.tableName = tableName; this.terminating = terminating; + this.sleepAfterElements = sleepAfterElements; + this.sleepTimeMillis = sleepTimeMillis; ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputViewStreamWrapper wrapper = new DataOutputViewStreamWrapper(baos); @@ -325,6 +334,13 @@ public RelativeClock getInputActivityClock() { generator.onEvent(next, Long.MIN_VALUE, output); generator.onPeriodicEmit(output); } + + // If enabled, throttle emission of values + if (sleepAfterElements > 0 + && sleepTimeMillis > 0 + && numElementsEmitted % sleepAfterElements == 0) { + Thread.sleep(sleepTimeMillis); + } } if (terminating == TerminatingLogic.INFINITE) { diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java index 3387f522232e44..4e260a3e75fc6c 100644 --- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java @@ -716,7 +716,9 @@ public DynamicTableSource createDynamicTableSource(Context context) { partitions, readableMetadata, null, - enableAggregatePushDown); + enableAggregatePushDown, + sleepAfterElements, + sleepTimeMillis); source.setEnableMetadataFilterPushDown(enableMetadataFilterPushDown); return source; } else { @@ -1750,6 +1752,8 @@ private static class TestValuesScanTableSourceWithWatermarkPushDown extends TestValuesScanTableSource implements SupportsWatermarkPushDown, SupportsSourceWatermark { private final String tableName; + private final int sleepAfterElements; + private final long sleepTimeMillis; private WatermarkStrategy watermarkStrategy = WatermarkStrategy.noWatermarks(); @@ -1771,7 +1775,9 @@ private TestValuesScanTableSourceWithWatermarkPushDown( List> allPartitions, Map readableMetadata, @Nullable int[] projectedMetadataFields, - boolean enableAggregatePushDown) { + boolean enableAggregatePushDown, + int sleepAfterElements, + long sleepTimeMillis) { super( producedDataType, changelogMode, @@ -1792,6 +1798,8 @@ private TestValuesScanTableSourceWithWatermarkPushDown( projectedMetadataFields, enableAggregatePushDown); this.tableName = tableName; + this.sleepAfterElements = sleepAfterElements; + this.sleepTimeMillis = sleepTimeMillis; } @Override @@ -1817,7 +1825,13 @@ public ScanRuntimeProvider getScanRuntimeProvider(ScanContext runtimeProviderCon try { return SourceFunctionProvider.of( new TestValuesRuntimeFunctions.FromElementSourceFunctionWithWatermark( - tableName, serializer, values, watermarkStrategy, terminating), + tableName, + serializer, + values, + watermarkStrategy, + terminating, + sleepAfterElements, + sleepTimeMillis), false); } catch (IOException e) { throw new TableException("Fail to init source function", e); @@ -1845,7 +1859,9 @@ public DynamicTableSource copy() { allPartitions, readableMetadata, projectedMetadataFields, - enableAggregatePushDown); + enableAggregatePushDown, + sleepAfterElements, + sleepTimeMillis); newSource.watermarkStrategy = watermarkStrategy; newSource.setEnableMetadataFilterPushDown(enableMetadataFilterPushDown); return newSource; From f1cd480d1b63f7f67819156e64bbde7b42cec06e Mon Sep 17 00:00:00 2001 From: Fabian Hueske Date: Thu, 9 Jul 2026 12:19:23 +0200 Subject: [PATCH 3/3] [FLINK-39785][table] Add LATERAL SNAPSHOT e2e and restore tests Adds end-to-end coverage for the LATERAL SNAPSHOT processing-time temporal join: * LateralSnapshotJoinITCase: result tests over HEAP and ROCKSDB backends covering different configurations and input scenarios * LateralSnapshotJoinRestoreTest / LateralSnapshotJoinTestPrograms: savepoint restore tests Generated-By: Claude Opus 4.8 (1M context) --- .../LateralSnapshotJoinRestoreTest.java | 40 ++ .../LateralSnapshotJoinTestPrograms.java | 145 +++++ .../testutils/RestoreTestCompleteness.java | 6 - .../sql/join/LateralSnapshotJoinITCase.java | 606 ++++++++++++++++++ .../plan/lateral-snapshot-join-inner.json | 469 ++++++++++++++ .../savepoint/_metadata | Bin 0 -> 19149 bytes .../plan/lateral-snapshot-join-left.json | 468 ++++++++++++++ .../savepoint/_metadata | Bin 0 -> 19163 bytes 8 files changed, 1728 insertions(+), 6 deletions(-) create mode 100644 flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinRestoreTest.java create mode 100644 flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinTestPrograms.java create mode 100644 flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/join/LateralSnapshotJoinITCase.java create mode 100644 flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/plan/lateral-snapshot-join-inner.json create mode 100644 flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/savepoint/_metadata create mode 100644 flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/plan/lateral-snapshot-join-left.json create mode 100644 flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/savepoint/_metadata diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinRestoreTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinRestoreTest.java new file mode 100644 index 00000000000000..5a4fdf644c49ce --- /dev/null +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinRestoreTest.java @@ -0,0 +1,40 @@ +/* + * 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.RestoreTestBase; +import org.apache.flink.table.test.program.TableTestProgram; + +import java.util.Arrays; +import java.util.List; + +/** Restore tests for {@link StreamExecLateralSnapshotJoin}. */ +public class LateralSnapshotJoinRestoreTest extends RestoreTestBase { + + public LateralSnapshotJoinRestoreTest() { + super(StreamExecLateralSnapshotJoin.class); + } + + @Override + public List programs() { + return Arrays.asList( + LateralSnapshotJoinTestPrograms.LATERAL_SNAPSHOT_JOIN_INNER, + LateralSnapshotJoinTestPrograms.LATERAL_SNAPSHOT_JOIN_LEFT); + } +} diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinTestPrograms.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinTestPrograms.java new file mode 100644 index 00000000000000..9af4e9049d1d27 --- /dev/null +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/LateralSnapshotJoinTestPrograms.java @@ -0,0 +1,145 @@ +/* + * 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.api.config.TableConfigOptions; +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; + +/** + * {@link TableTestProgram} definitions for testing {@link StreamExecLateralSnapshotJoin}. + * + *

Both programs load the whole build side before the savepoint: the {@code 'user_time'} gate at + * {@code 00:00:03} is reached by the last build-side row, so the operator has flipped to the JOIN + * phase and its build-side snapshot is materialized and frozen when the stop-with-savepoint fires + * (after the "before restore" rows have been joined). After restore, no further build changes + * arrive; the "after restore" probe rows join the restored snapshot, which verifies that the + * materialized build state and the LOAD/JOIN phase (union operator state) survive the savepoint. + */ +public class LateralSnapshotJoinTestPrograms { + + static final String[] PROBE_SCHEMA = { + "pk STRING", + "pv INT", + "pts_str STRING", + "pts AS TO_TIMESTAMP(pts_str)", + "WATERMARK FOR pts AS pts" + }; + + static final String[] BUILD_SCHEMA = { + "bk STRING", + "bv INT", + "bts_str STRING", + "bts AS TO_TIMESTAMP(bts_str)", + "WATERMARK FOR bts AS bts" + }; + + static final String[] SINK_SCHEMA = {"pk STRING", "pv INT", "bk STRING", "bv INT"}; + + // Two rows for key 'a' exercise the per-key multi-set; the last row's watermark (00:00:03) + // reaches the gate and flips the operator to JOIN. + static final Row[] BUILD_BEFORE_DATA = { + Row.of("a", 10, "2020-01-01 00:00:01"), + Row.of("b", 20, "2020-01-01 00:00:02"), + Row.of("a", 11, "2020-01-01 00:00:03") + }; + + static final Row[] PROBE_BEFORE_DATA = { + Row.of("a", 100, "2020-01-01 00:00:06"), Row.of("b", 200, "2020-01-01 00:00:07") + }; + + // 'a' matches the restored snapshot; 'c' has no match. + static final Row[] PROBE_AFTER_DATA = { + Row.of("a", 101, "2020-01-01 00:00:10"), Row.of("c", 300, "2020-01-01 00:00:11") + }; + + private static final String SNAPSHOT_BUILD = + "LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + "load_completed_condition => 'user_time', " + + "load_completed_time => CAST(TIMESTAMP '2020-01-01 00:00:03' AS TIMESTAMP_LTZ(3))" + + ")) AS s ON probe.pk = s.bk"; + + public static final TableTestProgram LATERAL_SNAPSHOT_JOIN_INNER = + TableTestProgram.of( + "lateral-snapshot-join-inner", + "validates a LATERAL SNAPSHOT inner join across a restore") + .setupConfig(TableConfigOptions.LOCAL_TIME_ZONE, "UTC") + .setupTableSource( + SourceTestStep.newBuilder("probe") + .addSchema(PROBE_SCHEMA) + .producedBeforeRestore(PROBE_BEFORE_DATA) + .producedAfterRestore(PROBE_AFTER_DATA) + .build()) + .setupTableSource( + SourceTestStep.newBuilder("b") + .addSchema(BUILD_SCHEMA) + .producedBeforeRestore(BUILD_BEFORE_DATA) + .build()) + .setupTableSink( + SinkTestStep.newBuilder("sink") + .addSchema(SINK_SCHEMA) + .consumedBeforeRestore( + "+I[a, 100, a, 10]", + "+I[a, 100, a, 11]", + "+I[b, 200, b, 20]") + .consumedAfterRestore("+I[a, 101, a, 10]", "+I[a, 101, a, 11]") + .build()) + .runSql( + "INSERT INTO sink SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe JOIN " + + SNAPSHOT_BUILD) + .build(); + + public static final TableTestProgram LATERAL_SNAPSHOT_JOIN_LEFT = + TableTestProgram.of( + "lateral-snapshot-join-left", + "validates a LATERAL SNAPSHOT left join across a restore") + .setupConfig(TableConfigOptions.LOCAL_TIME_ZONE, "UTC") + .setupTableSource( + SourceTestStep.newBuilder("probe") + .addSchema(PROBE_SCHEMA) + .producedBeforeRestore(PROBE_BEFORE_DATA) + .producedAfterRestore(PROBE_AFTER_DATA) + .build()) + .setupTableSource( + SourceTestStep.newBuilder("b") + .addSchema(BUILD_SCHEMA) + .producedBeforeRestore(BUILD_BEFORE_DATA) + .build()) + .setupTableSink( + SinkTestStep.newBuilder("sink") + .addSchema(SINK_SCHEMA) + .consumedBeforeRestore( + "+I[a, 100, a, 10]", + "+I[a, 100, a, 11]", + "+I[b, 200, b, 20]") + .consumedAfterRestore( + "+I[a, 101, a, 10]", + "+I[a, 101, a, 11]", + "+I[c, 300, null, null]") + .build()) + .runSql( + "INSERT INTO sink SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe LEFT JOIN " + + SNAPSHOT_BUILD) + .build(); +} diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/RestoreTestCompleteness.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/RestoreTestCompleteness.java index 29d9f0db3b9c0e..ea183bf3dbad39 100644 --- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/RestoreTestCompleteness.java +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/RestoreTestCompleteness.java @@ -19,7 +19,6 @@ package org.apache.flink.table.planner.plan.nodes.exec.testutils; import org.apache.flink.table.planner.plan.nodes.exec.ExecNode; -import org.apache.flink.table.planner.plan.nodes.exec.stream.StreamExecLateralSnapshotJoin; import org.apache.flink.table.planner.plan.nodes.exec.stream.StreamExecPythonAsyncCalc; import org.apache.flink.table.planner.plan.nodes.exec.stream.StreamExecPythonCalc; import org.apache.flink.table.planner.plan.nodes.exec.stream.StreamExecPythonCorrelate; @@ -50,11 +49,6 @@ public class RestoreTestCompleteness { private static final Set>> SKIP_EXEC_NODES = new HashSet>>() { { - // TODO: FLINK-39781 - the LATERAL SNAPSHOT runtime operator is still a stub, - // so a restore test cannot generate a savepoint yet. Remove this entry and - // add LateralSnapshotJoinRestoreTest once the operator is implemented. - add(StreamExecLateralSnapshotJoin.class); - /** Ignoring python based exec nodes temporarily. */ add(StreamExecPythonCalc.class); add(StreamExecPythonCorrelate.class); diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/join/LateralSnapshotJoinITCase.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/join/LateralSnapshotJoinITCase.java new file mode 100644 index 00000000000000..1aa873271a3f46 --- /dev/null +++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/join/LateralSnapshotJoinITCase.java @@ -0,0 +1,606 @@ +/* + * 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.runtime.stream.sql.join; + +import org.apache.flink.table.planner.factories.TestValuesTableFactory; +import org.apache.flink.table.planner.runtime.utils.StreamingWithStateTestBase; +import org.apache.flink.testutils.junit.extensions.parameterized.ParameterizedTestExtension; +import org.apache.flink.testutils.junit.extensions.parameterized.Parameters; +import org.apache.flink.types.Row; +import org.apache.flink.types.RowKind; +import org.apache.flink.util.CollectionUtil; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThatList; + +/** + * End-to-end result tests for the {@code LATERAL SNAPSHOT} processing-time temporal table join. + * + *

Processing-time semantics make the join non-deterministic in general. To get stable results, + * each build source appends a non-matching "flip-trigger" row at the gate timestamp ({@link + * #MID_GATE}) while all real build rows are earlier. With per-record ({@code on-event}) watermarks + * the operator flips to the JOIN phase. Since no more build-records are received, all probe-side + * records see the same build-side table and the append-only output can be asserted as an unordered + * set. + * + *

Some tests throttle source emission ({@code source.sleep-*}) to place probes deterministically + * around the flip. Purely processing-time-driven behavior (idle-timeout flip, state-TTL eviction) + * is covered deterministically by {@code LateralSnapshotJoinOperatorTest}. + */ +@ExtendWith(ParameterizedTestExtension.class) +public class LateralSnapshotJoinITCase extends StreamingWithStateTestBase { + + /** The {@code 'user_time'} gate reached mid-stream by the build-side flip-trigger row. */ + private static final String MID_GATE = + "load_completed_condition => 'user_time', " + + "load_completed_time => CAST(TIMESTAMP '2020-01-01 00:00:10' AS TIMESTAMP_LTZ(3))"; + + /** A far-future gate: the flip happens only at end of all input. */ + private static final String END_GATE = + "load_completed_condition => 'user_time', " + + "load_completed_time => CAST(TIMESTAMP '2100-01-01 00:00:00' AS TIMESTAMP_LTZ(3))"; + + /** Event time of the flip-trigger row; equal to the {@link #MID_GATE} timestamp. */ + private static final String FLIP_TRIGGER_TS = "00:00:10"; + + /** A build-side key that never matches any probe row. */ + private static final String FLIP_TRIGGER_KEY = "__flip_trigger__"; + + public LateralSnapshotJoinITCase(StateBackendMode state) { + super(state); + } + + @BeforeEach + @Override + public void before() { + super.before(); + env().setParallelism(1); + tEnv().getConfig().setLocalTimeZone(ZoneId.of("UTC")); + } + + @Parameters(name = "StateBackend={0}") + public static Collection parameters() { + return Arrays.asList( + new Object[][] { + {StreamingWithStateTestBase.HEAP_BACKEND()}, + {StreamingWithStateTestBase.ROCKSDB_BACKEND()} + }); + } + + // ------------------------------------------------------------------------------------------ + // Core join semantics + // ------------------------------------------------------------------------------------------ + + @TestTemplate + void testInnerJoin() { + // Throttle the probe so the fast build flips first; the probes are then joined live in the + // JOIN phase (the snapshot is frozen, so buffered-vs-live does not change the result). + createProbe(defaultProbe(), 40L); + createAppendBuild(defaultBuild()); + + final List actual = + collect( + "SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual) + .containsExactlyInAnyOrder( + Row.of("a", 100, "a", 10), + Row.of("a", 100, "a", 11), + Row.of("b", 200, "b", 20)); + } + + @TestTemplate + void testLeftJoin() { + createProbe(defaultProbe(), 40L); + createAppendBuild(defaultBuild()); + + final List actual = + collect( + "SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe LEFT JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual) + .containsExactlyInAnyOrder( + Row.of("a", 100, "a", 10), + Row.of("a", 100, "a", 11), + Row.of("b", 200, "b", 20), + Row.of("c", 300, null, null)); + } + + @TestTemplate + void testSelectStarMaterializesBuildRowtime() { + createProbe(Arrays.asList(Row.of("a", 100, ts("00:01:00")))); + createAppendBuild(Arrays.asList(Row.of("a", 10, ts("00:00:01")))); + + // SELECT * yields probe columns then build columns; the build rowtime is materialized as a + // regular TIMESTAMP(3), not a time attribute. + final List actual = + collect( + "SELECT * FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual) + .containsExactlyInAnyOrder( + Row.of( + "a", + 100, + LocalDateTime.parse("2020-01-01T00:01:00"), + "a", + 10, + LocalDateTime.parse("2020-01-01T00:00:01"))); + } + + @TestTemplate + void testCompositeKeys() { + createProbe( + Arrays.asList( + Row.of("a", 10, ts("00:01:00")), + Row.of("a", 11, ts("00:01:01")), + Row.of("b", 20, ts("00:01:02")))); + createAppendBuild( + Arrays.asList( + Row.of("a", 10, ts("00:00:01")), + Row.of("a", 99, ts("00:00:02")), + Row.of("b", 20, ts("00:00:03")))); + + final List actual = + collect( + "SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk AND probe.pv = s.bv"); + + assertThatList(actual) + .containsExactlyInAnyOrder(Row.of("a", 10, "a", 10), Row.of("b", 20, "b", 20)); + } + + @TestTemplate + void testNonEquiCondition() { + createProbe(Arrays.asList(Row.of("a", 15, ts("00:01:00")), Row.of("a", 5, ts("00:01:01")))); + createAppendBuild( + Arrays.asList(Row.of("a", 10, ts("00:00:01")), Row.of("a", 20, ts("00:00:02")))); + + final List actual = + collect( + "SELECT probe.pk, probe.pv, s.bv " + + "FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk AND probe.pv > s.bv"); + + assertThatList(actual).containsExactlyInAnyOrder(Row.of("a", 15, 10)); + } + + @TestTemplate + void testEmptyBuildSide() { + createProbe(defaultProbe()); + createAppendBuild(Collections.emptyList()); + + final List inner = + collect( + "SELECT probe.pk, s.bk FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + assertThatList(inner).isEmpty(); + + final List left = + collect( + "SELECT probe.pk, s.bk FROM probe LEFT JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + assertThatList(left) + .containsExactlyInAnyOrder(Row.of("a", null), Row.of("b", null), Row.of("c", null)); + } + + // ------------------------------------------------------------------------------------------ + // Build-side changelog consolidation (all changes have rowtime < the gate) + // ------------------------------------------------------------------------------------------ + + @TestTemplate + void testRetractingBuildChangelog() { + createProbe( + Arrays.asList(Row.of("a", 100, ts("00:01:00")), Row.of("b", 200, ts("00:01:01")))); + // Key a is updated 10 -> 11; key b is inserted then deleted. + createChangelogBuild( + Arrays.asList( + Row.ofKind(RowKind.INSERT, "a", 10, ts("00:00:01")), + Row.ofKind(RowKind.INSERT, "b", 20, ts("00:00:03")), + Row.ofKind(RowKind.UPDATE_BEFORE, "a", 10, ts("00:00:01")), + Row.ofKind(RowKind.UPDATE_AFTER, "a", 11, ts("00:00:02")), + Row.ofKind(RowKind.DELETE, "b", 20, ts("00:00:03")))); + + final List actual = + collect( + "SELECT probe.pk, s.bv FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual).containsExactlyInAnyOrder(Row.of("a", 11)); + } + + @TestTemplate + void testUpsertBuildSource() { + createProbe( + Arrays.asList(Row.of("a", 100, ts("00:01:00")), Row.of("b", 200, ts("00:01:01")))); + // Upsert source (I,UA,D with PK): a is upserted 10 -> 11, b is deleted. + createUpsertBuild( + Arrays.asList( + Row.ofKind(RowKind.INSERT, "a", 10, ts("00:00:01")), + Row.ofKind(RowKind.UPDATE_AFTER, "a", 11, ts("00:00:02")), + Row.ofKind(RowKind.INSERT, "b", 20, ts("00:00:03")), + Row.ofKind(RowKind.DELETE, "b", 20, ts("00:00:04")))); + + final List actual = + collect( + "SELECT probe.pk, s.bv FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual).containsExactlyInAnyOrder(Row.of("a", 11)); + } + + // ------------------------------------------------------------------------------------------ + // Flip timing (when LOAD flips to JOIN, and probe placement around the flip) + // ------------------------------------------------------------------------------------------ + + @TestTemplate + void testInnerJoinFlipsAtEndOfInput() { + // Far-future gate: the operator stays in LOAD and flips only at end-of-input, buffering and + // draining every probe. The frozen snapshot and result match the mid-flip case. + createProbe(defaultProbe()); + createAppendBuild(defaultBuild()); + + final List actual = + collect( + "SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + END_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual) + .containsExactlyInAnyOrder( + Row.of("a", 100, "a", 10), + Row.of("a", 100, "a", 11), + Row.of("b", 200, "b", 20)); + } + + @TestTemplate + void testDefaultCompileTimeCondition() { + // No options: 'load_completed_condition' defaults to 'compile_time', so the gate is the + // wall-clock time at planning. The 2020 build watermarks never reach it, so the flip + // happens at end-of-input. + createProbe(defaultProbe()); + createAppendBuild(defaultBuild()); + + final List actual = + collect( + "SELECT probe.pk, probe.pv, s.bk, s.bv " + + "FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b" + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual) + .containsExactlyInAnyOrder( + Row.of("a", 100, "a", 10), + Row.of("a", 100, "a", 11), + Row.of("b", 200, "b", 20)); + } + + @TestTemplate + void testProbesJoinedLiveInJoinPhase() { + // Fast build flips almost immediately; the throttled probe stream is consumed entirely in + // the JOIN phase, exercising the live per-record path (including a non-matching key). The + // snapshot is static after the flip, so the result is deterministic. + final List probes = + Arrays.asList( + Row.of("a", 1, ts("00:00:01")), + Row.of("b", 2, ts("00:00:02")), + Row.of("c", 3, ts("00:00:03")), // non-matching + Row.of("a", 4, ts("00:00:04")), + Row.of("b", 5, ts("00:00:05")), + Row.of("c", 6, ts("00:00:06"))); // non-matching + createProbe(probes, 40L); + createAppendBuild( + Arrays.asList(Row.of("a", 10, ts("00:00:01")), Row.of("b", 20, ts("00:00:02")))); + + final List actual = + collect( + "SELECT probe.pv, probe.pk, s.bv FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThatList(actual) + .containsExactlyInAnyOrder( + Row.of(1, "a", 10), + Row.of(4, "a", 10), + Row.of(2, "b", 20), + Row.of(5, "b", 20)); + } + + @TestTemplate + void testManyProbesBufferedDuringLoadThenDrained() { + // Throttled build flips at ~240 ms, by which time the un-throttled probe stream is fully + // buffered in LOAD. The flip drains all buffered probes against the final loaded version; + // all must match and none may be lost. + final int probeCount = 20; + final List probes = new ArrayList<>(); + for (int i = 1; i <= probeCount; i++) { + probes.add(Row.of("k", i, ts(String.format("00:00:%02d", i)))); + } + createProbe(probes); // un-throttled: buffered within a few ms, long before the flip + createUpsertBuildVerbatim( + Arrays.asList( + Row.ofKind(RowKind.INSERT, "k", 1, ts("00:00:01")), + Row.ofKind(RowKind.UPDATE_AFTER, "k", 2, ts("00:00:02")), + Row.ofKind(RowKind.UPDATE_AFTER, "k", 3, ts("00:00:05")), + Row.ofKind(RowKind.INSERT, FLIP_TRIGGER_KEY, 0, ts(FLIP_TRIGGER_TS))), + 80L); // ~240 ms until the gate-crossing trigger row is emitted + + final List actual = + collect( + "SELECT probe.pv, s.bv FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk"); + + assertThat(actual).hasSize(probeCount); + assertThat(actual).allSatisfy(r -> assertThat((int) r.getField(1)).isEqualTo(3)); + } + + @TestTemplate + void testProbesStraddlingFlip() { + // Throttled build loads v1, flips at ~100 ms, then applies v2 at ~150 ms. The throttled + // probe stream straddles both: the first probe is buffered then drained (sees v1) while the + // last is joined live after v2 (sees v2). Per-probe versions vary with timing, but the + // endpoints and non-decreasing order are deterministic. + final int probeCount = 8; + final List probes = new ArrayList<>(); + for (int i = 1; i <= probeCount; i++) { + probes.add(Row.of("k", i, ts(String.format("00:00:%02d", i)))); + } + createProbe(probes, 200L); // last probe at ~1400 ms, well past the post-flip update + createUpsertBuildVerbatim( + Arrays.asList( + Row.ofKind(RowKind.INSERT, "k", 1, ts("00:00:01")), + Row.ofKind(RowKind.INSERT, FLIP_TRIGGER_KEY, 0, ts(FLIP_TRIGGER_TS)), + Row.ofKind(RowKind.UPDATE_AFTER, "k", 2, ts("00:00:20"))), + 50L); + + final List byProbeId = + sortedByProbeId( + collect( + "SELECT probe.pv, s.bv FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk")); + + assertThat(byProbeId).hasSize(probeCount); + assertMonotonicVersions(byProbeId); + assertThat((int) byProbeId.get(0).getField(1)).isEqualTo(1); // buffered -> v1 + assertThat((int) byProbeId.get(probeCount - 1).getField(1)).isEqualTo(2); // live -> v2 + } + + @TestTemplate + void testProbesObserveProgressiveBuildVersions() { + // Throttled build applies v2, v3, v4 progressively after the flip while a throttled probe + // stream spans the progression. Observed versions are non-decreasing; the first probe sees + // v1 and the last sees v4. Exact per-probe versions are not asserted (source timing and + // post-flip visibility latency are not precise enough across environments). + final int probeCount = 8; + final List probes = new ArrayList<>(); + for (int i = 1; i <= probeCount; i++) { + probes.add(Row.of("k", i, ts(String.format("00:00:%02d", i)))); + } + createProbe(probes, 200L); // last probe at ~1400 ms, well past the last post-flip update + createUpsertBuildVerbatim( + Arrays.asList( + Row.ofKind(RowKind.INSERT, "k", 1, ts("00:00:01")), + Row.ofKind(RowKind.INSERT, FLIP_TRIGGER_KEY, 0, ts(FLIP_TRIGGER_TS)), + Row.ofKind(RowKind.UPDATE_AFTER, "k", 2, ts("00:00:20")), + Row.ofKind(RowKind.UPDATE_AFTER, "k", 3, ts("00:00:30")), + Row.ofKind(RowKind.UPDATE_AFTER, "k", 4, ts("00:00:40"))), + 50L); + + final List byProbeId = + sortedByProbeId( + collect( + "SELECT probe.pv, s.bv FROM probe JOIN LATERAL TABLE(SNAPSHOT(" + + "input => TABLE b, " + + MID_GATE + + ")) AS s ON probe.pk = s.bk")); + + assertThat(byProbeId).hasSize(probeCount); + assertMonotonicVersions(byProbeId); + assertThat((Integer) byProbeId.get(0).getField(1)).isEqualTo(1); + assertThat((Integer) byProbeId.get(probeCount - 1).getField(1)).isEqualTo(4); + } + + // ------------------------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------------------------ + + private List defaultProbe() { + return Arrays.asList( + Row.of("a", 100, ts("00:01:00")), + Row.of("b", 200, ts("00:01:01")), + Row.of("c", 300, ts("00:01:02"))); + } + + private List defaultBuild() { + return Arrays.asList( + Row.of("a", 10, ts("00:00:01")), + Row.of("b", 20, ts("00:00:02")), + Row.of("a", 11, ts("00:00:03"))); + } + + private List collect(String query) { + return CollectionUtil.iteratorToList(tEnv().executeSql(query).collect()); + } + + /** + * Appends a non-matching build row at the {@link #MID_GATE} timestamp; its watermark flips the + * operator to the JOIN phase mid-stream (all real build rows are earlier). + */ + private List withFlipTrigger(List data) { + final List withTrigger = new ArrayList<>(data); + withTrigger.add(Row.of(FLIP_TRIGGER_KEY, 0, ts(FLIP_TRIGGER_TS))); + return withTrigger; + } + + private void createProbe(List data) { + createProbe(data, 0L); + } + + /** + * Creates the append-only probe table {@code probe}. {@code sleepMillis > 0} throttles emission + * to control when probe rows are consumed relative to the build-side flip. + */ + private void createProbe(List data, long sleepMillis) { + final String id = TestValuesTableFactory.registerData(data); + tEnv().executeSql( + "CREATE TABLE probe (" + + " pk STRING," + + " pv INT," + + " pts TIMESTAMP(3)," + + " WATERMARK FOR pts AS pts" + + ") WITH (" + + valuesOptions(id, sleepMillis) + + ")"); + } + + private void createAppendBuild(List data) { + final String id = TestValuesTableFactory.registerData(withFlipTrigger(data)); + tEnv().executeSql( + "CREATE TABLE b (" + + " bk STRING," + + " bv INT," + + " bts TIMESTAMP(3)," + + " WATERMARK FOR bts AS bts" + + ") WITH (" + + valuesOptions(id, 0L) + + ")"); + } + + private void createChangelogBuild(List data) { + final String id = TestValuesTableFactory.registerData(withFlipTrigger(data)); + tEnv().executeSql( + "CREATE TABLE b (" + + " bk STRING," + + " bv INT," + + " bts TIMESTAMP(3)," + + " WATERMARK FOR bts AS bts" + + ") WITH (" + + valuesOptions(id, 0L, "'changelog-mode' = 'I,UB,UA,D'") + + ")"); + } + + private void createUpsertBuild(List data) { + createUpsertBuildVerbatim(withFlipTrigger(data), 0L); + } + + /** + * Creates the upsert build table {@code b} from the given rows verbatim -- the caller places + * the flip-trigger row explicitly, so the flip and any post-flip updates can be ordered + * precisely. + */ + private void createUpsertBuildVerbatim(List data, long sleepMillis) { + final String id = TestValuesTableFactory.registerData(data); + tEnv().executeSql( + "CREATE TABLE b (" + + " bk STRING," + + " bv INT," + + " bts TIMESTAMP(3)," + + " WATERMARK FOR bts AS bts," + + " PRIMARY KEY (bk) NOT ENFORCED" + + ") WITH (" + + valuesOptions(id, sleepMillis, "'changelog-mode' = 'I,UA,D'") + + ")"); + } + + private static String valuesOptions(String id, long sleepMillis, String... extraOptions) { + final StringBuilder options = + new StringBuilder() + .append(" 'connector' = 'values',") + .append(" 'bounded' = 'false',") + .append(" 'disable-lookup' = 'true',") + .append(" 'enable-watermark-push-down' = 'true',") + .append(" 'scan.watermark.emit.strategy' = 'on-event',"); + for (String extra : extraOptions) { + options.append(" ").append(extra).append(","); + } + if (sleepMillis > 0) { + options.append(" 'source.sleep-after-elements' = '1',") + .append(" 'source.sleep-time' = '") + .append(sleepMillis) + .append("ms',"); + } + return options.append(" 'data-id' = '").append(id).append("'").toString(); + } + + /** Sorts join results (pv, bv) by the probe id pv, i.e. into probe processing order. */ + private static List sortedByProbeId(List results) { + return results.stream() + .sorted(Comparator.comparingInt(r -> (int) r.getField(0))) + .collect(Collectors.toList()); + } + + /** Asserts the build version (field 1) is non-decreasing across the probe-ordered results. */ + private static void assertMonotonicVersions(List byProbeId) { + int previous = Integer.MIN_VALUE; + for (Row r : byProbeId) { + final int version = (int) r.getField(1); + assertThat(version).isGreaterThanOrEqualTo(previous); + previous = version; + } + } + + private static LocalDateTime ts(String time) { + return LocalDateTime.parse("2020-01-01T" + time); + } +} diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/plan/lateral-snapshot-join-inner.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/plan/lateral-snapshot-join-inner.json new file mode 100644 index 00000000000000..eea074a15352b3 --- /dev/null +++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/plan/lateral-snapshot-join-inner.json @@ -0,0 +1,469 @@ +{ + "flinkVersion" : "2.4", + "nodes" : [ { + "id" : 1, + "type" : "stream-exec-table-source-scan_2", + "scanTableSource" : { + "table" : { + "identifier" : "`default_catalog`.`default_database`.`probe`", + "resolvedTable" : { + "schema" : { + "columns" : [ { + "name" : "pk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "pv", + "dataType" : "INT" + }, { + "name" : "pts_str", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "pts", + "kind" : "COMPUTED", + "expression" : { + "rexNode" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "TO_TIMESTAMP(`pts_str`)" + } + } ], + "watermarkSpecs" : [ { + "rowtimeAttribute" : "pts", + "expression" : { + "rexNode" : { + "kind" : "INPUT_REF", + "inputIndex" : 3, + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "`pts`" + } + } ] + } + } + }, + "abilities" : [ { + "type" : "WatermarkPushDown", + "watermarkExpr" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "rowtimeExpr" : { + "kind" : "CALL", + "syntax" : "SPECIAL", + "internalName" : "$REINTERPRET$1", + "operands" : [ { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + } ], + "type" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + }, + "idleTimeoutMillis" : -1, + "producedType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `pts_str` VARCHAR(2147483647)> NOT NULL", + "watermarkParams" : { + "emitStrategy" : "ON_EVENT", + "alignGroupName" : null, + "alignMaxDrift" : "PT0S", + "alignUpdateInterval" : "PT1S", + "sourceIdleTimeout" : -1 + } + } ] + }, + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `pts_str` VARCHAR(2147483647)>", + "description" : "TableSourceScan(table=[[default_catalog, default_database, probe, watermark=[TO_TIMESTAMP(pts_str)], watermarkEmitStrategy=[on-event]]], fields=[pk, pv, pts_str])" + }, { + "id" : 2, + "type" : "stream-exec-calc_1", + "projection" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 0, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 1, + "type" : "INT" + } ], + "condition" : null, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT>", + "description" : "Calc(select=[pk, pv])" + }, { + "id" : 3, + "type" : "stream-exec-exchange_1", + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "HASH", + "keys" : [ 0 ] + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT>", + "description" : "Exchange(distribution=[hash[pk]])" + }, { + "id" : 4, + "type" : "stream-exec-table-source-scan_2", + "scanTableSource" : { + "table" : { + "identifier" : "`default_catalog`.`default_database`.`b`", + "resolvedTable" : { + "schema" : { + "columns" : [ { + "name" : "bk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "dataType" : "INT" + }, { + "name" : "bts_str", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "bts", + "kind" : "COMPUTED", + "expression" : { + "rexNode" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "TO_TIMESTAMP(`bts_str`)" + } + } ], + "watermarkSpecs" : [ { + "rowtimeAttribute" : "bts", + "expression" : { + "rexNode" : { + "kind" : "INPUT_REF", + "inputIndex" : 3, + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "`bts`" + } + } ] + } + } + }, + "abilities" : [ { + "type" : "WatermarkPushDown", + "watermarkExpr" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "rowtimeExpr" : { + "kind" : "CALL", + "syntax" : "SPECIAL", + "internalName" : "$REINTERPRET$1", + "operands" : [ { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + } ], + "type" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + }, + "idleTimeoutMillis" : -1, + "producedType" : "ROW<`bk` VARCHAR(2147483647), `bv` INT, `bts_str` VARCHAR(2147483647)> NOT NULL", + "watermarkParams" : { + "emitStrategy" : "ON_EVENT", + "alignGroupName" : null, + "alignMaxDrift" : "PT0S", + "alignUpdateInterval" : "PT1S", + "sourceIdleTimeout" : -1 + } + } ] + }, + "outputType" : "ROW<`bk` VARCHAR(2147483647), `bv` INT, `bts_str` VARCHAR(2147483647)>", + "description" : "TableSourceScan(table=[[default_catalog, default_database, b, watermark=[TO_TIMESTAMP(bts_str)], watermarkEmitStrategy=[on-event]]], fields=[bk, bv, bts_str])" + }, { + "id" : 5, + "type" : "stream-exec-calc_1", + "projection" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 0, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 1, + "type" : "INT" + }, { + "kind" : "CALL", + "syntax" : "SPECIAL", + "internalName" : "$REINTERPRET$1", + "operands" : [ { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + } ], + "type" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + } ], + "condition" : null, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : { + "type" : "ROW", + "fields" : [ { + "name" : "bk", + "fieldType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "fieldType" : "INT" + }, { + "name" : "bts", + "fieldType" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + } ] + }, + "description" : "Calc(select=[bk, bv, Reinterpret(TO_TIMESTAMP(bts_str)) AS bts])" + }, { + "id" : 6, + "type" : "stream-exec-exchange_1", + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "HASH", + "keys" : [ 0 ] + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : { + "type" : "ROW", + "fields" : [ { + "name" : "bk", + "fieldType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "fieldType" : "INT" + }, { + "name" : "bts", + "fieldType" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + } ] + }, + "description" : "Exchange(distribution=[hash[bk]])" + }, { + "id" : 7, + "type" : "stream-exec-lateral-snapshot-join_1", + "joinSpec" : { + "joinType" : "INNER", + "leftKeys" : [ 0 ], + "rightKeys" : [ 0 ], + "filterNulls" : [ true ], + "nonEquiCondition" : null + }, + "rightTimeAttributeIndex" : 2, + "loadCompletedCondition" : "user_time", + "loadCompletedTime" : 1577836803000, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + }, { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `bk` VARCHAR(2147483647), `bv` INT, `bts` TIMESTAMP(3)>", + "description" : "LateralSnapshotJoin(joinType=[InnerJoin], where=[(pk = bk)], select=[pk, pv, bk, bv, bts], loadCompletedCondition=[user_time], loadCompletedTime=[1577836803000])" + }, { + "id" : 8, + "type" : "stream-exec-calc_1", + "projection" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 0, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 1, + "type" : "INT" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 3, + "type" : "INT" + } ], + "condition" : null, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `bk` VARCHAR(2147483647), `bv` INT>", + "description" : "Calc(select=[pk, pv, bk, bv])" + }, { + "id" : 9, + "type" : "stream-exec-sink_2", + "configuration" : { + "table.exec.sink.keyed-shuffle" : "AUTO", + "table.exec.sink.not-null-enforcer" : "ERROR", + "table.exec.sink.rowtime-inserter" : "ENABLED", + "table.exec.sink.type-length-enforcer" : "IGNORE", + "table.exec.sink.upsert-materialize" : "AUTO" + }, + "dynamicTableSink" : { + "table" : { + "identifier" : "`default_catalog`.`default_database`.`sink`", + "resolvedTable" : { + "schema" : { + "columns" : [ { + "name" : "pk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "pv", + "dataType" : "INT" + }, { + "name" : "bk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "dataType" : "INT" + } ] + } + } + } + }, + "inputChangelogMode" : [ "INSERT" ], + "upsertMaterializeStrategy" : "VALUE", + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `bk` VARCHAR(2147483647), `bv` INT>", + "description" : "Sink(table=[default_catalog.default_database.sink], fields=[pk, pv, bk, bv])" + } ], + "edges" : [ { + "source" : 1, + "target" : 2, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 2, + "target" : 3, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 4, + "target" : 5, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 5, + "target" : 6, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 3, + "target" : 7, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 6, + "target" : 7, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 7, + "target" : 8, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 8, + "target" : 9, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + } ] +} \ No newline at end of file diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-inner/savepoint/_metadata new file mode 100644 index 0000000000000000000000000000000000000000..ea9e93db4c1e8b7483a49fb3ecae269359be01c1 GIT binary patch literal 19149 zcmeGkZEV}d_4E7(}e^i9`EDbyLa#I-M#nj5@%*8gisg!@Z-{7XfxV8#){l@=rrmIdtIT_Dq;}eNkGBFjOfLJ#=DJwHHTVc6bo-T<%by{N!B2Oz-NfXLEtzD?_RZS38 zIzF%Qio}XpUJ+PPxX3F5tx;Ktt*EoI2CzM7$KP>px~{&DHmM5w(h4=<8dUnIz>7t7 zoYmMIe1G`*)dN3Yd^zt%t|Z!7!f9rbs-fQG99v@eJ{w;n8>OCc$W@XWThE~M2#lVHc_4tV1Qt>G!#feJ&aB-hR};4 z^kPUW7$Yx}Y?K+DuhbN@zZE^w>H}XPlq^(CTq}Vdl)?;bt3AQpije3mq zjI0V8Ag-Z3(-x%Dg*l$n0Bi4AK`LsB7skQYTyvpD(gsM{Ap&Y2iTP|( zkE}k~#x%JsaOR}A|Jm#pKDziwzzqzHYjneW)!SfWo&J|UxDvTI`pY9YQ^giN28!)! z6oYp2GrY3f-wuRMMA#r|zC{Pnp8c%y}+Dh8t! zu&)&-g-Mc06NLIs{THaAi_6>!yxA(?fG;sgEb^@-rb)a3?;E1+-Y{Of47_erqH8^- zMC$-E$l0EVu^CtoCCVP@^s5W3BSpVMe^vy(N_E#UmG-4WqMR zq*vW>HgXD@#-<70VaEG_8J!IyR&%c+Jvy<;#VYtm@gIIz{k3Q+=FRV|dOZHWW8Q5xc?){n z;+saA`Q2xe*Q|2`aA;tI4C8nNOxF=&?mqXQGk+eAeC1OjzKS9j_-X%X)a}o!B<}5hA9(HhCp~|uz4^xT)9-%nRVZEuhrm3;f~6G< zt%fc2I2e|v!>7?_!g*0op;RM@nuD|#_=U@l?$=)WeE(0&?6+@Xqgx%Ci5#)3yU^Yi z0F_x#ir`#EPS@deH;+eyLs5UIz)`_s$xj6%p%TRw!U2loLs5?N4-G{Ezz(iKKoZR3 zD>lMtLpuiYO_M1wmXiYZR`JXl6iV34XVEA%f0}&=b$QU^El`_n?Q#XWU6U1+#@$Mr zolM*CL)yqXkiq>2(6RM69UDj9@ITUax zu)!2q_gTGE3=M(1(;yWp2E$ZvmUaX{q@~t$ zLUJs%4bP;zF^*YQ>#mNQce>HGLRAop6cLxkKysnp$Kp@K#|P=GI@M^PAptG#&aqPN|U01bpE!;qzQJ`fg=}TGcmetUl=fHzh z)!1^S*#pX*KwByb&k5M|vhJ%GY|nGA4wLvV-hY0C>!QJXD3>@92lw9-nf$3l?pS&< zm&Y;rsdOq16QBnich=ZLn3~4SN<0~{TU8ghzk$OdhHRPbJRP6!JE7?~vjW4mx`EXh ztXUT`utK~=HsCF^2d!oAn3ScNhL{;>{Yv!Jn0<3e{4HR}3z>ujFrMXdY|fnU&WI*iW`W+$JKnE*M-L zUF$jv27QftS8hxew_nFA5sU9N?-rVkOluTJ&Vs?_P{m!?shkA^oM2j$|KIv87&b<< z@6@F^U!+)#I&U6Sv6JeX$iXoX`)*{vukMWW-59cLC)Sz=ZmH8r-J9D{U^ohl`$mCb z*~z*;l{?vunHPJl^CG3f**pDt;04B3O%o|N5Gg1Z9FpkgzL~rRia#Y!^bZU$4#%r- zI)A3seSze|XZekD{b1dAO9>pDPt8pLR-u{_;chb_-hZ8>AiUEmJO(-B?5KiIV(8R zmk}rV@E?B6NYlab5C2*j94WBfbQLU22fc9)(hZXux*VMCv1h>0#?ZEWHu8|jsFjo?>U43 literal 0 HcmV?d00001 diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/plan/lateral-snapshot-join-left.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/plan/lateral-snapshot-join-left.json new file mode 100644 index 00000000000000..fa475e5b1c68ab --- /dev/null +++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/plan/lateral-snapshot-join-left.json @@ -0,0 +1,468 @@ +{ + "flinkVersion" : "2.4", + "nodes" : [ { + "id" : 10, + "type" : "stream-exec-table-source-scan_2", + "scanTableSource" : { + "table" : { + "identifier" : "`default_catalog`.`default_database`.`probe`", + "resolvedTable" : { + "schema" : { + "columns" : [ { + "name" : "pk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "pv", + "dataType" : "INT" + }, { + "name" : "pts_str", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "pts", + "kind" : "COMPUTED", + "expression" : { + "rexNode" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "TO_TIMESTAMP(`pts_str`)" + } + } ], + "watermarkSpecs" : [ { + "rowtimeAttribute" : "pts", + "expression" : { + "rexNode" : { + "kind" : "INPUT_REF", + "inputIndex" : 3, + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "`pts`" + } + } ] + } + } + }, + "abilities" : [ { + "type" : "WatermarkPushDown", + "watermarkExpr" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "rowtimeExpr" : { + "kind" : "CALL", + "syntax" : "SPECIAL", + "internalName" : "$REINTERPRET$1", + "operands" : [ { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + } ], + "type" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + }, + "idleTimeoutMillis" : -1, + "producedType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `pts_str` VARCHAR(2147483647)> NOT NULL", + "watermarkParams" : { + "emitStrategy" : "ON_EVENT", + "alignGroupName" : null, + "alignMaxDrift" : "PT0S", + "alignUpdateInterval" : "PT1S", + "sourceIdleTimeout" : -1 + } + } ] + }, + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `pts_str` VARCHAR(2147483647)>", + "description" : "TableSourceScan(table=[[default_catalog, default_database, probe, watermark=[TO_TIMESTAMP(pts_str)], watermarkEmitStrategy=[on-event]]], fields=[pk, pv, pts_str])" + }, { + "id" : 11, + "type" : "stream-exec-calc_1", + "projection" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 0, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 1, + "type" : "INT" + } ], + "condition" : null, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT>", + "description" : "Calc(select=[pk, pv])" + }, { + "id" : 12, + "type" : "stream-exec-exchange_1", + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "HASH", + "keys" : [ 0 ] + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT>", + "description" : "Exchange(distribution=[hash[pk]])" + }, { + "id" : 13, + "type" : "stream-exec-table-source-scan_2", + "scanTableSource" : { + "table" : { + "identifier" : "`default_catalog`.`default_database`.`b`", + "resolvedTable" : { + "schema" : { + "columns" : [ { + "name" : "bk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "dataType" : "INT" + }, { + "name" : "bts_str", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "bts", + "kind" : "COMPUTED", + "expression" : { + "rexNode" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "TO_TIMESTAMP(`bts_str`)" + } + } ], + "watermarkSpecs" : [ { + "rowtimeAttribute" : "bts", + "expression" : { + "rexNode" : { + "kind" : "INPUT_REF", + "inputIndex" : 3, + "type" : "TIMESTAMP(3)" + }, + "serializableString" : "`bts`" + } + } ] + } + } + }, + "abilities" : [ { + "type" : "WatermarkPushDown", + "watermarkExpr" : { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + }, + "rowtimeExpr" : { + "kind" : "CALL", + "syntax" : "SPECIAL", + "internalName" : "$REINTERPRET$1", + "operands" : [ { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + } ], + "type" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + }, + "idleTimeoutMillis" : -1, + "producedType" : "ROW<`bk` VARCHAR(2147483647), `bv` INT, `bts_str` VARCHAR(2147483647)> NOT NULL", + "watermarkParams" : { + "emitStrategy" : "ON_EVENT", + "alignGroupName" : null, + "alignMaxDrift" : "PT0S", + "alignUpdateInterval" : "PT1S", + "sourceIdleTimeout" : -1 + } + } ] + }, + "outputType" : "ROW<`bk` VARCHAR(2147483647), `bv` INT, `bts_str` VARCHAR(2147483647)>", + "description" : "TableSourceScan(table=[[default_catalog, default_database, b, watermark=[TO_TIMESTAMP(bts_str)], watermarkEmitStrategy=[on-event]]], fields=[bk, bv, bts_str])" + }, { + "id" : 14, + "type" : "stream-exec-calc_1", + "projection" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 0, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 1, + "type" : "INT" + }, { + "kind" : "CALL", + "syntax" : "SPECIAL", + "internalName" : "$REINTERPRET$1", + "operands" : [ { + "kind" : "CALL", + "internalName" : "$TO_TIMESTAMP$1", + "operands" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + } ], + "type" : "TIMESTAMP(3)" + } ], + "type" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + } ], + "condition" : null, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : { + "type" : "ROW", + "fields" : [ { + "name" : "bk", + "fieldType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "fieldType" : "INT" + }, { + "name" : "bts", + "fieldType" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + } ] + }, + "description" : "Calc(select=[bk, bv, Reinterpret(TO_TIMESTAMP(bts_str)) AS bts])" + }, { + "id" : 15, + "type" : "stream-exec-exchange_1", + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "HASH", + "keys" : [ 0 ] + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : { + "type" : "ROW", + "fields" : [ { + "name" : "bk", + "fieldType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "fieldType" : "INT" + }, { + "name" : "bts", + "fieldType" : { + "type" : "TIMESTAMP_WITHOUT_TIME_ZONE", + "precision" : 3, + "kind" : "ROWTIME" + } + } ] + }, + "description" : "Exchange(distribution=[hash[bk]])" + }, { + "id" : 16, + "type" : "stream-exec-lateral-snapshot-join_1", + "joinSpec" : { + "joinType" : "LEFT", + "leftKeys" : [ 0 ], + "rightKeys" : [ 0 ], + "filterNulls" : [ true ], + "nonEquiCondition" : null + }, + "rightTimeAttributeIndex" : 2, + "loadCompletedCondition" : "user_time", + "loadCompletedTime" : 1577836803000, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + }, { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `bk` VARCHAR(2147483647), `bv` INT, `bts` TIMESTAMP(3)>", + "description" : "LateralSnapshotJoin(joinType=[LeftOuterJoin], where=[(pk = bk)], select=[pk, pv, bk, bv, bts], loadCompletedCondition=[user_time], loadCompletedTime=[1577836803000])" + }, { + "id" : 17, + "type" : "stream-exec-calc_1", + "projection" : [ { + "kind" : "INPUT_REF", + "inputIndex" : 0, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 1, + "type" : "INT" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 2, + "type" : "VARCHAR(2147483647)" + }, { + "kind" : "INPUT_REF", + "inputIndex" : 3, + "type" : "INT" + } ], + "condition" : null, + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `bk` VARCHAR(2147483647), `bv` INT>", + "description" : "Calc(select=[pk, pv, bk, bv])" + }, { + "id" : 18, + "type" : "stream-exec-sink_2", + "configuration" : { + "table.exec.sink.keyed-shuffle" : "AUTO", + "table.exec.sink.not-null-enforcer" : "ERROR", + "table.exec.sink.rowtime-inserter" : "ENABLED", + "table.exec.sink.type-length-enforcer" : "IGNORE", + "table.exec.sink.upsert-materialize" : "AUTO" + }, + "dynamicTableSink" : { + "table" : { + "identifier" : "`default_catalog`.`default_database`.`sink`", + "resolvedTable" : { + "schema" : { + "columns" : [ { + "name" : "pk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "pv", + "dataType" : "INT" + }, { + "name" : "bk", + "dataType" : "VARCHAR(2147483647)" + }, { + "name" : "bv", + "dataType" : "INT" + } ] + } + } + } + }, + "inputChangelogMode" : [ "INSERT" ], + "inputProperties" : [ { + "requiredDistribution" : { + "type" : "UNKNOWN" + }, + "damBehavior" : "PIPELINED", + "priority" : 0 + } ], + "outputType" : "ROW<`pk` VARCHAR(2147483647), `pv` INT, `bk` VARCHAR(2147483647), `bv` INT>", + "description" : "Sink(table=[default_catalog.default_database.sink], fields=[pk, pv, bk, bv])" + } ], + "edges" : [ { + "source" : 10, + "target" : 11, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 11, + "target" : 12, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 13, + "target" : 14, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 14, + "target" : 15, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 12, + "target" : 16, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 15, + "target" : 16, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 16, + "target" : 17, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + }, { + "source" : 17, + "target" : 18, + "shuffle" : { + "type" : "FORWARD" + }, + "shuffleMode" : "PIPELINED" + } ] +} \ No newline at end of file diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-lateral-snapshot-join_1/lateral-snapshot-join-left/savepoint/_metadata new file mode 100644 index 0000000000000000000000000000000000000000..5b03d9a12596802a5b473ab1829b91076c1ba980 GIT binary patch literal 19163 zcmeGkTWlOx_3k4NCynhSrfpuWQQh*&?s)d)wJo(-+iSAvX4iPvNoykt0nKdbBZt_Frf1j8YU z;Zga^H~#*{&$`!te(cbR`L|D4+nY1fv2FanFU@IK;l!N8Rn-MqJ0T0w92+}>?q_2K zQLhWAMrB|TY6pI>ek}QcXRa-ceRKA~>plN!Hj2kjOYe06s4j3SCb$^*c#e9|i)BH? zBg`_VFlAXRVMS%4!;G?WVR=wl91bm>V~3V2;dnf9u2Nix55hHHW%A5oO%k*~I5spC zi^oH;cr+S`M}~$5BaF%|<0@dP1&YJ55L*sJN_;6C8jf+%VUEG27#E9$h6BUJVk8iW zg@yvf2+IeGI26Ls;c$eF@_^qUAPH>YK`ka>%)x{Dvs20GL_RxR$mJ9HWMLwe%TK4q zX7Z_Qrf@Plkwn{OGVn}0^qxwjXOe|nayperr)HDWQ0hffvN9j!svN(7gJn^Wmf%GQ z@^YmjOF?b5iffu6s=*@AE|`>Rl^j+CP82R+Wx)Pwt|CD7p(h+rHLfV)pi+}Gp@M^Q z6&7BT6*WkfHE1q!un9+Gjiv)rWc?`4{&Y4mf&3@3DKax-L3hx{`lc}6THgBiwye5o zMgO;MTJ2W46mTi9sT8>5QoY26xgst{1I6-iG!Th|;VBjh2jcN!B!ojeAB~g{>VF5Q zLY2K2<=;a*HstQKe13w{IE&PEl-L9>2*a!YMB7va4YnAAz*d8zJTHJ{5j@2y;|rXU zheBh~JYM8Uq3t4;=Cy^DcHyLTcy>76S+q+~Gc{2p!at36lCh^{S<_G~4d^kt-eIWT zVW@^+D0+;69%BJZ`i-tsJr@-eX=`d5`kFks4vQ;F&E zuMzBZg_+Y7L7LyNcFhWH%eNElF4hFG6rjW~R|NGw zo_sPnVJP|hlT%6LJDHe*YAvLIG-<7wC zbq#VJbufc&jM1%Dmjo$E%iE*?*(U7?5evE|%X-n>yZKX@eDdSTX@h#9wy2i}t5(&x zO4Uw7Zwhr)70e5&AWLcFUlOE}h9d0=zdFpvq1Wj9bTtm;Qzw()-JF~%oKEGBXJ_&S zQc{@BW|F{cK2Qg@xI%=Q2Ad+H49=m8XgkRK>Fj9&*&^?KsMGR3<6y4VavC;hLrxFa z-fU9)tOKUq_iE^M6&} zOuY+RPs71evQWx!uo1zX#g@TZl-ow#uj^fY0ZalMDAvklteC=VE9u``@z?D{9 zn8&p?6Jbjj44b3cyLD}jO94&`oUhzOt8b%6q)yE$aJhsJ&0ccxA!&)&T5 z0fu{kp>F!=rD2a)JX!Bu>B(-6TV#ej;>xe9|g6h5>|)5HK?sS3)fOA?(VOhJQ# zKczs=9~fX99;+&x`#aMYXgihCQ74|K?W22DQ!rNns5CU+2%VE-8l`c}(Zmz?+C)Gn zHJ=-Gy1kybW#K#>m))H_SNo`Khu~!_Y3}Bz=h`$2#JC~ZEftdO4Q7f=8&!D5CM{k!RqoJy5%iey?6{0t$lOh*q_TS&VdidLGFBc$BC5^hvq|U5THPo9XnH>yC!eB_Gg2YW9zA1FG^DjPsz3*$@ zPgfvxokl&pEGdtMNS34dv(AhWrU$cqiM8TY^1$tDAZn9*K$@5`7Fx{I_N|X zrl|%Y(?@4ZQ)U<=AZ{GgQj5{;?i+WT_D4+R3Yc6y-WjuZ;z70I&P=9t0Wfd z*?N&~bSILId=P*J8a_fZlL-F?$7NB3C4;3@2WF&-ToTFx&7=ap{$J0-?DzliLNDC~ zei2Gc+8upw01dzg#?YnLo?3r^g7NSgy4SM8jb{4t?r%U&C2S+!?}R2f zHycJUj&d5%i%azR0S{@|PZHEDb<6a5fYv=i6B`cDlKX8da&uvu(Bz81n?mv6XL28T z=fXoFuaPL>1$os=*?{KG>GZ$+_LcaB(eI6sM%AY1G0<&qqZ`Z{&tqlZpD$loUwbZ2 zGY+>cb7Bp{-nW3y)G9@+JbU#^M|OYl_2(PljTzFR7($3p^HE9F5Unz4f^3deCp}`n zdlu}T1#g+NV3!$PX52E&XpzDGjvsDyBE{}*ObguT1G^3*F+g31u{5^oI*hmbS1PW< zNY4XYhjD{DXZc+bbxT}_(RCR0NHQFjxHh_Lqi-1--DO6X8Mh2Gx(=iB$Hca1S|^-b zas>Zj;~c&omo^XPIlW@d3`w`s){Hq@+nNF7Ul{|L?`A;R+SVL%6L2uFqc`K@H!KNW zv-IOP-+1iVTHl3{mxivfU;j?`MaQ>&ncyGPU0~Hhf1?(k5M$q!KZBCy~lhPBtc!UmgH(hwN@b| zCYW4JtzxM}inf>;6V@- zVcQ`>&isT9>3;dS-|f8eMBl444zvq!tZ?*a${LC~!qvsqj59sr-(0SRaUfyA(b2n6q79q(WhyeSFcUppT! z=o(LCvYFI)A`L-nV}<+cFaZG|$A(Y-G-1Vh2eJxHXJf^QxH(-A5X;TvrjnV7{{zqV BIFtYY literal 0 HcmV?d00001