You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by fp...@apache.org on 2026/04/02 14:49:51 UTC

(flink) branch release-2.1 updated (b6aeb26c7e2 -> a0976628c6c)

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

fpaul pushed a change to branch release-2.1
in repository https://gitbox.apache.org/repos/asf/flink.git


    from b6aeb26c7e2 [FLINK-38815] Mask sensitive values in Pekko debug configuration logs
     new ac6cc0b8bf1 [FLINK-38209][table-planner] Use UniqueKeys instead of Upsertkeys for inputSpec and state management for MultiJoin
     new 0e0a9d90407 [FLINK-38379][table] Adjust `visitMultiJoin` in `RelTimeIndicatorConverter` to use fields and not rows
     new 74404f8e8bc [FLINK-38554][table] Fix rowCount cost for FlinkLogicalMultiJoin
     new ce30d0317bb [FLINK-38576][table] Align commonJoinKey in MultiJoin for logical and physical rules
     new a0976628c6c [FLINK-38230][table-planner] Add visitMultiJoin to StreamNDUPlanVisitor

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../table/utils/NoCommonJoinKeyException.java      |   20 +-
 .../planner/calcite/RelTimeIndicatorConverter.java |    4 +-
 .../nodes/exec/stream/StreamExecMultiJoin.java     |   31 +-
 .../plan/nodes/logical/FlinkLogicalMultiJoin.java  |    2 +-
 .../physical/stream/StreamPhysicalMultiJoin.java   |   79 +-
 .../StreamNonDeterministicUpdatePlanVisitor.java   |  100 ++
 .../plan/rules/logical/JoinToMultiJoinRule.java    |  169 +--
 .../stream/StreamPhysicalMultiJoinRule.java        |  114 +-
 .../table/planner/plan/utils/MultiJoinUtil.java    |  155 ++
 .../NonDeterministicUpdateAnalyzerTest.java        |  436 +++---
 .../exec/serde/ExecNodeMultiJoinJsonSerdeTest.java |    6 +-
 .../nodes/exec/stream/MultiJoinSemanticTests.java  |   15 +-
 .../nodes/exec/stream/MultiJoinTestPrograms.java   |  178 ++-
 .../nodes/exec/testutils/SemanticTestBase.java     |   10 +
 .../planner/plan/stream/sql/MultiJoinTest.java     | 1516 ++++++++++++++++++--
 .../analyze/NonDeterministicUpdateAnalyzerTest.xml |   36 +
 .../planner/plan/stream/sql/MultiJoinTest.xml      | 1391 +++++++++++-------
 ...our-way-complex-updating-join-with-restore.json |  527 ++++---
 .../savepoint/_metadata                            |  Bin 35162 -> 36360 bytes
 ...r-way-join-no-common-join-key-with-restore.json |  431 +++---
 .../savepoint/_metadata                            |  Bin 41248 -> 34081 bytes
 .../plan/three-way-inner-join-with-restore.json    |  244 ++--
 .../savepoint/_metadata                            |  Bin 21326 -> 20195 bytes
 ...way-join-with-time-attributes-with-restore.json |   89 +-
 .../savepoint/_metadata                            |  Bin 24946 -> 25432 bytes
 .../three-way-left-outer-join-with-restore.json    |  231 +--
 .../savepoint/_metadata                            |  Bin 21759 -> 21005 bytes
 .../AttributeBasedJoinKeyExtractor.java            |    3 +-
 28 files changed, 4014 insertions(+), 1773 deletions(-)
 copy flink-rpc/flink-rpc-akka/src/main/java/org/apache/flink/runtime/rpc/pekko/exceptions/RpcInvalidStateException.java => flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/NoCommonJoinKeyException.java (68%)
 create mode 100644 flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/utils/MultiJoinUtil.java


(flink) 01/05: [FLINK-38209][table-planner] Use UniqueKeys instead of Upsertkeys for inputSpec and state management for MultiJoin

Posted by fp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit ac6cc0b8bf12d35dc505e16b766f95d17de0c3f9
Author: Gustavo de Morais <gd...@confluent.io>
AuthorDate: Tue Mar 24 11:43:07 2026 +0100

    [FLINK-38209][table-planner] Use UniqueKeys instead of Upsertkeys for inputSpec and state management for MultiJoin
---
 .../nodes/exec/stream/StreamExecMultiJoin.java     |  31 +++++-----
 .../physical/stream/StreamPhysicalMultiJoin.java   |  19 +++---
 .../exec/serde/ExecNodeMultiJoinJsonSerdeTest.java |   6 +-
 .../nodes/exec/stream/MultiJoinSemanticTests.java  |   3 +-
 .../nodes/exec/stream/MultiJoinTestPrograms.java   |  66 ++++++++++++++++++++-
 ...our-way-complex-updating-join-with-restore.json |   2 +-
 .../savepoint/_metadata                            | Bin 35162 -> 35375 bytes
 ...r-way-join-no-common-join-key-with-restore.json |   4 +-
 .../savepoint/_metadata                            | Bin 41248 -> 38180 bytes
 .../plan/three-way-inner-join-with-restore.json    |   2 +-
 .../savepoint/_metadata                            | Bin 21326 -> 21331 bytes
 ...way-join-with-time-attributes-with-restore.json |   2 +-
 .../savepoint/_metadata                            | Bin 24946 -> 23917 bytes
 .../three-way-left-outer-join-with-restore.json    |   2 +-
 .../savepoint/_metadata                            | Bin 21759 -> 21588 bytes
 15 files changed, 100 insertions(+), 37 deletions(-)

diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecMultiJoin.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecMultiJoin.java
index b6ca3571e1a..31e04a693c7 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecMultiJoin.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecMultiJoin.java
@@ -82,7 +82,7 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
     private static final String FIELD_NAME_JOIN_TYPES = "joinTypes";
     private static final String FIELD_NAME_JOIN_CONDITIONS = "joinConditions";
     private static final String FIELD_NAME_JOIN_ATTRIBUTE_MAP = "joinAttributeMap";
-    private static final String FIELD_NAME_INPUT_UPSERT_KEYS = "inputUpsertKeys";
+    private static final String FIELD_NAME_INPUT_UNIQUE_KEYS = "inputUniqueKeys";
     private static final String FIELD_NAME_MULTI_JOIN_CONDITION = "multiJoinCondition";
 
     @JsonProperty(FIELD_NAME_JOIN_TYPES)
@@ -100,12 +100,13 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private final Map<Integer, List<ConditionAttributeRef>> joinAttributeMap;
 
-    @JsonProperty(FIELD_NAME_INPUT_UPSERT_KEYS)
+    // Why List<List<int[]>> as a type
+    // Each unique key can be also a composite key with multiple fields, thus -> int[].
+    // Theoretically, each input can have multiple unique keys, thus -> List<int[]>
+    // Since we have multiple inputs -> List<List<int[]>>
+    @JsonProperty(FIELD_NAME_INPUT_UNIQUE_KEYS)
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
-    // List of upsert keys for each input, where each inner list corresponds to an input
-    // The reason it's a List<List<int[]>> is that SQL allows only one primary key but
-    // multiple upsert (unique) keys per input
-    private final List<List<int[]>> inputUpsertKeys;
+    private final List<List<int[]>> inputUniqueKeys;
 
     @JsonProperty(FIELD_NAME_STATE)
     @JsonInclude(JsonInclude.Include.NON_NULL)
@@ -117,7 +118,7 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
             final List<? extends @Nullable RexNode> joinConditions,
             @Nullable final RexNode multiJoinCondition,
             final Map<Integer, List<ConditionAttributeRef>> joinAttributeMap,
-            final List<List<int[]>> inputUpsertKeys,
+            final List<List<int[]>> inputUniqueKeys,
             final Map<Integer, Long> stateTtlFromHint,
             final List<InputProperty> inputProperties,
             final RowType outputType,
@@ -130,7 +131,7 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
                 joinConditions,
                 multiJoinCondition,
                 joinAttributeMap,
-                inputUpsertKeys,
+                inputUniqueKeys,
                 StateMetadata.getMultiInputOperatorDefaultMeta(
                         stateTtlFromHint, tableConfig, generateStateNames(inputProperties.size())),
                 inputProperties,
@@ -150,18 +151,18 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
                     final RexNode multiJoinCondition,
             @JsonProperty(FIELD_NAME_JOIN_ATTRIBUTE_MAP)
                     final Map<Integer, List<ConditionAttributeRef>> joinAttributeMap,
-            @JsonProperty(FIELD_NAME_INPUT_UPSERT_KEYS) final List<List<int[]>> inputUpsertKeys,
+            @JsonProperty(FIELD_NAME_INPUT_UNIQUE_KEYS) final List<List<int[]>> inputUniqueKeys,
             @Nullable @JsonProperty(FIELD_NAME_STATE) final List<StateMetadata> stateMetadataList,
             @JsonProperty(FIELD_NAME_INPUT_PROPERTIES) final List<InputProperty> inputProperties,
             @JsonProperty(FIELD_NAME_OUTPUT_TYPE) final RowType outputType,
             @JsonProperty(FIELD_NAME_DESCRIPTION) final String description) {
         super(id, context, persistedConfig, inputProperties, outputType, description);
-        validateInputs(inputProperties, joinTypes, joinConditions, inputUpsertKeys);
+        validateInputs(inputProperties, joinTypes, joinConditions, inputUniqueKeys);
         this.joinTypes = checkNotNull(joinTypes);
         this.joinConditions = checkNotNull(joinConditions);
         this.multiJoinCondition = multiJoinCondition;
-        this.inputUpsertKeys = checkNotNull(inputUpsertKeys);
         this.joinAttributeMap = Objects.requireNonNullElseGet(joinAttributeMap, Map::of);
+        this.inputUniqueKeys = checkNotNull(inputUniqueKeys);
         this.stateMetadataList = stateMetadataList;
     }
 
@@ -169,7 +170,7 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
             final List<InputProperty> inputProperties,
             final List<FlinkJoinType> joinTypes,
             final List<? extends @Nullable RexNode> joinConditions,
-            final List<List<int[]>> inputUpsertKeys) {
+            final List<List<int[]>> inputUniqueKeys) {
         checkArgument(
                 inputProperties.size() >= 2, "Multi-input join operator needs at least 2 inputs.");
         checkArgument(
@@ -179,8 +180,8 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
                 joinConditions.size() == inputProperties.size(),
                 "Size of joinConditions must match the number of inputs.");
         checkArgument(
-                inputUpsertKeys.size() == inputProperties.size(),
-                "Size of inputUpsertKeys must match the number of inputs.");
+                inputUniqueKeys.size() == inputProperties.size(),
+                "Size of inputUniqueKeys must match the number of inputs.");
     }
 
     private static String[] generateStateNames(int numInputs) {
@@ -220,7 +221,7 @@ public class StreamExecMultiJoin extends ExecNodeBase<RowData>
                             planner.getFlinkContext().getClassLoader(),
                             inputTypeInfos.get(i),
                             keyExtractor.getJoinKeyIndices(i),
-                            inputUpsertKeys.get(i)));
+                            inputUniqueKeys.get(i)));
         }
 
         final GeneratedJoinCondition[] generatedJoinConditions =
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
index 0d5ee9c8b76..fb25cab268f 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
@@ -167,7 +167,7 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
     @Override
     public ExecNode<?> translateToExecNode() {
         final RexNode multiJoinCondition = createMultiJoinCondition();
-        final List<List<int[]>> inputUpsertKeys = getUpsertKeysForInputs();
+        final List<List<int[]>> inputUniqueKeys = getUniqueKeysForInputs();
         final List<FlinkJoinType> execJoinTypes = getExecJoinTypes();
         final List<InputProperty> inputProperties = createInputProperties();
 
@@ -177,7 +177,7 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
                 joinConditions,
                 multiJoinCondition,
                 joinAttributeMap,
-                inputUpsertKeys,
+                inputUniqueKeys,
                 Collections.emptyMap(), // TODO Enable hint-based state ttl. See ticket
                 // TODO https://issues.apache.org/jira/browse/FLINK-37936
                 inputProperties,
@@ -194,26 +194,27 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
         return RexUtil.composeConjunction(getCluster().getRexBuilder(), conjunctions, true);
     }
 
-    private List<List<int[]>> getUpsertKeysForInputs() {
+    private List<List<int[]>> getUniqueKeysForInputs() {
         return inputs.stream()
                 .map(
                         input -> {
-                            final Set<ImmutableBitSet> upsertKeys = getUpsertKeys(input);
+                            final Set<ImmutableBitSet> uniqueKeys = getUniqueKeys(input);
 
-                            if (upsertKeys == null) {
+                            if (uniqueKeys == null) {
                                 return Collections.<int[]>emptyList();
                             }
-                            return upsertKeys.stream()
+
+                            return uniqueKeys.stream()
                                     .map(ImmutableBitSet::toArray)
                                     .collect(Collectors.toList());
                         })
                 .collect(Collectors.toList());
     }
 
-    private @Nullable Set<ImmutableBitSet> getUpsertKeys(RelNode input) {
+    private @Nullable Set<ImmutableBitSet> getUniqueKeys(RelNode input) {
         final FlinkRelMetadataQuery fmq =
                 FlinkRelMetadataQuery.reuseOrCreate(input.getCluster().getMetadataQuery());
-        return fmq.getUpsertKeys(input);
+        return fmq.getUniqueKeys(input);
     }
 
     private List<FlinkJoinType> getExecJoinTypes() {
@@ -255,7 +256,7 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
      */
     public boolean inputUniqueKeyContainsCommonJoinKey(int inputId) {
         final RelNode input = getInputs().get(inputId);
-        final Set<ImmutableBitSet> inputUniqueKeys = getUpsertKeys(input);
+        final Set<ImmutableBitSet> inputUniqueKeys = getUniqueKeys(input);
         if (inputUniqueKeys == null || inputUniqueKeys.isEmpty()) {
             return false;
         }
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/serde/ExecNodeMultiJoinJsonSerdeTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/serde/ExecNodeMultiJoinJsonSerdeTest.java
index 38454ac9fed..14e2489a06d 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/serde/ExecNodeMultiJoinJsonSerdeTest.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/serde/ExecNodeMultiJoinJsonSerdeTest.java
@@ -101,7 +101,7 @@ class ExecNodeMultiJoinJsonSerdeTest {
         // MultiJoin specific fields
         JsonSerdeTestUtil.assertThatJsonContains(jsonNode, "nodes", "0", "joinTypes");
         JsonSerdeTestUtil.assertThatJsonContains(jsonNode, "nodes", "0", "joinAttributeMap");
-        JsonSerdeTestUtil.assertThatJsonContains(jsonNode, "nodes", "0", "inputUpsertKeys");
+        JsonSerdeTestUtil.assertThatJsonContains(jsonNode, "nodes", "0", "inputUniqueKeys");
         JsonSerdeTestUtil.assertThatJsonContains(jsonNode, "nodes", "0", "joinConditions");
 
         // Verify specific field values
@@ -112,8 +112,8 @@ class ExecNodeMultiJoinJsonSerdeTest {
         assertThat(node.get("joinTypes"))
                 .containsExactly(new TextNode("INNER"), new TextNode("INNER"));
         assertThat(node.get("joinAttributeMap").isObject()).isTrue();
-        assertThat(node.get("inputUpsertKeys").isArray()).isTrue();
-        assertThat(node.get("inputUpsertKeys")).hasSize(2);
+        assertThat(node.get("inputUniqueKeys").isArray()).isTrue();
+        assertThat(node.get("inputUniqueKeys")).hasSize(2);
         assertThat(node.get("joinConditions").isArray()).isTrue();
         assertThat(node.get("joinConditions")).hasSize(2);
         assertThat(node.get("inputProperties").isArray()).isTrue();
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
index ef069feba05..42db5806102 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
@@ -39,6 +39,7 @@ public class MultiJoinSemanticTests extends SemanticTestBase {
                 MultiJoinTestPrograms.MULTI_JOIN_FOUR_WAY_NO_COMMON_JOIN_KEY,
                 MultiJoinTestPrograms.MULTI_JOIN_THREE_WAY_LEFT_OUTER_JOIN_WITH_CTE,
                 MultiJoinTestPrograms.MULTI_JOIN_LEFT_OUTER_WITH_NULL_KEYS,
-                MultiJoinTestPrograms.MULTI_JOIN_NULL_SAFE_JOIN_WITH_NULL_KEYS);
+                MultiJoinTestPrograms.MULTI_JOIN_NULL_SAFE_JOIN_WITH_NULL_KEYS,
+                MultiJoinTestPrograms.MULTI_JOIN_MIXED_CHANGELOG_MODES);
     }
 }
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
index 38db1af79e1..ffc47c62a20 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
@@ -545,7 +545,7 @@ public class MultiJoinTestPrograms {
                                             "order_id STRING",
                                             "payment_id STRING",
                                             "location STRING")
-                                    .addOption("changelog-mode", "I,UA,UB,D")
+                                    .addOption("sink-changelog-mode-enforced", "I,UA,UB,D")
                                     .consumedBeforeRestore(
                                             "+I[1, Gus, order0, 1, London]",
                                             "+I[1, Gus, order1, 1, London]",
@@ -650,7 +650,7 @@ public class MultiJoinTestPrograms {
                                             "order_id STRING",
                                             "payment_id STRING",
                                             "location STRING")
-                                    .addOption("changelog-mode", "I,UA,UB,D")
+                                    .addOption("sink-changelog-mode-enforced", "I,UA,UB,D")
                                     .consumedValues(
                                             "+I[1, Gus, order0, 1, London]",
                                             "+I[1, Gus, order1, 1, London]",
@@ -758,7 +758,7 @@ public class MultiJoinTestPrograms {
                                             "order_id STRING",
                                             "payment_id STRING",
                                             "location STRING")
-                                    .addOption("changelog-mode", "I,UA,UB,D")
+                                    .addOption("sink-changelog-mode-enforced", "I,UA,UB,D")
                                     .consumedBeforeRestore(
                                             "+I[1, Gus, order0, payment1, London]",
                                             "+I[1, Gus, order1, payment1, London]",
@@ -1042,4 +1042,64 @@ public class MultiJoinTestPrograms {
                                     + "FROM UsersNullSafe u "
                                     + "INNER JOIN OrdersNullSafe o ON u.user_id IS NOT DISTINCT FROM o.user_id")
                     .build();
+
+    public static final TableTestProgram MULTI_JOIN_MIXED_CHANGELOG_MODES =
+            TableTestProgram.of(
+                            "three-way-mixed-changelog-modes",
+                            "three way join with mixed changelog modes and primary key configurations")
+                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("AppendTable")
+                                    .addSchema("id STRING PRIMARY KEY NOT ENFORCED, val STRING")
+                                    .addOption("changelog-mode", "I")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, "1", "append1"),
+                                            Row.ofKind(RowKind.INSERT, "2", "append2"),
+                                            Row.ofKind(RowKind.INSERT, "3", "append3"))
+                                    .build())
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("RetractTable")
+                                    .addSchema("ref_id STRING, data STRING")
+                                    .addOption("changelog-mode", "I,UA,UB,D")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, "1", "retract1"),
+                                            Row.ofKind(RowKind.INSERT, "2", "retract2"),
+                                            Row.ofKind(RowKind.INSERT, "3", "retract3"),
+                                            Row.ofKind(RowKind.DELETE, "3", "retract3"),
+                                            Row.ofKind(RowKind.INSERT, "1", "retract1_new"))
+                                    .build())
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("UpsertTable")
+                                    .addSchema(
+                                            "key_id STRING PRIMARY KEY NOT ENFORCED, status STRING")
+                                    .addOption("changelog-mode", "I,UA,D")
+                                    .producedValues(
+                                            Row.ofKind(RowKind.INSERT, "1", "active"),
+                                            Row.ofKind(RowKind.INSERT, "2", "pending"),
+                                            Row.ofKind(RowKind.UPDATE_AFTER, "2", "active"),
+                                            Row.ofKind(RowKind.INSERT, "3", "inactive"),
+                                            Row.ofKind(RowKind.DELETE, "3", "inactive"))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink")
+                                    .addSchema(
+                                            "id STRING",
+                                            "val STRING",
+                                            "data STRING",
+                                            "status STRING")
+                                    .addOption("sink-changelog-mode-enforced", "I,UA,UB,D")
+                                    .consumedValues(
+                                            "+I[1, append1, retract1, active]",
+                                            "+I[2, append2, retract2, active]",
+                                            "+I[1, append1, retract1_new, active]",
+                                            "+I[3, append3, null, null]")
+                                    .testMaterializedData()
+                                    .build())
+                    .runSql(
+                            "INSERT INTO sink "
+                                    + "SELECT a.id, a.val, r.data, u.status "
+                                    + "FROM AppendTable a "
+                                    + "LEFT JOIN RetractTable r ON a.id = r.ref_id "
+                                    + "LEFT JOIN UpsertTable u ON a.id = u.key_id")
+                    .build();
 }
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json
index 0a9fd86b289..8a415d1da8c 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json
@@ -362,7 +362,7 @@
         "rightFieldIndex" : 1
       } ]
     },
-    "inputUpsertKeys" : [ [ [ 1 ] ], [ ], [ ], [ ] ],
+    "inputUniqueKeys" : [ [ [ 1 ] ], [ [ 0 ] ], [ [ 1 ] ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata
index b176b457713..061d0b08163 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json
index ecfa2f8e9f9..4b020388a61 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json
@@ -228,7 +228,7 @@
         "rightFieldIndex" : 0
       } ]
     },
-    "inputUpsertKeys" : [ [ [ 1 ] ], [ ], [ ] ],
+    "inputUniqueKeys" : [ [ [ 1 ] ], [ [ 0 ] ], [ [ 1 ] ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
@@ -344,7 +344,7 @@
         "rightFieldIndex" : 1
       } ]
     },
-    "inputUpsertKeys" : [ [ ], [ ] ],
+    "inputUniqueKeys" : [ [ ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata
index 5282c8c245b..f53211043d6 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json
index 1a5a9ee06c8..3b650218933 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json
@@ -188,7 +188,7 @@
         "rightFieldIndex" : 0
       } ]
     },
-    "inputUpsertKeys" : [ [ ], [ ], [ ] ],
+    "inputUniqueKeys" : [ [ ], [ ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata
index fd072e08e9c..377ec1862c7 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json
index 03324337ecc..3fabcaceda1 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json
@@ -348,7 +348,7 @@
         "rightFieldIndex" : 2
       } ]
     },
-    "inputUpsertKeys" : [ [ [ 0 ] ], [ ], [ ] ],
+    "inputUniqueKeys" : [ [ [ 0 ] ], [ [ 0 ] ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata
index c21172f3c7d..f11caa837fe 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json
index 72c40250564..5105a4410d9 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json
@@ -153,7 +153,7 @@
         "rightFieldIndex" : 0
       } ]
     },
-    "inputUpsertKeys" : [ [ ], [ ], [ ] ],
+    "inputUniqueKeys" : [ [ ], [ ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata
index e3228781a06..f0288325eee 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata differ


(flink) 05/05: [FLINK-38230][table-planner] Add visitMultiJoin to StreamNDUPlanVisitor

Posted by fp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a0976628c6c5e9380f223708920ab9614f77990a
Author: Gustavo de Morais <gd...@confluent.io>
AuthorDate: Wed Aug 13 08:00:27 2025 +0100

    [FLINK-38230][table-planner] Add visitMultiJoin to StreamNDUPlanVisitor
    
    This closes #26894.
---
 .../physical/stream/StreamPhysicalMultiJoin.java   |   76 +-
 .../StreamNonDeterministicUpdatePlanVisitor.java   |  100 +
 .../NonDeterministicUpdateAnalyzerTest.java        |  436 ++--
 .../nodes/exec/stream/MultiJoinSemanticTests.java  |   10 +
 .../nodes/exec/stream/MultiJoinTestPrograms.java   |   15 -
 .../nodes/exec/testutils/SemanticTestBase.java     |   10 +
 .../analyze/NonDeterministicUpdateAnalyzerTest.xml |   36 +
 .../planner/plan/stream/sql/MultiJoinTest.xml      | 2190 ++++----------------
 ...our-way-complex-updating-join-with-restore.json |  527 ++---
 .../savepoint/_metadata                            |  Bin 35375 -> 36360 bytes
 ...r-way-join-no-common-join-key-with-restore.json |  431 ++--
 .../savepoint/_metadata                            |  Bin 38180 -> 34081 bytes
 .../plan/three-way-inner-join-with-restore.json    |  244 +--
 .../savepoint/_metadata                            |  Bin 21331 -> 20195 bytes
 ...way-join-with-time-attributes-with-restore.json |   89 +-
 .../savepoint/_metadata                            |  Bin 23917 -> 25432 bytes
 .../three-way-left-outer-join-with-restore.json    |  231 ++-
 .../savepoint/_metadata                            |  Bin 21588 -> 21005 bytes
 18 files changed, 1689 insertions(+), 2706 deletions(-)

diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
index fb25cab268f..a4907e92d0c 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalMultiJoin.java
@@ -79,6 +79,10 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
     private final @Nullable RexNode postJoinFilter;
     private final List<RelHint> hints;
 
+    // Cached derived properties to avoid recomputation
+    private @Nullable RexNode multiJoinCondition;
+    private @Nullable List<List<int[]>> inputUniqueKeys;
+
     public StreamPhysicalMultiJoin(
             final RelOptCluster cluster,
             final RelTraitSet traitSet,
@@ -101,6 +105,8 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
         this.postJoinFilter = postJoinFilter;
         this.hints = hints;
         this.keyExtractor = keyExtractor;
+        this.multiJoinCondition = getMultiJoinCondition();
+        this.inputUniqueKeys = getUniqueKeysForInputs();
     }
 
     @Override
@@ -119,6 +125,9 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
         final List<RelNode> newInputs = new ArrayList<>(inputs);
         newInputs.set(ordinalInParent, p);
         this.inputs = List.copyOf(newInputs);
+        // Invalidate cached derived properties since inputs changed
+        this.multiJoinCondition = null;
+        this.inputUniqueKeys = null;
         recomputeDigest();
     }
 
@@ -166,8 +175,8 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
 
     @Override
     public ExecNode<?> translateToExecNode() {
-        final RexNode multiJoinCondition = createMultiJoinCondition();
-        final List<List<int[]>> inputUniqueKeys = getUniqueKeysForInputs();
+        final RexNode multijoinCondition = getMultiJoinCondition();
+        final List<List<int[]>> localInputUniqueKeys = getUniqueKeysForInputs();
         final List<FlinkJoinType> execJoinTypes = getExecJoinTypes();
         final List<InputProperty> inputProperties = createInputProperties();
 
@@ -175,9 +184,9 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
                 unwrapTableConfig(this),
                 execJoinTypes,
                 joinConditions,
-                multiJoinCondition,
+                multijoinCondition,
                 joinAttributeMap,
-                inputUniqueKeys,
+                localInputUniqueKeys,
                 Collections.emptyMap(), // TODO Enable hint-based state ttl. See ticket
                 // TODO https://issues.apache.org/jira/browse/FLINK-37936
                 inputProperties,
@@ -187,28 +196,43 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
 
     private RexNode createMultiJoinCondition() {
         final List<RexNode> conjunctions = new ArrayList<>();
+
+        for (RexNode joinCondition : joinConditions) {
+            if (joinCondition != null) {
+                conjunctions.add(joinCondition);
+            }
+        }
+
         conjunctions.add(joinFilter);
+
         if (postJoinFilter != null) {
             conjunctions.add(postJoinFilter);
         }
+
         return RexUtil.composeConjunction(getCluster().getRexBuilder(), conjunctions, true);
     }
 
-    private List<List<int[]>> getUniqueKeysForInputs() {
-        return inputs.stream()
-                .map(
-                        input -> {
-                            final Set<ImmutableBitSet> uniqueKeys = getUniqueKeys(input);
-
-                            if (uniqueKeys == null) {
-                                return Collections.<int[]>emptyList();
-                            }
-
-                            return uniqueKeys.stream()
-                                    .map(ImmutableBitSet::toArray)
-                                    .collect(Collectors.toList());
-                        })
-                .collect(Collectors.toList());
+    public List<List<int[]>> getUniqueKeysForInputs() {
+        if (inputUniqueKeys == null) {
+            final List<List<int[]>> computed =
+                    inputs.stream()
+                            .map(
+                                    input -> {
+                                        final Set<ImmutableBitSet> uniqueKeys =
+                                                getUniqueKeys(input);
+
+                                        if (uniqueKeys == null) {
+                                            return Collections.<int[]>emptyList();
+                                        }
+
+                                        return uniqueKeys.stream()
+                                                .map(ImmutableBitSet::toArray)
+                                                .collect(Collectors.toList());
+                                    })
+                            .collect(Collectors.toList());
+            inputUniqueKeys = Collections.unmodifiableList(computed);
+        }
+        return inputUniqueKeys;
     }
 
     private @Nullable Set<ImmutableBitSet> getUniqueKeys(RelNode input) {
@@ -217,6 +241,13 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
         return fmq.getUniqueKeys(input);
     }
 
+    public RexNode getMultiJoinCondition() {
+        if (multiJoinCondition == null) {
+            multiJoinCondition = createMultiJoinCondition();
+        }
+        return multiJoinCondition;
+    }
+
     private List<FlinkJoinType> getExecJoinTypes() {
         return joinTypes.stream()
                 .map(
@@ -256,8 +287,8 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
      */
     public boolean inputUniqueKeyContainsCommonJoinKey(int inputId) {
         final RelNode input = getInputs().get(inputId);
-        final Set<ImmutableBitSet> inputUniqueKeys = getUniqueKeys(input);
-        if (inputUniqueKeys == null || inputUniqueKeys.isEmpty()) {
+        final Set<ImmutableBitSet> inputUniqueKeysSet = getUniqueKeys(input);
+        if (inputUniqueKeysSet == null || inputUniqueKeysSet.isEmpty()) {
             return false;
         }
 
@@ -267,7 +298,8 @@ public class StreamPhysicalMultiJoin extends AbstractRelNode implements StreamPh
         }
 
         final ImmutableBitSet commonJoinKeys = ImmutableBitSet.of(commonJoinKeyIndices);
-        return inputUniqueKeys.stream().anyMatch(uniqueKey -> uniqueKey.contains(commonJoinKeys));
+        return inputUniqueKeysSet.stream()
+                .anyMatch(uniqueKey -> uniqueKey.contains(commonJoinKeys));
     }
 
     private List<InputProperty> createInputProperties() {
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
index b1f4d622027..875a088ec59 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/StreamNonDeterministicUpdatePlanVisitor.java
@@ -45,6 +45,7 @@ import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalL
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalLookupJoin;
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMatch;
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMiniBatchAssigner;
+import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMultiJoin;
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalOverAggregateBase;
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalRank;
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalRel;
@@ -77,6 +78,7 @@ import org.apache.flink.types.RowKind;
 
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexProgram;
@@ -180,6 +182,8 @@ public class StreamNonDeterministicUpdatePlanVisitor {
             return visitExpand((StreamPhysicalExpand) rel, requireDeterminism);
         } else if (rel instanceof CommonPhysicalJoin) {
             return visitJoin((CommonPhysicalJoin) rel, requireDeterminism);
+        } else if (rel instanceof StreamPhysicalMultiJoin) {
+            return visitMultiJoin((StreamPhysicalMultiJoin) rel, requireDeterminism);
         } else if (rel instanceof StreamPhysicalOverAggregateBase) {
             return visitOverAggregate((StreamPhysicalOverAggregateBase) rel, requireDeterminism);
         } else if (rel instanceof StreamPhysicalRank) {
@@ -621,6 +625,102 @@ public class StreamNonDeterministicUpdatePlanVisitor {
                         join.isSemiJoin());
     }
 
+    /**
+     * Multi-join determinism handling, mirroring the binary join logic:
+     *
+     * <p>If all inputs are insert-only and every join is INNER, the output is insert-only → no
+     * determinism required downstream.
+     *
+     * <p>Otherwise the combined join condition must be deterministic, and we propagate per-input
+     * determinism:
+     *
+     * <ul>
+     *   <li>If an input can produce updates, and we cannot guarantee uniqueness, we must require
+     *       determinism for the entire input row (retract-by-row correctness).
+     *   <li>If uniqueness is guaranteed, we pass through the part of the requirement that belongs
+     *       to that input.
+     * </ul>
+     */
+    private StreamPhysicalRel visitMultiJoin(
+            final StreamPhysicalMultiJoin multiJoin, final ImmutableBitSet requireDeterminism) {
+        final List<RelNode> inputs = multiJoin.getInputs();
+        final boolean allInputsInsertOnly =
+                inputs.stream().allMatch(in -> inputInsertOnly((StreamPhysicalRel) in));
+        final boolean allInner =
+                multiJoin.getJoinTypes().stream().allMatch(t -> t == JoinRelType.INNER);
+
+        // Fast path: pure insert-only inner join produces insert-only output -> nothing to require.
+        if (allInputsInsertOnly && allInner) {
+            return transmitDeterminismRequirement(multiJoin, NO_REQUIRED_DETERMINISM);
+        }
+
+        // Output may carry updates (some input updates or some non-inner join): condition must be
+        // deterministic.
+        final RexNode multiJoinCondition = multiJoin.getMultiJoinCondition();
+        if (multiJoinCondition != null) {
+            final Optional<String> ndCall =
+                    FlinkRexUtil.getNonDeterministicCallName(multiJoinCondition);
+            ndCall.ifPresent(
+                    s -> throwNonDeterministicConditionError(s, multiJoinCondition, multiJoin));
+        }
+
+        // Output may carry updates: we need to propagate determinism requirements to inputs.
+        final List<RelNode> newInputs = rewriteMultiJoinInputs(multiJoin, requireDeterminism);
+
+        return (StreamPhysicalRel) multiJoin.copy(multiJoin.getTraitSet(), newInputs);
+    }
+
+    private ImmutableBitSet projectToInput(
+            final ImmutableBitSet globalRequired, final int inputStart, final int inputFieldCount) {
+        final List<Integer> local =
+                globalRequired.toList().stream()
+                        .filter(idx -> idx >= inputStart && idx < inputStart + inputFieldCount)
+                        .map(idx -> idx - inputStart)
+                        .collect(Collectors.toList());
+        return ImmutableBitSet.of(local);
+    }
+
+    private ImmutableBitSet requiredForUpdatingMultiJoinInput(
+            final StreamPhysicalMultiJoin multiJoin,
+            final int inputIndex,
+            final ImmutableBitSet localRequired,
+            final int inputFieldCount) {
+        final List<int[]> uniqueKeys = multiJoin.getUniqueKeysForInputs().get(inputIndex);
+        final boolean hasUniqueKey = !uniqueKeys.isEmpty();
+
+        if (hasUniqueKey) {
+            return localRequired;
+        }
+        // Without uniqueness guarantees we must retract by entire row for correctness.
+        return ImmutableBitSet.range(inputFieldCount);
+    }
+
+    private List<RelNode> rewriteMultiJoinInputs(
+            final StreamPhysicalMultiJoin multiJoin, final ImmutableBitSet requireDeterminism) {
+        final List<RelNode> inputs = multiJoin.getInputs();
+        final List<RelNode> newInputs = new ArrayList<>(inputs.size());
+        int fieldStartOffset = 0;
+        for (int i = 0; i < inputs.size(); i++) {
+            final StreamPhysicalRel input = (StreamPhysicalRel) inputs.get(i);
+            final int inputFieldCount = input.getRowType().getFieldCount();
+
+            final ImmutableBitSet localRequired =
+                    projectToInput(requireDeterminism, fieldStartOffset, inputFieldCount);
+
+            final ImmutableBitSet inputRequired =
+                    inputInsertOnly(input)
+                            ? NO_REQUIRED_DETERMINISM
+                            : requiredForUpdatingMultiJoinInput(
+                                    multiJoin, i, localRequired, inputFieldCount);
+
+            final ImmutableBitSet finalRequired =
+                    requireDeterminismExcludeUpsertKey(input, inputRequired);
+            newInputs.add(visit(input, finalRequired));
+            fieldStartOffset += inputFieldCount;
+        }
+        return newInputs;
+    }
+
     private StreamPhysicalRel visitOverAggregate(
             final StreamPhysicalOverAggregateBase overAgg,
             final ImmutableBitSet requireDeterminism) {
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.java
index 045130d0814..99cc20b60da 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.java
@@ -21,6 +21,7 @@ package org.apache.flink.table.planner.analyze;
 import org.apache.flink.table.api.ExplainDetail;
 import org.apache.flink.table.api.StatementSet;
 import org.apache.flink.table.api.TableConfig;
+import org.apache.flink.table.api.TableEnvironment;
 import org.apache.flink.table.api.TableException;
 import org.apache.flink.table.api.config.OptimizerConfigOptions;
 import org.apache.flink.table.api.config.OptimizerConfigOptions.NonDeterministicUpdateStrategy;
@@ -34,6 +35,7 @@ import org.junit.jupiter.api.Test;
 
 import scala.Enumeration;
 
+import static org.apache.flink.table.api.config.OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static scala.runtime.BoxedUnit.UNIT;
@@ -41,6 +43,7 @@ import static scala.runtime.BoxedUnit.UNIT;
 /** Test for {@link NonDeterministicUpdateAnalyzer}. */
 class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
 
+    private TableEnvironment tEnv;
     private final StreamTableTestUtil util = streamTestUtil(TableConfig.getDefault());
 
     private static final String expectedOverAggNduErrorMsg =
@@ -52,67 +55,63 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
 
     @BeforeEach
     void before() {
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table cdc (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d bigint,\n"
-                                + "  primary key (a) not enforced\n"
-                                + ") with (\n"
-                                + "  'connector' = 'values',\n"
-                                + "  'changelog-mode' = 'I,UA,UB,D'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table cdc_with_meta (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d boolean,\n"
-                                + "  metadata_1 int metadata,\n"
-                                + "  metadata_2 string metadata,\n"
-                                + "  metadata_3 bigint metadata,\n"
-                                + "  primary key (a) not enforced\n"
-                                + ") with (\n"
-                                + "  'connector' = 'values',\n"
-                                + "  'changelog-mode' = 'I,UA,UB,D',\n"
-                                + "  'readable-metadata' = 'metadata_1:INT, metadata_2:STRING, metadata_3:BIGINT'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table sink_with_pk (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  primary key (a) not enforced\n"
-                                + ") with (\n"
-                                + "  'connector' = 'values',\n"
-                                + "  'sink-insert-only' = 'false'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table sink_without_pk (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string\n"
-                                + ") with (\n"
-                                + "  'connector' = 'values',\n"
-                                + "  'sink-insert-only' = 'false'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table dim_with_pk (\n"
-                                + " a int,\n"
-                                + " b bigint,\n"
-                                + " c string,\n"
-                                + " primary key (a) not enforced\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values'\n"
-                                + ")");
+        tEnv = util.getTableEnv();
+        tEnv.executeSql(
+                "create temporary table cdc (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d bigint,\n"
+                        + "  primary key (a) not enforced\n"
+                        + ") with (\n"
+                        + "  'connector' = 'values',\n"
+                        + "  'changelog-mode' = 'I,UA,UB,D'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table cdc_with_meta (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d boolean,\n"
+                        + "  metadata_1 int metadata,\n"
+                        + "  metadata_2 string metadata,\n"
+                        + "  metadata_3 bigint metadata,\n"
+                        + "  primary key (a) not enforced\n"
+                        + ") with (\n"
+                        + "  'connector' = 'values',\n"
+                        + "  'changelog-mode' = 'I,UA,UB,D',\n"
+                        + "  'readable-metadata' = 'metadata_1:INT, metadata_2:STRING, metadata_3:BIGINT'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink_with_pk (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  primary key (a) not enforced\n"
+                        + ") with (\n"
+                        + "  'connector' = 'values',\n"
+                        + "  'sink-insert-only' = 'false'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink_without_pk (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string\n"
+                        + ") with (\n"
+                        + "  'connector' = 'values',\n"
+                        + "  'sink-insert-only' = 'false'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table dim_with_pk (\n"
+                        + " a int,\n"
+                        + " b bigint,\n"
+                        + " c string,\n"
+                        + " primary key (a) not enforced\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values'\n"
+                        + ")");
         // custom ND function
-        util.getTableEnv().createTemporaryFunction("ndFunc", new TestNonDeterministicUdf());
+        tEnv.createTemporaryFunction("ndFunc", new TestNonDeterministicUdf());
 
         String sourceTable =
                 "CREATE TABLE source_t(\n"
@@ -123,7 +122,7 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
                         + "  'connector' = 'values',\n"
                         + "  'bounded' = 'false',\n"
                         + "  'changelog-mode' = 'I,UB,UA')";
-        util.getTableEnv().executeSql(sourceTable);
+        tEnv.executeSql(sourceTable);
 
         String sinkTable =
                 "CREATE TABLE sink_t(\n"
@@ -136,39 +135,37 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
                         + "  'bounded' = 'false',\n"
                         + "  'sink-insert-only' = 'false',\n"
                         + "  'changelog-mode' = 'I,UB,UA')";
-        util.getTableEnv().executeSql(sinkTable);
+        tEnv.executeSql(sinkTable);
     }
 
     @Test
     void testCdcWithMetaRenameSinkWithCompositePk() {
         // from NonDeterministicDagTest#testCdcWithMetaRenameSinkWithCompositePk
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table cdc_with_meta_rename (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d boolean,\n"
-                                + "  metadata_3 bigint metadata,\n"
-                                + "  e as metadata_3,\n"
-                                + "  primary key (a) not enforced\n"
-                                + ") with (\n"
-                                + "  'connector' = 'values',\n"
-                                + "  'changelog-mode' = 'I,UA,UB,D',\n"
-                                + "  'readable-metadata' = 'metadata_3:BIGINT'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table sink_with_composite_pk (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d bigint,\n"
-                                + "  primary key (a,d) not enforced\n"
-                                + ") with (\n"
-                                + "  'connector' = 'values',\n"
-                                + "  'sink-insert-only' = 'false'\n"
-                                + ")");
+        tEnv.executeSql(
+                "create temporary table cdc_with_meta_rename (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d boolean,\n"
+                        + "  metadata_3 bigint metadata,\n"
+                        + "  e as metadata_3,\n"
+                        + "  primary key (a) not enforced\n"
+                        + ") with (\n"
+                        + "  'connector' = 'values',\n"
+                        + "  'changelog-mode' = 'I,UA,UB,D',\n"
+                        + "  'readable-metadata' = 'metadata_3:BIGINT'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink_with_composite_pk (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d bigint,\n"
+                        + "  primary key (a,d) not enforced\n"
+                        + ") with (\n"
+                        + "  'connector' = 'values',\n"
+                        + "  'sink-insert-only' = 'false'\n"
+                        + ")");
         util.doVerifyPlanInsert(
                 "insert into sink_with_composite_pk\n"
                         + "select a, b, c, e from cdc_with_meta_rename",
@@ -180,20 +177,19 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
     @Test
     void testCdcSourceWithoutPkSinkWithoutPk() {
         // from NonDeterministicDagTest#testCdcSourceWithoutPkSinkWithoutPk
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table cdc_without_pk (\n"
-                                + " a int,\n"
-                                + " b bigint,\n"
-                                + " c string,\n"
-                                + " d boolean,\n"
-                                + " metadata_1 int metadata,\n"
-                                + " metadata_2 string metadata\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values',\n"
-                                + " 'changelog-mode' = 'I,UA,UB,D',\n"
-                                + " 'readable-metadata' = 'metadata_1:INT, metadata_2:STRING'\n"
-                                + ")");
+        tEnv.executeSql(
+                "create temporary table cdc_without_pk (\n"
+                        + " a int,\n"
+                        + " b bigint,\n"
+                        + " c string,\n"
+                        + " d boolean,\n"
+                        + " metadata_1 int metadata,\n"
+                        + " metadata_2 string metadata\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'changelog-mode' = 'I,UA,UB,D',\n"
+                        + " 'readable-metadata' = 'metadata_1:INT, metadata_2:STRING'\n"
+                        + ")");
 
         util.doVerifyPlanInsert(
                 "insert into sink_without_pk\n"
@@ -207,19 +203,18 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
     @Test
     void testSourceWithComputedColumnSinkWithPk() {
         // from NonDeterministicDagTest#testSourceWithComputedColumnSinkWithPk
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table cdc_with_computed_col (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d int,\n"
-                                + "  `day` as DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd'),\n"
-                                + "  primary key(a, c) not enforced\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values',\n"
-                                + " 'changelog-mode' = 'I,UA,UB,D'\n"
-                                + ")");
+        tEnv.executeSql(
+                "create temporary table cdc_with_computed_col (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d int,\n"
+                        + "  `day` as DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd'),\n"
+                        + "  primary key(a, c) not enforced\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                        + ")");
         util.doVerifyPlanInsert(
                 "insert into sink_with_pk\n"
                         + "select a, b, `day`\n"
@@ -274,68 +269,63 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
     @Test
     void testMultiSinkOnJoinedView() {
         // from NonDeterministicDagTest#testMultiSinkOnJoinedView
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table src1 (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d int,\n"
-                                + "  primary key(a, c) not enforced\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values',\n"
-                                + " 'changelog-mode' = 'I,UA,UB,D'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table src2 (\n"
-                                + "  a int,\n"
-                                + "  b bigint,\n"
-                                + "  c string,\n"
-                                + "  d int,\n"
-                                + "  primary key(a, c) not enforced\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values',\n"
-                                + " 'changelog-mode' = 'I,UA,UB,D'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table sink1 (\n"
-                                + "  a int,\n"
-                                + "  b string,\n"
-                                + "  c bigint,\n"
-                                + "  d bigint\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values',\n"
-                                + " 'sink-insert-only' = 'false'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary table sink2 (\n"
-                                + "  a int,\n"
-                                + "  b string,\n"
-                                + "  c bigint,\n"
-                                + "  d string\n"
-                                + ") with (\n"
-                                + " 'connector' = 'values',\n"
-                                + " 'sink-insert-only' = 'false'\n"
-                                + ")");
-        util.getTableEnv()
-                .executeSql(
-                        "create temporary view v1 as\n"
-                                + "select\n"
-                                + "  t1.a as a, t1.`day` as `day`, t2.b as b, t2.c as c\n"
-                                + "from (\n"
-                                + "  select a, b, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd') as `day`\n"
-                                + "  from src1\n"
-                                + " ) t1\n"
-                                + "join (\n"
-                                + "  select b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd')) as `day`, c, d\n"
-                                + "  from src2\n"
-                                + ") t2\n"
-                                + " on t1.a = t2.d");
-
-        StatementSet stmtSet = util.getTableEnv().createStatementSet();
+        tEnv.executeSql(
+                "create temporary table src1 (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d int,\n"
+                        + "  primary key(a, c) not enforced\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table src2 (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d int,\n"
+                        + "  primary key(a, c) not enforced\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink1 (\n"
+                        + "  a int,\n"
+                        + "  b string,\n"
+                        + "  c bigint,\n"
+                        + "  d bigint\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'sink-insert-only' = 'false'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink2 (\n"
+                        + "  a int,\n"
+                        + "  b string,\n"
+                        + "  c bigint,\n"
+                        + "  d string\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'sink-insert-only' = 'false'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary view v1 as\n"
+                        + "select\n"
+                        + "  t1.a as a, t1.`day` as `day`, t2.b as b, t2.c as c\n"
+                        + "from (\n"
+                        + "  select a, b, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd') as `day`\n"
+                        + "  from src1\n"
+                        + " ) t1\n"
+                        + "join (\n"
+                        + "  select b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd')) as `day`, c, d\n"
+                        + "  from src2\n"
+                        + ") t2\n"
+                        + " on t1.a = t2.d");
+
+        StatementSet stmtSet = tEnv.createStatementSet();
         stmtSet.addInsertSql(
                 "insert into sink1\n"
                         + "  select a, `day`, sum(b), count(distinct c)\n"
@@ -356,6 +346,87 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
                 false);
     }
 
+    @Test
+    void testMultiSinkOnMultiJoinedView() {
+        tEnv.getConfig().set(TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true);
+        tEnv.executeSql(
+                "create temporary table src1 (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d int,\n"
+                        + "  primary key(a, c) not enforced\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table src2 (\n"
+                        + "  a int,\n"
+                        + "  b bigint,\n"
+                        + "  c string,\n"
+                        + "  d int,\n"
+                        + "  primary key(a, c) not enforced\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink1 (\n"
+                        + "  a int,\n"
+                        + "  b string,\n"
+                        + "  c bigint,\n"
+                        + "  d bigint\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'sink-insert-only' = 'false'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary table sink2 (\n"
+                        + "  a int,\n"
+                        + "  b string,\n"
+                        + "  c bigint,\n"
+                        + "  d string\n"
+                        + ") with (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'sink-insert-only' = 'false'\n"
+                        + ")");
+        tEnv.executeSql(
+                "create temporary view v1 as\n"
+                        + "select\n"
+                        + "  t1.a as a, t1.`day` as `day`, t2.b as b, t2.c as c\n"
+                        + "from (\n"
+                        + "  select a, b, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd') as `day`\n"
+                        + "  from src1\n"
+                        + " ) t1\n"
+                        + "join (\n"
+                        + "  select b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd')) as `day`, c, d\n"
+                        + "  from src2\n"
+                        + ") t2\n"
+                        + " on t1.a = t2.d");
+
+        StatementSet stmtSet = tEnv.createStatementSet();
+        stmtSet.addInsertSql(
+                "insert into sink1\n"
+                        + "  select a, `day`, sum(b), count(distinct c)\n"
+                        + "  from v1\n"
+                        + "  group by a, `day`");
+        stmtSet.addInsertSql(
+                "insert into sink2\n"
+                        + "  select a, `day`, b, c\n"
+                        + "  from v1\n"
+                        + "  where b > 100");
+
+        util.doVerifyPlan(
+                stmtSet,
+                new ExplainDetail[] {ExplainDetail.PLAN_ADVICE},
+                false,
+                new Enumeration.Value[] {PlanKind.OPT_REL_WITH_ADVICE()},
+                () -> UNIT,
+                false,
+                false);
+    }
+
     @Test
     void testCdcJoinDimWithPkOutputNoPkSinkWithoutPk() {
         // from NonDeterministicDagTest#testCdcJoinDimWithPkOutputNoPkSinkWithoutPk
@@ -373,8 +444,7 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
 
     @Test
     void testOverAggregateWithNonDeterminismInPartitionBy() {
-        util.getTableEnv()
-                .getConfig()
+        tEnv.getConfig()
                 .set(
                         OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
                         NonDeterministicUpdateStrategy.TRY_RESOLVE);
@@ -395,8 +465,7 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
 
     @Test
     void testOverAggregateWithNonDeterminismInOrderBy() {
-        util.getTableEnv()
-                .getConfig()
+        tEnv.getConfig()
                 .set(
                         OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
                         NonDeterministicUpdateStrategy.TRY_RESOLVE);
@@ -417,8 +486,7 @@ class NonDeterministicUpdateAnalyzerTest extends TableTestBase {
 
     @Test
     void testOverAggregateWithNonDeterminismInProjection() {
-        util.getTableEnv()
-                .getConfig()
+        tEnv.getConfig()
                 .set(
                         OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
                         NonDeterministicUpdateStrategy.TRY_RESOLVE);
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
index fb24e92e1d7..d9f371bcd47 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
@@ -18,6 +18,8 @@
 
 package org.apache.flink.table.planner.plan.nodes.exec.stream;
 
+import org.apache.flink.table.api.TableConfig;
+import org.apache.flink.table.api.config.OptimizerConfigOptions;
 import org.apache.flink.table.planner.plan.nodes.exec.testutils.SemanticTestBase;
 import org.apache.flink.table.test.program.TableTestProgram;
 
@@ -26,6 +28,14 @@ import java.util.List;
 /** Semantic tests for {@link StreamExecMultiJoin}. */
 public class MultiJoinSemanticTests extends SemanticTestBase {
 
+    @Override
+    protected void applyDefaultEnvironmentOptions(TableConfig config) {
+        config.set(
+                        OptimizerConfigOptions.TABLE_OPTIMIZER_NONDETERMINISTIC_UPDATE_STRATEGY,
+                        OptimizerConfigOptions.NonDeterministicUpdateStrategy.TRY_RESOLVE)
+                .set(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true);
+    }
+
     @Override
     public List<TableTestProgram> programs() {
         return List.of(
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
index 3e8e27e9c30..2137a1e5642 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
@@ -36,7 +36,6 @@ import static org.apache.flink.table.planner.plan.nodes.exec.stream.MultiJoinTes
 public class MultiJoinTestPrograms {
     public static final TableTestProgram MULTI_JOIN_THREE_WAY_LEFT_OUTER_JOIN =
             TableTestProgram.of("three-way-left-outer-join", "three way left outer join")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(USERS_SOURCE)
                     .setupTableSource(ORDERS_SOURCE)
                     .setupTableSource(PAYMENTS_SOURCE)
@@ -67,7 +66,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-left-outer-join-updating",
                             "three way left outer join updating")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
                                     .addSchema(
@@ -110,7 +108,6 @@ public class MultiJoinTestPrograms {
 
     public static final TableTestProgram MULTI_JOIN_THREE_WAY_INNER_JOIN =
             TableTestProgram.of("three-way-inner-join", "three way inner join")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(USERS_SOURCE)
                     .setupTableSource(ORDERS_SOURCE)
                     .setupTableSource(PAYMENTS_SOURCE)
@@ -140,7 +137,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-inner-join-with-where",
                             "three way inner join with where clause")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(USERS_SOURCE)
                     .setupTableSource(ORDERS_SOURCE)
                     .setupTableSource(PAYMENTS_SOURCE)
@@ -169,7 +165,6 @@ public class MultiJoinTestPrograms {
 
     public static final TableTestProgram MULTI_JOIN_FOUR_WAY_COMPLEX =
             TableTestProgram.of("four-way-complex-updating-join", "four way complex updating join")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
                                     .addSchema(
@@ -281,7 +276,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-left-outer-join-with-restore",
                             "three way left outer join with restore")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
                                     .addSchema("user_id STRING", "name STRING")
@@ -335,7 +329,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-inner-join-with-restore",
                             "three way inner join with restore")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
                                     .addSchema("user_id STRING", "name STRING")
@@ -388,7 +381,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-inner-join-no-join-key",
                             "three way inner join with no join key")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(USERS_SOURCE)
                     .setupTableSource(ORDERS_SOURCE)
                     .setupTableSource(PAYMENTS_SOURCE)
@@ -462,7 +454,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "four-way-join-no-common-join-key-with-restore",
                             "four way join no common join key with restore")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupConfig(TableConfigOptions.PLAN_FORCE_RECOMPILE, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
@@ -572,7 +563,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "four-way-no-common-join-key-updating-join",
                             "four way no common join key updating join")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
                                     .addSchema(
@@ -675,7 +665,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "four-way-complex-updating-join-with-restore",
                             "four way complex updating join with restore")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupConfig(TableConfigOptions.PLAN_FORCE_RECOMPILE, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Users")
@@ -784,7 +773,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-join-with-time-attributes",
                             "three way join with time attributes materialization")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(
                             SourceTestStep.newBuilder("UsersWithProctime")
                                     .addSchema(
@@ -912,7 +900,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "left-outer-join-with-cte",
                             "CTE with three-way left outer join and aggregation")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupTableSource(USERS_SOURCE)
                     .setupTableSource(
                             SourceTestStep.newBuilder("Orders")
@@ -976,7 +963,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "three-way-left-outer-with-null-keys",
                             "left outer join with NULL keys on multiple inputs")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupSql(
                             "CREATE VIEW UsersNulls AS SELECT * FROM (VALUES "
                                     + "('1','Gus'),"
@@ -1018,7 +1004,6 @@ public class MultiJoinTestPrograms {
             TableTestProgram.of(
                             "null-safe-join-with-null-keys",
                             "join with IS NOT DISTINCT FROM to match NULL keys")
-                    .setupConfig(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true)
                     .setupSql(
                             "CREATE VIEW UsersNullSafe AS SELECT * FROM (VALUES "
                                     + "('1','Gus'),"
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/SemanticTestBase.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/SemanticTestBase.java
index ac3b0a13939..5e72649ac7e 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/SemanticTestBase.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/testutils/SemanticTestBase.java
@@ -19,6 +19,7 @@
 package org.apache.flink.table.planner.plan.nodes.exec.testutils;
 
 import org.apache.flink.table.api.EnvironmentSettings;
+import org.apache.flink.table.api.TableConfig;
 import org.apache.flink.table.api.TableEnvironment;
 import org.apache.flink.table.planner.factories.TestValuesTableFactory;
 import org.apache.flink.table.test.program.ConfigOptionTestStep;
@@ -81,6 +82,8 @@ public abstract class SemanticTestBase implements TableTestProgramRunner {
     void runSteps(TableTestProgram program) throws Exception {
         final TableEnvironment env = TableEnvironment.create(EnvironmentSettings.inStreamingMode());
 
+        applyDefaultEnvironmentOptions(env.getConfig());
+
         for (TestStep testStep : program.setupSteps) {
             runStep(testStep, env);
         }
@@ -149,6 +152,13 @@ public abstract class SemanticTestBase implements TableTestProgramRunner {
         }
     }
 
+    /**
+     * Hook for subclasses to apply suite-wide default table configuration options.
+     *
+     * <p>Default implementation is a no-op. Subclasses can override to set specific options.
+     */
+    protected void applyDefaultEnvironmentOptions(TableConfig config) {}
+
     private static Map<String, String> createSourceOptions(
             SourceTestStep sourceTestStep, String id) {
         final Map<String, String> options = new HashMap<>();
diff --git a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.xml b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.xml
index dc45a0b5c9d..7ee2cc9a66e 100644
--- a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.xml
+++ b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/analyze/NonDeterministicUpdateAnalyzerTest.xml
@@ -203,6 +203,42 @@ Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), _UTF-16LE'yyMMdd') AS day], cha
 +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a], changelogMode=[I,UB,UA,D])
 
 
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testMultiSinkOnMultiJoinedView">
+    <Resource name="optimized rel plan with advice">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.sink1], fields=[a, day, EXPR$2, EXPR$3])
++- GroupAggregate(advice=[1], groupBy=[a, day], select=[a, day, SUM_RETRACT(b) AS EXPR$2, COUNT_RETRACT(DISTINCT c) AS EXPR$3])
+   +- Exchange(distribution=[hash[a, day]])
+      +- Calc(select=[a, day, b0 AS b, c])
+         +- MultiJoin(joinFilter=[=($0, $6)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $6)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:3;]}], select=[a,b,day,b0,day0,c,d], rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) day0, VARCHAR(2147483647) c, INTEGER d)])
+            :- Exchange(distribution=[hash[a]])
+            :  +- Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
+            :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a, b], metadata=[]]], fields=[a, b])
+            +- Exchange(distribution=[hash[d]])
+               +- Calc(select=[b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd')) AS day, c, d])
+                  +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
+
+Sink(table=[default_catalog.default_database.sink2], fields=[a, day, b, c])
++- Calc(select=[a, day, b0 AS b, c])
+   +- MultiJoin(joinFilter=[=($0, $6)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $6)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:3;]}], select=[a,b,day,b0,day0,c,d], rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) day0, VARCHAR(2147483647) c, INTEGER d)])
+      :- Exchange(distribution=[hash[a]])
+      :  +- Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
+      :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a, b], metadata=[]]], fields=[a, b])
+      +- Exchange(distribution=[hash[d]])
+         +- Calc(select=[b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd')) AS day, c, d], where=[>(b, 100)])
+            +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
+
+advice[1]: [ADVICE] You might want to enable local-global two-phase optimization by configuring ('table.exec.mini-batch.enabled' to 'true', 'table.exec.mini-batch.allow-latency' to a positive long value, 'table.exec.mini-batch.size' to a positive long value).
+advice[2]: [WARNING] The column(s): day(generated by non-deterministic function: CURRENT_TIMESTAMP ) can not satisfy the determinism requirement for correctly processing update message('UB'/'UA'/'D' in changelogMode, not 'I' only), this usually happens when input node has no upsertKey(upsertKeys=[{}]) or current node outputs non-deterministic update messages. Please consider removing these non-deterministic columns or making them deterministic by using deterministic functions.
+
+related rel plan:
+Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), _UTF-16LE'yyMMdd') AS day], changelogMode=[I,UB,UA,D])
++- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a, b], metadata=[]]], fields=[a, b], changelogMode=[I,UB,UA,D])
+
+
 ]]>
     </Resource>
   </TestCase>
diff --git a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
index 6c3a455d789..56bf1998204 100644
--- a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
+++ b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
@@ -17,201 +17,78 @@ limitations under the License.
 -->
 <Root>
   <TestCase name="testAggregationAndGroupingWithMultiJoinNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    pc.category_name,
-    COUNT(DISTINCT o.order_id) as order_count,
-    SUM(oi.quantity) as total_items,
-    SUM(oi.quantity * oi.unit_price) as total_value,
-    AVG(oi.unit_price) as avg_item_price,
-    MAX(p.price) as max_payment,
-    MIN(p.price) as min_payment,
-    COUNT(CASE WHEN oi.quantity > 5 THEN 1 END) as bulk_orders
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN OrderItems oi
-    ON o.order_id = oi.order_id
-LEFT JOIN ProductCategories pc
-    ON oi.product_name = pc.category_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-GROUP BY u.user_id, u.name, pc.category_name
-HAVING COUNT(DISTINCT o.order_id) > 0]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalFilter(condition=[>($3, 0)])
-+- LogicalAggregate(group=[{0, 1, 2}], order_count=[COUNT(DISTINCT $3)], total_items=[SUM($4)], total_value=[SUM($5)], avg_item_price=[AVG($6)], max_payment=[MAX($7)], min_payment=[MIN($7)], bulk_orders=[COUNT($8)])
-   +- LogicalProject(user_id=[$0], name=[$1], category_name=[$12], order_id=[$3], quantity=[$9], $f5=[*($9, $10)], unit_price=[$10], price=[$15], $f8=[CASE(>($9, 5), 1, null:INTEGER)])
-      +- LogicalJoin(condition=[=($0, $16)], joinType=[left])
-         :- LogicalJoin(condition=[=($8, $11)], joinType=[left])
-         :  :- LogicalJoin(condition=[=($3, $7)], joinType=[left])
-         :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-         :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-         :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-         :  :  +- LogicalTableScan(table=[[default_catalog, default_database, OrderItems]])
-         :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductCategories]])
-         +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, category_name, order_count, total_items, total_value, avg_item_price, max_payment, min_payment, bulk_orders], where=[>(order_count, 0)])
 +- GroupAggregate(groupBy=[user_id, name, category_name], select=[user_id, name, category_name, COUNT_RETRACT(DISTINCT order_id) AS order_count, SUM_RETRACT(quantity) AS total_items, SUM_RETRACT($f5) AS total_value, AVG_RETRACT(unit_price) AS avg_item_price, MAX_RETRACT(price) AS max_payment, MIN_RETRACT(price) AS min_payment, COUNT_RETRACT($f8) AS bulk_orders])
    +- Exchange(distribution=[hash[user_id, name, category_name]])
       +- Calc(select=[user_id, name, category_name, order_id, quantity, *(quantity, unit_price) AS $f5, unit_price, price, CASE(>(quantity, 5), 1, null:INTEGER) AS $f8])
-         +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id1)], select=[user_id,name,order_id,quantity,unit_price,category_name,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, INTEGER quantity, DOUBLE unit_price, VARCHAR(2147483647) category_name, INTEGER price, VARCHAR(2147483647) user_id1)])
+         +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($8, $11), =($0, $16)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:8;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:8;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,item_id,order_id [...]
             :- Exchange(distribution=[hash[user_id]])
-            :  +- Calc(select=[user_id, name, order_id, quantity, unit_price, category_name])
-            :     +- MultiJoin(commonJoinKey=[product_name], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (category_id)], joinConditions=[=(product_name, category_id)], select=[user_id,name,order_id,product_name,quantity,unit_price,category_id,category_name], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product_name, INTEGER quantity, DOUBLE unit_price, VARCHAR(2147483647) category_id, VARCHAR(2147483647)  [...]
-            :        :- Exchange(distribution=[hash[product_name]])
-            :        :  +- Calc(select=[user_id, name, order_id, product_name, quantity, unit_price])
-            :        :     +- MultiJoin(commonJoinKey=[order_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(order_id, order_id0)], select=[user_id,name,order_id,order_id0,product_name,quantity,unit_price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) order_id0, VARCHAR(2147483647) product_name, INTEGER quantity, DOUBLE unit_price)])
-            :        :        :- Exchange(distribution=[hash[order_id]])
-            :        :        :  +- Calc(select=[user_id, name, order_id])
-            :        :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
-            :        :        :        :- Exchange(distribution=[hash[user_id]])
-            :        :        :        :  +- ChangelogNormalize(key=[user_id])
-            :        :        :        :     +- Exchange(distribution=[hash[user_id]])
-            :        :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
-            :        :        :        +- Exchange(distribution=[hash[user_id]])
-            :        :        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
-            :        :        +- Exchange(distribution=[hash[order_id]])
-            :        :           +- Calc(select=[order_id, product_name, quantity, unit_price])
-            :        :              +- ChangelogNormalize(key=[item_id])
-            :        :                 +- Exchange(distribution=[hash[item_id]])
-            :        :                    +- TableSourceScan(table=[[default_catalog, default_database, OrderItems]], fields=[item_id, order_id, product_name, quantity, unit_price])
-            :        +- Exchange(distribution=[hash[category_id]])
-            :           +- ChangelogNormalize(key=[category_id])
-            :              +- Exchange(distribution=[hash[category_id]])
-            :                 +- TableSourceScan(table=[[default_catalog, default_database, ProductCategories, project=[category_id, category_name], metadata=[]]], fields=[category_id, category_name])
+            :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($3, $7)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:3;], 1=[LeftInputId:0;LeftFieldIndex:3;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product,item_id,order_id0,product_name,quantity,unit_price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order [...]
+            :     :- Exchange(distribution=[hash[order_id]])
+            :     :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($0, $4)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(21 [...]
+            :     :     :- Exchange(distribution=[hash[user_id]])
+            :     :     :  +- ChangelogNormalize(key=[user_id])
+            :     :     :     +- Exchange(distribution=[hash[user_id]])
+            :     :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+            :     :     +- Exchange(distribution=[hash[user_id]])
+            :     :        +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+            :     +- Exchange(distribution=[hash[order_id]])
+            :        +- ChangelogNormalize(key=[item_id])
+            :           +- Exchange(distribution=[hash[item_id]])
+            :              +- TableSourceScan(table=[[default_catalog, default_database, OrderItems]], fields=[item_id, order_id, product_name, quantity, unit_price])
+            :- Exchange(distribution=[hash[category_id]])
+            :  +- ChangelogNormalize(key=[category_id])
+            :     +- Exchange(distribution=[hash[category_id]])
+            :        +- TableSourceScan(table=[[default_catalog, default_database, ProductCategories]], fields=[category_id, category_name, parent_category])
             +- Exchange(distribution=[hash[user_id]])
-               +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
+               +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testAggregationAndGroupingWithMultiJoinV2">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    c.category_name,
-    COUNT(DISTINCT u.user_id) AS unique_users,
-    COUNT(s.sale_id) AS total_sales,
-    SUM(s.amount) AS total_revenue,
-    AVG(s.amount) AS avg_sale_amount,
-    MAX(s.amount) AS max_sale_amount
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Categories c
-    ON u.user_id = c.user_id AND o.product = c.category_id
-LEFT JOIN Sales s
-    ON u.user_id = s.user_id
-GROUP BY c.category_name
-HAVING COUNT(s.sale_id) > 0]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalFilter(condition=[>($2, 0)])
-+- LogicalAggregate(group=[{0}], unique_users=[COUNT(DISTINCT $1)], total_sales=[COUNT($2)], total_revenue=[SUM($3)], avg_sale_amount=[AVG($3)], max_sale_amount=[MAX($3)])
-   +- LogicalProject(category_name=[$7], user_id=[$0], sale_id=[$10], amount=[$13])
-      +- LogicalJoin(condition=[=($0, $11)], joinType=[left])
-         :- LogicalJoin(condition=[AND(=($0, $9), =($5, $6))], joinType=[left])
-         :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-         :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-         :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-         :  +- LogicalTableScan(table=[[default_catalog, default_database, Categories]])
-         +- LogicalTableScan(table=[[default_catalog, default_database, Sales]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[category_name, unique_users, total_sales, total_revenue, avg_sale_amount, max_sale_amount], where=[>(total_sales, 0)])
 +- GroupAggregate(groupBy=[category_name], select=[category_name, COUNT_RETRACT(DISTINCT user_id) AS unique_users, COUNT_RETRACT(sale_id) AS total_sales, SUM_RETRACT(amount) AS total_revenue, AVG_RETRACT(amount) AS avg_sale_amount, MAX_RETRACT(amount) AS max_sale_amount])
    +- Exchange(distribution=[hash[category_name]])
       +- Calc(select=[category_name, user_id, sale_id, amount])
-         +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), noUniqueKey, (category_id), (sale_id)], joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), =(product, category_id)), =(user_id, user_id2)], select=[user_id,user_id0,product,category_id,category_name,user_id1,sale_id,user_id2,amount], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, VARCHAR(2147483647) category_id, [...]
+         +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $9), =($5, $6)), =($0, $11)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightI [...]
             :- Exchange(distribution=[hash[user_id]])
             :  +- ChangelogNormalize(key=[user_id])
             :     +- Exchange(distribution=[hash[user_id]])
-            :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id], metadata=[]]], fields=[user_id])
+            :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
             :- Exchange(distribution=[hash[user_id]])
-            :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[user_id, product], metadata=[]]], fields=[user_id, product])
+            :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
             :- Exchange(distribution=[hash[user_id]])
             :  +- ChangelogNormalize(key=[category_id])
             :     +- Exchange(distribution=[hash[category_id]])
-            :        +- TableSourceScan(table=[[default_catalog, default_database, Categories, project=[category_id, category_name, user_id], metadata=[]]], fields=[category_id, category_name, user_id])
+            :        +- TableSourceScan(table=[[default_catalog, default_database, Categories]], fields=[category_id, category_name, parent_category, user_id])
             +- Exchange(distribution=[hash[user_id]])
                +- ChangelogNormalize(key=[sale_id])
                   +- Exchange(distribution=[hash[sale_id]])
-                     +- TableSourceScan(table=[[default_catalog, default_database, Sales, project=[sale_id, user_id, amount], metadata=[]]], fields=[sale_id, user_id, amount])
+                     +- TableSourceScan(table=[[default_catalog, default_database, Sales]], fields=[sale_id, user_id, product_id, amount, sale_date])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testCTEWithMultiJoinV2">
-    <Resource name="sql">
-      <![CDATA[
-WITH high_budget_depts AS (
-    SELECT dept_id, dept_name, budget
-    FROM Departments
-    WHERE budget > 600000
-),
-active_projects AS (
-    SELECT project_id, project_name, dept_id
-    FROM Projects
-    WHERE status = 'ACTIVE'
-)
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    hbd.dept_name,
-    ap.project_name,
-    hbd.budget
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN high_budget_depts hbd
-    ON o.user_id = hbd.dept_id
-LEFT JOIN active_projects ap
-    ON hbd.dept_id = ap.dept_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], dept_name=[$7], project_name=[$10], budget=[$8])
-+- LogicalJoin(condition=[=($6, $11)], joinType=[left])
-   :- LogicalJoin(condition=[=($4, $6)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalProject(dept_id=[$0], dept_name=[$1], budget=[$2])
-   :     +- LogicalFilter(condition=[>($2, 600000)])
-   :        +- LogicalTableScan(table=[[default_catalog, default_database, Departments]])
-   +- LogicalProject(project_id=[$0], project_name=[$1], dept_id=[$2])
-      +- LogicalFilter(condition=[=($3, _UTF-16LE'ACTIVE')])
-         +- LogicalTableScan(table=[[default_catalog, default_database, Projects]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, dept_name, project_name, budget])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (dept_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id0, dept_id), =(dept_id, dept_id0)], select=[user_id,name,order_id,user_id0,dept_id,dept_name,budget,project_name,dept_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) dept_id, VARCHAR(2147483647) dept [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($4, $6), =($6, $11)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:1;LeftFieldIndex:1;RightInputId:2;RightFieldIndex:0;], 3=[LeftInputId:2;LeftFieldIndex:0;RightInputId:3;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,dept_id [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[dept_id]])
    :  +- ChangelogNormalize(key=[dept_id], condition=[>(budget, 600000)])
    :     +- Exchange(distribution=[hash[dept_id]])
    :        +- TableSourceScan(table=[[default_catalog, default_database, Departments, filter=[]]], fields=[dept_id, dept_name, budget])
    +- Exchange(distribution=[hash[dept_id]])
-      +- Calc(select=[project_name, dept_id])
+      +- Calc(select=[project_id, project_name, dept_id])
          +- ChangelogNormalize(key=[project_id], condition=[=(status, 'ACTIVE')])
             +- Exchange(distribution=[hash[project_id]])
                +- TableSourceScan(table=[[default_catalog, default_database, Projects, filter=[]]], fields=[project_id, project_name, dept_id, status])
@@ -219,131 +96,44 @@ Calc(select=[user_id, name, order_id, dept_name, project_name, budget])
     </Resource>
   </TestCase>
   <TestCase name="testComplexCTEWithMultiJoinNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-WITH user_orders AS (
-    SELECT u.user_id, u.name, o.order_id, o.product, p.payment_id, p.price
-    FROM Users u
-    LEFT JOIN Orders o ON
-        u.user_id = o.user_id
-    LEFT JOIN Payments p
-        ON u.user_id = p.user_id
-),
-order_details AS (
-    SELECT uo.*, os.status_name, os.is_final, pm.method_name, pm.processing_fee
-    FROM user_orders uo
-    LEFT JOIN OrderStatus os
-        ON uo.order_id = os.status_id
-    LEFT JOIN PaymentMethods pm
-        ON uo.payment_id = pm.method_id
-),
-final_summary AS (
-    SELECT
-        user_id,
-        name,
-        COUNT(order_id) as total_orders,
-        SUM(price) as total_spent,
-        AVG(price) as avg_order_value,
-        COUNT(CASE WHEN is_final = true THEN 1 END) as completed_orders
-    FROM order_details
-    GROUP BY user_id, name
-)
-SELECT * FROM final_summary]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], total_orders=[$2], total_spent=[$3], avg_order_value=[$4], completed_orders=[$5])
-+- LogicalAggregate(group=[{0, 1}], total_orders=[COUNT($2)], total_spent=[SUM($3)], avg_order_value=[AVG($3)], completed_orders=[COUNT($4)])
-   +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], price=[$5], $f4=[CASE($7, 1, null:INTEGER)])
-      +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], product=[$3], payment_id=[$4], price=[$5], status_name=[$7], is_final=[$8], method_name=[$10], processing_fee=[$11])
-         +- LogicalJoin(condition=[=($4, $9)], joinType=[left])
-            :- LogicalJoin(condition=[=($2, $6)], joinType=[left])
-            :  :- LogicalProject(user_id=[$0], name=[$1], order_id=[$3], product=[$5], payment_id=[$6], price=[$7])
-            :  :  +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-            :  :     :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-            :  :     :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-            :  :     :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-            :  :     +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-            :  +- LogicalTableScan(table=[[default_catalog, default_database, OrderStatus]])
-            +- LogicalTableScan(table=[[default_catalog, default_database, PaymentMethods]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 GroupAggregate(groupBy=[user_id, name], select=[user_id, name, COUNT_RETRACT(order_id) AS total_orders, SUM_RETRACT(price) AS total_spent, AVG_RETRACT(price) AS avg_order_value, COUNT_RETRACT($f4) AS completed_orders])
 +- Exchange(distribution=[hash[user_id, name]])
    +- Calc(select=[user_id, name, order_id, price, CASE(is_final, 1, null:INTEGER) AS $f4])
-      +- MultiJoin(commonJoinKey=[payment_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (method_id)], joinConditions=[=(payment_id, method_id)], select=[user_id,name,order_id,payment_id,price,is_final,method_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price, BOOLEAN is_final, VARCHAR(2147483647) method_id)])
-         :- Exchange(distribution=[hash[payment_id]])
-         :  +- Calc(select=[user_id, name, order_id, payment_id, price, is_final])
-         :     +- MultiJoin(commonJoinKey=[order_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (status_id)], joinConditions=[=(order_id, status_id)], select=[user_id,name,order_id,payment_id,price,status_id,is_final], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) status_id, BOOLEAN is_final)])
-         :        :- Exchange(distribution=[hash[order_id]])
-         :        :  +- Calc(select=[user_id, name, order_id, payment_id, price])
-         :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) user_id1)])
-         :        :        :- Exchange(distribution=[hash[user_id]])
-         :        :        :  +- ChangelogNormalize(key=[user_id])
-         :        :        :     +- Exchange(distribution=[hash[user_id]])
-         :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
-         :        :        :- Exchange(distribution=[hash[user_id]])
-         :        :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
-         :        :        +- Exchange(distribution=[hash[user_id]])
-         :        :           +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
-         :        +- Exchange(distribution=[hash[status_id]])
-         :           +- ChangelogNormalize(key=[status_id])
-         :              +- Exchange(distribution=[hash[status_id]])
-         :                 +- TableSourceScan(table=[[default_catalog, default_database, OrderStatus, project=[status_id, is_final], metadata=[]]], fields=[status_id, is_final])
+      +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($2, $6), =($4, $9)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:4;], 1=[LeftInputId:0;LeftFieldIndex:2;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:4;RightInputId:2;RightFieldIndex:0;]}], select=[user_id,name,order_id,product,payment_id,price,status_id,status_n [...]
+         :- Exchange(distribution=[hash[order_id]])
+         :  +- Calc(select=[user_id, name, order_id, product, payment_id, price])
+         :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,p [...]
+         :        :- Exchange(distribution=[hash[user_id]])
+         :        :  +- ChangelogNormalize(key=[user_id])
+         :        :     +- Exchange(distribution=[hash[user_id]])
+         :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+         :        :- Exchange(distribution=[hash[user_id]])
+         :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+         :        +- Exchange(distribution=[hash[user_id]])
+         :           +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+         :- Exchange(distribution=[hash[status_id]])
+         :  +- ChangelogNormalize(key=[status_id])
+         :     +- Exchange(distribution=[hash[status_id]])
+         :        +- TableSourceScan(table=[[default_catalog, default_database, OrderStatus]], fields=[status_id, status_name, is_final])
          +- Exchange(distribution=[hash[method_id]])
             +- ChangelogNormalize(key=[method_id])
                +- Exchange(distribution=[hash[method_id]])
-                  +- TableSourceScan(table=[[default_catalog, default_database, PaymentMethods, project=[method_id], metadata=[]]], fields=[method_id])
+                  +- TableSourceScan(table=[[default_catalog, default_database, PaymentMethods]], fields=[method_id, method_name, processing_fee])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testComplexCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT *
-FROM Assignments assignments
-LEFT JOIN Customers AS customer
-    ON assignments.user_id = customer.user_id
-    AND assignments.common_id = customer.common_id
-LEFT JOIN Documents AS documents
-    ON assignments.detail_id = documents.detail_id
-    AND assignments.common_id = documents.common_id
-LEFT JOIN PhaseDetails AS phase_details
-    ON documents.common_id = phase_details.common_id
-LEFT JOIN Organizations AS organizations
-    ON customer.depart_num = organizations.org_id
-    AND customer.common_id = organizations.common_id
-LEFT JOIN Customers AS creators
-    ON documents.creator_nm = creators.depart_num
-    AND documents.common_id = creators.common_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(assignment_id=[$0], user_id=[$1], detail_id=[$2], common_id=[$3], user_id0=[$4], name=[$5], depart_num=[$6], common_id0=[$7], detail_id0=[$8], creator_nm=[$9], common_id1=[$10], phase_id=[$11], common_id2=[$12], org_id=[$13], org_name=[$14], common_id3=[$15], user_id1=[$16], name0=[$17], depart_num0=[$18], common_id4=[$19])
-+- LogicalJoin(condition=[AND(=($9, $18), =($10, $19))], joinType=[left])
-   :- LogicalJoin(condition=[AND(=($6, $13), =($7, $15))], joinType=[left])
-   :  :- LogicalJoin(condition=[=($10, $12)], joinType=[left])
-   :  :  :- LogicalJoin(condition=[AND(=($2, $8), =($3, $10))], joinType=[left])
-   :  :  :  :- LogicalJoin(condition=[AND(=($1, $4), =($3, $7))], joinType=[left])
-   :  :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Assignments]])
-   :  :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Customers]])
-   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Documents]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, PhaseDetails]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Organizations]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Customers]])
-]]>
-    </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[common_id], joinTypes=[LEFT, LEFT, LEFT, LEFT, LEFT], inputUniqueKeys=[(assignment_id), (user_id), (detail_id), (phase_id), (org_id), (user_id)], joinConditions=[((user_id = user_id0) AND (common_id = common_id0)), ((detail_id = detail_id0) AND (common_id = common_id1)), (common_id1 = common_id2), ((depart_num = org_id) AND (common_id0 = common_id3)), ((creator_nm = depart_num0) AND (common_id1 = common_id4))], select=[assignment_id,user_id,detail_id,common_id,us [...]
-:- Exchange(distribution=[hash[common_id]])
+MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT, LEFT, LEFT]], joinConditions=[[true, AND(=($1, $4), =($3, $7)), AND(=($2, $8), =($3, $10)), =($10, $12), AND(=($6, $13), =($7, $15)), AND(=($9, $18), =($10, $19))]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:3;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;, LeftInputId:-1;LeftFieldIndex:-1;R [...]
+:- Exchange(distribution=[hash[user_id]])
 :  +- ChangelogNormalize(key=[assignment_id])
 :     +- Exchange(distribution=[hash[assignment_id]])
 :        +- TableSourceScan(table=[[default_catalog, default_database, Assignments]], fields=[assignment_id, user_id, detail_id, common_id])
-:- Exchange(distribution=[hash[common_id]])(reuse_id=[1])
-:  +- ChangelogNormalize(key=[user_id])
+:- Exchange(distribution=[hash[user_id]])
+:  +- ChangelogNormalize(key=[user_id])(reuse_id=[1])
 :     +- Exchange(distribution=[hash[user_id]])
 :        +- TableSourceScan(table=[[default_catalog, default_database, Customers]], fields=[user_id, name, depart_num, common_id])
 :- Exchange(distribution=[hash[common_id]])
@@ -358,44 +148,23 @@ MultiJoin(commonJoinKey=[common_id], joinTypes=[LEFT, LEFT, LEFT, LEFT, LEFT], i
 :  +- ChangelogNormalize(key=[org_id])
 :     +- Exchange(distribution=[hash[org_id]])
 :        +- TableSourceScan(table=[[default_catalog, default_database, Organizations]], fields=[org_id, org_name, common_id])
-+- Reused(reference_id=[1])
++- Exchange(distribution=[hash[common_id]])
+   +- Reused(reference_id=[1])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testComplexCommonJoinKeyMissingProjectionNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT *
-FROM Assignments assignments
-LEFT JOIN Documents AS documents
-    ON assignments.detail_id = documents.detail_id
-    AND assignments.common_id = documents.common_id
-LEFT JOIN Documents AS other_documents
-    ON assignments.user_id = other_documents.common_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(assignment_id=[$0], user_id=[$1], detail_id=[$2], common_id=[$3], detail_id0=[$4], creator_nm=[$5], common_id0=[$6], detail_id1=[$7], creator_nm0=[$8], common_id1=[$9])
-+- LogicalJoin(condition=[=($1, $9)], joinType=[left])
-   :- LogicalJoin(condition=[AND(=($2, $4), =($3, $6))], joinType=[left])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Assignments]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Documents]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Documents]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (detail_id)], joinConditions=[=(user_id, common_id1)], select=[assignment_id,user_id,detail_id,common_id,detail_id0,creator_nm,common_id0,detail_id1,creator_nm0,common_id1], rowType=[RecordType(VARCHAR(2147483647) assignment_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) detail_id, VARCHAR(2147483647) common_id, VARCHAR(2147483647) detail_id0, VARCHAR(2147483647) creator_nm, VARCHAR(2147483647) commo [...]
+MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, AND(=($2, $4), =($3, $6)), =($1, $9)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:3;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;], 1=[LeftInputId:0;LeftFieldIndex:2;RightInputId:1;RightFieldIndex:0;, LeftInputId:0;LeftFieldIndex:3;RightInputId:1;RightFieldIndex:2;], 2=[L [...]
 :- Exchange(distribution=[hash[user_id]])
-:  +- MultiJoin(commonJoinKey=[detail_id, common_id], joinTypes=[LEFT], inputUniqueKeys=[(assignment_id), (detail_id)], joinConditions=[AND(=(detail_id, detail_id0), =(common_id, common_id0))], select=[assignment_id,user_id,detail_id,common_id,detail_id0,creator_nm,common_id0], rowType=[RecordType(VARCHAR(2147483647) assignment_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) detail_id, VARCHAR(2147483647) common_id, VARCHAR(2147483647) detail_id0, VARCHAR(2147483647) creator_nm, VAR [...]
-:     :- Exchange(distribution=[hash[detail_id, common_id]])
-:     :  +- ChangelogNormalize(key=[assignment_id])
-:     :     +- Exchange(distribution=[hash[assignment_id]])
-:     :        +- TableSourceScan(table=[[default_catalog, default_database, Assignments]], fields=[assignment_id, user_id, detail_id, common_id])
-:     +- Exchange(distribution=[hash[detail_id, common_id]])
-:        +- ChangelogNormalize(key=[detail_id])
-:           +- Exchange(distribution=[hash[detail_id]])
-:              +- TableSourceScan(table=[[default_catalog, default_database, Documents]], fields=[detail_id, creator_nm, common_id])
+:  +- ChangelogNormalize(key=[assignment_id])
+:     +- Exchange(distribution=[hash[assignment_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Assignments]], fields=[assignment_id, user_id, detail_id, common_id])
+:- Exchange(distribution=[hash[detail_id]])
+:  +- ChangelogNormalize(key=[detail_id])
+:     +- Exchange(distribution=[hash[detail_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Documents]], fields=[detail_id, creator_nm, common_id])
 +- Exchange(distribution=[hash[common_id]])
    +- ChangelogNormalize(key=[detail_id])
       +- Exchange(distribution=[hash[detail_id]])
@@ -404,279 +173,85 @@ MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKe
     </Resource>
   </TestCase>
   <TestCase name="testComplexConditionalLogicWithMultiJoinNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    o.order_id,
-    p.payment_id,
-    pc.category_name,
-    CASE
-        WHEN pc.is_premium = true AND p.price > 1000 THEN 'High-Value Premium'
-        WHEN pc.is_premium = true THEN 'Premium'
-        WHEN p.price > 500 THEN 'Standard High-Value'
-        ELSE 'Standard'
-    END AS product_tier,
-    CASE
-        WHEN pr.rating >= 4 AND pr.is_verified = true THEN 'Highly Recommended'
-        WHEN pr.rating >= 3 THEN 'Recommended'
-        WHEN pr.rating >= 2 THEN 'Average'
-        ELSE 'Not Recommended'
-    END AS recommendation_status,
-    CASE
-        WHEN pc.discount_rate > 0.2 THEN p.price * (1 - pc.discount_rate)
-        ELSE p.price
-    END AS final_price
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-LEFT JOIN ProductCategories pc
-    ON o.product = pc.category_id
-LEFT JOIN ProductReviews pr
-    ON o.product = pr.product_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], order_id=[$3], payment_id=[$6], category_name=[$10], product_tier=[CASE(AND($11, >($7, 1000)), _UTF-16LE'High-Value Premium':VARCHAR(19) CHARACTER SET "UTF-16LE", $11, _UTF-16LE'Premium':VARCHAR(19) CHARACTER SET "UTF-16LE", >($7, 500), _UTF-16LE'Standard High-Value':VARCHAR(19) CHARACTER SET "UTF-16LE", _UTF-16LE'Standard':VARCHAR(19) CHARACTER SET "UTF-16LE")], recommendation_status=[CASE(AND(>=($15, 4), $16), _UTF-16LE'Highly Recommended':VARCHAR(18) CHARA [...]
-+- LogicalJoin(condition=[=($5, $14)], joinType=[left])
-   :- LogicalJoin(condition=[=($5, $9)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductCategories]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, ProductReviews]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, order_id, payment_id, category_name, CASE(AND(is_premium, >(price, 1000)), 'High-Value Premium', is_premium, 'Premium', >(price, 500), 'Standard High-Value', 'Standard') AS product_tier, CASE(AND(>=(rating, 4), is_verified), 'Highly Recommended', >=(rating, 3), 'Recommended', >=(rating, 2), 'Average', 'Not Recommended') AS recommendation_status, CASE(>(discount_rate, 0.2), *(price, -(1, discount_rate)), CAST(price AS DOUBLE)) AS final_price])
-+- MultiJoin(commonJoinKey=[product], joinTypes=[LEFT, LEFT], inputUniqueKeys=[noUniqueKey, (category_id), noUniqueKey], joinConditions=[=(product, category_id), =(product, product_id)], select=[user_id,order_id,product,payment_id,price,category_id,category_name,is_premium,discount_rate,product_id,rating,is_verified], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(214748364 [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($5, $9), =($5, $14)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:5;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:5;], 1=[LeftInputId:0;LeftFieldIndex:5;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:5;RightInputId:2;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1, [...]
    :- Exchange(distribution=[hash[product]])
-   :  +- Calc(select=[user_id, order_id, product, payment_id, price])
-   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,order_id,user_id0,product,payment_id,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) user_id1)])
-   :        :- Exchange(distribution=[hash[user_id]])
-   :        :  +- ChangelogNormalize(key=[user_id])
-   :        :     +- Exchange(distribution=[hash[user_id]])
-   :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id], metadata=[]]], fields=[user_id])
-   :        :- Exchange(distribution=[hash[user_id]])
-   :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user [...]
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- ChangelogNormalize(key=[user_id])
+   :     :     +- Exchange(distribution=[hash[user_id]])
+   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    :- Exchange(distribution=[hash[category_id]])
    :  +- ChangelogNormalize(key=[category_id])
    :     +- Exchange(distribution=[hash[category_id]])
    :        +- TableSourceScan(table=[[default_catalog, default_database, ProductCategories]], fields=[category_id, category_name, is_premium, discount_rate])
    +- Exchange(distribution=[hash[product_id]])
-      +- Calc(select=[product_id, rating, is_verified])
-         +- ChangelogNormalize(key=[review_id])
-            +- Exchange(distribution=[hash[review_id]])
-               +- TableSourceScan(table=[[default_catalog, default_database, ProductReviews]], fields=[review_id, product_id, rating, is_verified])
+      +- ChangelogNormalize(key=[review_id])
+         +- Exchange(distribution=[hash[review_id]])
+            +- TableSourceScan(table=[[default_catalog, default_database, ProductReviews]], fields=[review_id, product_id, rating, is_verified])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testComplexNestedCTEWithAggregationAndFunctionsMultiJoinChainExpected">
-    <Resource name="sql">
-      <![CDATA[
-WITH base_orders AS (
-    SELECT u.user_id, u.name, o.order_id, p.payment_id, p.price
-    FROM Users u
-    INNER JOIN Orders o
-        ON u.user_id = o.user_id
-    INNER JOIN Payments p
-        ON u.user_id = p.user_id
-),
-enriched_orders AS (
-    SELECT
-        bo.*,
-        om.metric_type,
-        om.metric_value,
-        CASE
-            WHEN bo.price > 1000 THEN 'Premium'
-            WHEN bo.price > 500 THEN 'Standard'
-            ELSE 'Basic'
-        END as order_tier
-    FROM base_orders bo
-    LEFT JOIN OrderMetrics om
-        ON bo.order_id = om.order_id
-),
-aggregated_metrics AS (
-    SELECT
-        user_id,
-        name,
-        COUNT(DISTINCT order_id) as total_orders,
-        SUM(price) as total_spent,
-        AVG(price) as avg_order_value,
-        MAX(metric_value) as max_metric,
-        MIN(metric_value) as min_metric,
-        COUNT(CASE WHEN order_tier = 'Premium' THEN 1 END) as premium_orders
-    FROM enriched_orders
-    GROUP BY user_id, name
-)
-SELECT
-    user_id,
-    UPPER(name) as user_name,
-    total_orders,
-    ROUND(total_spent, 2) as total_spent_rounded,
-    ROUND(avg_order_value, 2) as avg_order_value_rounded,
-    CONCAT('User: ', name, ' has ', CAST(total_orders AS STRING), ' orders') as summary,
-    CASE
-        WHEN total_orders > 10 THEN 'Frequent Customer'
-        WHEN total_orders > 5 THEN 'Regular Customer'
-        ELSE 'Occasional Customer'
-    END as customer_type
-FROM aggregated_metrics
-WHERE total_spent > 0]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], user_name=[UPPER($1)], total_orders=[$2], total_spent_rounded=[ROUND($3, 2)], avg_order_value_rounded=[ROUND($4, 2)], summary=[CONCAT(_UTF-16LE'User: ', $1, _UTF-16LE' has ', CAST($2):VARCHAR(2147483647) CHARACTER SET "UTF-16LE" NOT NULL, _UTF-16LE' orders')], customer_type=[CASE(>($2, 10), _UTF-16LE'Frequent Customer':VARCHAR(19) CHARACTER SET "UTF-16LE", >($2, 5), _UTF-16LE'Regular Customer':VARCHAR(19) CHARACTER SET "UTF-16LE", _UTF-16LE'Occasional Custome [...]
-+- LogicalFilter(condition=[>($3, 0)])
-   +- LogicalAggregate(group=[{0, 1}], total_orders=[COUNT(DISTINCT $2)], total_spent=[SUM($3)], avg_order_value=[AVG($3)], max_metric=[MAX($4)], min_metric=[MIN($4)], premium_orders=[COUNT($5)])
-      +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], price=[$4], metric_value=[$6], $f5=[CASE(=($7, _UTF-16LE'Premium'), 1, null:INTEGER)])
-         +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], payment_id=[$3], price=[$4], metric_type=[$7], metric_value=[$8], order_tier=[CASE(>($4, 1000), _UTF-16LE'Premium':VARCHAR(8) CHARACTER SET "UTF-16LE", >($4, 500), _UTF-16LE'Standard':VARCHAR(8) CHARACTER SET "UTF-16LE", _UTF-16LE'Basic':VARCHAR(8) CHARACTER SET "UTF-16LE")])
-            +- LogicalJoin(condition=[=($2, $6)], joinType=[left])
-               :- LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], price=[$7])
-               :  +- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
-               :     :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-               :     :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-               :     :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-               :     +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-               +- LogicalTableScan(table=[[default_catalog, default_database, OrderMetrics]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, UPPER(name) AS user_name, total_orders, ROUND(total_spent, 2) AS total_spent_rounded, ROUND(avg_order_value, 2) AS avg_order_value_rounded, CONCAT('User: ', name, ' has ', CAST(total_orders AS VARCHAR(2147483647)), ' orders') AS summary, CASE(>(total_orders, 10), 'Frequent Customer', >(total_orders, 5), 'Regular Customer', 'Occasional Customer') AS customer_type], where=[>(total_spent, 0)])
 +- GroupAggregate(groupBy=[user_id, name], select=[user_id, name, COUNT_RETRACT(DISTINCT order_id) AS total_orders, SUM_RETRACT(price) AS total_spent, AVG_RETRACT(price) AS avg_order_value])
    +- Exchange(distribution=[hash[user_id, name]])
-      +- MultiJoin(commonJoinKey=[order_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(order_id, order_id0)], select=[user_id,name,order_id,price,order_id0,metric_value], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, INTEGER price, VARCHAR(2147483647) order_id0, DOUBLE metric_value)])
+      +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($2, $6)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;], 1=[LeftInputId:0;LeftFieldIndex:2;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,order_id,payment_id,price,metric_id,order_id0,metric_type,metric_value], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_i [...]
          :- Exchange(distribution=[hash[order_id]])
-         :  +- Calc(select=[user_id, name, order_id, price])
-         :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, INTEGER price, VARCHAR(2147483647) user_id1)])
+         :  +- Calc(select=[user_id, name, order_id, payment_id, price])
+         :     +- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_i [...]
          :        :- Exchange(distribution=[hash[user_id]])
          :        :  +- ChangelogNormalize(key=[user_id])
          :        :     +- Exchange(distribution=[hash[user_id]])
-         :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+         :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
          :        :- Exchange(distribution=[hash[user_id]])
-         :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+         :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
          :        +- Exchange(distribution=[hash[user_id]])
-         :           +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
+         :           +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
          +- Exchange(distribution=[hash[order_id]])
-            +- Calc(select=[order_id, metric_value])
-               +- ChangelogNormalize(key=[metric_id])
-                  +- Exchange(distribution=[hash[metric_id]])
-                     +- TableSourceScan(table=[[default_catalog, default_database, OrderMetrics, project=[order_id, metric_value, metric_id], metadata=[]]], fields=[order_id, metric_value, metric_id])
+            +- ChangelogNormalize(key=[metric_id])
+               +- Exchange(distribution=[hash[metric_id]])
+                  +- TableSourceScan(table=[[default_catalog, default_database, OrderMetrics]], fields=[metric_id, order_id, metric_type, metric_value])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testCrossJoinUnnestWithMultiJoinInsert">
-    <Resource name="sql">
-      <![CDATA[
-INSERT INTO UnnestSink
-(
-    detail_id,
-    element_data,
-    data_value_id,
-    user_id,
-    order_id
-)
-SELECT
-    d.detail_id,
-    TRIM(REGEXP_REPLACE(edata, '[\[\]\"]', '')) AS element_data,
-    ARRAY_POSITION(split(REGEXP_REPLACE(d.data, '^\["|"\]$', '') , '", "'), edata) as data_value_id,
-    d.user_id,
-    o.order_id
-FROM Detail d
-INNER JOIN Orders o
-    ON o.user_id = d.user_id
-INNER JOIN Payments p
-    ON p.user_id = d.user_id
-LEFT JOIN Shipments s
-    ON s.user_id = d.user_id
-CROSS JOIN UNNEST(split(REGEXP_REPLACE(d.data, '^\["|"\]$', '') , '", "')) AS T(edata)
-WHERE NOT (s.location IS NOT NULL)]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalSink(table=[default_catalog.default_database.UnnestSink], targetColumns=[[0],[1],[2],[3],[4]], fields=[detail_id, element_data, data_value_id, user_id, order_id])
-+- LogicalProject(detail_id=[$0], element_data=[TRIM(FLAG(BOTH), _UTF-16LE' ', REGEXP_REPLACE($13, _UTF-16LE'[\[\]\"]', _UTF-16LE''))], data_value_id=[ARRAY_POSITION(split(REGEXP_REPLACE($3, _UTF-16LE'^\["|"\]$', _UTF-16LE''), _UTF-16LE'", "'), $13)], user_id=[$2], order_id=[$5])
-   +- LogicalFilter(condition=[NOT(IS NOT NULL($11))])
-      +- LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{3}])
-         :- LogicalJoin(condition=[=($12, $2)], joinType=[left])
-         :  :- LogicalJoin(condition=[=($10, $2)], joinType=[inner])
-         :  :  :- LogicalJoin(condition=[=($6, $2)], joinType=[inner])
-         :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Detail]])
-         :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-         :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-         :  +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-         +- LogicalProject(edata=[$0])
-            +- Uncollect
-               +- LogicalProject(EXPR$0=[split(REGEXP_REPLACE($cor0.data, _UTF-16LE'^\["|"\]$', _UTF-16LE''), _UTF-16LE'", "')])
-                  +- LogicalValues(tuples=[[{ 0 }]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Sink(table=[default_catalog.default_database.UnnestSink], targetColumns=[[0],[1],[2],[3],[4]], fields=[detail_id, element_data, data_value_id, user_id, order_id])
 +- Calc(select=[detail_id, TRIM(BOTH, ' ', REGEXP_REPLACE(f0, '[\[\]\"]', '')) AS element_data, ARRAY_POSITION(split(REGEXP_REPLACE(data, '^\["|"\]$', ''), '", "'), f0) AS data_value_id, user_id, order_id])
    +- Correlate(invocation=[$UNNEST_ROWS$1(split(REGEXP_REPLACE($cor0.data, _UTF-16LE'^\["|"\]$', _UTF-16LE''), _UTF-16LE'", "'))], correlate=[table($UNNEST_ROWS$1(split(REGEXP_REPLACE($cor0.data, '^\["|"\]$', ''), '", "')))], select=[detail_id,description,user_id,data,timestamp,order_id,user_id0,product,payment_id,price,user_id1,location,user_id2,f0], rowType=[RecordType(VARCHAR(2147483647) detail_id, VARCHAR(2147483647) description, VARCHAR(2147483647) user_id, VARCHAR(2147483647) data [...]
-      +- Calc(select=[detail_id, description, user_id, data, timestamp, order_id, user_id0, product, payment_id, price, user_id1, location, user_id2], where=[IS NULL(location)])
-         +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER, LEFT], inputUniqueKeys=[(detail_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id0, user_id), =(user_id1, user_id), =(user_id2, user_id)], select=[detail_id,description,user_id,data,timestamp,order_id,user_id0,product,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) detail_id, VARCHAR(2147483647) description, VARCHAR(2147483647) user_id, VARCHAR(2147483647) da [...]
-            :- Exchange(distribution=[hash[user_id]])
-            :  +- TableSourceScan(table=[[default_catalog, default_database, Detail]], fields=[detail_id, description, user_id, data, timestamp])
-            :- Exchange(distribution=[hash[user_id]])
-            :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
-            :- Exchange(distribution=[hash[user_id]])
-            :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
-            +- Exchange(distribution=[hash[user_id]])
-               +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+      +- MultiJoin(joinFilter=[AND(=($10, $2), =($6, $2))], joinTypes=[[INNER, INNER, INNER, LEFT]], joinConditions=[[true, =($6, $2), =($10, $2), =($12, $2)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;], 1=[LeftInputId:0;LeftFieldIndex:2;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:2; [...]
+         :- Exchange(distribution=[hash[user_id]])
+         :  +- TableSourceScan(table=[[default_catalog, default_database, Detail]], fields=[detail_id, description, user_id, data, timestamp])
+         :- Exchange(distribution=[hash[user_id]])
+         :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+         :- Exchange(distribution=[hash[user_id]])
+         :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+         +- Exchange(distribution=[hash[user_id]])
+            +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testFourWayComplexJoinExecPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.user_id = p.user_id
-    AND (u.cash >= p.price OR p.price < 0)
-LEFT JOIN Shipments s
-    ON p.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($8, $10)], joinType=[left])
-   :- LogicalJoin(condition=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinType=[inner])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[(user_id = user_id0), ((user_id = user_id1) AND ((cash >= price) OR (price < 0))), (user_id1 = user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
++- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex: [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
    :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
    :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
@@ -698,13 +273,13 @@ LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location
 
 == Optimized Physical Plan ==
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), OR(>=(cash, price), <(price, 0))), =(user_id1, user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
++- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex: [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
    :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
    :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
@@ -712,13 +287,13 @@ Calc(select=[user_id, name, order_id, payment_id, location])
 
 == Optimized Execution Plan ==
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[(user_id = user_id0), ((user_id = user_id1) AND ((cash >= price) OR (price < 0))), (user_id1 = user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
++- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex: [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
    :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
    :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
@@ -727,45 +302,16 @@ Calc(select=[user_id, name, order_id, payment_id, location])
     </Resource>
   </TestCase>
   <TestCase name="testFourWayComplexJoinRelPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.user_id = p.user_id
-    AND (u.cash >= p.price OR p.price < 0)
-LEFT JOIN Shipments s
-    ON p.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($8, $10)], joinType=[left])
-   :- LogicalJoin(condition=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinType=[inner])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), OR(>=(cash, price), <(price, 0))), =(user_id1, user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
++- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex: [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
    :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
    :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
@@ -774,192 +320,69 @@ Calc(select=[user_id, name, order_id, payment_id, location])
     </Resource>
   </TestCase>
   <TestCase name="testFourWayJoinRelPlanNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN LookupTable
-    ON u.name = LookupTable.name
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-LEFT JOIN Shipments s
-    ON o.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$9], location=[$12])
-+- LogicalJoin(condition=[=($4, $13)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $11)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($1, $7)], joinType=[left])
-   :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, LookupTable]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id0], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(user_id0, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) location, VARCHAR(2147483647) user_id2)])
-   :- Exchange(distribution=[hash[user_id0]])
-   :  +- Calc(select=[user_id, name, order_id, user_id0, payment_id])
-   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[=(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
-   :        :- Exchange(distribution=[hash[user_id]])
-   :        :  +- Calc(select=[user_id, name, order_id, user_id0])
-   :        :     +- MultiJoin(commonJoinKey=[name], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(name, name0)], select=[user_id,name,order_id,user_id0,name0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name0)])
-   :        :        :- Exchange(distribution=[hash[name]])
-   :        :        :  +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
-   :        :        :     :- Exchange(distribution=[hash[user_id]])
-   :        :        :     :  +- ChangelogNormalize(key=[user_id])
-   :        :        :     :     +- Exchange(distribution=[hash[user_id]])
-   :        :        :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
-   :        :        :     +- Exchange(distribution=[hash[user_id]])
-   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
-   :        :        +- Exchange(distribution=[hash[name]])
-   :        :           +- TableSourceScan(table=[[default_catalog, default_database, LookupTable, project=[name], metadata=[]]], fields=[name])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($1, $7), =($0, $11), =($4, $13)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:1;RightInputId: [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :- Exchange(distribution=[hash[name]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, LookupTable]], fields=[id, name, age])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
       +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testFourWayJoinTransitiveCommonJoinKeyRelPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON o.user_id = p.user_id
-LEFT JOIN Shipments s
-    ON p.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($8, $10)], joinType=[left])
-   :- LogicalJoin(condition=[=($4, $8)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id0, user_id1), =(user_id1, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_ [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($4, $8), =($8, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:1;LeftFieldIndex:1;RightInputId:2;RightFieldIndex:2;], 3=[LeftInputId:2;LeftFieldIndex:2;RightInputId:3;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product,payment [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
       +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testFourWayJoinWithFunctionInConditionMultiJoinChainExpected">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON o.user_id = u.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-    AND UPPER(u.name) = UPPER(p.payment_id)
-    AND (FLOOR(u.cash) >= FLOOR(p.price) OR p.price < 0)
-LEFT JOIN Shipments s
-    ON p.payment_id = s.location]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($6, $9)], joinType=[left])
-   :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], payment_id=[$7], price=[$8], user_id1=[$9])
-   :  +- LogicalJoin(condition=[AND(=($0, $9), =($6, $10), OR(>=(FLOOR($2), FLOOR($8)), <($8, 0)))], joinType=[left])
-   :     :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], $f6=[UPPER($1)])
-   :     :  +- LogicalJoin(condition=[=($4, $0)], joinType=[left])
-   :     :     :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :     :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :     +- LogicalProject(payment_id=[$0], price=[$1], user_id=[$2], $f3=[UPPER($0)])
-   :        +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[payment_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(payment_id, location)], select=[user_id,name,order_id,payment_id,location], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) location)])
-:- Exchange(distribution=[hash[payment_id]])
-:  +- Calc(select=[user_id, name, order_id, payment_id])
-:     +- MultiJoin(commonJoinKey=[user_id, $f6], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[AND(=(user_id, user_id1), =($f6, $f3), OR(>=(FLOOR(cash), FLOOR(price)), <(price, 0)))], select=[user_id,name,cash,order_id,$f6,payment_id,price,user_id1,$f3], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) $f6, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(21474 [...]
-:        :- Exchange(distribution=[hash[user_id, $f6]])
-:        :  +- Calc(select=[user_id, name, cash, order_id, UPPER(name) AS $f6])
-:        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id0, user_id)], select=[user_id,name,cash,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
-:        :        :- Exchange(distribution=[hash[user_id]])
-:        :        :  +- ChangelogNormalize(key=[user_id])
-:        :        :     +- Exchange(distribution=[hash[user_id]])
-:        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
-:        :        +- Exchange(distribution=[hash[user_id]])
-:        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
-:        +- Exchange(distribution=[hash[user_id, $f3]])
-:           +- Calc(select=[payment_id, price, user_id, UPPER(payment_id) AS $f3])
-:              +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
-+- Exchange(distribution=[hash[location]])
-   +- TableSourceScan(table=[[default_catalog, default_database, Shipments, project=[location], metadata=[]]], fields=[location])
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($6, $9)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:6;], 1=[LeftInputId:0;LeftFieldIndex:6;RightInputId:1;RightFieldIndex:0;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(214748364 [...]
+   :- Exchange(distribution=[hash[payment_id]])
+   :  +- Calc(select=[user_id, name, cash, order_id, user_id0, product, payment_id, price, user_id1])
+   :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, AND(=($0, $9), =($6, $10), OR(>=(FLOOR($2), FLOOR($8)), <($8, 0)))]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:6;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:2;, LeftInputId:0;LeftFieldIndex:6;RightInputId:1;RightFieldIndex:3;]}], select=[user_id,name,cash,order_id [...]
+   :        :- Exchange(distribution=[hash[user_id]])
+   :        :  +- Calc(select=[user_id, name, cash, order_id, user_id0, product, UPPER(name) AS $f6])
+   :        :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($4, $0)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(21474 [...]
+   :        :        :- Exchange(distribution=[hash[user_id]])
+   :        :        :  +- ChangelogNormalize(key=[user_id])
+   :        :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :        :        +- Exchange(distribution=[hash[user_id]])
+   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- Calc(select=[payment_id, price, user_id, UPPER(payment_id) AS $f3])
+   :              +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   +- Exchange(distribution=[hash[location]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testFullOuterNotSupported">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-FULL OUTER JOIN Orders o
-    ON u.user_id = o.user_id
-FULL OUTER JOIN Payments p
-    ON o.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($4, $8)], joinType=[full])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[full])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
@@ -978,75 +401,24 @@ Calc(select=[user_id, name, order_id, payment_id])
     </Resource>
   </TestCase>
   <TestCase name="testFunctionAndExpressionWithMultiJoinNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    UPPER(u.name) as user_name_upper,
-    LOWER(o.product) as product_lower,
-    CONCAT(u.name, ' - ', o.product) as user_product,
-    SUBSTRING(pd.description, 1, 50) as description_preview,
-    CHAR_LENGTH(pd.description) as description_length,
-    FLOOR(p.price / 100.0) * 100 as price_rounded,
-    CASE
-        WHEN p.price > 1000 THEN 'High'
-        WHEN p.price > 500 THEN 'Medium'
-        ELSE 'Low'
-    END as price_tier,
-    REGEXP_REPLACE(pd.tags, ',', ' | ') as formatted_tags,
-    TO_TIMESTAMP_LTZ(pd.created_date, 3) as product_created,
-    COALESCE(up.preferred_category, 'None') as user_preference,
-    CASE
-        WHEN up.notification_level = 'HIGH' THEN 'Frequent Updates'
-        WHEN up.notification_level = 'MEDIUM' THEN 'Daily Updates'
-        ELSE 'Weekly Updates'
-    END as notification_frequency
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-LEFT JOIN ProductDetails pd
-    ON o.product = pd.product_id
-LEFT JOIN UserPreferences up
-    ON u.user_id = up.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], user_name_upper=[UPPER($1)], product_lower=[LOWER($5)], user_product=[CONCAT($1, _UTF-16LE' - ', $5)], description_preview=[SUBSTRING($11, 1, 50)], description_length=[CHAR_LENGTH($11)], price_rounded=[*(FLOOR(/($7, 100.0:DECIMAL(4, 1))), 100)], price_tier=[CASE(>($7, 1000), _UTF-16LE'High':VARCHAR(6) CHARACTER SET "UTF-16LE", >($7, 500), _UTF-16LE'Medium':VARCHAR(6) CHARACTER SET "UTF-16LE", _UTF-16LE'Low':VARCHAR(6) CHARACTER SET "UTF-16LE")], formatted_tag [...]
-+- LogicalJoin(condition=[=($0, $14)], joinType=[left])
-   :- LogicalJoin(condition=[=($5, $9)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductDetails]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, UserPreferences]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, UPPER(name) AS user_name_upper, LOWER(product) AS product_lower, CONCAT(name, ' - ', product) AS user_product, SUBSTRING(description, 1, 50) AS description_preview, CHAR_LENGTH(description) AS description_length, *(FLOOR(/(price, 100.0)), 100) AS price_rounded, CASE(>(price, 1000), 'High', >(price, 500), 'Medium', 'Low') AS price_tier, REGEXP_REPLACE(tags, ',', ' | ') AS formatted_tags, TO_TIMESTAMP_LTZ(created_date, 3) AS product_created, COALESCE(preferred_categor [...]
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (user_id)], joinConditions=[=(user_id, user_id2)], select=[user_id,name,product,price,description,created_date,tags,user_id2,preferred_category,notification_level], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) product, INTEGER price, VARCHAR(2147483647) description, BIGINT created_date, VARCHAR(2147483647) tags, VARCHAR(2147483647) user_id2, VARCHAR(2147 [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($5, $9), =($0, $14)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:5;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:5;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:0;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1, [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id, name, product, price, description, created_date, tags])
-   :     +- MultiJoin(commonJoinKey=[product], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (product_id)], joinConditions=[=(product, product_id)], select=[user_id,name,product,price,product_id,description,created_date,tags], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) product, INTEGER price, VARCHAR(2147483647) product_id, VARCHAR(2147483647) description, BIGINT created_date, VARCHAR(2147483647) tags)])
-   :        :- Exchange(distribution=[hash[product]])
-   :        :  +- Calc(select=[user_id, name, product, price])
-   :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,user_id0,product,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, INTEGER price, VARCHAR(2147483647) user_id1)])
-   :        :        :- Exchange(distribution=[hash[user_id]])
-   :        :        :  +- ChangelogNormalize(key=[user_id])
-   :        :        :     +- Exchange(distribution=[hash[user_id]])
-   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
-   :        :        :- Exchange(distribution=[hash[user_id]])
-   :        :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[user_id, product], metadata=[]]], fields=[user_id, product])
-   :        :        +- Exchange(distribution=[hash[user_id]])
-   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
-   :        +- Exchange(distribution=[hash[product_id]])
-   :           +- ChangelogNormalize(key=[product_id])
-   :              +- Exchange(distribution=[hash[product_id]])
-   :                 +- TableSourceScan(table=[[default_catalog, default_database, ProductDetails, project=[product_id, description, created_date, tags], metadata=[]]], fields=[product_id, description, created_date, tags])
+   :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user [...]
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- ChangelogNormalize(key=[user_id])
+   :     :     +- Exchange(distribution=[hash[user_id]])
+   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   :- Exchange(distribution=[hash[product_id]])
+   :  +- ChangelogNormalize(key=[product_id])
+   :     +- Exchange(distribution=[hash[product_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, ProductDetails]], fields=[product_id, product_name, description, created_date, tags])
    +- Exchange(distribution=[hash[user_id]])
       +- ChangelogNormalize(key=[user_id])
          +- Exchange(distribution=[hash[user_id]])
@@ -1055,92 +427,30 @@ Calc(select=[user_id, UPPER(name) AS user_name_upper, LOWER(product) AS product_
     </Resource>
   </TestCase>
   <TestCase name="testFunctionAndExpressionWithMultiJoinNoCommonJoinKeyV2">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    pd.product_name,
-    pd.price,
-    ROUND(pd.price * 1.1, 2) AS price_with_tax,
-    CONCAT('Product: ', pd.product_name) AS product_description,
-    CHAR_LENGTH(r.review_text) AS review_length,
-    UPPER(SUBSTRING(r.review_text, 1, 10)) AS review_preview,
-    CASE
-        WHEN r.rating >= 4 THEN 'High Rating'
-        WHEN r.rating >= 3 THEN 'Medium Rating'
-        ELSE 'Low Rating'
-    END AS rating_category,
-    TIMESTAMPDIFF(DAY, pd.created_date, CURRENT_DATE) AS days_since_created
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN ProductDetails pd
-    ON o.product = pd.product_id
-LEFT JOIN Reviews r
-    ON pd.product_id = r.product_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], product_name=[$7], price=[$8], price_with_tax=[ROUND(*($8, 1.1:DECIMAL(2, 1)), 2)], product_description=[CONCAT(_UTF-16LE'Product: ', $7)], review_length=[CHAR_LENGTH($14)], review_preview=[UPPER(SUBSTRING($14, 1, 10))], rating_category=[CASE(>=($13, 4), _UTF-16LE'High Rating':VARCHAR(13) CHARACTER SET "UTF-16LE", >=($13, 3), _UTF-16LE'Medium Rating':VARCHAR(13) CHARACTER SET "UTF-16LE", _UTF-16LE'Low Rating':VARCHAR(13) CHARACTER SE [...]
-+- LogicalJoin(condition=[=($6, $12)], joinType=[left])
-   :- LogicalJoin(condition=[=($5, $6)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductDetails]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Reviews]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, product_name, price, ROUND(*(price, 1.1), 2) AS price_with_tax, CONCAT('Product: ', product_name) AS product_description, CHAR_LENGTH(review_text) AS review_length, UPPER(SUBSTRING(review_text, 1, 10)) AS review_preview, CASE(>=(rating, 4), 'High Rating', >=(rating, 3), 'Medium Rating', 'Low Rating') AS rating_category, CAST(/INT(Reinterpret(-(CURRENT_DATE(), created_date)), 86400000) AS INTEGER) AS days_since_created])
-+- MultiJoin(commonJoinKey=[product], joinTypes=[LEFT, LEFT], inputUniqueKeys=[noUniqueKey, (product_id), noUniqueKey], joinConditions=[=(product, product_id), =(product_id, product_id0)], select=[user_id,name,order_id,product,product_id,product_name,price,created_date,product_id0,rating,review_text], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) product_id, VARCHAR(2147483647) pro [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($5, $6), =($6, $12)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:5;], 1=[LeftInputId:0;LeftFieldIndex:5;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:1;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product,product_id,product_name,price,weight,created_date,review_id,product_id0,rating,review_text,rev [...]
    :- Exchange(distribution=[hash[product]])
-   :  +- Calc(select=[user_id, name, order_id, product])
-   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
-   :        :- Exchange(distribution=[hash[user_id]])
-   :        :  +- ChangelogNormalize(key=[user_id])
-   :        :     +- Exchange(distribution=[hash[user_id]])
-   :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($0, $4)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- ChangelogNormalize(key=[user_id])
+   :     :     +- Exchange(distribution=[hash[user_id]])
+   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[product_id]])
    :  +- ChangelogNormalize(key=[product_id])
    :     +- Exchange(distribution=[hash[product_id]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, ProductDetails, project=[product_id, product_name, price, created_date], metadata=[]]], fields=[product_id, product_name, price, created_date])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, ProductDetails]], fields=[product_id, product_name, price, weight, created_date])
    +- Exchange(distribution=[hash[product_id]])
-      +- Calc(select=[product_id, rating, review_text])
-         +- ChangelogNormalize(key=[review_id])
-            +- Exchange(distribution=[hash[review_id]])
-               +- TableSourceScan(table=[[default_catalog, default_database, Reviews, project=[product_id, rating, review_text, review_id], metadata=[]]], fields=[product_id, rating, review_text, review_id])
+      +- ChangelogNormalize(key=[review_id])
+         +- Exchange(distribution=[hash[review_id]])
+            +- TableSourceScan(table=[[default_catalog, default_database, Reviews]], fields=[review_id, product_id, rating, review_text, review_date])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testIntervalJoinExcludedFromMultiJoin">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    e1.id,
-    e1.val,
-    e2.price
-FROM EventTable1 e1
-JOIN EventTable2 e2
-    ON e1.id = e2.id
-    AND e1.`$rowtime` BETWEEN e2.`$rowtime` - INTERVAL '1' MINUTE
-    AND e2.`$rowtime` + INTERVAL '1' MINUTE]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(id=[$0], val=[$1], price=[$4])
-+- LogicalJoin(condition=[AND(=($0, $3), >=($2, -($5, 60000:INTERVAL MINUTE)), <=($2, +($5, 60000:INTERVAL MINUTE)))], joinType=[inner])
-   :- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
-   +- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-      +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[id, val, price])
@@ -1155,120 +465,48 @@ Calc(select=[id, val, price])
     </Resource>
   </TestCase>
   <TestCase name="testJoinConditionHasNestedFieldsMultiJoinChainExpected">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    d.developer_id,
-    d.person.info.name AS developer_name,
-    s.ticket_id,
-    s.reporter.info.priority AS ticket_priority,
-    f.feedback_id,
-    f.author.info.rating AS feedback_rating,
-    sub.sub_id,
-    sub.subscriber.info.plan AS subscription_plan
-FROM Developers AS d
-LEFT JOIN SupportTickets AS s
-    ON d.person.info.id = s.reporter.info.id
-LEFT JOIN Feedback AS f
-    ON d.person.info.id = f.author.info.id
-LEFT JOIN Subscriptions AS sub
-    ON d.person.info.id = sub.subscriber.info.id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(developer_id=[$0], developer_name=[$1.info.name], ticket_id=[$3], ticket_priority=[$4.info.priority], feedback_id=[$6], feedback_rating=[$7.info.rating], sub_id=[$9], subscription_plan=[$10.info.plan])
-+- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], feedback_id=[$6], author=[$7], message=[$8], sub_id=[$10], subscriber=[$11], active=[$12])
-   +- LogicalJoin(condition=[=($9, $13)], joinType=[left])
-      :- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], feedback_id=[$6], author=[$7], message=[$8], $f9=[$1.info.id])
-      :  +- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], feedback_id=[$7], author=[$8], message=[$9])
-      :     +- LogicalJoin(condition=[=($6, $10)], joinType=[left])
-      :        :- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], $f6=[$1.info.id])
-      :        :  +- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$4], reporter=[$5], issue=[$6])
-      :        :     +- LogicalJoin(condition=[=($3, $7)], joinType=[left])
-      :        :        :- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], $f3=[$1.info.id])
-      :        :        :  +- LogicalTableScan(table=[[default_catalog, default_database, Developers]])
-      :        :        +- LogicalProject(ticket_id=[$0], reporter=[$1], issue=[$2], $f3=[$1.info.id])
-      :        :           +- LogicalTableScan(table=[[default_catalog, default_database, SupportTickets]])
-      :        +- LogicalProject(feedback_id=[$0], author=[$1], message=[$2], $f3=[$1.info.id])
-      :           +- LogicalTableScan(table=[[default_catalog, default_database, Feedback]])
-      +- LogicalProject(sub_id=[$0], subscriber=[$1], active=[$2], $f3=[$1.info.id])
-         +- LogicalTableScan(table=[[default_catalog, default_database, Subscriptions]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[developer_id, person.info.name AS developer_name, ticket_id, reporter.info.priority AS ticket_priority, feedback_id, author.info.rating AS feedback_rating, sub_id, subscriber.info.plan AS subscription_plan])
-+- MultiJoin(commonJoinKey=[$f9], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (sub_id)], joinConditions=[=($f9, $f3)], select=[developer_id,person,ticket_id,reporter,feedback_id,author,$f9,sub_id,subscriber,$f3], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCHAR(2147483647) name, VARCHAR(2147483647) region) info) person, VARCHAR(2147483647) ticket_id, RecordType:peek_no_expand(RecordType:peek_no_e [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($9, $13)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:9;], 1=[LeftInputId:0;LeftFieldIndex:9;RightInputId:1;RightFieldIndex:3;]}], select=[developer_id,person,experience_years,ticket_id,reporter,issue,feedback_id,author,message,$f9,sub_id,subscriber,active,$f3], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_ex [...]
    :- Exchange(distribution=[hash[$f9]])
-   :  +- Calc(select=[developer_id, person, ticket_id, reporter, feedback_id, author, person.info.id AS $f9])
-   :     +- MultiJoin(commonJoinKey=[$f6], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (feedback_id)], joinConditions=[=($f6, $f3)], select=[developer_id,person,ticket_id,reporter,$f6,feedback_id,author,$f3], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCHAR(2147483647) name, VARCHAR(2147483647) region) info) person, VARCHAR(2147483647) ticket_id, RecordType:peek_no_expand(RecordType:peek_no_expan [...]
+   :  +- Calc(select=[developer_id, person, experience_years, ticket_id, reporter, issue, feedback_id, author, message, person.info.id AS $f9])
+   :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($6, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:6;], 1=[LeftInputId:0;LeftFieldIndex:6;RightInputId:1;RightFieldIndex:3;]}], select=[developer_id,person,experience_years,ticket_id,reporter,issue,$f6,feedback_id,author,message,$f3], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(214 [...]
    :        :- Exchange(distribution=[hash[$f6]])
-   :        :  +- Calc(select=[developer_id, person, ticket_id, reporter, person.info.id AS $f6])
-   :        :     +- MultiJoin(commonJoinKey=[$f3], joinTypes=[LEFT], inputUniqueKeys=[(developer_id), (ticket_id)], joinConditions=[=($f3, $f30)], select=[developer_id,person,$f3,ticket_id,reporter,$f30], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCHAR(2147483647) name, VARCHAR(2147483647) region) info) person, VARCHAR(2147483647) $f3, VARCHAR(2147483647) ticket_id, RecordType:peek_no_expand(Record [...]
+   :        :  +- Calc(select=[developer_id, person, experience_years, ticket_id, reporter, issue, person.info.id AS $f6])
+   :        :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($3, $7)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:3;], 1=[LeftInputId:0;LeftFieldIndex:3;RightInputId:1;RightFieldIndex:3;]}], select=[developer_id,person,experience_years,$f3,ticket_id,reporter,issue,$f30], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCH [...]
    :        :        :- Exchange(distribution=[hash[$f3]])
-   :        :        :  +- Calc(select=[developer_id, person, person.info.id AS $f3])
+   :        :        :  +- Calc(select=[developer_id, person, experience_years, person.info.id AS $f3])
    :        :        :     +- ChangelogNormalize(key=[developer_id])
    :        :        :        +- Exchange(distribution=[hash[developer_id]])
-   :        :        :           +- TableSourceScan(table=[[default_catalog, default_database, Developers, project=[developer_id, person], metadata=[]]], fields=[developer_id, person])
+   :        :        :           +- TableSourceScan(table=[[default_catalog, default_database, Developers]], fields=[developer_id, person, experience_years])
    :        :        +- Exchange(distribution=[hash[$f3]])
-   :        :           +- Calc(select=[ticket_id, reporter, reporter.info.id AS $f3])
+   :        :           +- Calc(select=[ticket_id, reporter, issue, reporter.info.id AS $f3])
    :        :              +- ChangelogNormalize(key=[ticket_id])
    :        :                 +- Exchange(distribution=[hash[ticket_id]])
-   :        :                    +- TableSourceScan(table=[[default_catalog, default_database, SupportTickets, project=[ticket_id, reporter], metadata=[]]], fields=[ticket_id, reporter])
+   :        :                    +- TableSourceScan(table=[[default_catalog, default_database, SupportTickets]], fields=[ticket_id, reporter, issue])
    :        +- Exchange(distribution=[hash[$f3]])
-   :           +- Calc(select=[feedback_id, author, author.info.id AS $f3])
+   :           +- Calc(select=[feedback_id, author, message, author.info.id AS $f3])
    :              +- ChangelogNormalize(key=[feedback_id])
    :                 +- Exchange(distribution=[hash[feedback_id]])
-   :                    +- TableSourceScan(table=[[default_catalog, default_database, Feedback, project=[feedback_id, author], metadata=[]]], fields=[feedback_id, author])
+   :                    +- TableSourceScan(table=[[default_catalog, default_database, Feedback]], fields=[feedback_id, author, message])
    +- Exchange(distribution=[hash[$f3]])
-      +- Calc(select=[sub_id, subscriber, subscriber.info.id AS $f3])
+      +- Calc(select=[sub_id, subscriber, active, subscriber.info.id AS $f3])
          +- ChangelogNormalize(key=[sub_id])
             +- Exchange(distribution=[hash[sub_id]])
-               +- TableSourceScan(table=[[default_catalog, default_database, Subscriptions, project=[sub_id, subscriber], metadata=[]]], fields=[sub_id, subscriber])
+               +- TableSourceScan(table=[[default_catalog, default_database, Subscriptions]], fields=[sub_id, subscriber, active])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testJoinOfProjections">
-    <Resource name="sql">
-      <![CDATA[
-SELECT u.user_id, o.order_id, o.product, p.price, s.location
-FROM (SELECT user_id, name, cash FROM Users WHERE cash > 100) AS u
-JOIN (SELECT user_id, order_id, product FROM Orders WHERE product IS NOT NULL) AS o
-    ON u.user_id = o.user_id
-LEFT JOIN (SELECT user_id, price FROM Payments WHERE price > 50) AS p
-    ON u.user_id = p.user_id
-LEFT JOIN (SELECT user_id, location FROM Shipments WHERE location IS NOT NULL) AS s
-    ON u.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], order_id=[$4], product=[$5], price=[$7], location=[$9])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $6)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
-   :  :  :- LogicalProject(user_id=[$0], name=[$1], cash=[$2])
-   :  :  :  +- LogicalFilter(condition=[>($2, 100)])
-   :  :  :     +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalProject(user_id=[$1], order_id=[$0], product=[$2])
-   :  :     +- LogicalFilter(condition=[IS NOT NULL($2)])
-   :  :        +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalProject(user_id=[$2], price=[$1])
-   :     +- LogicalFilter(condition=[>($1, 50)])
-   :        +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalProject(user_id=[$1], location=[$0])
-      +- LogicalFilter(condition=[IS NOT NULL($0)])
-         +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, order_id, product, price, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1), =(user_id, user_id2)], select=[user_id,user_id0,order_id,product,user_id1,price,user_id2,location], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id1, INTEGER price, VARCHAR(21474 [...]
++- MultiJoin(joinFilter=[=($0, $3)], joinTypes=[[INNER, INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $3), =($0, $6), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldI [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id])
-   :     +- ChangelogNormalize(key=[user_id], condition=[>(cash, 100)])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, cash], metadata=[]]], fields=[user_id, cash])
+   :  +- ChangelogNormalize(key=[user_id], condition=[>(cash, 100)])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
    :  +- Calc(select=[user_id, order_id, product], where=[IS NOT NULL(product)])
    :     +- TableSourceScan(table=[[default_catalog, default_database, Orders, filter=[]]], fields=[order_id, user_id, product])
@@ -1282,139 +520,40 @@ Calc(select=[user_id, order_id, product, price, location])
     </Resource>
   </TestCase>
   <TestCase name="testJoinWithNestedSubqueryMultiJoinChainExpected">
-    <Resource name="sql">
-      <![CDATA[
-SELECT *
-FROM Users u
-JOIN (
-    SELECT o.user_id, o.order_id, p.payment_id, p.price
-    FROM Orders o
-    JOIN (
-        SELECT payment_id, user_id, price
-        FROM Payments
-        WHERE price > 100
-    ) AS p
-    ON o.user_id = p.user_id
-) AS op
-ON u.user_id = op.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], cash=[$2], user_id0=[$3], order_id=[$4], payment_id=[$5], price=[$6])
-+- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
-   :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   +- LogicalProject(user_id=[$1], order_id=[$0], payment_id=[$3], price=[$5])
-      +- LogicalJoin(condition=[=($1, $4)], joinType=[inner])
-         :- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-         +- LogicalProject(payment_id=[$0], user_id=[$2], price=[$1])
-            +- LogicalFilter(condition=[>($1, 100)])
-               +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), noUniqueKey], joinConditions=[=(user_id, user_id0)], select=[user_id,name,cash,user_id0,order_id,payment_id,price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price)])
+MultiJoin(joinFilter=[=($0, $3)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $3)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:0;]}], select=[user_id,name,cash,user_id0,order_id,payment_id,price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) order_id, VARCHAR(2147483647) [...]
 :- Exchange(distribution=[hash[user_id]])
 :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
 +- Exchange(distribution=[hash[user_id]])
    +- Calc(select=[user_id, order_id, payment_id, price])
-      +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(order_id), (payment_id)], joinConditions=[=(user_id, user_id0)], select=[order_id,user_id,payment_id,user_id0,price], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id0, INTEGER price)])
+      +- MultiJoin(joinFilter=[=($1, $4)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($1, $4)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;], 1=[LeftInputId:0;LeftFieldIndex:1;RightInputId:1;RightFieldIndex:1;]}], select=[order_id,user_id,product,payment_id,user_id0,price], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) u [...]
          :- Exchange(distribution=[hash[user_id]])
-         :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+         :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
          +- Exchange(distribution=[hash[user_id]])
             +- Calc(select=[payment_id, user_id, price], where=[>(price, 100)])
                +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
-]]>
-    </Resource>
-  </TestCase>
-  <TestCase name="testMultiSinkOnMultiJoinedView">
-    <Resource name="optimized rel plan with advice">
-      <![CDATA[
-Sink(table=[default_catalog.default_database.sink1], fields=[a, day, EXPR$2, EXPR$3])
-+- GroupAggregate(advice=[1], groupBy=[a, day], select=[a, day, SUM_RETRACT(b) AS EXPR$2, COUNT_RETRACT(DISTINCT c) AS EXPR$3])
-   +- Exchange(distribution=[hash[a, day]])
-      +- Calc(select=[a, day, b0 AS b, c])
-         +- MultiJoin(commonJoinKey=[a], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(a, d)], select=[a,day,b0,c,d], rowType=[RecordType(INTEGER a, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) c, INTEGER d)])
-            :- Exchange(distribution=[hash[a]])
-            :  +- Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
-            :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a])
-            +- Exchange(distribution=[hash[d]])
-               +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
-
-Sink(table=[default_catalog.default_database.sink2], fields=[a, day, b, c])
-+- Calc(select=[a, day, b0 AS b, c])
-   +- MultiJoin(commonJoinKey=[a], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(a, d)], select=[a,day,b0,c,d], rowType=[RecordType(INTEGER a, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) c, INTEGER d)])
-      :- Exchange(distribution=[hash[a]])
-      :  +- Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
-      :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a])
-      +- Exchange(distribution=[hash[d]])
-         +- Calc(select=[b, c, d], where=[>(b, 100)])
-            +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
-
-advice[1]: [ADVICE] You might want to enable local-global two-phase optimization by configuring ('table.exec.mini-batch.enabled' to 'true', 'table.exec.mini-batch.allow-latency' to a positive long value, 'table.exec.mini-batch.size' to a positive long value).
-advice[2]: [WARNING] The column(s): day(generated by non-deterministic function: CURRENT_TIMESTAMP ) can not satisfy the determinism requirement for correctly processing update message('UB'/'UA'/'D' in changelogMode, not 'I' only), this usually happens when input node has no upsertKey(upsertKeys=[{}]) or current node outputs non-deterministic update messages. Please consider removing these non-deterministic columns or making them deterministic by using deterministic functions.
-
-related rel plan:
-Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), _UTF-16LE'yyMMdd') AS day], changelogMode=[I,UB,UA,D])
-+- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a], changelogMode=[I,UB,UA,D])
-
-
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testPreservesUpsertKeyFourWayComplex">
-    <Resource name="sql">
-      <![CDATA[
-INSERT INTO sink_four_way
-SELECT
-    u.user_id,
-    o.order_id,
-    o.user_id,
-    p.payment_id,
-    p.user_id,
-    u.name,
-    a.location
-FROM UsersPK u
-JOIN OrdersPK o
-    ON u.user_id = o.user_id
-    AND o.product IS NOT NULL
-JOIN PaymentsPK p
-    ON u.user_id = p.user_id
-    AND p.price >= 0
-JOIN AddressPK a
-    ON u.user_id = a.user_id
-    AND a.location IS NOT NULL]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalSink(table=[default_catalog.default_database.sink_four_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, name, location])
-+- LogicalProject(user_id=[$0], order_id=[$4], user_id0=[$5], payment_id=[$7], user_id1=[$8], name=[$1], location=[$11])
-   +- LogicalJoin(condition=[AND(=($0, $10), IS NOT NULL($11))], joinType=[inner])
-      :- LogicalJoin(condition=[AND(=($0, $8), >=($9, 0))], joinType=[inner])
-      :  :- LogicalJoin(condition=[AND(=($0, $5), IS NOT NULL($6))], joinType=[inner])
-      :  :  :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
-      :  :  +- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
-      :  +- LogicalTableScan(table=[[default_catalog, default_database, PaymentsPK]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, AddressPK]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Sink(table=[default_catalog.default_database.sink_four_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, name, location])
+Sink(table=[default_catalog.default_database.sink_four_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, name, location], upsertMaterialize=[true])
 +- Calc(select=[user_id, order_id, user_id0, payment_id, user_id1, name, location])
-   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER, INNER], inputUniqueKeys=[(user_id), (order_id, user_id), (payment_id, user_id), (user_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1), =(user_id, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,user_id2,location], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCH [...]
+   +- MultiJoin(joinFilter=[AND(=($0, $10), =($0, $8), =($0, $5))], joinTypes=[[INNER, INNER, INNER, INNER]], joinConditions=[[true, =($0, $5), =($0, $8), =($0, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftField [...]
       :- Exchange(distribution=[hash[user_id]])
-      :  +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+      :  +- ChangelogNormalize(key=[user_id])
+      :     +- Exchange(distribution=[hash[user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, UsersPK]], fields=[user_id, name, region_id, description])
       :- Exchange(distribution=[hash[user_id]])
-      :  +- Calc(select=[order_id, user_id])
-      :     +- ChangelogNormalize(key=[order_id, user_id], condition=[IS NOT NULL(product)])
-      :        +- Exchange(distribution=[hash[order_id, user_id]])
-      :           +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK, filter=[]]], fields=[order_id, user_id, product])
+      :  +- ChangelogNormalize(key=[order_id, user_id], condition=[IS NOT NULL(product)])
+      :     +- Exchange(distribution=[hash[order_id, user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK, filter=[]]], fields=[order_id, user_id, product])
       :- Exchange(distribution=[hash[user_id]])
-      :  +- Calc(select=[payment_id, user_id])
-      :     +- ChangelogNormalize(key=[payment_id, user_id], condition=[>=(price, 0)])
-      :        +- Exchange(distribution=[hash[payment_id, user_id]])
-      :           +- TableSourceScan(table=[[default_catalog, default_database, PaymentsPK, filter=[]]], fields=[payment_id, user_id, price])
+      :  +- ChangelogNormalize(key=[payment_id, user_id], condition=[>=(price, 0)])
+      :     +- Exchange(distribution=[hash[payment_id, user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, PaymentsPK, filter=[]]], fields=[payment_id, user_id, price])
       +- Exchange(distribution=[hash[user_id]])
          +- ChangelogNormalize(key=[user_id], condition=[IS NOT NULL(location)])
             +- Exchange(distribution=[hash[user_id]])
@@ -1422,113 +561,54 @@ Sink(table=[default_catalog.default_database.sink_four_way], fields=[user_id, or
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testPreservesUpsertKeyThreeWayJoin">
-    <Resource name="sql">
-      <![CDATA[
-INSERT INTO sink_three_way
-SELECT
-    o.user_id,
-    o.order_id,
-    p.user_id,
-    p.payment_id,
-    u.user_id,
-    u.description
-FROM UsersPK u
-JOIN OrdersPK o
-    ON o.user_id = u.user_id
-JOIN PaymentsPK p
-    ON o.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalSink(table=[default_catalog.default_database.sink_three_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, description])
-+- LogicalProject(user_id=[$5], order_id=[$4], user_id0=[$8], payment_id=[$7], user_id1=[$0], description=[$3])
-   +- LogicalJoin(condition=[=($5, $8)], joinType=[inner])
-      :- LogicalJoin(condition=[=($5, $0)], joinType=[inner])
-      :  :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
-      :  +- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, PaymentsPK]])
-]]>
-    </Resource>
+  <TestCase name="testPreservesUpsertKeyThreeWayJoin">
     <Resource name="optimized rel plan">
       <![CDATA[
-Sink(table=[default_catalog.default_database.sink_three_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, description])
+Sink(table=[default_catalog.default_database.sink_three_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, description], upsertMaterialize=[true])
 +- Calc(select=[user_id0 AS user_id, order_id, user_id1 AS user_id0, payment_id, user_id AS user_id1, description])
-   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id, user_id), (payment_id, user_id)], joinConditions=[=(user_id0, user_id), =(user_id0, user_id1)], select=[user_id,description,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) description, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   +- MultiJoin(joinFilter=[AND(=($5, $8), =($5, $0))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($5, $0), =($5, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:1;LeftFieldIndex:1;RightInputId:2;RightFieldIndex:1;]}], select=[user_id,name,region_id,description,order_id,user_id0,product,payment_id,user_id1,price], rowType=[RecordType(VARC [...]
       :- Exchange(distribution=[hash[user_id]])
-      :  +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, description], metadata=[]]], fields=[user_id, description])
+      :  +- ChangelogNormalize(key=[user_id])
+      :     +- Exchange(distribution=[hash[user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, UsersPK]], fields=[user_id, name, region_id, description])
       :- Exchange(distribution=[hash[user_id]])
-      :  +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+      :  +- ChangelogNormalize(key=[order_id, user_id])
+      :     +- Exchange(distribution=[hash[order_id, user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
       +- Exchange(distribution=[hash[user_id]])
-         +- TableSourceScan(table=[[default_catalog, default_database, PaymentsPK, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+         +- ChangelogNormalize(key=[payment_id, user_id])
+            +- Exchange(distribution=[hash[payment_id, user_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, PaymentsPK]], fields=[payment_id, user_id, price])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testPreservesUpsertKeyTwoWayInnerJoinOrders">
-    <Resource name="sql">
-      <![CDATA[
-INSERT INTO sink_two_way
-SELECT
-    o.user_id,
-    o.order_id,
-    o.product,
-    u.region_id
-FROM UsersPK u
-INNER JOIN OrdersPK o
-    ON u.user_id = o.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalSink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
-+- LogicalProject(user_id=[$5], order_id=[$4], product=[$6], region_id=[$2])
-   +- LogicalJoin(condition=[=($0, $5)], joinType=[inner])
-      :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
+Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id], upsertMaterialize=[true])
 +- Calc(select=[user_id0 AS user_id, order_id, product, region_id])
-   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), (order_id, user_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,region_id,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, INTEGER region_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+   +- MultiJoin(joinFilter=[=($0, $5)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $5)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,region_id,description,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER region_id, VARCHAR(2147483647) description, VARCHAR(2147483647) order_i [...]
       :- Exchange(distribution=[hash[user_id]])
-      :  +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, region_id], metadata=[]]], fields=[user_id, region_id])
+      :  +- ChangelogNormalize(key=[user_id])
+      :     +- Exchange(distribution=[hash[user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, UsersPK]], fields=[user_id, name, region_id, description])
       +- Exchange(distribution=[hash[user_id]])
-         +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
+         +- ChangelogNormalize(key=[order_id, user_id])
+            +- Exchange(distribution=[hash[order_id, user_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testPreservesUpsertKeyTwoWayInnerJoinOrdersDoesNot">
-    <Resource name="sql">
-      <![CDATA[
-INSERT INTO sink_two_way
-SELECT
-    o.user_id,
-    o.order_id,
-    o.product,
-    u.region_id
-FROM UsersPK u
-INNER JOIN OrdersSimplePK o
-    ON u.user_id = o.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalSink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
-+- LogicalProject(user_id=[$5], order_id=[$4], product=[$6], region_id=[$2])
-   +- LogicalJoin(condition=[=($0, $5)], joinType=[inner])
-      :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, OrdersSimplePK]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id], upsertMaterialize=[true])
 +- Calc(select=[user_id0 AS user_id, order_id, product, region_id])
-   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,region_id,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, INTEGER region_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+   +- MultiJoin(joinFilter=[=($0, $5)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $5)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,region_id,description,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER region_id, VARCHAR(2147483647) description, VARCHAR(2147483647) order_i [...]
       :- Exchange(distribution=[hash[user_id]])
       :  +- ChangelogNormalize(key=[user_id])
       :     +- Exchange(distribution=[hash[user_id]])
-      :        +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, region_id], metadata=[]]], fields=[user_id, region_id])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, UsersPK]], fields=[user_id, name, region_id, description])
       +- Exchange(distribution=[hash[user_id]])
          +- ChangelogNormalize(key=[order_id])
             +- Exchange(distribution=[hash[order_id]])
@@ -1537,80 +617,31 @@ Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, ord
     </Resource>
   </TestCase>
   <TestCase name="testPreservesUpsertKeyTwoWayLeftJoinOrders">
-    <Resource name="sql">
-      <![CDATA[
-INSERT INTO sink_two_way
-SELECT
-    o.user_id,
-    o.order_id,
-    o.product,
-    u.region_id
-FROM OrdersPK o
-LEFT JOIN UsersPK u
-    ON u.user_id = o.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalSink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
-+- LogicalProject(user_id=[$1], order_id=[$0], product=[$2], region_id=[$5])
-   +- LogicalJoin(condition=[=($3, $1)], joinType=[left])
-      :- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
+Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id], upsertMaterialize=[true])
 +- Calc(select=[user_id, order_id, product, region_id])
-   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(order_id, user_id), (user_id)], joinConditions=[=(user_id0, user_id)], select=[order_id,user_id,product,user_id0,region_id], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id0, INTEGER region_id)])
+   +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($3, $1)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;], 1=[LeftInputId:0;LeftFieldIndex:1;RightInputId:1;RightFieldIndex:0;]}], select=[order_id,user_id,product,user_id0,name,region_id,description], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) na [...]
       :- Exchange(distribution=[hash[user_id]])
-      :  +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
+      :  +- ChangelogNormalize(key=[order_id, user_id])
+      :     +- Exchange(distribution=[hash[order_id, user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
       +- Exchange(distribution=[hash[user_id]])
-         +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, region_id], metadata=[]]], fields=[user_id, region_id])
+         +- ChangelogNormalize(key=[user_id])
+            +- Exchange(distribution=[hash[user_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, UsersPK]], fields=[user_id, name, region_id, description])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testRegularJoinsAreMergedApartFromTemporalJoin">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    temporal.age FROM Users u
-INNER JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN (
-    SELECT s.user_id, l.age
-    FROM StreamTable s
-    JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l
-        ON s.user_id = l.id
-) temporal ON u.user_id = temporal.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], age=[$7])
-+- LogicalJoin(condition=[=($0, $6)], joinType=[inner])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalProject(user_id=[$0], age=[$5])
-      +- LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0, 2}])
-         :- LogicalProject(user_id=[$0], amount=[$1], proctime=[PROCTIME()])
-         :  +- LogicalTableScan(table=[[default_catalog, default_database, StreamTable]])
-         +- LogicalFilter(condition=[=($cor0.user_id, $0)])
-            +- LogicalSnapshot(period=[$cor0.proctime])
-               +- LogicalTableScan(table=[[default_catalog, default_database, LookupTable]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, age])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,user_id1,age], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) user_id1, INTEGER age)])
++- MultiJoin(joinFilter=[AND(=($0, $6), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $6)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:0;]}], select=[user_id,name,cash,order_id,user_id0,product,user [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
       +- Calc(select=[user_id, age])
          +- LookupJoin(table=[default_catalog.default_database.LookupTable], joinType=[InnerJoin], lookup=[id=user_id], select=[user_id, id, age])
@@ -1619,29 +650,6 @@ Calc(select=[user_id, name, order_id, age])
     </Resource>
   </TestCase>
   <TestCase name="testRightJoinNotSupported">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-RIGHT JOIN Orders o
-    ON u.user_id = o.user_id
-RIGHT JOIN Payments p
-    ON o.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($4, $8)], joinType=[right])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[right])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
@@ -1660,32 +668,9 @@ Calc(select=[user_id, name, order_id, payment_id])
     </Resource>
   </TestCase>
   <TestCase name="testSameTableMultipleAliases">
-    <Resource name="sql">
-      <![CDATA[
-SELECT *
-FROM Users u
-LEFT JOIN Users u1
-    ON u.user_id = u1.user_id
-LEFT JOIN Users u2
-    ON u1.user_id = u2.user_id
-LEFT JOIN Users u3
-    ON u2.user_id = u3.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], cash=[$2], user_id0=[$3], name0=[$4], cash0=[$5], user_id1=[$6], name1=[$7], cash1=[$8], user_id2=[$9], name2=[$10], cash2=[$11])
-+- LogicalJoin(condition=[=($6, $9)], joinType=[left])
-   :- LogicalJoin(condition=[=($3, $6)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $3)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (user_id), (user_id), (user_id)], joinConditions=[=(user_id, user_id0), =(user_id0, user_id1), =(user_id1, user_id2)], select=[user_id,name,cash,user_id0,name0,cash0,user_id1,name1,cash1,user_id2,name2,cash2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name0, INTEGER cash0, VARCHAR(2147483647) user_id1 [...]
+MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($0, $3), =($3, $6), =($6, $9)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:1;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:0;], 3=[LeftInputId:2;LeftFieldIndex:0;RightInputId:3;RightFieldIndex:0;]}], select=[user_id,name,cash,user_id0,name0,cash0,user_id1,name1,c [...]
 :- Exchange(distribution=[hash[user_id]])
 :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
 :- Exchange(distribution=[hash[user_id]])
@@ -1698,28 +683,6 @@ MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys
     </Resource>
   </TestCase>
   <TestCase name="testTemporalJoinExcludedFromMultiJoin">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    s.user_id,
-    s.amount,
-    l.name,
-    l.age
-FROM StreamTable s
-JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l
-    ON s.user_id = l.id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], amount=[$1], name=[$4], age=[$5])
-+- LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0, 2}])
-   :- LogicalProject(user_id=[$0], amount=[$1], proctime=[PROCTIME()])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, StreamTable]])
-   +- LogicalFilter(condition=[=($cor0.user_id, $0)])
-      +- LogicalSnapshot(period=[$cor0.proctime])
-         +- LogicalTableScan(table=[[default_catalog, default_database, LookupTable]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, amount, name, age])
@@ -1729,39 +692,16 @@ Calc(select=[user_id, amount, name, age])
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayInnerJoinExecPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-INNER JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
@@ -1777,295 +717,128 @@ LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 
 == Optimized Physical Plan ==
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 
 == Optimized Execution Plan ==
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayInnerJoinRelPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-INNER JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayInnerJoinRelPlanNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-INNER JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.cash = p.price]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($2, $7)], joinType=[inner])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[cash], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[=(cash, price)], select=[user_id,name,cash,order_id,payment_id,price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price)])
-   :- Exchange(distribution=[hash[cash]])
-   :  +- Calc(select=[user_id, name, cash, order_id])
-   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,cash,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
-   :        :- Exchange(distribution=[hash[user_id]])
-   :        :  +- ChangelogNormalize(key=[user_id])
-   :        :     +- Exchange(distribution=[hash[user_id]])
-   :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
++- MultiJoin(joinFilter=[AND(=($2, $7), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($2, $7)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:2;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:2;RightInputId:2;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[price]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, price], metadata=[]]], fields=[payment_id, price])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayInnerJoinWithSingleTttlHint">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    /*+ STaTE_tTL(o='2d') */
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-INNER JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0] options:{o=2d}]]])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0, 0] options:{o=2d}]]])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]], hints=[[[ALIAS inheritPath:[] options:[u]]]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]], hints=[[[ALIAS inheritPath:[] options:[o]]]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]], hints=[[[ALIAS inheritPath:[] options:[p]]]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], stateTtlHints=[[[STATE_TTL options:[0s, 2d, 0s]]]], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayInnerJoinWithTttlHints">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    /*+ STATE_TTL(u='1d', o='2d', p='1h') */
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-INNER JOIN Orders o
-    ON u.user_id = o.user_id
-INNER JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0] options:{p=1h, u=1d, o=2d}]]])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0, 0] options:{p=1h, u=1d, o=2d}]]])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]], hints=[[[ALIAS inheritPath:[] options:[u]]]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]], hints=[[[ALIAS inheritPath:[] options:[o]]]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]], hints=[[[ALIAS inheritPath:[] options:[p]]]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], stateTtlHints=[[[STATE_TTL options:[1d, 2d, 1h]]]], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,paym [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayJoinExecPlanNoCommonJoinKey">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-LEFT JOIN Orders o ON TRUE
-INNER JOIN Payments p ON TRUE]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[true], joinType=[inner])
-   :- LogicalJoin(condition=[true], joinType=[left])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[noCommonJoinKey], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[true], select=[user_id,name,order_id,payment_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id)])
-:- Exchange(distribution=[single])
-:  +- MultiJoin(commonJoinKey=[noCommonJoinKey], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[true], select=[user_id,name,order_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id)])
-:     :- Exchange(distribution=[single])
-:     :  +- ChangelogNormalize(key=[user_id])
-:     :     +- Exchange(distribution=[hash[user_id]])
-:     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
-:     +- Exchange(distribution=[single])
-:        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id], metadata=[]]], fields=[order_id])
-+- Exchange(distribution=[single])
-   +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id], metadata=[]]], fields=[payment_id])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, INNER]], joinConditions=[[true, true]], joinAttributeMap=[{}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[single])
+   :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, true]], joinAttributeMap=[{}], select=[user_id,name,cash,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+   :     :- Exchange(distribution=[single])
+   :     :  +- ChangelogNormalize(key=[user_id])
+   :     :     +- Exchange(distribution=[hash[user_id]])
+   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :     +- Exchange(distribution=[single])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   +- Exchange(distribution=[single])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayJoinWithTimeAttributesMaterialization">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.name,
-    u.proctime,
-    o.`$rowtime`,
-    p.price
-FROM UsersWithProctime u
-JOIN OrdersWithRowtime o
-    ON u.user_id = o.user_id
-JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(name=[$1], proctime=[$2], $rowtime=[$5], price=[$7])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-   :  :- LogicalProject(user_id=[$0], name=[$1], proctime=[PROCTIME()])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, UsersWithProctime]])
-   :  +- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[$2])
-   :     +- LogicalTableScan(table=[[default_catalog, default_database, OrdersWithRowtime]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[name, proctime, $rowtime, price])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,proctime,user_id0,$rowtime,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, TIMESTAMP_WITH_LOCAL_TIME_ZONE(3) proctime, VARCHAR(2147483647) user_id0, TIMESTAMP(3) $rowtime, INTEGER price, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,proctime,order_id,user_id0,$rowtime [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- Calc(select=[user_id, name, PROCTIME_MATERIALIZE(PROCTIME()) AS proctime])
    :     +- TableSourceScan(table=[[default_catalog, default_database, UsersWithProctime]], fields=[user_id, name])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id, CAST($rowtime AS TIMESTAMP(3)) AS $rowtime])
+   :  +- Calc(select=[order_id, user_id, CAST($rowtime AS TIMESTAMP(3)) AS $rowtime])
    :     +- WatermarkAssigner(rowtime=[$rowtime], watermark=[$rowtime])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersWithRowtime, project=[user_id, $rowtime], metadata=[]]], fields=[user_id, $rowtime])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersWithRowtime]], fields=[order_id, user_id, $rowtime])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayLeftOuterJoinExecPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1], [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
@@ -2081,102 +854,53 @@ LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 
 == Optimized Physical Plan ==
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1], [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 
 == Optimized Execution Plan ==
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1], [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayLeftOuterJoinRelPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id1], [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseExecPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-WHERE u.name = 'Gus' AND p.price > 10]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalFilter(condition=[AND(=($1, _UTF-16LE'Gus'), >($7, 10))])
-   +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-      :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-      :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-      :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
 Calc(select=[user_id, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id])
-   :     +- ChangelogNormalize(key=[user_id], condition=[(name = 'Gus')])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- ChangelogNormalize(key=[user_id], condition=[(name = 'Gus')])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- Calc(select=[payment_id, user_id], where=[(price > 10)])
+      +- Calc(select=[payment_id, price, user_id], where=[(price > 10)])
          +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
@@ -2194,119 +918,56 @@ LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 
 == Optimized Physical Plan ==
 Calc(select=[user_id, CAST(_UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE" AS VARCHAR(2147483647) CHARACTER SET "UTF-16LE") AS name, order_id, CAST(payment_id AS VARCHAR(2147483647) CHARACTER SET "UTF-16LE") AS payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id])
-   :     +- ChangelogNormalize(key=[user_id], condition=[=(name, _UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE")])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- ChangelogNormalize(key=[user_id], condition=[=(name, _UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE")])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- Calc(select=[payment_id, user_id], where=[>(price, 10)])
+      +- Calc(select=[payment_id, price, user_id], where=[>(price, 10)])
          +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 
 == Optimized Execution Plan ==
 Calc(select=[user_id, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id])
-   :     +- ChangelogNormalize(key=[user_id], condition=[(name = 'Gus')])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- ChangelogNormalize(key=[user_id], condition=[(name = 'Gus')])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- Calc(select=[payment_id, user_id], where=[(price > 10)])
+      +- Calc(select=[payment_id, price, user_id], where=[(price > 10)])
          +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseRelPlan">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id
-FROM Users u
-LEFT JOIN Orders o
-    ON u.user_id = o.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id
-WHERE u.name = 'Gus' AND p.price > 10]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalFilter(condition=[AND(=($1, _UTF-16LE'Gus'), >($7, 10))])
-   +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-      :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-      :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-      :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id])
-   :     +- ChangelogNormalize(key=[user_id], condition=[=(name, 'Gus')])
-   :        +- Exchange(distribution=[hash[user_id]])
-   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :  +- ChangelogNormalize(key=[user_id], condition=[=(name, 'Gus')])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    +- Exchange(distribution=[hash[user_id]])
-      +- Calc(select=[payment_id, user_id], where=[>(price, 10)])
+      +- Calc(select=[payment_id, price, user_id], where=[>(price, 10)])
          +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testTwoWayJoinWithRank">
-    <Resource name="sql">
-      <![CDATA[
-WITH JoinedEvents as (
-SELECT
-    e1.id as id,
-    e1.val,
-    e1.`$rowtime` as `$rowtime`,
-    e2.price
-FROM EventTable1 e1
-JOIN EventTable2 e2
-    ON e1.id = e2.id)
-SELECT
-    id,
-    val,
-    `$rowtime`
-FROM (
-    SELECT
-        *,
-        ROW_NUMBER() OVER (PARTITION BY id ORDER BY `$rowtime` DESC) as ts
-    FROM JoinedEvents)
-WHERE ts = 1]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(id=[$0], val=[$1], $rowtime=[$2])
-+- LogicalFilter(condition=[=($4, 1)])
-   +- LogicalProject(id=[$0], val=[$1], $rowtime=[$2], price=[$3], ts=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
-      +- LogicalProject(id=[$0], val=[$1], $rowtime=[$2], price=[$4])
-         +- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
-            :- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-            :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
-            +- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-               +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], rankRange=[rankStart=1, rankEnd=1], partitionBy=[id], orderBy=[$rowtime DESC], select=[id, val, $rowtime])
 +- Exchange(distribution=[hash[id]])
    +- Calc(select=[id, val, $rowtime])
-      +- MultiJoin(commonJoinKey=[id], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(id, id0)], select=[id,val,$rowtime,id0,price,$rowtime0], rowType=[RecordType(VARCHAR(2147483647) id, INTEGER val, TIMESTAMP(3) $rowtime, VARCHAR(2147483647) id0, DOUBLE price, TIMESTAMP(3) $rowtime0)])
+      +- MultiJoin(joinFilter=[=($0, $3)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $3)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:0;]}], select=[id,val,$rowtime,id0,price,$rowtime0], rowType=[RecordType(VARCHAR(2147483647) id, INTEGER val, TIMESTAMP(3) $rowtime, VARCHAR(2147483647) id0, DOUBLE price, TIMESTAMP(3) $rowtime0)])
          :- Exchange(distribution=[hash[id]])
          :  +- Calc(select=[id, val, CAST($rowtime AS TIMESTAMP(3)) AS $rowtime])
          :     +- WatermarkAssigner(rowtime=[$rowtime], watermark=[-($rowtime, 5000:INTERVAL SECOND)])
@@ -2319,32 +980,9 @@ Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], rankRange=[rankStart=
     </Resource>
   </TestCase>
   <TestCase name="testTwoWayJoinWithUnion">
-    <Resource name="sql">
-      <![CDATA[
-WITH OrdersUnion as (
-SELECT * FROM Orders
-UNION ALL
-SELECT * FROM Orders2
-)
-SELECT * FROM OrdersUnion o
-LEFT JOIN Users u
-    ON o.user_id = u.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(order_id=[$0], user_id=[$1], product=[$2], user_id0=[$3], name=[$4], cash=[$5])
-+- LogicalJoin(condition=[=($1, $3)], joinType=[left])
-   :- LogicalUnion(all=[true])
-   :  :- LogicalProject(order_id=[$0], user_id=[$1], product=[$2])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalProject(order_id=[$0], user_id=[$1], product=[$2])
-   :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders2]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (user_id)], joinConditions=[=(user_id, user_id0)], select=[order_id,user_id,product,user_id0,name,cash], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name, INTEGER cash)])
+MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($1, $3)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;], 1=[LeftInputId:0;LeftFieldIndex:1;RightInputId:1;RightFieldIndex:0;]}], select=[order_id,user_id,product,user_id0,name,cash], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name, INTEGER cash)])
 :- Exchange(distribution=[hash[user_id]])
 :  +- Union(all=[true], union=[order_id, user_id, product])
 :     :- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
@@ -2355,205 +993,123 @@ MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKe
     </Resource>
   </TestCase>
   <TestCase name="testWithCastCommonJoinKeyToIntegerMultiJoinChainExpected">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON o.user_id = u.user_id
-LEFT JOIN Payments p
-    ON CAST(u.user_id as INTEGER) = CAST(p.user_id as INTEGER)
-LEFT JOIN Shipments s
-    ON u.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($0, $10)], joinType=[left])
-   :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], payment_id=[$7], price=[$8], user_id2=[$9])
-   :  +- LogicalJoin(condition=[=($6, $10)], joinType=[left])
-   :     :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], user_id1=[CAST($0):INTEGER NOT NULL])
-   :     :  +- LogicalJoin(condition=[=($4, $0)], joinType=[left])
-   :     :     :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :     :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :     +- LogicalProject(payment_id=[$0], price=[$1], user_id=[$2], user_id0=[CAST($2):INTEGER])
-   :        +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id1)], select=[user_id,name,order_id,payment_id,location,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) location, VARCHAR(2147483647) user_id1)])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($0, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product,payment_id,price,user_id2,location,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(21474836 [...]
    :- Exchange(distribution=[hash[user_id]])
-   :  +- Calc(select=[user_id, name, order_id, payment_id])
-   :     +- MultiJoin(commonJoinKey=[user_id1], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[=(user_id1, user_id00)], select=[user_id,name,order_id,user_id1,payment_id,user_id00], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, INTEGER user_id1, VARCHAR(2147483647) payment_id, INTEGER user_id00)])
+   :  +- Calc(select=[user_id, name, cash, order_id, user_id0, product, payment_id, price, user_id2])
+   :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($6, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:6;], 1=[LeftInputId:0;LeftFieldIndex:6;RightInputId:1;RightFieldIndex:3;]}], select=[user_id,name,cash,order_id,user_id0,product,user_id1,payment_id,price,user_id2,user_id00], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHA [...]
    :        :- Exchange(distribution=[hash[user_id1]])
-   :        :  +- Calc(select=[user_id, name, order_id, CAST(user_id AS INTEGER) AS user_id1])
-   :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id0, user_id)], select=[user_id,name,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
+   :        :  +- Calc(select=[user_id, name, cash, order_id, user_id0, product, CAST(user_id AS INTEGER) AS user_id1])
+   :        :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($4, $0)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id,name,cash,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(21474 [...]
    :        :        :- Exchange(distribution=[hash[user_id]])
    :        :        :  +- ChangelogNormalize(key=[user_id])
    :        :        :     +- Exchange(distribution=[hash[user_id]])
-   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :        :        +- Exchange(distribution=[hash[user_id]])
-   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :        +- Exchange(distribution=[hash[user_id0]])
-   :           +- Calc(select=[payment_id, CAST(user_id AS INTEGER) AS user_id0])
-   :              +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   :           +- Calc(select=[payment_id, price, user_id, CAST(user_id AS INTEGER) AS user_id0])
+   :              +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
       +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testWithCastCommonJoinKeyToVarchar">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON o.user_id = u.user_id
-LEFT JOIN Payments p
-    ON CAST(u.user_id as VARCHAR) = CAST(p.user_id as VARCHAR)
-LEFT JOIN Shipments s
-    ON u.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($0, $10)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($4, $0)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id0, user_id), =(user_id, user_id1), =(user_id, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($4, $0), =($0, $8), =($0, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex: [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
       +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testWithExpressionInJoinConditionMultiJoinChainExpected">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    p.product_id,
-    (p.price - p.discount) AS net_price,
-    s.quantity,
-    pr.promo_text
-FROM Products AS p
-LEFT JOIN Sales AS s
-    ON (p.price - p.discount) = s.product_key
-LEFT JOIN Promotions AS pr
-    ON (p.price - p.discount) = pr.product_key
-WHERE (p.price - p.discount) > 100]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(product_id=[$0], net_price=[-($1, $2)], quantity=[$5], promo_text=[$8])
-+- LogicalFilter(condition=[>(-($1, $2), 100)])
-   +- LogicalProject(product_id=[$0], price=[$1], discount=[$2], sale_id=[$3], product_key=[$4], quantity=[$5], promo_id=[$7], product_key0=[$8], promo_text=[$9])
-      +- LogicalJoin(condition=[=($6, $8)], joinType=[left])
-         :- LogicalProject(product_id=[$0], price=[$1], discount=[$2], sale_id=[$3], product_key=[$4], quantity=[$5], $f6=[-($1, $2)])
-         :  +- LogicalProject(product_id=[$0], price=[$1], discount=[$2], sale_id=[$4], product_key=[$5], quantity=[$6])
-         :     +- LogicalJoin(condition=[=($3, $5)], joinType=[left])
-         :        :- LogicalProject(product_id=[$0], price=[$1], discount=[$2], $f3=[-($1, $2)])
-         :        :  +- LogicalTableScan(table=[[default_catalog, default_database, Products]])
-         :        +- LogicalTableScan(table=[[default_catalog, default_database, Sales]])
-         +- LogicalTableScan(table=[[default_catalog, default_database, Promotions]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[product_id, -(price, discount) AS net_price, quantity, promo_text])
-+- MultiJoin(commonJoinKey=[$f6], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=($f6, product_key0)], select=[product_id,price,discount,quantity,$f6,product_key0,promo_text], rowType=[RecordType(VARCHAR(2147483647) product_id, DOUBLE price, DOUBLE discount, INTEGER quantity, DOUBLE $f6, DOUBLE product_key0, VARCHAR(2147483647) promo_text)])
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($6, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:6;], 1=[LeftInputId:0;LeftFieldIndex:6;RightInputId:1;RightFieldIndex:1;]}], select=[product_id,price,discount,sale_id,product_key,quantity,$f6,promo_id,product_key0,promo_text], rowType=[RecordType(VARCHAR(2147483647) product_id, DOUBLE price, DOUBLE discount, VARCHAR(2147483647) sale_id, DOUBLE product_key,  [...]
    :- Exchange(distribution=[hash[$f6]])
-   :  +- Calc(select=[product_id, price, discount, quantity, -(price, discount) AS $f6])
-   :     +- MultiJoin(commonJoinKey=[$f3], joinTypes=[LEFT], inputUniqueKeys=[(product_id), noUniqueKey], joinConditions=[=($f3, product_key)], select=[product_id,price,discount,$f3,product_key,quantity], rowType=[RecordType(VARCHAR(2147483647) product_id, DOUBLE price, DOUBLE discount, DOUBLE $f3, DOUBLE product_key, INTEGER quantity)])
+   :  +- Calc(select=[product_id, price, discount, sale_id, product_key, quantity, -(price, discount) AS $f6])
+   :     +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($3, $5)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:3;], 1=[LeftInputId:0;LeftFieldIndex:3;RightInputId:1;RightFieldIndex:1;]}], select=[product_id,price,discount,$f3,sale_id,product_key,quantity], rowType=[RecordType(VARCHAR(2147483647) product_id, DOUBLE price, DOUBLE discount, DOUBLE $f3, VARCHAR(2147483647) sale_id, DOUBLE product_key, INTEGER quantity)])
    :        :- Exchange(distribution=[hash[$f3]])
    :        :  +- Calc(select=[product_id, price, discount, -(price, discount) AS $f3])
    :        :     +- ChangelogNormalize(key=[product_id], condition=[>(-(price, discount), 100)])
    :        :        +- Exchange(distribution=[hash[product_id]])
    :        :           +- TableSourceScan(table=[[default_catalog, default_database, Products, filter=[]]], fields=[product_id, price, discount])
    :        +- Exchange(distribution=[hash[product_key]])
-   :           +- Calc(select=[product_key, quantity])
-   :              +- ChangelogNormalize(key=[sale_id])
-   :                 +- Exchange(distribution=[hash[sale_id]])
-   :                    +- TableSourceScan(table=[[default_catalog, default_database, Sales]], fields=[sale_id, product_key, quantity])
+   :           +- ChangelogNormalize(key=[sale_id])
+   :              +- Exchange(distribution=[hash[sale_id]])
+   :                 +- TableSourceScan(table=[[default_catalog, default_database, Sales]], fields=[sale_id, product_key, quantity])
    +- Exchange(distribution=[hash[product_key]])
-      +- Calc(select=[product_key, promo_text])
-         +- ChangelogNormalize(key=[promo_id])
-            +- Exchange(distribution=[hash[promo_id]])
-               +- TableSourceScan(table=[[default_catalog, default_database, Promotions]], fields=[promo_id, product_key, promo_text])
+      +- ChangelogNormalize(key=[promo_id])
+         +- Exchange(distribution=[hash[promo_id]])
+            +- TableSourceScan(table=[[default_catalog, default_database, Promotions]], fields=[promo_id, product_key, promo_text])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testWithOrInJoinCondition">
-    <Resource name="sql">
-      <![CDATA[
-SELECT
-    u.user_id,
-    u.name,
-    o.order_id,
-    p.payment_id,
-    s.location
-FROM Users u
-LEFT JOIN Orders o
-    ON o.user_id = u.user_id
-LEFT JOIN Payments p
-    ON u.user_id = p.user_id OR u.name = p.payment_id
-LEFT JOIN Shipments s
-    ON p.user_id = s.user_id]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($8, $10)], joinType=[left])
-   :- LogicalJoin(condition=[OR(=($0, $8), =($1, $6))], joinType=[left])
-   :  :- LogicalJoin(condition=[=($4, $0)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
-]]>
-    </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
 Calc(select=[user_id, name, order_id, payment_id, location])
-+- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id0, user_id), OR(=(user_id, user_id1), =(name, payment_id)), =(user_id1, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, V [...]
++- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($4, $0), OR(=($0, $8), =($1, $6)), =($8, $10)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:1;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;R [...]
    :- Exchange(distribution=[hash[user_id]])
    :  +- ChangelogNormalize(key=[user_id])
    :     +- Exchange(distribution=[hash[user_id]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
    :- Exchange(distribution=[hash[user_id]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
    +- Exchange(distribution=[hash[user_id]])
       +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testMultiSinkOnMultiJoinedView">
+    <Resource name="optimized rel plan with advice">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.sink1], fields=[a, day, EXPR$2, EXPR$3])
++- GroupAggregate(advice=[1], groupBy=[a, day], select=[a, day, SUM_RETRACT(b) AS EXPR$2, COUNT_RETRACT(DISTINCT c) AS EXPR$3])
+   +- Exchange(distribution=[hash[a, day]])
+      +- Calc(select=[a, day, b0 AS b, c])
+         +- MultiJoin(joinFilter=[=($0, $6)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $6)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:3;]}], select=[a,b,day,b0,day0,c,d], rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) day0, VARCHAR(2147483647) c, INTEGER d)])
+            :- Exchange(distribution=[hash[a]])
+            :  +- Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
+            :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a, b], metadata=[]]], fields=[a, b])
+            +- Exchange(distribution=[hash[d]])
+               +- Calc(select=[b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd')) AS day, c, d])
+                  +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
+
+Sink(table=[default_catalog.default_database.sink2], fields=[a, day, b, c])
++- Calc(select=[a, day, b0 AS b, c])
+   +- MultiJoin(joinFilter=[=($0, $6)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $6)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:3;]}], select=[a,b,day,b0,day0,c,d], rowType=[RecordType(INTEGER a, BIGINT b, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) day0, VARCHAR(2147483647) c, INTEGER d)])
+      :- Exchange(distribution=[hash[a]])
+      :  +- Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
+      :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a, b], metadata=[]]], fields=[a, b])
+      +- Exchange(distribution=[hash[d]])
+         +- Calc(select=[b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd')) AS day, c, d], where=[>(b, 100)])
+            +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
+
+advice[1]: [ADVICE] You might want to enable local-global two-phase optimization by configuring ('table.exec.mini-batch.enabled' to 'true', 'table.exec.mini-batch.allow-latency' to a positive long value, 'table.exec.mini-batch.size' to a positive long value).
+advice[2]: [WARNING] The column(s): day(generated by non-deterministic function: CURRENT_TIMESTAMP ) can not satisfy the determinism requirement for correctly processing update message('UB'/'UA'/'D' in changelogMode, not 'I' only), this usually happens when input node has no upsertKey(upsertKeys=[{}]) or current node outputs non-deterministic update messages. Please consider removing these non-deterministic columns or making them deterministic by using deterministic functions.
+
+related rel plan:
+Calc(select=[a, b, DATE_FORMAT(CURRENT_TIMESTAMP(), _UTF-16LE'yyMMdd') AS day], changelogMode=[I,UB,UA,D])
++- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a, b], metadata=[]]], fields=[a, b], changelogMode=[I,UB,UA,D])
+
+
 ]]>
     </Resource>
   </TestCase>
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json
index 8a415d1da8c..3c6ebe65a68 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/plan/four-way-complex-updating-join-with-restore.json
@@ -1,7 +1,7 @@
 {
   "flinkVersion" : "2.1",
   "nodes" : [ {
-    "id" : 19,
+    "id" : 25,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -30,7 +30,7 @@
     "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[name, user_id_0, cash])"
   }, {
-    "id" : 20,
+    "id" : 26,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -43,7 +43,7 @@
     "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
     "description" : "Exchange(distribution=[hash[user_id_0]])"
   }, {
-    "id" : 21,
+    "id" : 27,
     "type" : "stream-exec-changelog-normalize_1",
     "configuration" : {
       "table.exec.mini-batch.enabled" : "false",
@@ -66,7 +66,7 @@
     "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
     "description" : "ChangelogNormalize(key=[user_id_0])"
   }, {
-    "id" : 22,
+    "id" : 28,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -79,7 +79,7 @@
     "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
     "description" : "Exchange(distribution=[hash[user_id_0]])"
   }, {
-    "id" : 23,
+    "id" : 29,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -103,25 +103,113 @@
             }
           }
         }
-      }
+      },
+      "abilities" : [ {
+        "type" : "ProjectPushDown",
+        "projectedFields" : [ [ 0 ], [ 2 ] ],
+        "producedType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)> NOT NULL"
+      }, {
+        "type" : "ReadingMetadata",
+        "metadataKeys" : [ ],
+        "producedType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)> NOT NULL"
+      } ]
     },
-    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647)>",
-    "description" : "TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, product, user_id_1])"
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)>",
+    "description" : "TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id_1], metadata=[]]], fields=[order_id, user_id_1])"
   }, {
-    "id" : 24,
+    "id" : 30,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
         "type" : "HASH",
-        "keys" : [ 2 ]
+        "keys" : [ 1 ]
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647)>",
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id_1]])"
   }, {
-    "id" : 25,
+    "id" : 31,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "LEFT",
+      "leftKeys" : [ 1 ],
+      "rightKeys" : [ 1 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
+    },
+    "leftUpsertKeys" : [ [ 1 ] ],
+    "rightUpsertKeys" : [ [ 0 ] ],
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "leftState"
+    }, {
+      "index" : 1,
+      "ttl" : "0 ms",
+      "name" : "rightState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    }, {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[LeftOuterJoin], where=[(user_id_0 = user_id_1)], select=[name, user_id_0, cash, order_id, user_id_1], leftInputSpec=[JoinKeyContainsUniqueKey], rightInputSpec=[HasUniqueKey])"
+  }, {
+    "id" : 32,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 0,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 1,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 2,
+      "type" : "INT"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 3,
+      "type" : "VARCHAR(2147483647)"
+    } ],
+    "condition" : null,
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647)>",
+    "description" : "Calc(select=[name, user_id_0, cash, order_id])"
+  }, {
+    "id" : 33,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id_0]])"
+  }, {
+    "id" : 34,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -150,7 +238,7 @@
     "outputType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[user_id_2, payment_id, price])"
   }, {
-    "id" : 26,
+    "id" : 35,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -163,149 +251,14 @@
     "outputType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
     "description" : "Exchange(distribution=[hash[user_id_2]])"
   }, {
-    "id" : 27,
-    "type" : "stream-exec-table-source-scan_1",
-    "scanTableSource" : {
-      "table" : {
-        "identifier" : "`default_catalog`.`default_database`.`Shipments`",
-        "resolvedTable" : {
-          "schema" : {
-            "columns" : [ {
-              "name" : "location",
-              "dataType" : "VARCHAR(2147483647)"
-            }, {
-              "name" : "user_id_3",
-              "dataType" : "VARCHAR(2147483647)"
-            } ]
-          }
-        }
-      }
-    },
-    "outputType" : "ROW<`location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
-    "description" : "TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])"
-  }, {
-    "id" : 28,
-    "type" : "stream-exec-exchange_1",
-    "inputProperties" : [ {
-      "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 1 ]
-      },
-      "damBehavior" : "PIPELINED",
-      "priority" : 0
-    } ],
-    "outputType" : "ROW<`location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
-    "description" : "Exchange(distribution=[hash[user_id_3]])"
-  }, {
-    "id" : 29,
-    "type" : "stream-exec-multi-join_1",
-    "joinTypes" : [ "INNER", "LEFT", "INNER", "LEFT" ],
-    "joinConditions" : [ {
-      "kind" : "LITERAL",
-      "value" : true,
-      "type" : "BOOLEAN NOT NULL"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 1,
-        "type" : "VARCHAR(2147483647) NOT NULL"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 5,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$AND$1",
-      "operands" : [ {
-        "kind" : "CALL",
-        "syntax" : "BINARY",
-        "internalName" : "$=$1",
-        "operands" : [ {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 1,
-          "type" : "VARCHAR(2147483647) NOT NULL"
-        }, {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 6,
-          "type" : "VARCHAR(2147483647)"
-        } ],
-        "type" : "BOOLEAN"
-      }, {
-        "kind" : "CALL",
-        "syntax" : "BINARY",
-        "internalName" : "$OR$1",
-        "operands" : [ {
-          "kind" : "CALL",
-          "syntax" : "BINARY",
-          "internalName" : "$>=$1",
-          "operands" : [ {
-            "kind" : "INPUT_REF",
-            "inputIndex" : 2,
-            "type" : "INT"
-          }, {
-            "kind" : "INPUT_REF",
-            "inputIndex" : 8,
-            "type" : "INT"
-          } ],
-          "type" : "BOOLEAN"
-        }, {
-          "kind" : "CALL",
-          "syntax" : "BINARY",
-          "internalName" : "$<$1",
-          "operands" : [ {
-            "kind" : "INPUT_REF",
-            "inputIndex" : 8,
-            "type" : "INT"
-          }, {
-            "kind" : "LITERAL",
-            "value" : 0,
-            "type" : "INT NOT NULL"
-          } ],
-          "type" : "BOOLEAN"
-        } ],
-        "type" : "BOOLEAN"
-      } ],
-      "type" : "BOOLEAN"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 6,
-        "type" : "VARCHAR(2147483647)"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 10,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    } ],
-    "multiJoinCondition" : {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$AND$1",
-      "operands" : [ {
-        "kind" : "CALL",
-        "syntax" : "BINARY",
-        "internalName" : "$=$1",
-        "operands" : [ {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 1,
-          "type" : "VARCHAR(2147483647) NOT NULL"
-        }, {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 6,
-          "type" : "VARCHAR(2147483647)"
-        } ],
-        "type" : "BOOLEAN"
-      }, {
+    "id" : 36,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "INNER",
+      "leftKeys" : [ 1 ],
+      "rightKeys" : [ 0 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : {
         "kind" : "CALL",
         "syntax" : "BINARY",
         "internalName" : "$OR$1",
@@ -319,7 +272,7 @@
             "type" : "INT"
           }, {
             "kind" : "INPUT_REF",
-            "inputIndex" : 8,
+            "inputIndex" : 6,
             "type" : "INT"
           } ],
           "type" : "BOOLEAN"
@@ -329,7 +282,7 @@
           "internalName" : "$<$1",
           "operands" : [ {
             "kind" : "INPUT_REF",
-            "inputIndex" : 8,
+            "inputIndex" : 6,
             "type" : "INT"
           }, {
             "kind" : "LITERAL",
@@ -339,80 +292,150 @@
           "type" : "BOOLEAN"
         } ],
         "type" : "BOOLEAN"
-      } ],
-      "type" : "BOOLEAN"
-    },
-    "joinAttributeMap" : {
-      "1" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 1,
-        "rightInputId" : 1,
-        "rightFieldIndex" : 2
-      } ],
-      "2" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 1,
-        "rightInputId" : 2,
-        "rightFieldIndex" : 0
-      } ],
-      "3" : [ {
-        "leftInputId" : 2,
-        "leftFieldIndex" : 0,
-        "rightInputId" : 3,
-        "rightFieldIndex" : 1
-      } ]
+      }
     },
-    "inputUniqueKeys" : [ [ [ 1 ] ], [ [ 0 ] ], [ [ 1 ] ], [ ] ],
+    "rightUpsertKeys" : [ [ 1 ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
-      "name" : "input-state-0"
+      "name" : "leftState"
     }, {
       "index" : 1,
       "ttl" : "0 ms",
-      "name" : "input-state-1"
+      "name" : "rightState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
     }, {
-      "index" : 2,
-      "ttl" : "0 ms",
-      "name" : "input-state-2"
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
+    "description" : "Join(joinType=[InnerJoin], where=[((user_id_0 = user_id_2) AND ((cash >= price) OR (price < 0)))], select=[name, user_id_0, cash, order_id, user_id_2, payment_id, price], leftInputSpec=[NoUniqueKey], rightInputSpec=[HasUniqueKey])"
+  }, {
+    "id" : 37,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 0,
+      "type" : "VARCHAR(2147483647)"
     }, {
-      "index" : 3,
-      "ttl" : "0 ms",
-      "name" : "input-state-3"
+      "kind" : "INPUT_REF",
+      "inputIndex" : 1,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 3,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 4,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 5,
+      "type" : "VARCHAR(2147483647) NOT NULL"
     } ],
+    "condition" : null,
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
+    "description" : "Calc(select=[name, user_id_0, order_id, user_id_2, payment_id])"
+  }, {
+    "id" : 38,
+    "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
         "type" : "HASH",
-        "keys" : [ 1 ]
+        "keys" : [ 3 ]
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
-    }, {
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
+    "description" : "Exchange(distribution=[hash[user_id_2]])"
+  }, {
+    "id" : 39,
+    "type" : "stream-exec-table-source-scan_1",
+    "scanTableSource" : {
+      "table" : {
+        "identifier" : "`default_catalog`.`default_database`.`Shipments`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "location",
+              "dataType" : "VARCHAR(2147483647)"
+            }, {
+              "name" : "user_id_3",
+              "dataType" : "VARCHAR(2147483647)"
+            } ]
+          }
+        }
+      }
+    },
+    "outputType" : "ROW<`location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
+    "description" : "TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])"
+  }, {
+    "id" : 40,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
       "requiredDistribution" : {
         "type" : "HASH",
-        "keys" : [ 2 ]
+        "keys" : [ 1 ]
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 1
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id_3]])"
+  }, {
+    "id" : 41,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "LEFT",
+      "leftKeys" : [ 3 ],
+      "rightKeys" : [ 1 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
+    },
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "leftState"
     }, {
+      "index" : 1,
+      "ttl" : "0 ms",
+      "name" : "rightState"
+    } ],
+    "inputProperties" : [ {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 2
+      "priority" : 0
     }, {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 1 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 3
+      "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647), `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT, `location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
-    "description" : "MultiJoin(joinFilter=[AND(=($1, $6), OR(>=($2, $8), <($8, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($1, $5), AND(=($1, $6), OR(>=($2, $8), <($8, 0))), =($6, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:1;RightInputId:1;RightFieldIndex:2;], 2=[LeftInputId:0;LeftFieldIndex:1;RightInputId:2;RightFieldIndex:0;], 3=[LeftInputId:2;LeftFieldIndex:0;RightInputId:3;RightFieldIndex:1;]}], select=[name,user_id_0,cash,order_id,product, [...]
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[LeftOuterJoin], where=[(user_id_2 = user_id_3)], select=[name, user_id_0, order_id, user_id_2, payment_id, location, user_id_3], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey])"
   }, {
-    "id" : 30,
+    "id" : 42,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -424,15 +447,15 @@
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 3,
+      "inputIndex" : 2,
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 7,
+      "inputIndex" : 4,
       "type" : "VARCHAR(2147483647) NOT NULL"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 9,
+      "inputIndex" : 5,
       "type" : "VARCHAR(2147483647)"
     } ],
     "condition" : null,
@@ -446,7 +469,7 @@
     "outputType" : "ROW<`user_id_0` VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647)>",
     "description" : "Calc(select=[user_id_0, name, order_id, payment_id, location])"
   }, {
-    "id" : 31,
+    "id" : 43,
     "type" : "stream-exec-sink_1",
     "configuration" : {
       "table.exec.sink.keyed-shuffle" : "AUTO",
@@ -492,88 +515,130 @@
     "description" : "Sink(table=[default_catalog.default_database.sink], fields=[user_id_0, name, order_id, payment_id, location])"
   } ],
   "edges" : [ {
-    "source" : 19,
-    "target" : 20,
+    "source" : 25,
+    "target" : 26,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 20,
-    "target" : 21,
+    "source" : 26,
+    "target" : 27,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 21,
-    "target" : 22,
+    "source" : 27,
+    "target" : 28,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 23,
-    "target" : 24,
+    "source" : 29,
+    "target" : 30,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 25,
-    "target" : 26,
+    "source" : 28,
+    "target" : 31,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 27,
-    "target" : 28,
+    "source" : 30,
+    "target" : 31,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 22,
-    "target" : 29,
+    "source" : 31,
+    "target" : 32,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 24,
-    "target" : 29,
+    "source" : 32,
+    "target" : 33,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 26,
-    "target" : 29,
+    "source" : 34,
+    "target" : 35,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 28,
-    "target" : 29,
+    "source" : 33,
+    "target" : 36,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 29,
-    "target" : 30,
+    "source" : 35,
+    "target" : 36,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 30,
-    "target" : 31,
+    "source" : 36,
+    "target" : 37,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 37,
+    "target" : 38,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 39,
+    "target" : 40,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 38,
+    "target" : 41,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 40,
+    "target" : 41,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 41,
+    "target" : 42,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 42,
+    "target" : 43,
     "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-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata
index 061d0b08163..cd2f1cf00c9 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-complex-updating-join-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json
index 4b020388a61..72047cabe92 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/plan/four-way-join-no-common-join-key-with-restore.json
@@ -1,7 +1,7 @@
 {
   "flinkVersion" : "2.1",
   "nodes" : [ {
-    "id" : 32,
+    "id" : 44,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -25,12 +25,21 @@
             }
           }
         }
-      }
+      },
+      "abilities" : [ {
+        "type" : "ProjectPushDown",
+        "projectedFields" : [ [ 0 ], [ 1 ] ],
+        "producedType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL> NOT NULL"
+      }, {
+        "type" : "ReadingMetadata",
+        "metadataKeys" : [ ],
+        "producedType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL> NOT NULL"
+      } ]
     },
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
-    "description" : "TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[name, user_id_0, cash])"
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL>",
+    "description" : "TableSourceScan(table=[[default_catalog, default_database, Users, project=[name, user_id_0], metadata=[]]], fields=[name, user_id_0])"
   }, {
-    "id" : 33,
+    "id" : 45,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -40,10 +49,10 @@
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL>",
     "description" : "Exchange(distribution=[hash[user_id_0]])"
   }, {
-    "id" : 34,
+    "id" : 46,
     "type" : "stream-exec-changelog-normalize_1",
     "configuration" : {
       "table.exec.mini-batch.enabled" : "false",
@@ -63,10 +72,10 @@
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL>",
     "description" : "ChangelogNormalize(key=[user_id_0])"
   }, {
-    "id" : 35,
+    "id" : 47,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -76,10 +85,10 @@
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT>",
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL>",
     "description" : "Exchange(distribution=[hash[user_id_0]])"
   }, {
-    "id" : 36,
+    "id" : 48,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -103,25 +112,109 @@
             }
           }
         }
-      }
+      },
+      "abilities" : [ {
+        "type" : "ProjectPushDown",
+        "projectedFields" : [ [ 0 ], [ 2 ] ],
+        "producedType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)> NOT NULL"
+      }, {
+        "type" : "ReadingMetadata",
+        "metadataKeys" : [ ],
+        "producedType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)> NOT NULL"
+      } ]
     },
-    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647)>",
-    "description" : "TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, product, user_id_1])"
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)>",
+    "description" : "TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id_1], metadata=[]]], fields=[order_id, user_id_1])"
   }, {
-    "id" : 37,
+    "id" : 49,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
         "type" : "HASH",
-        "keys" : [ 2 ]
+        "keys" : [ 1 ]
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647)>",
+    "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id_1]])"
   }, {
-    "id" : 38,
+    "id" : 50,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "LEFT",
+      "leftKeys" : [ 1 ],
+      "rightKeys" : [ 1 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
+    },
+    "leftUpsertKeys" : [ [ 1 ] ],
+    "rightUpsertKeys" : [ [ 0 ] ],
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "leftState"
+    }, {
+      "index" : 1,
+      "ttl" : "0 ms",
+      "name" : "rightState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    }, {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[LeftOuterJoin], where=[(user_id_0 = user_id_1)], select=[name, user_id_0, order_id, user_id_1], leftInputSpec=[JoinKeyContainsUniqueKey], rightInputSpec=[HasUniqueKey])"
+  }, {
+    "id" : 51,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 0,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 1,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 2,
+      "type" : "VARCHAR(2147483647)"
+    } ],
+    "condition" : null,
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647)>",
+    "description" : "Calc(select=[name, user_id_0, order_id])"
+  }, {
+    "id" : 52,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 1 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id_0]])"
+  }, {
+    "id" : 53,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -145,12 +238,21 @@
             }
           }
         }
-      }
+      },
+      "abilities" : [ {
+        "type" : "ProjectPushDown",
+        "projectedFields" : [ [ 0 ], [ 1 ] ],
+        "producedType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL> NOT NULL"
+      }, {
+        "type" : "ReadingMetadata",
+        "metadataKeys" : [ ],
+        "producedType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL> NOT NULL"
+      } ]
     },
-    "outputType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
-    "description" : "TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[user_id_2, payment_id, price])"
+    "outputType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
+    "description" : "TableSourceScan(table=[[default_catalog, default_database, Payments, project=[user_id_2, payment_id], metadata=[]]], fields=[user_id_2, payment_id])"
   }, {
-    "id" : 39,
+    "id" : 54,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -160,127 +262,88 @@
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
+    "outputType" : "ROW<`user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
     "description" : "Exchange(distribution=[hash[user_id_2]])"
   }, {
-    "id" : 40,
-    "type" : "stream-exec-multi-join_1",
-    "joinTypes" : [ "INNER", "LEFT", "INNER" ],
-    "joinConditions" : [ {
-      "kind" : "LITERAL",
-      "value" : true,
-      "type" : "BOOLEAN NOT NULL"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 1,
-        "type" : "VARCHAR(2147483647) NOT NULL"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 5,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 1,
-        "type" : "VARCHAR(2147483647) NOT NULL"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 6,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    } ],
-    "multiJoinCondition" : {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 1,
-        "type" : "VARCHAR(2147483647) NOT NULL"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 6,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
+    "id" : 55,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "INNER",
+      "leftKeys" : [ 1 ],
+      "rightKeys" : [ 0 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
     },
-    "joinAttributeMap" : {
-      "1" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 1,
-        "rightInputId" : 1,
-        "rightFieldIndex" : 2
-      } ],
-      "2" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 1,
-        "rightInputId" : 2,
-        "rightFieldIndex" : 0
-      } ]
-    },
-    "inputUniqueKeys" : [ [ [ 1 ] ], [ [ 0 ] ], [ [ 1 ] ] ],
+    "rightUpsertKeys" : [ [ 1 ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
-      "name" : "input-state-0"
+      "name" : "leftState"
     }, {
       "index" : 1,
       "ttl" : "0 ms",
-      "name" : "input-state-1"
-    }, {
-      "index" : 2,
-      "ttl" : "0 ms",
-      "name" : "input-state-2"
+      "name" : "rightState"
     } ],
     "inputProperties" : [ {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 1 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     }, {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 2 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 1
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
+    "description" : "Join(joinType=[InnerJoin], where=[(user_id_0 = user_id_2)], select=[name, user_id_0, order_id, user_id_2, payment_id], leftInputSpec=[NoUniqueKey], rightInputSpec=[HasUniqueKey])"
+  }, {
+    "id" : 56,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 0,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 1,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 2,
+      "type" : "VARCHAR(2147483647)"
     }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 4,
+      "type" : "VARCHAR(2147483647) NOT NULL"
+    } ],
+    "condition" : null,
+    "inputProperties" : [ {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 2
+      "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647), `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
-    "description" : "MultiJoin(joinFilter=[=($1, $6)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($1, $5), =($1, $6)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:1;RightInputId:1;RightFieldIndex:2;], 2=[LeftInputId:0;LeftFieldIndex:1;RightInputId:2;RightFieldIndex:0;]}], select=[name,user_id_0,cash,order_id,product,user_id_1,user_id_2,payment_id,price], rowType=[RecordType(VARCHAR(2147483647) name, VARCHAR(2147483647) user_id_0, INTEGER cash, VARCHAR(2147483647)  [...]
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
+    "description" : "Calc(select=[name, user_id_0, order_id, payment_id])"
   }, {
-    "id" : 41,
+    "id" : 57,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
         "type" : "HASH",
-        "keys" : [ 7 ]
+        "keys" : [ 3 ]
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647), `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT>",
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL>",
     "description" : "Exchange(distribution=[hash[payment_id]])"
   }, {
-    "id" : 42,
+    "id" : 58,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -301,7 +364,7 @@
     "outputType" : "ROW<`location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])"
   }, {
-    "id" : 43,
+    "id" : 59,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -314,65 +377,41 @@
     "outputType" : "ROW<`location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id_3]])"
   }, {
-    "id" : 44,
-    "type" : "stream-exec-multi-join_1",
-    "joinTypes" : [ "INNER", "LEFT" ],
-    "joinConditions" : [ {
-      "kind" : "LITERAL",
-      "value" : true,
-      "type" : "BOOLEAN NOT NULL"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 7,
-        "type" : "VARCHAR(2147483647) NOT NULL"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 10,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    } ],
-    "joinAttributeMap" : {
-      "1" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 7,
-        "rightInputId" : 1,
-        "rightFieldIndex" : 1
-      } ]
+    "id" : 60,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "LEFT",
+      "leftKeys" : [ 3 ],
+      "rightKeys" : [ 1 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
     },
-    "inputUniqueKeys" : [ [ ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
-      "name" : "input-state-0"
+      "name" : "leftState"
     }, {
       "index" : 1,
       "ttl" : "0 ms",
-      "name" : "input-state-1"
+      "name" : "rightState"
     } ],
     "inputProperties" : [ {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 7 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     }, {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 1 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 1
+      "priority" : 0
     } ],
-    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `cash` INT, `order_id` VARCHAR(2147483647), `product` VARCHAR(2147483647), `user_id_1` VARCHAR(2147483647), `user_id_2` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT, `location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
-    "description" : "MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($7, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:7;RightInputId:1;RightFieldIndex:1;]}], select=[name,user_id_0,cash,order_id,product,user_id_1,user_id_2,payment_id,price,location,user_id_3], rowType=[RecordType(VARCHAR(2147483647) name, VARCHAR(2147483647) user_id_0, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id_1, VAR [...]
+    "outputType" : "ROW<`name` VARCHAR(2147483647), `user_id_0` VARCHAR(2147483647) NOT NULL, `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647), `user_id_3` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[LeftOuterJoin], where=[(payment_id = user_id_3)], select=[name, user_id_0, order_id, payment_id, location, user_id_3], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey])"
   }, {
-    "id" : 45,
+    "id" : 61,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -384,15 +423,15 @@
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 3,
+      "inputIndex" : 2,
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 7,
+      "inputIndex" : 3,
       "type" : "VARCHAR(2147483647) NOT NULL"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 9,
+      "inputIndex" : 4,
       "type" : "VARCHAR(2147483647)"
     } ],
     "condition" : null,
@@ -406,7 +445,7 @@
     "outputType" : "ROW<`user_id_0` VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647) NOT NULL, `location` VARCHAR(2147483647)>",
     "description" : "Calc(select=[user_id_0, name, order_id, payment_id, location])"
   }, {
-    "id" : 46,
+    "id" : 62,
     "type" : "stream-exec-sink_1",
     "configuration" : {
       "table.exec.sink.keyed-shuffle" : "AUTO",
@@ -452,102 +491,130 @@
     "description" : "Sink(table=[default_catalog.default_database.sink], fields=[user_id_0, name, order_id, payment_id, location])"
   } ],
   "edges" : [ {
-    "source" : 32,
-    "target" : 33,
+    "source" : 44,
+    "target" : 45,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 33,
-    "target" : 34,
+    "source" : 45,
+    "target" : 46,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 34,
-    "target" : 35,
+    "source" : 46,
+    "target" : 47,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 36,
-    "target" : 37,
+    "source" : 48,
+    "target" : 49,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 38,
-    "target" : 39,
+    "source" : 47,
+    "target" : 50,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 35,
-    "target" : 40,
+    "source" : 49,
+    "target" : 50,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 37,
-    "target" : 40,
+    "source" : 50,
+    "target" : 51,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 39,
-    "target" : 40,
+    "source" : 51,
+    "target" : 52,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 40,
-    "target" : 41,
+    "source" : 53,
+    "target" : 54,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 42,
-    "target" : 43,
+    "source" : 52,
+    "target" : 55,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 41,
-    "target" : 44,
+    "source" : 54,
+    "target" : 55,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 43,
-    "target" : 44,
+    "source" : 55,
+    "target" : 56,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 44,
-    "target" : 45,
+    "source" : 56,
+    "target" : 57,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 45,
-    "target" : 46,
+    "source" : 58,
+    "target" : 59,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 57,
+    "target" : 60,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 59,
+    "target" : 60,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 60,
+    "target" : 61,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 61,
+    "target" : 62,
     "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-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata
index f53211043d6..6b5de7c2a7d 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/four-way-join-no-common-join-key-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json
index 3b650218933..155df3b6522 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/plan/three-way-inner-join-with-restore.json
@@ -70,6 +70,79 @@
     "description" : "Exchange(distribution=[hash[user_id]])"
   }, {
     "id" : 5,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "INNER",
+      "leftKeys" : [ 0 ],
+      "rightKeys" : [ 0 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
+    },
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "leftState"
+    }, {
+      "index" : 1,
+      "ttl" : "0 ms",
+      "name" : "rightState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    }, {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `user_id0` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[InnerJoin], where=[(user_id = user_id0)], select=[user_id, name, user_id0, order_id], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey])"
+  }, {
+    "id" : 6,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 0,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 1,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 3,
+      "type" : "VARCHAR(2147483647)"
+    } ],
+    "condition" : null,
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
+    "description" : "Calc(select=[user_id, name, order_id])"
+  }, {
+    "id" : 7,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id]])"
+  }, {
+    "id" : 8,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -90,7 +163,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[user_id, payment_id])"
   }, {
-    "id" : 6,
+    "id" : 9,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -103,131 +176,41 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id]])"
   }, {
-    "id" : 7,
-    "type" : "stream-exec-multi-join_1",
-    "joinTypes" : [ "INNER", "INNER", "INNER" ],
-    "joinConditions" : [ {
-      "kind" : "LITERAL",
-      "value" : true,
-      "type" : "BOOLEAN NOT NULL"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 0,
-        "type" : "VARCHAR(2147483647)"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 2,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 0,
-        "type" : "VARCHAR(2147483647)"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 4,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    } ],
-    "multiJoinCondition" : {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$AND$1",
-      "operands" : [ {
-        "kind" : "CALL",
-        "syntax" : "BINARY",
-        "internalName" : "$=$1",
-        "operands" : [ {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 0,
-          "type" : "VARCHAR(2147483647)"
-        }, {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 4,
-          "type" : "VARCHAR(2147483647)"
-        } ],
-        "type" : "BOOLEAN"
-      }, {
-        "kind" : "CALL",
-        "syntax" : "BINARY",
-        "internalName" : "$=$1",
-        "operands" : [ {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 0,
-          "type" : "VARCHAR(2147483647)"
-        }, {
-          "kind" : "INPUT_REF",
-          "inputIndex" : 2,
-          "type" : "VARCHAR(2147483647)"
-        } ],
-        "type" : "BOOLEAN"
-      } ],
-      "type" : "BOOLEAN"
-    },
-    "joinAttributeMap" : {
-      "1" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 0,
-        "rightInputId" : 1,
-        "rightFieldIndex" : 0
-      } ],
-      "2" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 0,
-        "rightInputId" : 2,
-        "rightFieldIndex" : 0
-      } ]
+    "id" : 10,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "INNER",
+      "leftKeys" : [ 0 ],
+      "rightKeys" : [ 0 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
     },
-    "inputUniqueKeys" : [ [ ], [ ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
-      "name" : "input-state-0"
+      "name" : "leftState"
     }, {
       "index" : 1,
       "ttl" : "0 ms",
-      "name" : "input-state-1"
-    }, {
-      "index" : 2,
-      "ttl" : "0 ms",
-      "name" : "input-state-2"
+      "name" : "rightState"
     } ],
     "inputProperties" : [ {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     }, {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
-      },
-      "damBehavior" : "PIPELINED",
-      "priority" : 1
-    }, {
-      "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 2
+      "priority" : 0
     } ],
-    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `user_id0` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `user_id1` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
-    "description" : "MultiJoin(joinFilter=[AND(=($0, $4), =($0, $2))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $2), =($0, $4)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:0;]}], select=[user_id,name,user_id0,order_id,user_id1,payment_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) user_id0, VARCHAR(2147 [...]
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `user_id0` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[InnerJoin], where=[(user_id = user_id0)], select=[user_id, name, order_id, user_id0, payment_id], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey])"
   }, {
-    "id" : 8,
+    "id" : 11,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -239,11 +222,11 @@
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 3,
+      "inputIndex" : 2,
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 5,
+      "inputIndex" : 4,
       "type" : "VARCHAR(2147483647)"
     } ],
     "condition" : null,
@@ -257,7 +240,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
     "description" : "Calc(select=[user_id, name, order_id, payment_id])"
   }, {
-    "id" : 9,
+    "id" : 12,
     "type" : "stream-exec-sink_1",
     "configuration" : {
       "table.exec.sink.keyed-shuffle" : "AUTO",
@@ -314,22 +297,22 @@
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 5,
-    "target" : 6,
+    "source" : 2,
+    "target" : 5,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 2,
-    "target" : 7,
+    "source" : 4,
+    "target" : 5,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 4,
-    "target" : 7,
+    "source" : 5,
+    "target" : 6,
     "shuffle" : {
       "type" : "FORWARD"
     },
@@ -341,19 +324,40 @@
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 8,
+    "target" : 9,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
   }, {
     "source" : 7,
-    "target" : 8,
+    "target" : 10,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 8,
-    "target" : 9,
+    "source" : 9,
+    "target" : 10,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 10,
+    "target" : 11,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 11,
+    "target" : 12,
     "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-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata
index 377ec1862c7..66aa54c2fbe 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-inner-join-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json
index 3fabcaceda1..8081f99a719 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/plan/three-way-join-with-time-attributes-with-restore.json
@@ -1,7 +1,7 @@
 {
   "flinkVersion" : "2.1",
   "nodes" : [ {
-    "id" : 47,
+    "id" : 63,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -44,7 +44,7 @@
     "outputType" : "ROW<`user_id_0` VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, UsersWithProctime]], fields=[user_id_0, name])"
   }, {
-    "id" : 48,
+    "id" : 64,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -81,7 +81,7 @@
     "outputType" : "ROW<`user_id_0` VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `proctime` TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL>",
     "description" : "Calc(select=[user_id_0, name, PROCTIME_MATERIALIZE(PROCTIME()) AS proctime])"
   }, {
-    "id" : 49,
+    "id" : 65,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -94,7 +94,7 @@
     "outputType" : "ROW<`user_id_0` VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `proctime` TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL>",
     "description" : "Exchange(distribution=[hash[user_id_0]])"
   }, {
-    "id" : 50,
+    "id" : 66,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -138,7 +138,7 @@
     "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647), `rowtime` TIMESTAMP(3)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, OrdersWithRowtime]], fields=[order_id, user_id_1, rowtime])"
   }, {
-    "id" : 51,
+    "id" : 67,
     "type" : "stream-exec-watermark-assigner_1",
     "watermarkExpr" : {
       "kind" : "INPUT_REF",
@@ -172,7 +172,7 @@
     },
     "description" : "WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])"
   }, {
-    "id" : 52,
+    "id" : 68,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -208,7 +208,7 @@
     "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647), `rowtime` TIMESTAMP(3)>",
     "description" : "Calc(select=[order_id, user_id_1, CAST(rowtime AS TIMESTAMP(3)) AS rowtime])"
   }, {
-    "id" : 53,
+    "id" : 69,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -221,7 +221,7 @@
     "outputType" : "ROW<`order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647), `rowtime` TIMESTAMP(3)>",
     "description" : "Exchange(distribution=[hash[user_id_1]])"
   }, {
-    "id" : 54,
+    "id" : 70,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -250,7 +250,7 @@
     "outputType" : "ROW<`payment_id` VARCHAR(2147483647) NOT NULL, `price` INT, `user_id_2` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])"
   }, {
-    "id" : 55,
+    "id" : 71,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -263,7 +263,7 @@
     "outputType" : "ROW<`payment_id` VARCHAR(2147483647) NOT NULL, `price` INT, `user_id_2` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id_2]])"
   }, {
-    "id" : 56,
+    "id" : 72,
     "type" : "stream-exec-multi-join_1",
     "joinTypes" : [ "INNER", "INNER", "INNER" ],
     "joinConditions" : [ {
@@ -313,7 +313,7 @@
           "type" : "VARCHAR(2147483647) NOT NULL"
         }, {
           "kind" : "INPUT_REF",
-          "inputIndex" : 8,
+          "inputIndex" : 4,
           "type" : "VARCHAR(2147483647)"
         } ],
         "type" : "BOOLEAN"
@@ -327,7 +327,7 @@
           "type" : "VARCHAR(2147483647) NOT NULL"
         }, {
           "kind" : "INPUT_REF",
-          "inputIndex" : 4,
+          "inputIndex" : 8,
           "type" : "VARCHAR(2147483647)"
         } ],
         "type" : "BOOLEAN"
@@ -335,6 +335,17 @@
       "type" : "BOOLEAN"
     },
     "joinAttributeMap" : {
+      "0" : [ {
+        "leftInputId" : -1,
+        "leftFieldIndex" : -1,
+        "rightInputId" : 0,
+        "rightFieldIndex" : 0
+      }, {
+        "leftInputId" : -1,
+        "leftFieldIndex" : -1,
+        "rightInputId" : 0,
+        "rightFieldIndex" : 0
+      } ],
       "1" : [ {
         "leftInputId" : 0,
         "leftFieldIndex" : 0,
@@ -348,7 +359,7 @@
         "rightFieldIndex" : 2
       } ]
     },
-    "inputUniqueKeys" : [ [ [ 0 ] ], [ [ 0 ] ], [ ] ],
+    "inputUniqueKeys" : [ [ [ 0 ] ], [ [ 0 ] ], [ [ 0 ] ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
@@ -385,9 +396,9 @@
       "priority" : 2
     } ],
     "outputType" : "ROW<`user_id_0` VARCHAR(2147483647) NOT NULL, `name` VARCHAR(2147483647), `proctime` TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL, `order_id` VARCHAR(2147483647) NOT NULL, `user_id_1` VARCHAR(2147483647), `rowtime` TIMESTAMP(3), `payment_id` VARCHAR(2147483647) NOT NULL, `price` INT, `user_id_2` VARCHAR(2147483647)>",
-    "description" : "MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,proctime,order_id,user_id_1,rowtime,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, TIMESTAMP_WIT [...]
+    "description" : "MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{0=[LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;, LeftInputId:-1;LeftFieldIndex:-1;RightInputId:0;RightFieldIndex:0;], 1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,proctime,order_ [...]
   }, {
-    "id" : 57,
+    "id" : 73,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -417,7 +428,7 @@
     "outputType" : "ROW<`name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647) NOT NULL, `rowtime` TIMESTAMP(3), `price` INT>",
     "description" : "Calc(select=[name, order_id, rowtime, price])"
   }, {
-    "id" : 58,
+    "id" : 74,
     "type" : "stream-exec-sink_1",
     "configuration" : {
       "table.exec.sink.keyed-shuffle" : "AUTO",
@@ -460,81 +471,81 @@
     "description" : "Sink(table=[default_catalog.default_database.sink], fields=[name, order_id, rowtime, price])"
   } ],
   "edges" : [ {
-    "source" : 47,
-    "target" : 48,
+    "source" : 63,
+    "target" : 64,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 48,
-    "target" : 49,
+    "source" : 64,
+    "target" : 65,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 50,
-    "target" : 51,
+    "source" : 66,
+    "target" : 67,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 51,
-    "target" : 52,
+    "source" : 67,
+    "target" : 68,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 52,
-    "target" : 53,
+    "source" : 68,
+    "target" : 69,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 54,
-    "target" : 55,
+    "source" : 70,
+    "target" : 71,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 49,
-    "target" : 56,
+    "source" : 65,
+    "target" : 72,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 53,
-    "target" : 56,
+    "source" : 69,
+    "target" : 72,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 55,
-    "target" : 56,
+    "source" : 71,
+    "target" : 72,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 56,
-    "target" : 57,
+    "source" : 72,
+    "target" : 73,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 57,
-    "target" : 58,
+    "source" : 73,
+    "target" : 74,
     "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-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata
index f11caa837fe..c83557089f5 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-join-with-time-attributes-with-restore/savepoint/_metadata differ
diff --git a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json
index 5105a4410d9..aca670d1ff1 100644
--- a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json
+++ b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/plan/three-way-left-outer-join-with-restore.json
@@ -1,7 +1,7 @@
 {
   "flinkVersion" : "2.1",
   "nodes" : [ {
-    "id" : 10,
+    "id" : 13,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -22,7 +22,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name])"
   }, {
-    "id" : 11,
+    "id" : 14,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -35,7 +35,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id]])"
   }, {
-    "id" : 12,
+    "id" : 15,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -56,7 +56,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[user_id, order_id])"
   }, {
-    "id" : 13,
+    "id" : 16,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -69,7 +69,80 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id]])"
   }, {
-    "id" : 14,
+    "id" : 17,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "LEFT",
+      "leftKeys" : [ 0 ],
+      "rightKeys" : [ 0 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
+    },
+    "state" : [ {
+      "index" : 0,
+      "ttl" : "0 ms",
+      "name" : "leftState"
+    }, {
+      "index" : 1,
+      "ttl" : "0 ms",
+      "name" : "rightState"
+    } ],
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    }, {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `user_id0` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[LeftOuterJoin], where=[(user_id = user_id0)], select=[user_id, name, user_id0, order_id], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey])"
+  }, {
+    "id" : 18,
+    "type" : "stream-exec-calc_1",
+    "projection" : [ {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 0,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 1,
+      "type" : "VARCHAR(2147483647)"
+    }, {
+      "kind" : "INPUT_REF",
+      "inputIndex" : 3,
+      "type" : "VARCHAR(2147483647)"
+    } ],
+    "condition" : null,
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
+    "description" : "Calc(select=[user_id, name, order_id])"
+  }, {
+    "id" : 19,
+    "type" : "stream-exec-exchange_1",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "HASH",
+        "keys" : [ 0 ]
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647)>",
+    "description" : "Exchange(distribution=[hash[user_id]])"
+  }, {
+    "id" : 20,
     "type" : "stream-exec-table-source-scan_1",
     "scanTableSource" : {
       "table" : {
@@ -90,7 +163,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
     "description" : "TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[user_id, payment_id])"
   }, {
-    "id" : 15,
+    "id" : 21,
     "type" : "stream-exec-exchange_1",
     "inputProperties" : [ {
       "requiredDistribution" : {
@@ -103,96 +176,41 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
     "description" : "Exchange(distribution=[hash[user_id]])"
   }, {
-    "id" : 16,
-    "type" : "stream-exec-multi-join_1",
-    "joinTypes" : [ "INNER", "LEFT", "LEFT" ],
-    "joinConditions" : [ {
-      "kind" : "LITERAL",
-      "value" : true,
-      "type" : "BOOLEAN NOT NULL"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 0,
-        "type" : "VARCHAR(2147483647)"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 2,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    }, {
-      "kind" : "CALL",
-      "syntax" : "BINARY",
-      "internalName" : "$=$1",
-      "operands" : [ {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 0,
-        "type" : "VARCHAR(2147483647)"
-      }, {
-        "kind" : "INPUT_REF",
-        "inputIndex" : 4,
-        "type" : "VARCHAR(2147483647)"
-      } ],
-      "type" : "BOOLEAN"
-    } ],
-    "joinAttributeMap" : {
-      "1" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 0,
-        "rightInputId" : 1,
-        "rightFieldIndex" : 0
-      } ],
-      "2" : [ {
-        "leftInputId" : 0,
-        "leftFieldIndex" : 0,
-        "rightInputId" : 2,
-        "rightFieldIndex" : 0
-      } ]
+    "id" : 22,
+    "type" : "stream-exec-join_1",
+    "joinSpec" : {
+      "joinType" : "LEFT",
+      "leftKeys" : [ 0 ],
+      "rightKeys" : [ 0 ],
+      "filterNulls" : [ true ],
+      "nonEquiCondition" : null
     },
-    "inputUniqueKeys" : [ [ ], [ ], [ ] ],
     "state" : [ {
       "index" : 0,
       "ttl" : "0 ms",
-      "name" : "input-state-0"
+      "name" : "leftState"
     }, {
       "index" : 1,
       "ttl" : "0 ms",
-      "name" : "input-state-1"
-    }, {
-      "index" : 2,
-      "ttl" : "0 ms",
-      "name" : "input-state-2"
+      "name" : "rightState"
     } ],
     "inputProperties" : [ {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
       "priority" : 0
     }, {
       "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
-      },
-      "damBehavior" : "PIPELINED",
-      "priority" : 1
-    }, {
-      "requiredDistribution" : {
-        "type" : "HASH",
-        "keys" : [ 0 ]
+        "type" : "UNKNOWN"
       },
       "damBehavior" : "PIPELINED",
-      "priority" : 2
+      "priority" : 0
     } ],
-    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `user_id0` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `user_id1` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
-    "description" : "MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $2), =($0, $4)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:0;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:0;]}], select=[user_id,name,user_id0,order_id,user_id1,payment_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) order_id, VARCH [...]
+    "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `user_id0` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
+    "description" : "Join(joinType=[LeftOuterJoin], where=[(user_id = user_id0)], select=[user_id, name, order_id, user_id0, payment_id], leftInputSpec=[NoUniqueKey], rightInputSpec=[NoUniqueKey])"
   }, {
-    "id" : 17,
+    "id" : 23,
     "type" : "stream-exec-calc_1",
     "projection" : [ {
       "kind" : "INPUT_REF",
@@ -204,11 +222,11 @@
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 3,
+      "inputIndex" : 2,
       "type" : "VARCHAR(2147483647)"
     }, {
       "kind" : "INPUT_REF",
-      "inputIndex" : 5,
+      "inputIndex" : 4,
       "type" : "VARCHAR(2147483647)"
     } ],
     "condition" : null,
@@ -222,7 +240,7 @@
     "outputType" : "ROW<`user_id` VARCHAR(2147483647), `name` VARCHAR(2147483647), `order_id` VARCHAR(2147483647), `payment_id` VARCHAR(2147483647)>",
     "description" : "Calc(select=[user_id, name, order_id, payment_id])"
   }, {
-    "id" : 18,
+    "id" : 24,
     "type" : "stream-exec-sink_1",
     "configuration" : {
       "table.exec.sink.keyed-shuffle" : "AUTO",
@@ -265,60 +283,81 @@
     "description" : "Sink(table=[default_catalog.default_database.sink], fields=[user_id, name, order_id, payment_id])"
   } ],
   "edges" : [ {
-    "source" : 10,
-    "target" : 11,
+    "source" : 13,
+    "target" : 14,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 12,
-    "target" : 13,
+    "source" : 15,
+    "target" : 16,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
     "source" : 14,
-    "target" : 15,
+    "target" : 17,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 11,
-    "target" : 16,
+    "source" : 16,
+    "target" : 17,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 13,
-    "target" : 16,
+    "source" : 17,
+    "target" : 18,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 15,
-    "target" : 16,
+    "source" : 18,
+    "target" : 19,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 16,
-    "target" : 17,
+    "source" : 20,
+    "target" : 21,
     "shuffle" : {
       "type" : "FORWARD"
     },
     "shuffleMode" : "PIPELINED"
   }, {
-    "source" : 17,
-    "target" : 18,
+    "source" : 19,
+    "target" : 22,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 21,
+    "target" : 22,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 22,
+    "target" : 23,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  }, {
+    "source" : 23,
+    "target" : 24,
     "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-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata
index f0288325eee..7b6e48d89ce 100644
Binary files a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata and b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-multi-join_1/three-way-left-outer-join-with-restore/savepoint/_metadata differ


(flink) 04/05: [FLINK-38576][table] Align commonJoinKey in MultiJoin for logical and physical rules

Posted by fp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit ce30d0317bb67bd31cfe517a6426453a16342d7a
Author: Stepan Stepanishchev <11...@users.noreply.github.com>
AuthorDate: Tue Nov 18 16:05:17 2025 +0700

    [FLINK-38576][table] Align commonJoinKey in MultiJoin for logical and physical rules
---
 .claude/CLAUDE.local.md                            |    8 -
 .../table/utils/NoCommonJoinKeyException.java      |   40 +
 .../plan/rules/logical/JoinToMultiJoinRule.java    |  169 +-
 .../stream/StreamPhysicalMultiJoinRule.java        |  114 +-
 .../table/planner/plan/utils/MultiJoinUtil.java    |  155 ++
 .../planner/plan/stream/sql/MultiJoinTest.java     | 1521 ++++++++++--
 .../planner/plan/stream/sql/MultiJoinTest.xml      | 2589 +++++++++++++++++---
 .../AttributeBasedJoinKeyExtractor.java            |    3 +-
 8 files changed, 3811 insertions(+), 788 deletions(-)

diff --git a/.claude/CLAUDE.local.md b/.claude/CLAUDE.local.md
deleted file mode 100644
index 8d9ff2b2c09..00000000000
--- a/.claude/CLAUDE.local.md
+++ /dev/null
@@ -1,8 +0,0 @@
-- You're a respected open source apache flink maintainer.
-
-- Test your changes with one appropriate existing test using this format just to check if we have no compilation errors "cd /Users/gdemorais/qdev/flink2 && ./mvnw test -Dtest="MultiJoinTest#testTwoWayJoinWithUnion" -pl flink-table/flink-table-planner -q -Dcheckstyle.skip -Drat.skip -Dscalastyle.skip -Denforcer.skip=true -Pgenerate-config-docs -Dspotless.check.skip=true"
-- If you cannot run your tests because they get stuck downloading dependencies. You can run "./mvnw clean install -T1C -DskipTests -Pfast -Dcheckstyle.skip -Drat.skip -Dscalastyle.skip -Denforcer.skip=true -Pgenerate-config-docs -Dspotless.check.skip=true -DskipITs=true -Dmaven.javadoc.skip=true -Djapicmp.skip=true -Pskip-webui-build -T4 -fn" to build first. Just do this once and if necessary because it takes over 5 minutes!
-- If you have to install only one module, also use all the available flags to speed up the process. E.g. "./mvnw install -pl flink-table/flink-table-common -T1C -DskipTests -Pfast -Dcheckstyle.skip -Drat.skip -Dscalastyle.skip -Denforcer.skip=true -Pgenerate-config-docs -Dspotless.check.skip=true -DskipITs=true -Dmaven.javadoc.skip=true -Djapicmp.skip=true -Pskip-webui-build -T4"
-
-- We always want to keep the things we do in "eval" for functions and in the processRecord methods in operators to a minimum since this is a hot path for flink which is executed millions of times. Always do what you can in the constructor, intialize things else where. 
-    
diff --git a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/NoCommonJoinKeyException.java b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/NoCommonJoinKeyException.java
new file mode 100644
index 00000000000..21eb273b911
--- /dev/null
+++ b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/NoCommonJoinKeyException.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.utils;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.util.FlinkRuntimeException;
+
+/** Thrown when a MultiJoin node has no common join key. */
+@Internal
+public class NoCommonJoinKeyException extends FlinkRuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public NoCommonJoinKeyException(String message) {
+        super(message);
+    }
+
+    public NoCommonJoinKeyException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public NoCommonJoinKeyException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/JoinToMultiJoinRule.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/JoinToMultiJoinRule.java
index 762c16b784d..3dc3b76740b 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/JoinToMultiJoinRule.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/logical/JoinToMultiJoinRule.java
@@ -18,14 +18,17 @@
 
 package org.apache.flink.table.planner.plan.rules.logical;
 
-import org.apache.flink.api.java.tuple.Tuple2;
 import org.apache.flink.table.api.TableException;
+import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
 import org.apache.flink.table.planner.plan.nodes.logical.FlinkLogicalJoin;
 import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMultiJoin;
 import org.apache.flink.table.planner.plan.utils.IntervalJoinUtil;
+import org.apache.flink.table.runtime.operators.join.stream.keyselector.AttributeBasedJoinKeyExtractor;
+import org.apache.flink.table.runtime.operators.join.stream.keyselector.JoinKeyExtractor;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.table.utils.NoCommonJoinKeyException;
 
 import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.hep.HepRelVertex;
@@ -35,20 +38,14 @@ import org.apache.calcite.rel.SingleRel;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinInfo;
 import org.apache.calcite.rel.core.JoinRelType;
-import org.apache.calcite.rel.core.TableFunctionScan;
-import org.apache.calcite.rel.core.TableScan;
-import org.apache.calcite.rel.core.Values;
 import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.rel.logical.LogicalSnapshot;
-import org.apache.calcite.rel.metadata.RelColumnOrigin;
-import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.rules.CoreRules;
 import org.apache.calcite.rel.rules.FilterMultiJoinMergeRule;
 import org.apache.calcite.rel.rules.MultiJoin;
 import org.apache.calcite.rel.rules.ProjectMultiJoinMergeRule;
 import org.apache.calcite.rel.rules.TransformationRule;
 import org.apache.calcite.rex.RexBuilder;
-import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
@@ -62,11 +59,12 @@ import org.immutables.value.Value;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.apache.flink.table.planner.plan.utils.MultiJoinUtil.createJoinAttributeMap;
 
 /**
  * Flink Planner rule to flatten a tree of {@link Join}s into a single {@link MultiJoin} with N
@@ -462,134 +460,45 @@ public class JoinToMultiJoinRule extends RelRule<JoinToMultiJoinRule.Config>
 
     /**
      * Checks if original join and child multi-join have common join keys to decide if we can merge
-     * them into a single MultiJoin with one more input.
+     * them into a single MultiJoin with one more input. The method uses {@link
+     * AttributeBasedJoinKeyExtractor} to try to create valid common join key extractors.
      *
      * @param origJoin original Join
      * @param otherJoin child MultiJoin
      * @return true if original Join and child multi-join have at least one common JoinKey
      */
     private boolean haveCommonJoinKey(Join origJoin, MultiJoin otherJoin) {
-        Set<String> origJoinKeys = getJoinKeys(origJoin);
-        Set<String> otherJoinKeys = getJoinKeys(otherJoin);
-
-        origJoinKeys.retainAll(otherJoinKeys);
-
-        return !origJoinKeys.isEmpty();
-    }
-
-    /**
-     * Returns a set of join keys as strings following this format [table_name.field_name].
-     *
-     * @param join Join or MultiJoin node
-     * @return set of all the join keys (keys from join conditions)
-     */
-    public Set<String> getJoinKeys(RelNode join) {
-        Set<String> joinKeys = new HashSet<>();
-        List<RexCall> conditions = Collections.emptyList();
-        List<RelNode> inputs = join.getInputs();
-
-        if (join instanceof Join) {
-            conditions = collectConjunctions(((Join) join).getCondition());
-        } else if (join instanceof MultiJoin) {
-            conditions =
-                    ((MultiJoin) join)
-                            .getOuterJoinConditions().stream()
-                                    .flatMap(cond -> collectConjunctions(cond).stream())
-                                    .collect(Collectors.toList());
-        }
-
-        RelMetadataQuery mq = join.getCluster().getMetadataQuery();
-
-        for (RexCall condition : conditions) {
-            for (RexNode operand : condition.getOperands()) {
-                if (operand instanceof RexInputRef) {
-                    addJoinKeysByOperand((RexInputRef) operand, inputs, mq, joinKeys);
-                }
-            }
-        }
-
-        return joinKeys;
-    }
-
-    /**
-     * Retrieves conjunctions from joinCondition.
-     *
-     * @param joinCondition join condition
-     * @return List of RexCalls representing conditions
-     */
-    private List<RexCall> collectConjunctions(RexNode joinCondition) {
-        return RelOptUtil.conjunctions(joinCondition).stream()
-                .map(rexNode -> (RexCall) rexNode)
-                .collect(Collectors.toList());
-    }
-
-    /**
-     * Appends join key's string representation to the set of join keys.
-     *
-     * @param ref input ref to the operand
-     * @param inputs List of node's inputs
-     * @param mq RelMetadataQuery needed to retrieve column origins
-     * @param joinKeys Set of join keys to be added
-     */
-    private void addJoinKeysByOperand(
-            RexInputRef ref, List<RelNode> inputs, RelMetadataQuery mq, Set<String> joinKeys) {
-        int inputRefIndex = ref.getIndex();
-        Tuple2<RelNode, Integer> targetInputAndIdx = getTargetInputAndIdx(inputRefIndex, inputs);
-        RelNode targetInput = targetInputAndIdx.f0;
-        int idxInTargetInput = targetInputAndIdx.f1;
-
-        Set<RelColumnOrigin> origins = mq.getColumnOrigins(targetInput, idxInTargetInput);
-        if (origins != null) {
-            for (RelColumnOrigin origin : origins) {
-                RelOptTable originTable = origin.getOriginTable();
-                List<String> qualifiedName = originTable.getQualifiedName();
-                String fieldName =
-                        originTable
-                                .getRowType()
-                                .getFieldList()
-                                .get(origin.getOriginColumnOrdinal())
-                                .getName();
-                joinKeys.add(qualifiedName.get(qualifiedName.size() - 1) + "." + fieldName);
-            }
+        final List<RelNode> combinedJoinInputs =
+                Stream.concat(otherJoin.getInputs().stream(), Stream.of(origJoin.getRight()))
+                        .collect(Collectors.toUnmodifiableList());
+
+        final List<RowType> combinedInputTypes =
+                combinedJoinInputs.stream()
+                        .map(i -> FlinkTypeFactory.toLogicalRowType(i.getRowType()))
+                        .collect(Collectors.toUnmodifiableList());
+
+        final List<RexNode> combinedJoinConditions =
+                Stream.concat(
+                                otherJoin.getOuterJoinConditions().stream(),
+                                List.of(origJoin.getCondition()).stream())
+                        .collect(Collectors.toUnmodifiableList());
+
+        final Map<Integer, List<AttributeBasedJoinKeyExtractor.ConditionAttributeRef>>
+                joinAttributeMap =
+                        createJoinAttributeMap(combinedJoinInputs, combinedJoinConditions);
+
+        boolean haveCommonJoinKey = false;
+        try {
+            // we probe to instantiate AttributeBasedJoinKeyExtractor's constructor to check whether
+            // it's possible to initialize common join key structures
+            final JoinKeyExtractor keyExtractor =
+                    new AttributeBasedJoinKeyExtractor(joinAttributeMap, combinedInputTypes);
+            haveCommonJoinKey = keyExtractor.getCommonJoinKeyIndices(0).length > 0;
+        } catch (NoCommonJoinKeyException ignored) {
+            // failed to instantiate common join key structures => no common join key
         }
-    }
 
-    /**
-     * Get real table that contains needed input ref (join key).
-     *
-     * @param inputRefIndex index of the required field
-     * @param inputs inputs of the node
-     * @return target input + idx of the required field as target input's
-     */
-    private Tuple2<RelNode, Integer> getTargetInputAndIdx(int inputRefIndex, List<RelNode> inputs) {
-        RelNode targetInput = null;
-        int idxInTargetInput = 0;
-        int inputFieldEnd = 0;
-        for (RelNode input : inputs) {
-            inputFieldEnd += input.getRowType().getFieldCount();
-            if (inputRefIndex < inputFieldEnd) {
-                targetInput = input;
-                int targetInputStartIdx = inputFieldEnd - input.getRowType().getFieldCount();
-                idxInTargetInput = inputRefIndex - targetInputStartIdx;
-                break;
-            }
-        }
-
-        targetInput =
-                (targetInput instanceof HepRelVertex)
-                        ? ((HepRelVertex) targetInput).getCurrentRel()
-                        : targetInput;
-
-        assert targetInput != null;
-
-        if (targetInput instanceof TableScan
-                || targetInput instanceof Values
-                || targetInput instanceof TableFunctionScan
-                || targetInput.getInputs().isEmpty()) {
-            return new Tuple2<>(targetInput, idxInTargetInput);
-        } else {
-            return getTargetInputAndIdx(idxInTargetInput, targetInput.getInputs());
-        }
+        return haveCommonJoinKey;
     }
 
     /**
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/physical/stream/StreamPhysicalMultiJoinRule.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/physical/stream/StreamPhysicalMultiJoinRule.java
index e59ea9df9b9..da3b99d669a 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/physical/stream/StreamPhysicalMultiJoinRule.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/rules/physical/stream/StreamPhysicalMultiJoinRule.java
@@ -32,18 +32,14 @@ import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlKind;
-import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import static org.apache.flink.table.planner.plan.utils.MultiJoinUtil.createJoinAttributeMap;
+
 /** Rule that converts {@link FlinkLogicalMultiJoin} to {@link StreamPhysicalMultiJoin}. */
 public class StreamPhysicalMultiJoinRule extends ConverterRule {
     public static final RelOptRule INSTANCE = new StreamPhysicalMultiJoinRule();
@@ -61,7 +57,7 @@ public class StreamPhysicalMultiJoinRule extends ConverterRule {
     public RelNode convert(final RelNode rel) {
         final FlinkLogicalMultiJoin multiJoin = (FlinkLogicalMultiJoin) rel;
         final Map<Integer, List<ConditionAttributeRef>> joinAttributeMap =
-                createJoinAttributeMap(multiJoin);
+                createJoinAttributeMap(multiJoin.getInputs(), multiJoin.getJoinConditions());
         final List<RowType> inputRowTypes =
                 multiJoin.getInputs().stream()
                         .map(i -> FlinkTypeFactory.toLogicalRowType(i.getRowType()))
@@ -117,108 +113,4 @@ public class StreamPhysicalMultiJoinRule extends ConverterRule {
 
         return inputTraitSet;
     }
-
-    private Map<Integer, List<ConditionAttributeRef>> createJoinAttributeMap(
-            final FlinkLogicalMultiJoin multiJoin) {
-        final Map<Integer, List<ConditionAttributeRef>> joinAttributeMap = new HashMap<>();
-        final List<Integer> inputFieldCounts =
-                multiJoin.getInputs().stream()
-                        .map(input -> input.getRowType().getFieldCount())
-                        .collect(Collectors.toList());
-
-        final List<Integer> inputOffsets = new ArrayList<>();
-        int currentOffset = 0;
-        for (final Integer count : inputFieldCounts) {
-            inputOffsets.add(currentOffset);
-            currentOffset += count;
-        }
-
-        final List<? extends RexNode> joinConditions = multiJoin.getJoinConditions();
-        for (final RexNode condition : joinConditions) {
-            extractEqualityConditions(condition, inputOffsets, inputFieldCounts, joinAttributeMap);
-        }
-        return joinAttributeMap;
-    }
-
-    private void extractEqualityConditions(
-            final RexNode condition,
-            final List<Integer> inputOffsets,
-            final List<Integer> inputFieldCounts,
-            final Map<Integer, List<ConditionAttributeRef>> joinAttributeMap) {
-        if (!(condition instanceof RexCall)) {
-            return;
-        }
-
-        final RexCall call = (RexCall) condition;
-        final SqlKind kind = call.getOperator().getKind();
-
-        if (kind != SqlKind.EQUALS) {
-            for (final RexNode operand : call.getOperands()) {
-                extractEqualityConditions(
-                        operand, inputOffsets, inputFieldCounts, joinAttributeMap);
-            }
-            return;
-        }
-
-        if (call.getOperands().size() != 2) {
-            return;
-        }
-
-        final RexNode op1 = call.getOperands().get(0);
-        final RexNode op2 = call.getOperands().get(1);
-
-        if (!(op1 instanceof RexInputRef) || !(op2 instanceof RexInputRef)) {
-            return;
-        }
-
-        final InputRef inputRef1 =
-                findInputRef(((RexInputRef) op1).getIndex(), inputOffsets, inputFieldCounts);
-        final InputRef inputRef2 =
-                findInputRef(((RexInputRef) op2).getIndex(), inputOffsets, inputFieldCounts);
-
-        if (inputRef1 == null || inputRef2 == null) {
-            return;
-        }
-
-        final InputRef leftRef;
-        final InputRef rightRef;
-        if (inputRef1.inputIndex < inputRef2.inputIndex) {
-            leftRef = inputRef1;
-            rightRef = inputRef2;
-        } else {
-            leftRef = inputRef2;
-            rightRef = inputRef1;
-        }
-
-        final ConditionAttributeRef attrRef =
-                new ConditionAttributeRef(
-                        leftRef.inputIndex,
-                        leftRef.attributeIndex,
-                        rightRef.inputIndex,
-                        rightRef.attributeIndex);
-        joinAttributeMap.computeIfAbsent(rightRef.inputIndex, k -> new ArrayList<>()).add(attrRef);
-    }
-
-    private @Nullable InputRef findInputRef(
-            final int fieldIndex,
-            final List<Integer> inputOffsets,
-            final List<Integer> inputFieldCounts) {
-        for (int i = 0; i < inputOffsets.size(); i++) {
-            final int offset = inputOffsets.get(i);
-            if (fieldIndex >= offset && fieldIndex < offset + inputFieldCounts.get(i)) {
-                return new InputRef(i, fieldIndex - offset);
-            }
-        }
-        return null;
-    }
-
-    private static final class InputRef {
-        private final int inputIndex;
-        private final int attributeIndex;
-
-        private InputRef(final int inputIndex, final int attributeIndex) {
-            this.inputIndex = inputIndex;
-            this.attributeIndex = attributeIndex;
-        }
-    }
 }
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/utils/MultiJoinUtil.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/utils/MultiJoinUtil.java
new file mode 100644
index 00000000000..83bf9be50c1
--- /dev/null
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/utils/MultiJoinUtil.java
@@ -0,0 +1,155 @@
+/*
+ * 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.utils;
+
+import org.apache.flink.table.runtime.operators.join.stream.keyselector.AttributeBasedJoinKeyExtractor;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlKind;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class MultiJoinUtil {
+    public static Map<Integer, List<AttributeBasedJoinKeyExtractor.ConditionAttributeRef>>
+            createJoinAttributeMap(
+                    List<RelNode> joinInputs, List<? extends RexNode> joinConditions) {
+        final Map<Integer, List<AttributeBasedJoinKeyExtractor.ConditionAttributeRef>>
+                joinAttributeMap = new HashMap<>();
+        final List<Integer> inputFieldCounts =
+                joinInputs.stream()
+                        .map(input -> input.getRowType().getFieldCount())
+                        .collect(Collectors.toList());
+
+        final List<Integer> inputOffsets = new ArrayList<>();
+        int currentOffset = 0;
+        for (final Integer count : inputFieldCounts) {
+            inputOffsets.add(currentOffset);
+            currentOffset += count;
+        }
+
+        for (final RexNode condition : joinConditions) {
+            extractEqualityConditions(condition, inputOffsets, inputFieldCounts, joinAttributeMap);
+        }
+        return joinAttributeMap;
+    }
+
+    private static void extractEqualityConditions(
+            final RexNode condition,
+            final List<Integer> inputOffsets,
+            final List<Integer> inputFieldCounts,
+            final Map<Integer, List<AttributeBasedJoinKeyExtractor.ConditionAttributeRef>>
+                    joinAttributeMap) {
+        if (!(condition instanceof RexCall)) {
+            return;
+        }
+
+        final RexCall call = (RexCall) condition;
+        final SqlKind kind = call.getOperator().getKind();
+
+        if (kind != SqlKind.EQUALS) {
+            for (final RexNode operand : call.getOperands()) {
+                extractEqualityConditions(
+                        operand, inputOffsets, inputFieldCounts, joinAttributeMap);
+            }
+            return;
+        }
+
+        if (call.getOperands().size() != 2) {
+            return;
+        }
+
+        final RexNode op1 = call.getOperands().get(0);
+        final RexNode op2 = call.getOperands().get(1);
+
+        if (!(op1 instanceof RexInputRef) || !(op2 instanceof RexInputRef)) {
+            return;
+        }
+
+        final InputRef inputRef1 =
+                findInputRef(((RexInputRef) op1).getIndex(), inputOffsets, inputFieldCounts);
+        final InputRef inputRef2 =
+                findInputRef(((RexInputRef) op2).getIndex(), inputOffsets, inputFieldCounts);
+
+        if (inputRef1 == null || inputRef2 == null) {
+            return;
+        }
+
+        final InputRef leftRef;
+        final InputRef rightRef;
+        if (inputRef1.inputIndex < inputRef2.inputIndex) {
+            leftRef = inputRef1;
+            rightRef = inputRef2;
+        } else {
+            leftRef = inputRef2;
+            rightRef = inputRef1;
+        }
+
+        // Special case for input 0:
+        // Since we are building attribute references that do left -> right index,
+        // we need a special base case for input 0 which has no input to the left.
+        // So we do {-1, -1} -> {0, attributeIndex}
+        if (leftRef.inputIndex == 0) {
+            final AttributeBasedJoinKeyExtractor.ConditionAttributeRef firstAttrRef =
+                    new AttributeBasedJoinKeyExtractor.ConditionAttributeRef(
+                            -1, -1, leftRef.inputIndex, leftRef.attributeIndex);
+            joinAttributeMap
+                    .computeIfAbsent(leftRef.inputIndex, k -> new ArrayList<>())
+                    .add(firstAttrRef);
+        }
+
+        final AttributeBasedJoinKeyExtractor.ConditionAttributeRef attrRef =
+                new AttributeBasedJoinKeyExtractor.ConditionAttributeRef(
+                        leftRef.inputIndex,
+                        leftRef.attributeIndex,
+                        rightRef.inputIndex,
+                        rightRef.attributeIndex);
+        joinAttributeMap.computeIfAbsent(rightRef.inputIndex, k -> new ArrayList<>()).add(attrRef);
+    }
+
+    private static @Nullable InputRef findInputRef(
+            final int fieldIndex,
+            final List<Integer> inputOffsets,
+            final List<Integer> inputFieldCounts) {
+        for (int i = 0; i < inputOffsets.size(); i++) {
+            final int offset = inputOffsets.get(i);
+            if (fieldIndex >= offset && fieldIndex < offset + inputFieldCounts.get(i)) {
+                return new InputRef(i, fieldIndex - offset);
+            }
+        }
+        return null;
+    }
+
+    private static final class InputRef {
+        private final int inputIndex;
+        private final int attributeIndex;
+
+        private InputRef(final int inputIndex, final int attributeIndex) {
+            this.inputIndex = inputIndex;
+            this.attributeIndex = attributeIndex;
+        }
+    }
+}
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
index 8121d763d84..81cc1a10ef9 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
@@ -18,18 +18,26 @@
 
 package org.apache.flink.table.planner.plan.stream.sql;
 
+import org.apache.flink.table.api.ExplainDetail;
+import org.apache.flink.table.api.StatementSet;
 import org.apache.flink.table.api.TableConfig;
 import org.apache.flink.table.api.config.OptimizerConfigOptions;
+import org.apache.flink.table.planner.utils.PlanKind;
+import org.apache.flink.table.planner.utils.StreamTableTestUtil;
 import org.apache.flink.table.planner.utils.TableTestBase;
-import org.apache.flink.table.planner.utils.TableTestUtil;
 
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
+import scala.Enumeration;
+
+import static scala.runtime.BoxedUnit.UNIT;
+
 /** Tests for multi-join plans. */
 public class MultiJoinTest extends TableTestBase {
 
-    private TableTestUtil util;
+    private StreamTableTestUtil util;
 
     @BeforeEach
     void setup() {
@@ -43,7 +51,7 @@ public class MultiJoinTest extends TableTestBase {
         util.tableEnv()
                 .executeSql(
                         "CREATE TABLE Users ("
-                                + "  user_id_0 STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
                                 + "  name STRING,"
                                 + "  cash INT"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
@@ -52,7 +60,7 @@ public class MultiJoinTest extends TableTestBase {
                 .executeSql(
                         "CREATE TABLE Orders ("
                                 + "  order_id STRING PRIMARY KEY NOT ENFORCED,"
-                                + "  user_id_1 STRING,"
+                                + "  user_id STRING,"
                                 + "  product STRING"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,D')");
 
@@ -61,16 +69,26 @@ public class MultiJoinTest extends TableTestBase {
                         "CREATE TABLE Payments ("
                                 + "  payment_id STRING PRIMARY KEY NOT ENFORCED,"
                                 + "  price INT,"
-                                + "  user_id_2 STRING"
+                                + "  user_id STRING"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I')");
 
         util.tableEnv()
                 .executeSql(
                         "CREATE TABLE Shipments ("
                                 + "  location STRING,"
-                                + "  user_id_3 STRING"
+                                + "  user_id STRING"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,UB,D')");
 
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Detail ("
+                                + "  detail_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  description STRING,"
+                                + "  user_id STRING,"
+                                + "  data STRING,"
+                                + "  `timestamp` BIGINT"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I')");
+
         // Tables for testing temporal join exclusion
         util.tableEnv()
                 .executeSql(
@@ -94,8 +112,8 @@ public class MultiJoinTest extends TableTestBase {
                         "CREATE TABLE EventTable1 ("
                                 + "  id STRING,"
                                 + "  val INT,"
-                                + "  rowtime TIMESTAMP(3),"
-                                + "  WATERMARK FOR rowtime AS rowtime - INTERVAL '5' SECOND"
+                                + "  `$rowtime` TIMESTAMP(3),"
+                                + "  WATERMARK FOR `$rowtime` AS `$rowtime` - INTERVAL '5' SECOND"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I')");
 
         util.tableEnv()
@@ -103,15 +121,15 @@ public class MultiJoinTest extends TableTestBase {
                         "CREATE TABLE EventTable2 ("
                                 + "  id STRING,"
                                 + "  price DOUBLE,"
-                                + "  rowtime TIMESTAMP(3),"
-                                + "  WATERMARK FOR rowtime AS rowtime - INTERVAL '5' SECOND"
+                                + "  `$rowtime` TIMESTAMP(3),"
+                                + "  WATERMARK FOR `$rowtime` AS `$rowtime` - INTERVAL '5' SECOND"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I')");
 
         // Tables for testing time attribute materialization in multi-join
         util.tableEnv()
                 .executeSql(
                         "CREATE TABLE UsersWithProctime ("
-                                + "  user_id_0 STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
                                 + "  name STRING,"
                                 + "  proctime AS PROCTIME()"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I')");
@@ -120,55 +138,152 @@ public class MultiJoinTest extends TableTestBase {
                 .executeSql(
                         "CREATE TABLE OrdersWithRowtime ("
                                 + "  order_id STRING PRIMARY KEY NOT ENFORCED,"
-                                + "  user_id_1 STRING,"
-                                + "  rowtime TIMESTAMP(3),"
-                                + "  WATERMARK FOR rowtime AS rowtime"
+                                + "  user_id STRING,"
+                                + "  `$rowtime` TIMESTAMP(3),"
+                                + "  WATERMARK FOR `$rowtime` AS `$rowtime`"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I')");
+        // Tables for testing upsert key preservation
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE UsersPK ("
+                                + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  name STRING,"
+                                + "  region_id INT,"
+                                + "  description STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE OrdersPK ("
+                                + "  order_id STRING NOT NULL,"
+                                + "  user_id STRING NOT NULL,"
+                                + "  product STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (order_id, user_id) NOT ENFORCED"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE PaymentsPK ("
+                                + "  payment_id STRING NOT NULL,"
+                                + "  user_id STRING NOT NULL,"
+                                + "  price INT,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (payment_id, user_id) NOT ENFORCED"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE AddressPK ("
+                                + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  location STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
     }
 
     @Test
     void testThreeWayInnerJoinRelPlan() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
     }
 
     @Test
-    void testThreeWayInnerJoinNoCommonJoinKeyRelPlan() {
+    @Tag("no-common-join-key")
+    void testThreeWayInnerJoinRelPlanNoCommonJoinKey() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.cash = p.price");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.cash = p.price");
     }
 
     @Test
     void testThreeWayInnerJoinExecPlan() {
         util.verifyExecPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
     }
 
     @Test
     void testThreeWayLeftOuterJoinRelPlan() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
+    }
+
+    @Test
+    void testThreeWayInnerJoinWithTttlHints() {
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    /*+ STATE_TTL(u='1d', o='2d', p='1h') */\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
+    }
+
+    @Test
+    void testThreeWayInnerJoinWithSingleTttlHint() {
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    /*+ STaTE_tTL(o='2d') */\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
     }
 
     @Test
     void testThreeWayLeftOuterJoinExecPlan() {
         util.verifyExecPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
     }
 
     @Test
@@ -177,154 +292,241 @@ public class MultiJoinTest extends TableTestBase {
                 .executeSql(
                         "CREATE TABLE Orders2 ("
                                 + "  order_id STRING PRIMARY KEY NOT ENFORCED,"
-                                + "  user_id_1 STRING,"
+                                + "  user_id STRING,"
                                 + "  product STRING"
                                 + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,D')");
 
         util.verifyRelPlan(
-                "WITH OrdersUnion as ("
-                        + "SELECT * FROM Orders "
-                        + "UNION ALL "
-                        + "SELECT * FROM Orders2"
-                        + ") "
-                        + "SELECT * FROM OrdersUnion o "
-                        + "LEFT JOIN Users u "
-                        + "ON o.user_id_1 = u.user_id_0");
+                "\nWITH OrdersUnion as (\n"
+                        + "SELECT * FROM Orders\n"
+                        + "UNION ALL\n"
+                        + "SELECT * FROM Orders2\n"
+                        + ")\n"
+                        + "SELECT * FROM OrdersUnion o\n"
+                        + "LEFT JOIN Users u\n"
+                        + "    ON o.user_id = u.user_id");
     }
 
     @Test
     void testTwoWayJoinWithRank() {
-        util.getTableEnv()
-                .getConfig()
-                .set(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true);
-
-        util.verifyRelPlan(
-                "WITH JoinedEvents as ("
-                        + "SELECT e1.id as id, e1.val, e1.rowtime as `rowtime`, e2.price "
-                        + "FROM EventTable1 e1 "
-                        + "JOIN EventTable2 e2 ON e1.id = e2.id) "
-                        + "SELECT id, val, `rowtime` FROM ("
-                        + "SELECT *, "
-                        + "ROW_NUMBER() OVER (PARTITION BY id ORDER BY `rowtime` DESC) as ts "
-                        + "FROM JoinedEvents) "
+        util.verifyRelPlan(
+                "\nWITH JoinedEvents as (\n"
+                        + "SELECT\n"
+                        + "    e1.id as id,\n"
+                        + "    e1.val,\n"
+                        + "    e1.`$rowtime` as `$rowtime`,\n"
+                        + "    e2.price\n"
+                        + "FROM EventTable1 e1\n"
+                        + "JOIN EventTable2 e2\n"
+                        + "    ON e1.id = e2.id)\n"
+                        + "SELECT\n"
+                        + "    id,\n"
+                        + "    val,\n"
+                        + "    `$rowtime`\n"
+                        + "FROM (\n"
+                        + "    SELECT\n"
+                        + "        *,\n"
+                        + "        ROW_NUMBER() OVER (PARTITION BY id ORDER BY `$rowtime` DESC) as ts\n"
+                        + "    FROM JoinedEvents)\n"
                         + "WHERE ts = 1");
     }
 
     @Test
     void testFourWayComplexJoinRelPlan() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 AND (u.cash >= p.price OR p.price < 0) "
-                        + "LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "    AND (u.cash >= p.price OR p.price < 0)\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON p.user_id = s.user_id");
     }
 
     @Test
-    void testThreeWayJoinNoJoinKeyExecPlan() {
+    @Tag("no-common-join-key")
+    void testThreeWayJoinExecPlanNoCommonJoinKey() {
         util.verifyExecPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON TRUE "
-                        + "INNER JOIN Payments p ON TRUE ");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o ON TRUE\n"
+                        + "INNER JOIN Payments p ON TRUE");
     }
 
     @Test
-    void testFourWayJoinNoCommonJoinKeyRelPlan() {
+    @Tag("no-common-join-key")
+    void testFourWayJoinRelPlanNoCommonJoinKey() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 "
-                        + "LEFT JOIN Shipments s ON p.payment_id = s.user_id_3");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN LookupTable\n"
+                        + "    ON u.name = LookupTable.name\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON o.user_id = s.user_id");
     }
 
     @Test
     void testFourWayComplexJoinExecPlan() {
         util.verifyExecPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 AND (u.cash >= p.price OR p.price < 0) "
-                        + "LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "    AND (u.cash >= p.price OR p.price < 0)\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON p.user_id = s.user_id");
     }
 
     @Test
     void testThreeWayInnerJoinExplain() {
         util.verifyExplain(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
     }
 
     @Test
     void testThreeWayLeftOuterJoinExplain() {
         util.verifyExplain(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
     }
 
     @Test
     void testFourWayComplexJoinExplain() {
         util.verifyExplain(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 AND (u.cash >= p.price OR p.price < 0) "
-                        + "LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "    AND (u.cash >= p.price OR p.price < 0)\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON p.user_id = s.user_id");
     }
 
     @Test
     void testTemporalJoinExcludedFromMultiJoin() {
         // Temporal joins should remain as lookup joins, not be merged into MultiJoin
         util.verifyRelPlan(
-                "SELECT s.user_id, s.amount, l.name, l.age "
-                        + "FROM StreamTable s "
-                        + "JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l "
-                        + "ON s.user_id = l.id");
+                "\nSELECT\n"
+                        + "    s.user_id,\n"
+                        + "    s.amount,\n"
+                        + "    l.name,\n"
+                        + "    l.age\n"
+                        + "FROM StreamTable s\n"
+                        + "JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l\n"
+                        + "    ON s.user_id = l.id");
     }
 
     @Test
     void testIntervalJoinExcludedFromMultiJoin() {
         // Interval joins (event-time and processing-time) should remain as interval joins
         util.verifyRelPlan(
-                "SELECT e1.id, e1.val, e2.price "
-                        + "FROM EventTable1 e1 "
-                        + "JOIN EventTable2 e2 ON e1.id = e2.id "
-                        + "AND e1.rowtime BETWEEN e2.rowtime - INTERVAL '1' MINUTE "
-                        + "AND e2.rowtime + INTERVAL '1' MINUTE");
+                "\nSELECT\n"
+                        + "    e1.id,\n"
+                        + "    e1.val,\n"
+                        + "    e2.price\n"
+                        + "FROM EventTable1 e1\n"
+                        + "JOIN EventTable2 e2\n"
+                        + "    ON e1.id = e2.id\n"
+                        + "    AND e1.`$rowtime` BETWEEN e2.`$rowtime` - INTERVAL '1' MINUTE\n"
+                        + "    AND e2.`$rowtime` + INTERVAL '1' MINUTE");
     }
 
     @Test
     void testThreeWayLeftOuterJoinWithWhereClauseRelPlan() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2 "
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
                         + "WHERE u.name = 'Gus' AND p.price > 10");
     }
 
     @Test
     void testThreeWayLeftOuterJoinWithWhereClauseExecPlan() {
         util.verifyExecPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2 "
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
                         + "WHERE u.name = 'Gus' AND p.price > 10");
     }
 
     @Test
     void testThreeWayLeftOuterJoinWithWhereClauseExplain() {
         util.verifyExplain(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2 "
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
                         + "WHERE u.name = 'Gus' AND p.price > 10");
     }
 
@@ -332,52 +534,1095 @@ public class MultiJoinTest extends TableTestBase {
     void testRegularJoinsAreMergedApartFromTemporalJoin() {
         // Regular joins should still be eligible for MultiJoin but not mixed with temporal joins
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, temporal.age "
-                        + "FROM Users u "
-                        + "INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "INNER JOIN ("
-                        + "  SELECT s.user_id, l.age "
-                        + "  FROM StreamTable s "
-                        + "  JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l "
-                        + "  ON s.user_id = l.id"
-                        + ") temporal ON u.user_id_0 = temporal.user_id");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    temporal.age "
+                        + "FROM Users u\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "INNER JOIN (\n"
+                        + "    SELECT s.user_id, l.age\n"
+                        + "    FROM StreamTable s\n"
+                        + "    JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l\n"
+                        + "        ON s.user_id = l.id\n"
+                        + ") temporal ON u.user_id = temporal.user_id");
     }
 
     @Test
     void testFourWayJoinTransitiveCommonJoinKeyRelPlan() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location "
-                        + "FROM Users u "
-                        + "LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "LEFT JOIN Payments p ON o.user_id_1 = p.user_id_2 "
-                        + "LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON o.user_id = p.user_id\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON p.user_id = s.user_id");
     }
 
     /* Update this to supported with FLINK-37973 https://issues.apache.org/jira/browse/FLINK-37973 */
     @Test
     void testRightJoinNotSupported() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "RIGHT JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "RIGHT JOIN Payments p ON o.user_id_1 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "RIGHT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "RIGHT JOIN Payments p\n"
+                        + "    ON o.user_id = p.user_id");
     }
 
     @Test
     void testFullOuterNotSupported() {
         util.verifyRelPlan(
-                "SELECT u.user_id_0, u.name, o.order_id, p.payment_id "
-                        + "FROM Users u "
-                        + "FULL OUTER JOIN Orders o ON u.user_id_0 = o.user_id_1 "
-                        + "FULL OUTER JOIN Payments p ON o.user_id_1 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id\n"
+                        + "FROM Users u\n"
+                        + "FULL OUTER JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "FULL OUTER JOIN Payments p\n"
+                        + "    ON o.user_id = p.user_id");
     }
 
     @Test
     void testThreeWayJoinWithTimeAttributesMaterialization() {
         util.verifyRelPlan(
-                "SELECT u.name, u.proctime, o.rowtime, p.price "
-                        + "FROM UsersWithProctime u "
-                        + "JOIN OrdersWithRowtime o ON u.user_id_0 = o.user_id_1 "
-                        + "JOIN Payments p ON u.user_id_0 = p.user_id_2");
+                "\nSELECT\n"
+                        + "    u.name,\n"
+                        + "    u.proctime,\n"
+                        + "    o.`$rowtime`,\n"
+                        + "    p.price\n"
+                        + "FROM UsersWithProctime u\n"
+                        + "JOIN OrdersWithRowtime o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id");
+    }
+
+    @Test
+    void testPreservesUpsertKeyTwoWayLeftJoinOrders() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_two_way ("
+                                + "  `user_id` STRING NOT NULL,"
+                                + "  `order_id` STRING NOT NULL,"
+                                + "  product STRING,"
+                                + "  user_region_id INT,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (`user_id`, `order_id`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_two_way\n"
+                        + "SELECT\n"
+                        + "    o.user_id,\n"
+                        + "    o.order_id,\n"
+                        + "    o.product,\n"
+                        + "    u.region_id\n"
+                        + "FROM OrdersPK o\n"
+                        + "LEFT JOIN UsersPK u\n"
+                        + "    ON u.user_id = o.user_id");
+    }
+
+    @Test
+    void testPreservesUpsertKeyTwoWayInnerJoinOrders() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_two_way ("
+                                + "  `user_id` STRING NOT NULL,"
+                                + "  `order_id` STRING NOT NULL,"
+                                + "  product STRING,"
+                                + "  user_region_id INT,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (`user_id`, `order_id`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_two_way\n"
+                        + "SELECT\n"
+                        + "    o.user_id,\n"
+                        + "    o.order_id,\n"
+                        + "    o.product,\n"
+                        + "    u.region_id\n"
+                        + "FROM UsersPK u\n"
+                        + "INNER JOIN OrdersPK o\n"
+                        + "    ON u.user_id = o.user_id");
+    }
+
+    @Test
+    void testPreservesUpsertKeyTwoWayInnerJoinOrdersDoesNot() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE OrdersSimplePK ("
+                                + "  order_id STRING NOT NULL,"
+                                + "  user_id STRING NOT NULL,"
+                                + "  product STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (order_id) NOT ENFORCED"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_two_way ("
+                                + "  `user_id` STRING NOT NULL,"
+                                + "  `order_id` STRING NOT NULL,"
+                                + "  product STRING,"
+                                + "  user_region_id INT,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (`order_id`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_two_way\n"
+                        + "SELECT\n"
+                        + "    o.user_id,\n"
+                        + "    o.order_id,\n"
+                        + "    o.product,\n"
+                        + "    u.region_id\n"
+                        + "FROM UsersPK u\n"
+                        + "INNER JOIN OrdersSimplePK o\n"
+                        + "    ON u.user_id = o.user_id");
+    }
+
+    @Test
+    void testPreservesUpsertKeyThreeWayJoin() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_three_way ("
+                                + "  `user_id` STRING NOT NULL,"
+                                + "  `order_id` STRING NOT NULL,"
+                                + "  `user_id2` STRING NOT NULL,"
+                                + "  `payment_id` STRING NOT NULL,"
+                                + "  `user_id3` STRING NOT NULL,"
+                                + "  `description` STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (`user_id`, `order_id`, `user_id2`, `payment_id`, `user_id3`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_three_way\n"
+                        + "SELECT\n"
+                        + "    o.user_id,\n"
+                        + "    o.order_id,\n"
+                        + "    p.user_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    u.user_id,\n"
+                        + "    u.description\n"
+                        + "FROM UsersPK u\n"
+                        + "JOIN OrdersPK o\n"
+                        + "    ON o.user_id = u.user_id\n"
+                        + "JOIN PaymentsPK p\n"
+                        + "    ON o.user_id = p.user_id");
+    }
+
+    @Test
+    void testPreservesUpsertKeyFourWayComplex() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE sink_four_way ("
+                                + "  user_id STRING NOT NULL,"
+                                + "  order_id STRING NOT NULL,"
+                                + "  user_id1 STRING NOT NULL,"
+                                + "  payment_id STRING NOT NULL,"
+                                + "  user_id2 STRING NOT NULL,"
+                                + "  name STRING,"
+                                + "  location STRING,"
+                                + "  CONSTRAINT `PRIMARY` PRIMARY KEY (`user_id`, `order_id`, `user_id1`, `payment_id`, `user_id2`) NOT ENFORCED"
+                                + ") WITH ("
+                                + "  'connector' = 'values',"
+                                + "  'sink-insert-only' = 'false'"
+                                + ")");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO sink_four_way\n"
+                        + "SELECT\n"
+                        + "    u.user_id,\n"
+                        + "    o.order_id,\n"
+                        + "    o.user_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    p.user_id,\n"
+                        + "    u.name,\n"
+                        + "    a.location\n"
+                        + "FROM UsersPK u\n"
+                        + "JOIN OrdersPK o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "    AND o.product IS NOT NULL\n"
+                        + "JOIN PaymentsPK p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "    AND p.price >= 0\n"
+                        + "JOIN AddressPK a\n"
+                        + "    ON u.user_id = a.user_id\n"
+                        + "    AND a.location IS NOT NULL");
+    }
+
+    @Test
+    void testMultiSinkOnMultiJoinedView() {
+        util.tableEnv()
+                .executeSql(
+                        "create temporary table src1 (\n"
+                                + "  a int,\n"
+                                + "  b bigint,\n"
+                                + "  c string,\n"
+                                + "  d int,\n"
+                                + "  primary key(a, c) not enforced\n"
+                                + ") with (\n"
+                                + " 'connector' = 'values',\n"
+                                + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                                + ")");
+        util.tableEnv()
+                .executeSql(
+                        "create temporary table src2 (\n"
+                                + "  a int,\n"
+                                + "  b bigint,\n"
+                                + "  c string,\n"
+                                + "  d int,\n"
+                                + "  primary key(a, c) not enforced\n"
+                                + ") with (\n"
+                                + " 'connector' = 'values',\n"
+                                + " 'changelog-mode' = 'I,UA,UB,D'\n"
+                                + ")");
+        util.tableEnv()
+                .executeSql(
+                        "create temporary table sink1 (\n"
+                                + "  a int,\n"
+                                + "  b string,\n"
+                                + "  c bigint,\n"
+                                + "  d bigint\n"
+                                + ") with (\n"
+                                + " 'connector' = 'values',\n"
+                                + " 'sink-insert-only' = 'false'\n"
+                                + ")");
+        util.tableEnv()
+                .executeSql(
+                        "create temporary table sink2 (\n"
+                                + "  a int,\n"
+                                + "  b string,\n"
+                                + "  c bigint,\n"
+                                + "  d string\n"
+                                + ") with (\n"
+                                + " 'connector' = 'values',\n"
+                                + " 'sink-insert-only' = 'false'\n"
+                                + ")");
+        util.tableEnv()
+                .executeSql(
+                        "create temporary view v1 as\n"
+                                + "select\n"
+                                + "  t1.a as a, t1.`day` as `day`, t2.b as b, t2.c as c\n"
+                                + "from (\n"
+                                + "  select a, b, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd') as `day`\n"
+                                + "  from src1\n"
+                                + " ) t1\n"
+                                + "join (\n"
+                                + "  select b, CONCAT(c, DATE_FORMAT(CURRENT_TIMESTAMP, 'yyMMdd')) as `day`, c, d\n"
+                                + "  from src2\n"
+                                + ") t2\n"
+                                + " on t1.a = t2.d");
+
+        StatementSet stmtSet = util.tableEnv().createStatementSet();
+        stmtSet.addInsertSql(
+                "insert into sink1\n"
+                        + "  select a, `day`, sum(b), count(distinct c)\n"
+                        + "  from v1\n"
+                        + "  group by a, `day`");
+        stmtSet.addInsertSql(
+                "insert into sink2\n"
+                        + "  select a, `day`, b, c\n"
+                        + "  from v1\n"
+                        + "  where b > 100");
+
+        util.doVerifyPlan(
+                stmtSet,
+                new ExplainDetail[] {ExplainDetail.PLAN_ADVICE},
+                false,
+                new Enumeration.Value[] {PlanKind.OPT_REL_WITH_ADVICE()},
+                () -> UNIT,
+                false,
+                false);
+    }
+
+    /*
+     * Calcite adds a LogicalProject to compute expressions such as UPPER and FLOOR
+     * on the necessary fields. As a result, the planner cannot fuse all joins into
+     * a single MultiJoin node initially.
+     */
+    @Test
+    @Tag("multijoin-chain-expected")
+    void testFourWayJoinWithFunctionInConditionMultiJoinChainExpected() {
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON o.user_id = u.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "    AND UPPER(u.name) = UPPER(p.payment_id)\n"
+                        + "    AND (FLOOR(u.cash) >= FLOOR(p.price) OR p.price < 0)\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON p.payment_id = s.location");
+    }
+
+    /*
+     * We expect the join inputs to **not** merge into a single MultiJoin node in this case,
+     * because `documents.common_id` is different from `other_documents.common_id`.
+     */
+    @Test
+    @Tag("no-common-join-key")
+    void testComplexCommonJoinKeyMissingProjectionNoCommonJoinKey() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Assignments ("
+                                + "  assignment_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  user_id STRING,"
+                                + "  detail_id STRING,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Documents ("
+                                + "  detail_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  creator_nm STRING,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT *\n"
+                        + "FROM Assignments assignments\n"
+                        + "LEFT JOIN Documents AS documents\n"
+                        + "    ON assignments.detail_id = documents.detail_id\n"
+                        + "    AND assignments.common_id = documents.common_id\n"
+                        + "LEFT JOIN Documents AS other_documents\n"
+                        + "    ON assignments.user_id = other_documents.common_id");
+    }
+
+    @Test
+    void testComplexCommonJoinKey() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Assignments ("
+                                + "  assignment_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  user_id STRING,"
+                                + "  detail_id STRING,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Customers ("
+                                + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  name STRING,"
+                                + "  depart_num STRING,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Documents ("
+                                + "  detail_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  creator_nm STRING,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE PhaseDetails ("
+                                + "  phase_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Organizations ("
+                                + "  org_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  org_name STRING,"
+                                + "  common_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyExecPlan(
+                "\nSELECT *\n"
+                        + "FROM Assignments assignments\n"
+                        + "LEFT JOIN Customers AS customer\n"
+                        + "    ON assignments.user_id = customer.user_id\n"
+                        + "    AND assignments.common_id = customer.common_id\n"
+                        + "LEFT JOIN Documents AS documents\n"
+                        + "    ON assignments.detail_id = documents.detail_id\n"
+                        + "    AND assignments.common_id = documents.common_id\n"
+                        + "LEFT JOIN PhaseDetails AS phase_details\n"
+                        + "    ON documents.common_id = phase_details.common_id\n"
+                        + "LEFT JOIN Organizations AS organizations\n"
+                        + "    ON customer.depart_num = organizations.org_id\n"
+                        + "    AND customer.common_id = organizations.common_id\n"
+                        + "LEFT JOIN Customers AS creators\n"
+                        + "    ON documents.creator_nm = creators.depart_num\n"
+                        + "    AND documents.common_id = creators.common_id");
+    }
+
+    @Test
+    @Tag("no-common-join-key")
+    void testComplexConditionalLogicWithMultiJoinNoCommonJoinKey() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE ProductCategories ("
+                                + "  category_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  category_name STRING,"
+                                + "  is_premium BOOLEAN,"
+                                + "  discount_rate DOUBLE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE ProductReviews ("
+                                + "  review_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  product_id STRING,"
+                                + "  rating INT,"
+                                + "  is_verified BOOLEAN"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    pc.category_name,\n"
+                        + "    CASE\n"
+                        + "        WHEN pc.is_premium = true AND p.price > 1000 THEN 'High-Value Premium'\n"
+                        + "        WHEN pc.is_premium = true THEN 'Premium'\n"
+                        + "        WHEN p.price > 500 THEN 'Standard High-Value'\n"
+                        + "        ELSE 'Standard'\n"
+                        + "    END AS product_tier,\n"
+                        + "    CASE\n"
+                        + "        WHEN pr.rating >= 4 AND pr.is_verified = true THEN 'Highly Recommended'\n"
+                        + "        WHEN pr.rating >= 3 THEN 'Recommended'\n"
+                        + "        WHEN pr.rating >= 2 THEN 'Average'\n"
+                        + "        ELSE 'Not Recommended'\n"
+                        + "    END AS recommendation_status,\n"
+                        + "    CASE\n"
+                        + "        WHEN pc.discount_rate > 0.2 THEN p.price * (1 - pc.discount_rate)\n"
+                        + "        ELSE p.price\n"
+                        + "    END AS final_price\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "LEFT JOIN ProductCategories pc\n"
+                        + "    ON o.product = pc.category_id\n"
+                        + "LEFT JOIN ProductReviews pr\n"
+                        + "    ON o.product = pr.product_id");
+    }
+
+    @Test
+    @Tag("no-common-join-key")
+    void testComplexCTEWithMultiJoinNoCommonJoinKey() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE OrderStatus ("
+                                + "  status_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  status_name STRING,"
+                                + "  is_final BOOLEAN"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE PaymentMethods ("
+                                + "  method_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  method_name STRING,"
+                                + "  processing_fee DOUBLE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nWITH user_orders AS (\n"
+                        + "    SELECT u.user_id, u.name, o.order_id, o.product, p.payment_id, p.price\n"
+                        + "    FROM Users u\n"
+                        + "    LEFT JOIN Orders o ON\n"
+                        + "        u.user_id = o.user_id\n"
+                        + "    LEFT JOIN Payments p\n"
+                        + "        ON u.user_id = p.user_id\n"
+                        + "),\n"
+                        + "order_details AS (\n"
+                        + "    SELECT uo.*, os.status_name, os.is_final, pm.method_name, pm.processing_fee\n"
+                        + "    FROM user_orders uo\n"
+                        + "    LEFT JOIN OrderStatus os\n"
+                        + "        ON uo.order_id = os.status_id\n"
+                        + "    LEFT JOIN PaymentMethods pm\n"
+                        + "        ON uo.payment_id = pm.method_id\n"
+                        + "),\n"
+                        + "final_summary AS (\n"
+                        + "    SELECT\n"
+                        + "        user_id,\n"
+                        + "        name,\n"
+                        + "        COUNT(order_id) as total_orders,\n"
+                        + "        SUM(price) as total_spent,\n"
+                        + "        AVG(price) as avg_order_value,\n"
+                        + "        COUNT(CASE WHEN is_final = true THEN 1 END) as completed_orders\n"
+                        + "    FROM order_details\n"
+                        + "    GROUP BY user_id, name\n"
+                        + ")\n"
+                        + "SELECT * FROM final_summary");
+    }
+
+    @Test
+    @Tag("no-common-join-key")
+    void testAggregationAndGroupingWithMultiJoinNoCommonJoinKey() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE OrderItems ("
+                                + "  item_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  order_id STRING,"
+                                + "  product_name STRING,"
+                                + "  quantity INT,"
+                                + "  unit_price DOUBLE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE ProductCategories ("
+                                + "  category_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  category_name STRING,"
+                                + "  parent_category STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    pc.category_name,\n"
+                        + "    COUNT(DISTINCT o.order_id) as order_count,\n"
+                        + "    SUM(oi.quantity) as total_items,\n"
+                        + "    SUM(oi.quantity * oi.unit_price) as total_value,\n"
+                        + "    AVG(oi.unit_price) as avg_item_price,\n"
+                        + "    MAX(p.price) as max_payment,\n"
+                        + "    MIN(p.price) as min_payment,\n"
+                        + "    COUNT(CASE WHEN oi.quantity > 5 THEN 1 END) as bulk_orders\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN OrderItems oi\n"
+                        + "    ON o.order_id = oi.order_id\n"
+                        + "LEFT JOIN ProductCategories pc\n"
+                        + "    ON oi.product_name = pc.category_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "GROUP BY u.user_id, u.name, pc.category_name\n"
+                        + "HAVING COUNT(DISTINCT o.order_id) > 0");
+    }
+
+    @Test
+    @Tag("no-common-join-key")
+    void testFunctionAndExpressionWithMultiJoinNoCommonJoinKey() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE ProductDetails ("
+                                + "  product_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  product_name STRING,"
+                                + "  description STRING,"
+                                + "  created_date BIGINT,"
+                                + "  tags STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE UserPreferences ("
+                                + "  user_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  preferred_category STRING,"
+                                + "  notification_level STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    UPPER(u.name) as user_name_upper,\n"
+                        + "    LOWER(o.product) as product_lower,\n"
+                        + "    CONCAT(u.name, ' - ', o.product) as user_product,\n"
+                        + "    SUBSTRING(pd.description, 1, 50) as description_preview,\n"
+                        + "    CHAR_LENGTH(pd.description) as description_length,\n"
+                        + "    FLOOR(p.price / 100.0) * 100 as price_rounded,\n"
+                        + "    CASE\n"
+                        + "        WHEN p.price > 1000 THEN 'High'\n"
+                        + "        WHEN p.price > 500 THEN 'Medium'\n"
+                        + "        ELSE 'Low'\n"
+                        + "    END as price_tier,\n"
+                        + "    REGEXP_REPLACE(pd.tags, ',', ' | ') as formatted_tags,\n"
+                        + "    TO_TIMESTAMP_LTZ(pd.created_date, 3) as product_created,\n"
+                        + "    COALESCE(up.preferred_category, 'None') as user_preference,\n"
+                        + "    CASE\n"
+                        + "        WHEN up.notification_level = 'HIGH' THEN 'Frequent Updates'\n"
+                        + "        WHEN up.notification_level = 'MEDIUM' THEN 'Daily Updates'\n"
+                        + "        ELSE 'Weekly Updates'\n"
+                        + "    END as notification_frequency\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "LEFT JOIN ProductDetails pd\n"
+                        + "    ON o.product = pd.product_id\n"
+                        + "LEFT JOIN UserPreferences up\n"
+                        + "    ON u.user_id = up.user_id");
+    }
+
+    /*
+     * Calcite automatically generates LogicalProject nodes for nested field access.
+     * As a result, each join input in this test is wrapped in a projection, which prevents
+     * the planner from fusing all joins into a single MultiJoin node initially.
+     * Therefore, in this test, each Join is still converted to a MultiJoin individually.
+     */
+    @Test
+    @Tag("multijoin-chain-expected")
+    void testJoinConditionHasNestedFieldsMultiJoinChainExpected() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Developers ("
+                                + "  developer_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  person ROW<info ROW<id STRING, name STRING, region STRING>>,"
+                                + "  experience_years INT"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE SupportTickets ("
+                                + "  ticket_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  reporter ROW<info ROW<id STRING, priority STRING>>,"
+                                + "  issue STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Feedback ("
+                                + "  feedback_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  author ROW<info ROW<id STRING, rating INT>>,"
+                                + "  message STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Subscriptions ("
+                                + "  sub_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  subscriber ROW<info ROW<id STRING, plan STRING>>,"
+                                + "  active BOOLEAN"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    d.developer_id,\n"
+                        + "    d.person.info.name AS developer_name,\n"
+                        + "    s.ticket_id,\n"
+                        + "    s.reporter.info.priority AS ticket_priority,\n"
+                        + "    f.feedback_id,\n"
+                        + "    f.author.info.rating AS feedback_rating,\n"
+                        + "    sub.sub_id,\n"
+                        + "    sub.subscriber.info.plan AS subscription_plan\n"
+                        + "FROM Developers AS d\n"
+                        + "LEFT JOIN SupportTickets AS s\n"
+                        + "    ON d.person.info.id = s.reporter.info.id\n"
+                        + "LEFT JOIN Feedback AS f\n"
+                        + "    ON d.person.info.id = f.author.info.id\n"
+                        + "LEFT JOIN Subscriptions AS sub\n"
+                        + "    ON d.person.info.id = sub.subscriber.info.id");
+    }
+
+    @Test
+    @Tag("multijoin-chain-expected")
+    void testComplexNestedCTEWithAggregationAndFunctionsMultiJoinChainExpected() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE OrderMetrics ("
+                                + "  metric_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  order_id STRING,"
+                                + "  metric_type STRING,"
+                                + "  metric_value DOUBLE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nWITH base_orders AS (\n"
+                        + "    SELECT u.user_id, u.name, o.order_id, p.payment_id, p.price\n"
+                        + "    FROM Users u\n"
+                        + "    INNER JOIN Orders o\n"
+                        + "        ON u.user_id = o.user_id\n"
+                        + "    INNER JOIN Payments p\n"
+                        + "        ON u.user_id = p.user_id\n"
+                        + "),\n"
+                        + "enriched_orders AS (\n"
+                        + "    SELECT\n"
+                        + "        bo.*,\n"
+                        + "        om.metric_type,\n"
+                        + "        om.metric_value,\n"
+                        + "        CASE\n"
+                        + "            WHEN bo.price > 1000 THEN 'Premium'\n"
+                        + "            WHEN bo.price > 500 THEN 'Standard'\n"
+                        + "            ELSE 'Basic'\n"
+                        + "        END as order_tier\n"
+                        + "    FROM base_orders bo\n"
+                        + "    LEFT JOIN OrderMetrics om\n"
+                        + "        ON bo.order_id = om.order_id\n"
+                        + "),\n"
+                        + "aggregated_metrics AS (\n"
+                        + "    SELECT\n"
+                        + "        user_id,\n"
+                        + "        name,\n"
+                        + "        COUNT(DISTINCT order_id) as total_orders,\n"
+                        + "        SUM(price) as total_spent,\n"
+                        + "        AVG(price) as avg_order_value,\n"
+                        + "        MAX(metric_value) as max_metric,\n"
+                        + "        MIN(metric_value) as min_metric,\n"
+                        + "        COUNT(CASE WHEN order_tier = 'Premium' THEN 1 END) as premium_orders\n"
+                        + "    FROM enriched_orders\n"
+                        + "    GROUP BY user_id, name\n"
+                        + ")\n"
+                        + "SELECT\n"
+                        + "    user_id,\n"
+                        + "    UPPER(name) as user_name,\n"
+                        + "    total_orders,\n"
+                        + "    ROUND(total_spent, 2) as total_spent_rounded,\n"
+                        + "    ROUND(avg_order_value, 2) as avg_order_value_rounded,\n"
+                        + "    CONCAT('User: ', name, ' has ', CAST(total_orders AS STRING), ' orders') as summary,\n"
+                        + "    CASE\n"
+                        + "        WHEN total_orders > 10 THEN 'Frequent Customer'\n"
+                        + "        WHEN total_orders > 5 THEN 'Regular Customer'\n"
+                        + "        ELSE 'Occasional Customer'\n"
+                        + "    END as customer_type\n"
+                        + "FROM aggregated_metrics\n"
+                        + "WHERE total_spent > 0");
+    }
+
+    @Test
+    void testJoinOfProjections() {
+        util.verifyRelPlan(
+                "\nSELECT u.user_id, o.order_id, o.product, p.price, s.location\n"
+                        + "FROM (SELECT user_id, name, cash FROM Users WHERE cash > 100) AS u\n"
+                        + "JOIN (SELECT user_id, order_id, product FROM Orders WHERE product IS NOT NULL) AS o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN (SELECT user_id, price FROM Payments WHERE price > 50) AS p\n"
+                        + "    ON u.user_id = p.user_id\n"
+                        + "LEFT JOIN (SELECT user_id, location FROM Shipments WHERE location IS NOT NULL) AS s\n"
+                        + "    ON u.user_id = s.user_id");
+    }
+
+    @Test
+    @Tag("multijoin-chain-expected")
+    void testJoinWithNestedSubqueryMultiJoinChainExpected() {
+        util.verifyRelPlan(
+                "\nSELECT *\n"
+                        + "FROM Users u\n"
+                        + "JOIN (\n"
+                        + "    SELECT o.user_id, o.order_id, p.payment_id, p.price\n"
+                        + "    FROM Orders o\n"
+                        + "    JOIN (\n"
+                        + "        SELECT payment_id, user_id, price\n"
+                        + "        FROM Payments\n"
+                        + "        WHERE price > 100\n"
+                        + "    ) AS p\n"
+                        + "    ON o.user_id = p.user_id\n"
+                        + ") AS op\n"
+                        + "ON u.user_id = op.user_id");
+    }
+
+    @Test
+    void testCTEWithMultiJoinV2() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Departments ("
+                                + "  dept_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  dept_name STRING,"
+                                + "  budget DOUBLE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Projects ("
+                                + "  project_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  project_name STRING,"
+                                + "  dept_id STRING,"
+                                + "  status STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nWITH high_budget_depts AS (\n"
+                        + "    SELECT dept_id, dept_name, budget\n"
+                        + "    FROM Departments\n"
+                        + "    WHERE budget > 600000\n"
+                        + "),\n"
+                        + "active_projects AS (\n"
+                        + "    SELECT project_id, project_name, dept_id\n"
+                        + "    FROM Projects\n"
+                        + "    WHERE status = 'ACTIVE'\n"
+                        + ")\n"
+                        + "SELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    hbd.dept_name,\n"
+                        + "    ap.project_name,\n"
+                        + "    hbd.budget\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN high_budget_depts hbd\n"
+                        + "    ON o.user_id = hbd.dept_id\n"
+                        + "LEFT JOIN active_projects ap\n"
+                        + "    ON hbd.dept_id = ap.dept_id");
+    }
+
+    @Test
+    void testWithOrInJoinCondition() {
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON o.user_id = u.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON u.user_id = p.user_id OR u.name = p.payment_id\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON p.user_id = s.user_id");
+    }
+
+    @Test
+    @Tag("multijoin-chain-expected")
+    void testWithCastCommonJoinKeyToIntegerMultiJoinChainExpected() {
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON o.user_id = u.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON CAST(u.user_id as INTEGER) = CAST(p.user_id as INTEGER)\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON u.user_id = s.user_id");
+    }
+
+    @Test
+    void testWithCastCommonJoinKeyToVarchar() {
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    p.payment_id,\n"
+                        + "    s.location\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON o.user_id = u.user_id\n"
+                        + "LEFT JOIN Payments p\n"
+                        + "    ON CAST(u.user_id as VARCHAR) = CAST(p.user_id as VARCHAR)\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON u.user_id = s.user_id");
+    }
+
+    @Test
+    void testAggregationAndGroupingWithMultiJoinV2() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Categories ("
+                                + "  category_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  category_name STRING,"
+                                + "  parent_category STRING,"
+                                + "  user_id STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Sales ("
+                                + "  sale_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  user_id STRING,"
+                                + "  product_id STRING,"
+                                + "  amount DOUBLE,"
+                                + "  sale_date DATE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    c.category_name,\n"
+                        + "    COUNT(DISTINCT u.user_id) AS unique_users,\n"
+                        + "    COUNT(s.sale_id) AS total_sales,\n"
+                        + "    SUM(s.amount) AS total_revenue,\n"
+                        + "    AVG(s.amount) AS avg_sale_amount,\n"
+                        + "    MAX(s.amount) AS max_sale_amount\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN Categories c\n"
+                        + "    ON u.user_id = c.user_id AND o.product = c.category_id\n"
+                        + "LEFT JOIN Sales s\n"
+                        + "    ON u.user_id = s.user_id\n"
+                        + "GROUP BY c.category_name\n"
+                        + "HAVING COUNT(s.sale_id) > 0");
+    }
+
+    @Test
+    void testSameTableMultipleAliases() {
+        util.verifyRelPlan(
+                "\nSELECT *\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Users u1\n"
+                        + "    ON u.user_id = u1.user_id\n"
+                        + "LEFT JOIN Users u2\n"
+                        + "    ON u1.user_id = u2.user_id\n"
+                        + "LEFT JOIN Users u3\n"
+                        + "    ON u2.user_id = u3.user_id");
+    }
+
+    @Test
+    @Tag("multijoin-chain-expected")
+    void testWithExpressionInJoinConditionMultiJoinChainExpected() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Products ("
+                                + "  product_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  price DOUBLE,"
+                                + "  discount DOUBLE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Sales ("
+                                + "  sale_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  product_key DOUBLE,"
+                                + "  quantity INT"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Promotions ("
+                                + "  promo_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  product_key DOUBLE,"
+                                + "  promo_text STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    p.product_id,\n"
+                        + "    (p.price - p.discount) AS net_price,\n"
+                        + "    s.quantity,\n"
+                        + "    pr.promo_text\n"
+                        + "FROM Products AS p\n"
+                        + "LEFT JOIN Sales AS s\n"
+                        + "    ON (p.price - p.discount) = s.product_key\n"
+                        + "LEFT JOIN Promotions AS pr\n"
+                        + "    ON (p.price - p.discount) = pr.product_key\n"
+                        + "WHERE (p.price - p.discount) > 100");
+    }
+
+    @Test
+    @Tag("no-common-join-key")
+    void testFunctionAndExpressionWithMultiJoinNoCommonJoinKeyV2() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE ProductDetails ("
+                                + "  product_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  product_name STRING,"
+                                + "  price DOUBLE,"
+                                + "  weight DOUBLE,"
+                                + "  created_date DATE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Reviews ("
+                                + "  review_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  product_id STRING,"
+                                + "  rating INT,"
+                                + "  review_text STRING,"
+                                + "  review_date DATE"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,UA,D')");
+
+        util.verifyRelPlan(
+                "\nSELECT\n"
+                        + "    u.user_id,\n"
+                        + "    u.name,\n"
+                        + "    o.order_id,\n"
+                        + "    pd.product_name,\n"
+                        + "    pd.price,\n"
+                        + "    ROUND(pd.price * 1.1, 2) AS price_with_tax,\n"
+                        + "    CONCAT('Product: ', pd.product_name) AS product_description,\n"
+                        + "    CHAR_LENGTH(r.review_text) AS review_length,\n"
+                        + "    UPPER(SUBSTRING(r.review_text, 1, 10)) AS review_preview,\n"
+                        + "    CASE\n"
+                        + "        WHEN r.rating >= 4 THEN 'High Rating'\n"
+                        + "        WHEN r.rating >= 3 THEN 'Medium Rating'\n"
+                        + "        ELSE 'Low Rating'\n"
+                        + "    END AS rating_category,\n"
+                        + "    TIMESTAMPDIFF(DAY, pd.created_date, CURRENT_DATE) AS days_since_created\n"
+                        + "FROM Users u\n"
+                        + "LEFT JOIN Orders o\n"
+                        + "    ON u.user_id = o.user_id\n"
+                        + "LEFT JOIN ProductDetails pd\n"
+                        + "    ON o.product = pd.product_id\n"
+                        + "LEFT JOIN Reviews r\n"
+                        + "    ON pd.product_id = r.product_id");
+    }
+
+    @Test
+    void testCrossJoinUnnestWithMultiJoinInsert() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE UnnestSink ("
+                                + "  detail_id STRING,"
+                                + "  element_data STRING,"
+                                + "  data_value_id INT,"
+                                + "  user_id STRING,"
+                                + "  order_id STRING"
+                                + ") WITH ('connector' = 'values', 'sink-insert-only' = 'false')");
+
+        util.verifyRelPlanInsert(
+                "\nINSERT INTO UnnestSink\n"
+                        + "(\n"
+                        + "    detail_id,\n"
+                        + "    element_data,\n"
+                        + "    data_value_id,\n"
+                        + "    user_id,\n"
+                        + "    order_id\n"
+                        + ")\n"
+                        + "SELECT\n"
+                        + "    d.detail_id,\n"
+                        + "    TRIM(REGEXP_REPLACE(edata, '[\\[\\]\\\"]', '')) AS element_data,\n"
+                        + "    ARRAY_POSITION(split(REGEXP_REPLACE(d.data, '^\\[\"|\"\\]$', '') , '\", \"'), edata) as data_value_id,\n"
+                        + "    d.user_id,\n"
+                        + "    o.order_id\n"
+                        + "FROM Detail d\n"
+                        + "INNER JOIN Orders o\n"
+                        + "    ON o.user_id = d.user_id\n"
+                        + "INNER JOIN Payments p\n"
+                        + "    ON p.user_id = d.user_id\n"
+                        + "LEFT JOIN Shipments s\n"
+                        + "    ON s.user_id = d.user_id\n"
+                        + "CROSS JOIN UNNEST(split(REGEXP_REPLACE(d.data, '^\\[\"|\"\\]$', '') , '\", \"')) AS T(edata)\n"
+                        + "WHERE NOT (s.location IS NOT NULL)");
     }
 }
diff --git a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
index 7df8d36563a..6c3a455d789 100644
--- a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
+++ b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
@@ -16,13 +16,648 @@ See the License for the specific language governing permissions and
 limitations under the License.
 -->
 <Root>
+  <TestCase name="testAggregationAndGroupingWithMultiJoinNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    pc.category_name,
+    COUNT(DISTINCT o.order_id) as order_count,
+    SUM(oi.quantity) as total_items,
+    SUM(oi.quantity * oi.unit_price) as total_value,
+    AVG(oi.unit_price) as avg_item_price,
+    MAX(p.price) as max_payment,
+    MIN(p.price) as min_payment,
+    COUNT(CASE WHEN oi.quantity > 5 THEN 1 END) as bulk_orders
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN OrderItems oi
+    ON o.order_id = oi.order_id
+LEFT JOIN ProductCategories pc
+    ON oi.product_name = pc.category_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+GROUP BY u.user_id, u.name, pc.category_name
+HAVING COUNT(DISTINCT o.order_id) > 0]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalFilter(condition=[>($3, 0)])
++- LogicalAggregate(group=[{0, 1, 2}], order_count=[COUNT(DISTINCT $3)], total_items=[SUM($4)], total_value=[SUM($5)], avg_item_price=[AVG($6)], max_payment=[MAX($7)], min_payment=[MIN($7)], bulk_orders=[COUNT($8)])
+   +- LogicalProject(user_id=[$0], name=[$1], category_name=[$12], order_id=[$3], quantity=[$9], $f5=[*($9, $10)], unit_price=[$10], price=[$15], $f8=[CASE(>($9, 5), 1, null:INTEGER)])
+      +- LogicalJoin(condition=[=($0, $16)], joinType=[left])
+         :- LogicalJoin(condition=[=($8, $11)], joinType=[left])
+         :  :- LogicalJoin(condition=[=($3, $7)], joinType=[left])
+         :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+         :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+         :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+         :  :  +- LogicalTableScan(table=[[default_catalog, default_database, OrderItems]])
+         :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductCategories]])
+         +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, category_name, order_count, total_items, total_value, avg_item_price, max_payment, min_payment, bulk_orders], where=[>(order_count, 0)])
++- GroupAggregate(groupBy=[user_id, name, category_name], select=[user_id, name, category_name, COUNT_RETRACT(DISTINCT order_id) AS order_count, SUM_RETRACT(quantity) AS total_items, SUM_RETRACT($f5) AS total_value, AVG_RETRACT(unit_price) AS avg_item_price, MAX_RETRACT(price) AS max_payment, MIN_RETRACT(price) AS min_payment, COUNT_RETRACT($f8) AS bulk_orders])
+   +- Exchange(distribution=[hash[user_id, name, category_name]])
+      +- Calc(select=[user_id, name, category_name, order_id, quantity, *(quantity, unit_price) AS $f5, unit_price, price, CASE(>(quantity, 5), 1, null:INTEGER) AS $f8])
+         +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id1)], select=[user_id,name,order_id,quantity,unit_price,category_name,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, INTEGER quantity, DOUBLE unit_price, VARCHAR(2147483647) category_name, INTEGER price, VARCHAR(2147483647) user_id1)])
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- Calc(select=[user_id, name, order_id, quantity, unit_price, category_name])
+            :     +- MultiJoin(commonJoinKey=[product_name], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (category_id)], joinConditions=[=(product_name, category_id)], select=[user_id,name,order_id,product_name,quantity,unit_price,category_id,category_name], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product_name, INTEGER quantity, DOUBLE unit_price, VARCHAR(2147483647) category_id, VARCHAR(2147483647)  [...]
+            :        :- Exchange(distribution=[hash[product_name]])
+            :        :  +- Calc(select=[user_id, name, order_id, product_name, quantity, unit_price])
+            :        :     +- MultiJoin(commonJoinKey=[order_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(order_id, order_id0)], select=[user_id,name,order_id,order_id0,product_name,quantity,unit_price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) order_id0, VARCHAR(2147483647) product_name, INTEGER quantity, DOUBLE unit_price)])
+            :        :        :- Exchange(distribution=[hash[order_id]])
+            :        :        :  +- Calc(select=[user_id, name, order_id])
+            :        :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
+            :        :        :        :- Exchange(distribution=[hash[user_id]])
+            :        :        :        :  +- ChangelogNormalize(key=[user_id])
+            :        :        :        :     +- Exchange(distribution=[hash[user_id]])
+            :        :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+            :        :        :        +- Exchange(distribution=[hash[user_id]])
+            :        :        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+            :        :        +- Exchange(distribution=[hash[order_id]])
+            :        :           +- Calc(select=[order_id, product_name, quantity, unit_price])
+            :        :              +- ChangelogNormalize(key=[item_id])
+            :        :                 +- Exchange(distribution=[hash[item_id]])
+            :        :                    +- TableSourceScan(table=[[default_catalog, default_database, OrderItems]], fields=[item_id, order_id, product_name, quantity, unit_price])
+            :        +- Exchange(distribution=[hash[category_id]])
+            :           +- ChangelogNormalize(key=[category_id])
+            :              +- Exchange(distribution=[hash[category_id]])
+            :                 +- TableSourceScan(table=[[default_catalog, default_database, ProductCategories, project=[category_id, category_name], metadata=[]]], fields=[category_id, category_name])
+            +- Exchange(distribution=[hash[user_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testAggregationAndGroupingWithMultiJoinV2">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    c.category_name,
+    COUNT(DISTINCT u.user_id) AS unique_users,
+    COUNT(s.sale_id) AS total_sales,
+    SUM(s.amount) AS total_revenue,
+    AVG(s.amount) AS avg_sale_amount,
+    MAX(s.amount) AS max_sale_amount
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Categories c
+    ON u.user_id = c.user_id AND o.product = c.category_id
+LEFT JOIN Sales s
+    ON u.user_id = s.user_id
+GROUP BY c.category_name
+HAVING COUNT(s.sale_id) > 0]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalFilter(condition=[>($2, 0)])
++- LogicalAggregate(group=[{0}], unique_users=[COUNT(DISTINCT $1)], total_sales=[COUNT($2)], total_revenue=[SUM($3)], avg_sale_amount=[AVG($3)], max_sale_amount=[MAX($3)])
+   +- LogicalProject(category_name=[$7], user_id=[$0], sale_id=[$10], amount=[$13])
+      +- LogicalJoin(condition=[=($0, $11)], joinType=[left])
+         :- LogicalJoin(condition=[AND(=($0, $9), =($5, $6))], joinType=[left])
+         :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+         :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+         :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+         :  +- LogicalTableScan(table=[[default_catalog, default_database, Categories]])
+         +- LogicalTableScan(table=[[default_catalog, default_database, Sales]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[category_name, unique_users, total_sales, total_revenue, avg_sale_amount, max_sale_amount], where=[>(total_sales, 0)])
++- GroupAggregate(groupBy=[category_name], select=[category_name, COUNT_RETRACT(DISTINCT user_id) AS unique_users, COUNT_RETRACT(sale_id) AS total_sales, SUM_RETRACT(amount) AS total_revenue, AVG_RETRACT(amount) AS avg_sale_amount, MAX_RETRACT(amount) AS max_sale_amount])
+   +- Exchange(distribution=[hash[category_name]])
+      +- Calc(select=[category_name, user_id, sale_id, amount])
+         +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), noUniqueKey, (category_id), (sale_id)], joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), =(product, category_id)), =(user_id, user_id2)], select=[user_id,user_id0,product,category_id,category_name,user_id1,sale_id,user_id2,amount], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, VARCHAR(2147483647) category_id, [...]
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- ChangelogNormalize(key=[user_id])
+            :     +- Exchange(distribution=[hash[user_id]])
+            :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id], metadata=[]]], fields=[user_id])
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[user_id, product], metadata=[]]], fields=[user_id, product])
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- ChangelogNormalize(key=[category_id])
+            :     +- Exchange(distribution=[hash[category_id]])
+            :        +- TableSourceScan(table=[[default_catalog, default_database, Categories, project=[category_id, category_name, user_id], metadata=[]]], fields=[category_id, category_name, user_id])
+            +- Exchange(distribution=[hash[user_id]])
+               +- ChangelogNormalize(key=[sale_id])
+                  +- Exchange(distribution=[hash[sale_id]])
+                     +- TableSourceScan(table=[[default_catalog, default_database, Sales, project=[sale_id, user_id, amount], metadata=[]]], fields=[sale_id, user_id, amount])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCTEWithMultiJoinV2">
+    <Resource name="sql">
+      <![CDATA[
+WITH high_budget_depts AS (
+    SELECT dept_id, dept_name, budget
+    FROM Departments
+    WHERE budget > 600000
+),
+active_projects AS (
+    SELECT project_id, project_name, dept_id
+    FROM Projects
+    WHERE status = 'ACTIVE'
+)
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    hbd.dept_name,
+    ap.project_name,
+    hbd.budget
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN high_budget_depts hbd
+    ON o.user_id = hbd.dept_id
+LEFT JOIN active_projects ap
+    ON hbd.dept_id = ap.dept_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], dept_name=[$7], project_name=[$10], budget=[$8])
++- LogicalJoin(condition=[=($6, $11)], joinType=[left])
+   :- LogicalJoin(condition=[=($4, $6)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalProject(dept_id=[$0], dept_name=[$1], budget=[$2])
+   :     +- LogicalFilter(condition=[>($2, 600000)])
+   :        +- LogicalTableScan(table=[[default_catalog, default_database, Departments]])
+   +- LogicalProject(project_id=[$0], project_name=[$1], dept_id=[$2])
+      +- LogicalFilter(condition=[=($3, _UTF-16LE'ACTIVE')])
+         +- LogicalTableScan(table=[[default_catalog, default_database, Projects]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, dept_name, project_name, budget])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (dept_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id0, dept_id), =(dept_id, dept_id0)], select=[user_id,name,order_id,user_id0,dept_id,dept_name,budget,project_name,dept_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) dept_id, VARCHAR(2147483647) dept [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[dept_id]])
+   :  +- ChangelogNormalize(key=[dept_id], condition=[>(budget, 600000)])
+   :     +- Exchange(distribution=[hash[dept_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Departments, filter=[]]], fields=[dept_id, dept_name, budget])
+   +- Exchange(distribution=[hash[dept_id]])
+      +- Calc(select=[project_name, dept_id])
+         +- ChangelogNormalize(key=[project_id], condition=[=(status, 'ACTIVE')])
+            +- Exchange(distribution=[hash[project_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, Projects, filter=[]]], fields=[project_id, project_name, dept_id, status])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testComplexCTEWithMultiJoinNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+WITH user_orders AS (
+    SELECT u.user_id, u.name, o.order_id, o.product, p.payment_id, p.price
+    FROM Users u
+    LEFT JOIN Orders o ON
+        u.user_id = o.user_id
+    LEFT JOIN Payments p
+        ON u.user_id = p.user_id
+),
+order_details AS (
+    SELECT uo.*, os.status_name, os.is_final, pm.method_name, pm.processing_fee
+    FROM user_orders uo
+    LEFT JOIN OrderStatus os
+        ON uo.order_id = os.status_id
+    LEFT JOIN PaymentMethods pm
+        ON uo.payment_id = pm.method_id
+),
+final_summary AS (
+    SELECT
+        user_id,
+        name,
+        COUNT(order_id) as total_orders,
+        SUM(price) as total_spent,
+        AVG(price) as avg_order_value,
+        COUNT(CASE WHEN is_final = true THEN 1 END) as completed_orders
+    FROM order_details
+    GROUP BY user_id, name
+)
+SELECT * FROM final_summary]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], total_orders=[$2], total_spent=[$3], avg_order_value=[$4], completed_orders=[$5])
++- LogicalAggregate(group=[{0, 1}], total_orders=[COUNT($2)], total_spent=[SUM($3)], avg_order_value=[AVG($3)], completed_orders=[COUNT($4)])
+   +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], price=[$5], $f4=[CASE($7, 1, null:INTEGER)])
+      +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], product=[$3], payment_id=[$4], price=[$5], status_name=[$7], is_final=[$8], method_name=[$10], processing_fee=[$11])
+         +- LogicalJoin(condition=[=($4, $9)], joinType=[left])
+            :- LogicalJoin(condition=[=($2, $6)], joinType=[left])
+            :  :- LogicalProject(user_id=[$0], name=[$1], order_id=[$3], product=[$5], payment_id=[$6], price=[$7])
+            :  :  +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+            :  :     :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+            :  :     :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+            :  :     :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+            :  :     +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+            :  +- LogicalTableScan(table=[[default_catalog, default_database, OrderStatus]])
+            +- LogicalTableScan(table=[[default_catalog, default_database, PaymentMethods]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+GroupAggregate(groupBy=[user_id, name], select=[user_id, name, COUNT_RETRACT(order_id) AS total_orders, SUM_RETRACT(price) AS total_spent, AVG_RETRACT(price) AS avg_order_value, COUNT_RETRACT($f4) AS completed_orders])
++- Exchange(distribution=[hash[user_id, name]])
+   +- Calc(select=[user_id, name, order_id, price, CASE(is_final, 1, null:INTEGER) AS $f4])
+      +- MultiJoin(commonJoinKey=[payment_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (method_id)], joinConditions=[=(payment_id, method_id)], select=[user_id,name,order_id,payment_id,price,is_final,method_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price, BOOLEAN is_final, VARCHAR(2147483647) method_id)])
+         :- Exchange(distribution=[hash[payment_id]])
+         :  +- Calc(select=[user_id, name, order_id, payment_id, price, is_final])
+         :     +- MultiJoin(commonJoinKey=[order_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (status_id)], joinConditions=[=(order_id, status_id)], select=[user_id,name,order_id,payment_id,price,status_id,is_final], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) status_id, BOOLEAN is_final)])
+         :        :- Exchange(distribution=[hash[order_id]])
+         :        :  +- Calc(select=[user_id, name, order_id, payment_id, price])
+         :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) user_id1)])
+         :        :        :- Exchange(distribution=[hash[user_id]])
+         :        :        :  +- ChangelogNormalize(key=[user_id])
+         :        :        :     +- Exchange(distribution=[hash[user_id]])
+         :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+         :        :        :- Exchange(distribution=[hash[user_id]])
+         :        :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+         :        :        +- Exchange(distribution=[hash[user_id]])
+         :        :           +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+         :        +- Exchange(distribution=[hash[status_id]])
+         :           +- ChangelogNormalize(key=[status_id])
+         :              +- Exchange(distribution=[hash[status_id]])
+         :                 +- TableSourceScan(table=[[default_catalog, default_database, OrderStatus, project=[status_id, is_final], metadata=[]]], fields=[status_id, is_final])
+         +- Exchange(distribution=[hash[method_id]])
+            +- ChangelogNormalize(key=[method_id])
+               +- Exchange(distribution=[hash[method_id]])
+                  +- TableSourceScan(table=[[default_catalog, default_database, PaymentMethods, project=[method_id], metadata=[]]], fields=[method_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testComplexCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT *
+FROM Assignments assignments
+LEFT JOIN Customers AS customer
+    ON assignments.user_id = customer.user_id
+    AND assignments.common_id = customer.common_id
+LEFT JOIN Documents AS documents
+    ON assignments.detail_id = documents.detail_id
+    AND assignments.common_id = documents.common_id
+LEFT JOIN PhaseDetails AS phase_details
+    ON documents.common_id = phase_details.common_id
+LEFT JOIN Organizations AS organizations
+    ON customer.depart_num = organizations.org_id
+    AND customer.common_id = organizations.common_id
+LEFT JOIN Customers AS creators
+    ON documents.creator_nm = creators.depart_num
+    AND documents.common_id = creators.common_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(assignment_id=[$0], user_id=[$1], detail_id=[$2], common_id=[$3], user_id0=[$4], name=[$5], depart_num=[$6], common_id0=[$7], detail_id0=[$8], creator_nm=[$9], common_id1=[$10], phase_id=[$11], common_id2=[$12], org_id=[$13], org_name=[$14], common_id3=[$15], user_id1=[$16], name0=[$17], depart_num0=[$18], common_id4=[$19])
++- LogicalJoin(condition=[AND(=($9, $18), =($10, $19))], joinType=[left])
+   :- LogicalJoin(condition=[AND(=($6, $13), =($7, $15))], joinType=[left])
+   :  :- LogicalJoin(condition=[=($10, $12)], joinType=[left])
+   :  :  :- LogicalJoin(condition=[AND(=($2, $8), =($3, $10))], joinType=[left])
+   :  :  :  :- LogicalJoin(condition=[AND(=($1, $4), =($3, $7))], joinType=[left])
+   :  :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Assignments]])
+   :  :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Customers]])
+   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Documents]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, PhaseDetails]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Organizations]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Customers]])
+]]>
+    </Resource>
+    <Resource name="optimized exec plan">
+      <![CDATA[
+MultiJoin(commonJoinKey=[common_id], joinTypes=[LEFT, LEFT, LEFT, LEFT, LEFT], inputUniqueKeys=[(assignment_id), (user_id), (detail_id), (phase_id), (org_id), (user_id)], joinConditions=[((user_id = user_id0) AND (common_id = common_id0)), ((detail_id = detail_id0) AND (common_id = common_id1)), (common_id1 = common_id2), ((depart_num = org_id) AND (common_id0 = common_id3)), ((creator_nm = depart_num0) AND (common_id1 = common_id4))], select=[assignment_id,user_id,detail_id,common_id,us [...]
+:- Exchange(distribution=[hash[common_id]])
+:  +- ChangelogNormalize(key=[assignment_id])
+:     +- Exchange(distribution=[hash[assignment_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Assignments]], fields=[assignment_id, user_id, detail_id, common_id])
+:- Exchange(distribution=[hash[common_id]])(reuse_id=[1])
+:  +- ChangelogNormalize(key=[user_id])
+:     +- Exchange(distribution=[hash[user_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Customers]], fields=[user_id, name, depart_num, common_id])
+:- Exchange(distribution=[hash[common_id]])
+:  +- ChangelogNormalize(key=[detail_id])
+:     +- Exchange(distribution=[hash[detail_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Documents]], fields=[detail_id, creator_nm, common_id])
+:- Exchange(distribution=[hash[common_id]])
+:  +- ChangelogNormalize(key=[phase_id])
+:     +- Exchange(distribution=[hash[phase_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, PhaseDetails]], fields=[phase_id, common_id])
+:- Exchange(distribution=[hash[common_id]])
+:  +- ChangelogNormalize(key=[org_id])
+:     +- Exchange(distribution=[hash[org_id]])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Organizations]], fields=[org_id, org_name, common_id])
++- Reused(reference_id=[1])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testComplexCommonJoinKeyMissingProjectionNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT *
+FROM Assignments assignments
+LEFT JOIN Documents AS documents
+    ON assignments.detail_id = documents.detail_id
+    AND assignments.common_id = documents.common_id
+LEFT JOIN Documents AS other_documents
+    ON assignments.user_id = other_documents.common_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(assignment_id=[$0], user_id=[$1], detail_id=[$2], common_id=[$3], detail_id0=[$4], creator_nm=[$5], common_id0=[$6], detail_id1=[$7], creator_nm0=[$8], common_id1=[$9])
++- LogicalJoin(condition=[=($1, $9)], joinType=[left])
+   :- LogicalJoin(condition=[AND(=($2, $4), =($3, $6))], joinType=[left])
+   :  :- LogicalTableScan(table=[[default_catalog, default_database, Assignments]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Documents]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Documents]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (detail_id)], joinConditions=[=(user_id, common_id1)], select=[assignment_id,user_id,detail_id,common_id,detail_id0,creator_nm,common_id0,detail_id1,creator_nm0,common_id1], rowType=[RecordType(VARCHAR(2147483647) assignment_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) detail_id, VARCHAR(2147483647) common_id, VARCHAR(2147483647) detail_id0, VARCHAR(2147483647) creator_nm, VARCHAR(2147483647) commo [...]
+:- Exchange(distribution=[hash[user_id]])
+:  +- MultiJoin(commonJoinKey=[detail_id, common_id], joinTypes=[LEFT], inputUniqueKeys=[(assignment_id), (detail_id)], joinConditions=[AND(=(detail_id, detail_id0), =(common_id, common_id0))], select=[assignment_id,user_id,detail_id,common_id,detail_id0,creator_nm,common_id0], rowType=[RecordType(VARCHAR(2147483647) assignment_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) detail_id, VARCHAR(2147483647) common_id, VARCHAR(2147483647) detail_id0, VARCHAR(2147483647) creator_nm, VAR [...]
+:     :- Exchange(distribution=[hash[detail_id, common_id]])
+:     :  +- ChangelogNormalize(key=[assignment_id])
+:     :     +- Exchange(distribution=[hash[assignment_id]])
+:     :        +- TableSourceScan(table=[[default_catalog, default_database, Assignments]], fields=[assignment_id, user_id, detail_id, common_id])
+:     +- Exchange(distribution=[hash[detail_id, common_id]])
+:        +- ChangelogNormalize(key=[detail_id])
+:           +- Exchange(distribution=[hash[detail_id]])
+:              +- TableSourceScan(table=[[default_catalog, default_database, Documents]], fields=[detail_id, creator_nm, common_id])
++- Exchange(distribution=[hash[common_id]])
+   +- ChangelogNormalize(key=[detail_id])
+      +- Exchange(distribution=[hash[detail_id]])
+         +- TableSourceScan(table=[[default_catalog, default_database, Documents]], fields=[detail_id, creator_nm, common_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testComplexConditionalLogicWithMultiJoinNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    o.order_id,
+    p.payment_id,
+    pc.category_name,
+    CASE
+        WHEN pc.is_premium = true AND p.price > 1000 THEN 'High-Value Premium'
+        WHEN pc.is_premium = true THEN 'Premium'
+        WHEN p.price > 500 THEN 'Standard High-Value'
+        ELSE 'Standard'
+    END AS product_tier,
+    CASE
+        WHEN pr.rating >= 4 AND pr.is_verified = true THEN 'Highly Recommended'
+        WHEN pr.rating >= 3 THEN 'Recommended'
+        WHEN pr.rating >= 2 THEN 'Average'
+        ELSE 'Not Recommended'
+    END AS recommendation_status,
+    CASE
+        WHEN pc.discount_rate > 0.2 THEN p.price * (1 - pc.discount_rate)
+        ELSE p.price
+    END AS final_price
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+LEFT JOIN ProductCategories pc
+    ON o.product = pc.category_id
+LEFT JOIN ProductReviews pr
+    ON o.product = pr.product_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], order_id=[$3], payment_id=[$6], category_name=[$10], product_tier=[CASE(AND($11, >($7, 1000)), _UTF-16LE'High-Value Premium':VARCHAR(19) CHARACTER SET "UTF-16LE", $11, _UTF-16LE'Premium':VARCHAR(19) CHARACTER SET "UTF-16LE", >($7, 500), _UTF-16LE'Standard High-Value':VARCHAR(19) CHARACTER SET "UTF-16LE", _UTF-16LE'Standard':VARCHAR(19) CHARACTER SET "UTF-16LE")], recommendation_status=[CASE(AND(>=($15, 4), $16), _UTF-16LE'Highly Recommended':VARCHAR(18) CHARA [...]
++- LogicalJoin(condition=[=($5, $14)], joinType=[left])
+   :- LogicalJoin(condition=[=($5, $9)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+   :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductCategories]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, ProductReviews]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, order_id, payment_id, category_name, CASE(AND(is_premium, >(price, 1000)), 'High-Value Premium', is_premium, 'Premium', >(price, 500), 'Standard High-Value', 'Standard') AS product_tier, CASE(AND(>=(rating, 4), is_verified), 'Highly Recommended', >=(rating, 3), 'Recommended', >=(rating, 2), 'Average', 'Not Recommended') AS recommendation_status, CASE(>(discount_rate, 0.2), *(price, -(1, discount_rate)), CAST(price AS DOUBLE)) AS final_price])
++- MultiJoin(commonJoinKey=[product], joinTypes=[LEFT, LEFT], inputUniqueKeys=[noUniqueKey, (category_id), noUniqueKey], joinConditions=[=(product, category_id), =(product, product_id)], select=[user_id,order_id,product,payment_id,price,category_id,category_name,is_premium,discount_rate,product_id,rating,is_verified], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(214748364 [...]
+   :- Exchange(distribution=[hash[product]])
+   :  +- Calc(select=[user_id, order_id, product, payment_id, price])
+   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,order_id,user_id0,product,payment_id,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) user_id1)])
+   :        :- Exchange(distribution=[hash[user_id]])
+   :        :  +- ChangelogNormalize(key=[user_id])
+   :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id], metadata=[]]], fields=[user_id])
+   :        :- Exchange(distribution=[hash[user_id]])
+   :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   :- Exchange(distribution=[hash[category_id]])
+   :  +- ChangelogNormalize(key=[category_id])
+   :     +- Exchange(distribution=[hash[category_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, ProductCategories]], fields=[category_id, category_name, is_premium, discount_rate])
+   +- Exchange(distribution=[hash[product_id]])
+      +- Calc(select=[product_id, rating, is_verified])
+         +- ChangelogNormalize(key=[review_id])
+            +- Exchange(distribution=[hash[review_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, ProductReviews]], fields=[review_id, product_id, rating, is_verified])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testComplexNestedCTEWithAggregationAndFunctionsMultiJoinChainExpected">
+    <Resource name="sql">
+      <![CDATA[
+WITH base_orders AS (
+    SELECT u.user_id, u.name, o.order_id, p.payment_id, p.price
+    FROM Users u
+    INNER JOIN Orders o
+        ON u.user_id = o.user_id
+    INNER JOIN Payments p
+        ON u.user_id = p.user_id
+),
+enriched_orders AS (
+    SELECT
+        bo.*,
+        om.metric_type,
+        om.metric_value,
+        CASE
+            WHEN bo.price > 1000 THEN 'Premium'
+            WHEN bo.price > 500 THEN 'Standard'
+            ELSE 'Basic'
+        END as order_tier
+    FROM base_orders bo
+    LEFT JOIN OrderMetrics om
+        ON bo.order_id = om.order_id
+),
+aggregated_metrics AS (
+    SELECT
+        user_id,
+        name,
+        COUNT(DISTINCT order_id) as total_orders,
+        SUM(price) as total_spent,
+        AVG(price) as avg_order_value,
+        MAX(metric_value) as max_metric,
+        MIN(metric_value) as min_metric,
+        COUNT(CASE WHEN order_tier = 'Premium' THEN 1 END) as premium_orders
+    FROM enriched_orders
+    GROUP BY user_id, name
+)
+SELECT
+    user_id,
+    UPPER(name) as user_name,
+    total_orders,
+    ROUND(total_spent, 2) as total_spent_rounded,
+    ROUND(avg_order_value, 2) as avg_order_value_rounded,
+    CONCAT('User: ', name, ' has ', CAST(total_orders AS STRING), ' orders') as summary,
+    CASE
+        WHEN total_orders > 10 THEN 'Frequent Customer'
+        WHEN total_orders > 5 THEN 'Regular Customer'
+        ELSE 'Occasional Customer'
+    END as customer_type
+FROM aggregated_metrics
+WHERE total_spent > 0]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], user_name=[UPPER($1)], total_orders=[$2], total_spent_rounded=[ROUND($3, 2)], avg_order_value_rounded=[ROUND($4, 2)], summary=[CONCAT(_UTF-16LE'User: ', $1, _UTF-16LE' has ', CAST($2):VARCHAR(2147483647) CHARACTER SET "UTF-16LE" NOT NULL, _UTF-16LE' orders')], customer_type=[CASE(>($2, 10), _UTF-16LE'Frequent Customer':VARCHAR(19) CHARACTER SET "UTF-16LE", >($2, 5), _UTF-16LE'Regular Customer':VARCHAR(19) CHARACTER SET "UTF-16LE", _UTF-16LE'Occasional Custome [...]
++- LogicalFilter(condition=[>($3, 0)])
+   +- LogicalAggregate(group=[{0, 1}], total_orders=[COUNT(DISTINCT $2)], total_spent=[SUM($3)], avg_order_value=[AVG($3)], max_metric=[MAX($4)], min_metric=[MIN($4)], premium_orders=[COUNT($5)])
+      +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], price=[$4], metric_value=[$6], $f5=[CASE(=($7, _UTF-16LE'Premium'), 1, null:INTEGER)])
+         +- LogicalProject(user_id=[$0], name=[$1], order_id=[$2], payment_id=[$3], price=[$4], metric_type=[$7], metric_value=[$8], order_tier=[CASE(>($4, 1000), _UTF-16LE'Premium':VARCHAR(8) CHARACTER SET "UTF-16LE", >($4, 500), _UTF-16LE'Standard':VARCHAR(8) CHARACTER SET "UTF-16LE", _UTF-16LE'Basic':VARCHAR(8) CHARACTER SET "UTF-16LE")])
+            +- LogicalJoin(condition=[=($2, $6)], joinType=[left])
+               :- LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], price=[$7])
+               :  +- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
+               :     :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
+               :     :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+               :     :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+               :     +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+               +- LogicalTableScan(table=[[default_catalog, default_database, OrderMetrics]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, UPPER(name) AS user_name, total_orders, ROUND(total_spent, 2) AS total_spent_rounded, ROUND(avg_order_value, 2) AS avg_order_value_rounded, CONCAT('User: ', name, ' has ', CAST(total_orders AS VARCHAR(2147483647)), ' orders') AS summary, CASE(>(total_orders, 10), 'Frequent Customer', >(total_orders, 5), 'Regular Customer', 'Occasional Customer') AS customer_type], where=[>(total_spent, 0)])
++- GroupAggregate(groupBy=[user_id, name], select=[user_id, name, COUNT_RETRACT(DISTINCT order_id) AS total_orders, SUM_RETRACT(price) AS total_spent, AVG_RETRACT(price) AS avg_order_value])
+   +- Exchange(distribution=[hash[user_id, name]])
+      +- MultiJoin(commonJoinKey=[order_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(order_id, order_id0)], select=[user_id,name,order_id,price,order_id0,metric_value], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, INTEGER price, VARCHAR(2147483647) order_id0, DOUBLE metric_value)])
+         :- Exchange(distribution=[hash[order_id]])
+         :  +- Calc(select=[user_id, name, order_id, price])
+         :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, INTEGER price, VARCHAR(2147483647) user_id1)])
+         :        :- Exchange(distribution=[hash[user_id]])
+         :        :  +- ChangelogNormalize(key=[user_id])
+         :        :     +- Exchange(distribution=[hash[user_id]])
+         :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+         :        :- Exchange(distribution=[hash[user_id]])
+         :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+         :        +- Exchange(distribution=[hash[user_id]])
+         :           +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
+         +- Exchange(distribution=[hash[order_id]])
+            +- Calc(select=[order_id, metric_value])
+               +- ChangelogNormalize(key=[metric_id])
+                  +- Exchange(distribution=[hash[metric_id]])
+                     +- TableSourceScan(table=[[default_catalog, default_database, OrderMetrics, project=[order_id, metric_value, metric_id], metadata=[]]], fields=[order_id, metric_value, metric_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCrossJoinUnnestWithMultiJoinInsert">
+    <Resource name="sql">
+      <![CDATA[
+INSERT INTO UnnestSink
+(
+    detail_id,
+    element_data,
+    data_value_id,
+    user_id,
+    order_id
+)
+SELECT
+    d.detail_id,
+    TRIM(REGEXP_REPLACE(edata, '[\[\]\"]', '')) AS element_data,
+    ARRAY_POSITION(split(REGEXP_REPLACE(d.data, '^\["|"\]$', '') , '", "'), edata) as data_value_id,
+    d.user_id,
+    o.order_id
+FROM Detail d
+INNER JOIN Orders o
+    ON o.user_id = d.user_id
+INNER JOIN Payments p
+    ON p.user_id = d.user_id
+LEFT JOIN Shipments s
+    ON s.user_id = d.user_id
+CROSS JOIN UNNEST(split(REGEXP_REPLACE(d.data, '^\["|"\]$', '') , '", "')) AS T(edata)
+WHERE NOT (s.location IS NOT NULL)]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalSink(table=[default_catalog.default_database.UnnestSink], targetColumns=[[0],[1],[2],[3],[4]], fields=[detail_id, element_data, data_value_id, user_id, order_id])
++- LogicalProject(detail_id=[$0], element_data=[TRIM(FLAG(BOTH), _UTF-16LE' ', REGEXP_REPLACE($13, _UTF-16LE'[\[\]\"]', _UTF-16LE''))], data_value_id=[ARRAY_POSITION(split(REGEXP_REPLACE($3, _UTF-16LE'^\["|"\]$', _UTF-16LE''), _UTF-16LE'", "'), $13)], user_id=[$2], order_id=[$5])
+   +- LogicalFilter(condition=[NOT(IS NOT NULL($11))])
+      +- LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{3}])
+         :- LogicalJoin(condition=[=($12, $2)], joinType=[left])
+         :  :- LogicalJoin(condition=[=($10, $2)], joinType=[inner])
+         :  :  :- LogicalJoin(condition=[=($6, $2)], joinType=[inner])
+         :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Detail]])
+         :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+         :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+         :  +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+         +- LogicalProject(edata=[$0])
+            +- Uncollect
+               +- LogicalProject(EXPR$0=[split(REGEXP_REPLACE($cor0.data, _UTF-16LE'^\["|"\]$', _UTF-16LE''), _UTF-16LE'", "')])
+                  +- LogicalValues(tuples=[[{ 0 }]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.UnnestSink], targetColumns=[[0],[1],[2],[3],[4]], fields=[detail_id, element_data, data_value_id, user_id, order_id])
++- Calc(select=[detail_id, TRIM(BOTH, ' ', REGEXP_REPLACE(f0, '[\[\]\"]', '')) AS element_data, ARRAY_POSITION(split(REGEXP_REPLACE(data, '^\["|"\]$', ''), '", "'), f0) AS data_value_id, user_id, order_id])
+   +- Correlate(invocation=[$UNNEST_ROWS$1(split(REGEXP_REPLACE($cor0.data, _UTF-16LE'^\["|"\]$', _UTF-16LE''), _UTF-16LE'", "'))], correlate=[table($UNNEST_ROWS$1(split(REGEXP_REPLACE($cor0.data, '^\["|"\]$', ''), '", "')))], select=[detail_id,description,user_id,data,timestamp,order_id,user_id0,product,payment_id,price,user_id1,location,user_id2,f0], rowType=[RecordType(VARCHAR(2147483647) detail_id, VARCHAR(2147483647) description, VARCHAR(2147483647) user_id, VARCHAR(2147483647) data [...]
+      +- Calc(select=[detail_id, description, user_id, data, timestamp, order_id, user_id0, product, payment_id, price, user_id1, location, user_id2], where=[IS NULL(location)])
+         +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER, LEFT], inputUniqueKeys=[(detail_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id0, user_id), =(user_id1, user_id), =(user_id2, user_id)], select=[detail_id,description,user_id,data,timestamp,order_id,user_id0,product,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) detail_id, VARCHAR(2147483647) description, VARCHAR(2147483647) user_id, VARCHAR(2147483647) da [...]
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- TableSourceScan(table=[[default_catalog, default_database, Detail]], fields=[detail_id, description, user_id, data, timestamp])
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+            :- Exchange(distribution=[hash[user_id]])
+            :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+            +- Exchange(distribution=[hash[user_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
   <TestCase name="testFourWayComplexJoinExecPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 AND (u.cash >= p.price OR p.price < 0) LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.user_id = p.user_id
+    AND (u.cash >= p.price OR p.price < 0)
+LEFT JOIN Shipments s
+    ON p.user_id = s.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
 +- LogicalJoin(condition=[=($8, $10)], joinType=[left])
    :- LogicalJoin(condition=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinType=[inner])
    :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
@@ -34,25 +669,25 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], locati
     </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id, location])
-+- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;], 3=[LeftInputId:2;LeftFieldIndex:2;RightInputId:3;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_ [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   :- Exchange(distribution=[hash[user_id_2]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-   +- Exchange(distribution=[hash[user_id_3]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[(user_id = user_id0), ((user_id = user_id1) AND ((cash >= price) OR (price < 0))), (user_id1 = user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testFourWayComplexJoinExplain">
     <Resource name="explain">
       <![CDATA[== Abstract Syntax Tree ==
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
 +- LogicalJoin(condition=[=($8, $10)], joinType=[left])
    :- LogicalJoin(condition=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinType=[inner])
    :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
@@ -62,44 +697,264 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], locati
    +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
 
 == Optimized Physical Plan ==
-Calc(select=[user_id_0, name, order_id, payment_id, location])
-+- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;], 3=[LeftInputId:2;LeftFieldIndex:2;RightInputId:3;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_ [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   :- Exchange(distribution=[hash[user_id_2]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-   +- Exchange(distribution=[hash[user_id_3]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), OR(>=(cash, price), <(price, 0))), =(user_id1, user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 
 == Optimized Execution Plan ==
-Calc(select=[user_id_0, name, order_id, payment_id, location])
-+- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;], 3=[LeftInputId:2;LeftFieldIndex:2;RightInputId:3;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_ [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   :- Exchange(distribution=[hash[user_id_2]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-   +- Exchange(distribution=[hash[user_id_3]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[(user_id = user_id0), ((user_id = user_id1) AND ((cash >= price) OR (price < 0))), (user_id1 = user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayInnerJoinRelPlan">
+  <TestCase name="testFourWayComplexJoinRelPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN Payments p ON u.user_id_0 = p.user_id_2]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.user_id = p.user_id
+    AND (u.cash >= p.price OR p.price < 0)
+LEFT JOIN Shipments s
+    ON p.user_id = s.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
++- LogicalJoin(condition=[=($8, $10)], joinType=[left])
+   :- LogicalJoin(condition=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinType=[inner])
+   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id, user_id0), AND(=(user_id, user_id1), OR(>=(cash, price), <(price, 0))), =(user_id1, user_id2)], select=[user_id,name,cash,order_id,user_id0,payment_id,price,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) use [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFourWayJoinRelPlanNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN LookupTable
+    ON u.name = LookupTable.name
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+LEFT JOIN Shipments s
+    ON o.user_id = s.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$9], location=[$12])
++- LogicalJoin(condition=[=($4, $13)], joinType=[left])
+   :- LogicalJoin(condition=[=($0, $11)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($1, $7)], joinType=[left])
+   :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, LookupTable]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id0], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(user_id0, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) location, VARCHAR(2147483647) user_id2)])
+   :- Exchange(distribution=[hash[user_id0]])
+   :  +- Calc(select=[user_id, name, order_id, user_id0, payment_id])
+   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[=(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :        :- Exchange(distribution=[hash[user_id]])
+   :        :  +- Calc(select=[user_id, name, order_id, user_id0])
+   :        :     +- MultiJoin(commonJoinKey=[name], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(name, name0)], select=[user_id,name,order_id,user_id0,name0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name0)])
+   :        :        :- Exchange(distribution=[hash[name]])
+   :        :        :  +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
+   :        :        :     :- Exchange(distribution=[hash[user_id]])
+   :        :        :     :  +- ChangelogNormalize(key=[user_id])
+   :        :        :     :     +- Exchange(distribution=[hash[user_id]])
+   :        :        :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :        :        +- Exchange(distribution=[hash[name]])
+   :        :           +- TableSourceScan(table=[[default_catalog, default_database, LookupTable, project=[name], metadata=[]]], fields=[name])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFourWayJoinTransitiveCommonJoinKeyRelPlan">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON o.user_id = p.user_id
+LEFT JOIN Shipments s
+    ON p.user_id = s.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
++- LogicalJoin(condition=[=($8, $10)], joinType=[left])
+   :- LogicalJoin(condition=[=($4, $8)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id0, user_id1), =(user_id1, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_ [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFourWayJoinWithFunctionInConditionMultiJoinChainExpected">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON o.user_id = u.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+    AND UPPER(u.name) = UPPER(p.payment_id)
+    AND (FLOOR(u.cash) >= FLOOR(p.price) OR p.price < 0)
+LEFT JOIN Shipments s
+    ON p.payment_id = s.location]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
++- LogicalJoin(condition=[=($6, $9)], joinType=[left])
+   :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], payment_id=[$7], price=[$8], user_id1=[$9])
+   :  +- LogicalJoin(condition=[AND(=($0, $9), =($6, $10), OR(>=(FLOOR($2), FLOOR($8)), <($8, 0)))], joinType=[left])
+   :     :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], $f6=[UPPER($1)])
+   :     :  +- LogicalJoin(condition=[=($4, $0)], joinType=[left])
+   :     :     :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :     :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :     +- LogicalProject(payment_id=[$0], price=[$1], user_id=[$2], $f3=[UPPER($0)])
+   :        +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+MultiJoin(commonJoinKey=[payment_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(payment_id, location)], select=[user_id,name,order_id,payment_id,location], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) location)])
+:- Exchange(distribution=[hash[payment_id]])
+:  +- Calc(select=[user_id, name, order_id, payment_id])
+:     +- MultiJoin(commonJoinKey=[user_id, $f6], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[AND(=(user_id, user_id1), =($f6, $f3), OR(>=(FLOOR(cash), FLOOR(price)), <(price, 0)))], select=[user_id,name,cash,order_id,$f6,payment_id,price,user_id1,$f3], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) $f6, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(21474 [...]
+:        :- Exchange(distribution=[hash[user_id, $f6]])
+:        :  +- Calc(select=[user_id, name, cash, order_id, UPPER(name) AS $f6])
+:        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id0, user_id)], select=[user_id,name,cash,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
+:        :        :- Exchange(distribution=[hash[user_id]])
+:        :        :  +- ChangelogNormalize(key=[user_id])
+:        :        :     +- Exchange(distribution=[hash[user_id]])
+:        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+:        :        +- Exchange(distribution=[hash[user_id]])
+:        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+:        +- Exchange(distribution=[hash[user_id, $f3]])
+:           +- Calc(select=[payment_id, price, user_id, UPPER(payment_id) AS $f3])
+:              +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id])
++- Exchange(distribution=[hash[location]])
+   +- TableSourceScan(table=[[default_catalog, default_database, Shipments, project=[location], metadata=[]]], fields=[location])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFullOuterNotSupported">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+FULL OUTER JOIN Orders o
+    ON u.user_id = o.user_id
+FULL OUTER JOIN Payments p
+    ON o.user_id = p.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalJoin(condition=[=($4, $8)], joinType=[full])
+   :- LogicalJoin(condition=[=($0, $4)], joinType=[full])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
    :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
    +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
@@ -107,152 +962,634 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) o [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- Join(joinType=[FullOuterJoin], where=[=(user_id0, user_id1)], select=[user_id, name, order_id, user_id0, payment_id, user_id1], leftInputSpec=[NoUniqueKey], rightInputSpec=[HasUniqueKey])
+   :- Exchange(distribution=[hash[user_id0]])
+   :  +- Join(joinType=[FullOuterJoin], where=[=(user_id, user_id0)], select=[user_id, name, order_id, user_id0], leftInputSpec=[JoinKeyContainsUniqueKey], rightInputSpec=[HasUniqueKey])
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- ChangelogNormalize(key=[user_id])
+   :     :     +- Exchange(distribution=[hash[user_id]])
+   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFunctionAndExpressionWithMultiJoinNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    UPPER(u.name) as user_name_upper,
+    LOWER(o.product) as product_lower,
+    CONCAT(u.name, ' - ', o.product) as user_product,
+    SUBSTRING(pd.description, 1, 50) as description_preview,
+    CHAR_LENGTH(pd.description) as description_length,
+    FLOOR(p.price / 100.0) * 100 as price_rounded,
+    CASE
+        WHEN p.price > 1000 THEN 'High'
+        WHEN p.price > 500 THEN 'Medium'
+        ELSE 'Low'
+    END as price_tier,
+    REGEXP_REPLACE(pd.tags, ',', ' | ') as formatted_tags,
+    TO_TIMESTAMP_LTZ(pd.created_date, 3) as product_created,
+    COALESCE(up.preferred_category, 'None') as user_preference,
+    CASE
+        WHEN up.notification_level = 'HIGH' THEN 'Frequent Updates'
+        WHEN up.notification_level = 'MEDIUM' THEN 'Daily Updates'
+        ELSE 'Weekly Updates'
+    END as notification_frequency
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+LEFT JOIN ProductDetails pd
+    ON o.product = pd.product_id
+LEFT JOIN UserPreferences up
+    ON u.user_id = up.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], user_name_upper=[UPPER($1)], product_lower=[LOWER($5)], user_product=[CONCAT($1, _UTF-16LE' - ', $5)], description_preview=[SUBSTRING($11, 1, 50)], description_length=[CHAR_LENGTH($11)], price_rounded=[*(FLOOR(/($7, 100.0:DECIMAL(4, 1))), 100)], price_tier=[CASE(>($7, 1000), _UTF-16LE'High':VARCHAR(6) CHARACTER SET "UTF-16LE", >($7, 500), _UTF-16LE'Medium':VARCHAR(6) CHARACTER SET "UTF-16LE", _UTF-16LE'Low':VARCHAR(6) CHARACTER SET "UTF-16LE")], formatted_tag [...]
++- LogicalJoin(condition=[=($0, $14)], joinType=[left])
+   :- LogicalJoin(condition=[=($5, $9)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+   :  :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductDetails]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, UserPreferences]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, UPPER(name) AS user_name_upper, LOWER(product) AS product_lower, CONCAT(name, ' - ', product) AS user_product, SUBSTRING(description, 1, 50) AS description_preview, CHAR_LENGTH(description) AS description_length, *(FLOOR(/(price, 100.0)), 100) AS price_rounded, CASE(>(price, 1000), 'High', >(price, 500), 'Medium', 'Low') AS price_tier, REGEXP_REPLACE(tags, ',', ' | ') AS formatted_tags, TO_TIMESTAMP_LTZ(created_date, 3) AS product_created, COALESCE(preferred_categor [...]
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (user_id)], joinConditions=[=(user_id, user_id2)], select=[user_id,name,product,price,description,created_date,tags,user_id2,preferred_category,notification_level], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) product, INTEGER price, VARCHAR(2147483647) description, BIGINT created_date, VARCHAR(2147483647) tags, VARCHAR(2147483647) user_id2, VARCHAR(2147 [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id, name, product, price, description, created_date, tags])
+   :     +- MultiJoin(commonJoinKey=[product], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (product_id)], joinConditions=[=(product, product_id)], select=[user_id,name,product,price,product_id,description,created_date,tags], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) product, INTEGER price, VARCHAR(2147483647) product_id, VARCHAR(2147483647) description, BIGINT created_date, VARCHAR(2147483647) tags)])
+   :        :- Exchange(distribution=[hash[product]])
+   :        :  +- Calc(select=[user_id, name, product, price])
+   :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,user_id0,product,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product, INTEGER price, VARCHAR(2147483647) user_id1)])
+   :        :        :- Exchange(distribution=[hash[user_id]])
+   :        :        :  +- ChangelogNormalize(key=[user_id])
+   :        :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        :        :- Exchange(distribution=[hash[user_id]])
+   :        :        :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[user_id, product], metadata=[]]], fields=[user_id, product])
+   :        :        +- Exchange(distribution=[hash[user_id]])
+   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
+   :        +- Exchange(distribution=[hash[product_id]])
+   :           +- ChangelogNormalize(key=[product_id])
+   :              +- Exchange(distribution=[hash[product_id]])
+   :                 +- TableSourceScan(table=[[default_catalog, default_database, ProductDetails, project=[product_id, description, created_date, tags], metadata=[]]], fields=[product_id, description, created_date, tags])
+   +- Exchange(distribution=[hash[user_id]])
+      +- ChangelogNormalize(key=[user_id])
+         +- Exchange(distribution=[hash[user_id]])
+            +- TableSourceScan(table=[[default_catalog, default_database, UserPreferences]], fields=[user_id, preferred_category, notification_level])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFunctionAndExpressionWithMultiJoinNoCommonJoinKeyV2">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    pd.product_name,
+    pd.price,
+    ROUND(pd.price * 1.1, 2) AS price_with_tax,
+    CONCAT('Product: ', pd.product_name) AS product_description,
+    CHAR_LENGTH(r.review_text) AS review_length,
+    UPPER(SUBSTRING(r.review_text, 1, 10)) AS review_preview,
+    CASE
+        WHEN r.rating >= 4 THEN 'High Rating'
+        WHEN r.rating >= 3 THEN 'Medium Rating'
+        ELSE 'Low Rating'
+    END AS rating_category,
+    TIMESTAMPDIFF(DAY, pd.created_date, CURRENT_DATE) AS days_since_created
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN ProductDetails pd
+    ON o.product = pd.product_id
+LEFT JOIN Reviews r
+    ON pd.product_id = r.product_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], product_name=[$7], price=[$8], price_with_tax=[ROUND(*($8, 1.1:DECIMAL(2, 1)), 2)], product_description=[CONCAT(_UTF-16LE'Product: ', $7)], review_length=[CHAR_LENGTH($14)], review_preview=[UPPER(SUBSTRING($14, 1, 10))], rating_category=[CASE(>=($13, 4), _UTF-16LE'High Rating':VARCHAR(13) CHARACTER SET "UTF-16LE", >=($13, 3), _UTF-16LE'Medium Rating':VARCHAR(13) CHARACTER SET "UTF-16LE", _UTF-16LE'Low Rating':VARCHAR(13) CHARACTER SE [...]
++- LogicalJoin(condition=[=($6, $12)], joinType=[left])
+   :- LogicalJoin(condition=[=($5, $6)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, ProductDetails]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Reviews]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, product_name, price, ROUND(*(price, 1.1), 2) AS price_with_tax, CONCAT('Product: ', product_name) AS product_description, CHAR_LENGTH(review_text) AS review_length, UPPER(SUBSTRING(review_text, 1, 10)) AS review_preview, CASE(>=(rating, 4), 'High Rating', >=(rating, 3), 'Medium Rating', 'Low Rating') AS rating_category, CAST(/INT(Reinterpret(-(CURRENT_DATE(), created_date)), 86400000) AS INTEGER) AS days_since_created])
++- MultiJoin(commonJoinKey=[product], joinTypes=[LEFT, LEFT], inputUniqueKeys=[noUniqueKey, (product_id), noUniqueKey], joinConditions=[=(product, product_id), =(product_id, product_id0)], select=[user_id,name,order_id,product,product_id,product_name,price,created_date,product_id0,rating,review_text], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) product_id, VARCHAR(2147483647) pro [...]
+   :- Exchange(distribution=[hash[product]])
+   :  +- Calc(select=[user_id, name, order_id, product])
+   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+   :        :- Exchange(distribution=[hash[user_id]])
+   :        :  +- ChangelogNormalize(key=[user_id])
+   :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+   :- Exchange(distribution=[hash[product_id]])
+   :  +- ChangelogNormalize(key=[product_id])
+   :     +- Exchange(distribution=[hash[product_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, ProductDetails, project=[product_id, product_name, price, created_date], metadata=[]]], fields=[product_id, product_name, price, created_date])
+   +- Exchange(distribution=[hash[product_id]])
+      +- Calc(select=[product_id, rating, review_text])
+         +- ChangelogNormalize(key=[review_id])
+            +- Exchange(distribution=[hash[review_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, Reviews, project=[product_id, rating, review_text, review_id], metadata=[]]], fields=[product_id, rating, review_text, review_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testIntervalJoinExcludedFromMultiJoin">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    e1.id,
+    e1.val,
+    e2.price
+FROM EventTable1 e1
+JOIN EventTable2 e2
+    ON e1.id = e2.id
+    AND e1.`$rowtime` BETWEEN e2.`$rowtime` - INTERVAL '1' MINUTE
+    AND e2.`$rowtime` + INTERVAL '1' MINUTE]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], val=[$1], price=[$4])
++- LogicalJoin(condition=[AND(=($0, $3), >=($2, -($5, 60000:INTERVAL MINUTE)), <=($2, +($5, 60000:INTERVAL MINUTE)))], joinType=[inner])
+   :- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
+   +- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
+      +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[id, val, price])
++- IntervalJoin(joinType=[InnerJoin], windowBounds=[isRowTime=true, leftLowerBound=-60000, leftUpperBound=60000, leftTimeIndex=2, rightTimeIndex=2], where=[AND(=(id, id0), >=($rowtime, -($rowtime0, 60000:INTERVAL MINUTE)), <=($rowtime, +($rowtime0, 60000:INTERVAL MINUTE)))], select=[id, val, $rowtime, id0, price, $rowtime0])
+   :- Exchange(distribution=[hash[id]])
+   :  +- WatermarkAssigner(rowtime=[$rowtime], watermark=[-($rowtime, 5000:INTERVAL SECOND)])
+   :     +- TableSourceScan(table=[[default_catalog, default_database, EventTable1]], fields=[id, val, $rowtime])
+   +- Exchange(distribution=[hash[id]])
+      +- WatermarkAssigner(rowtime=[$rowtime], watermark=[-($rowtime, 5000:INTERVAL SECOND)])
+         +- TableSourceScan(table=[[default_catalog, default_database, EventTable2]], fields=[id, price, $rowtime])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinConditionHasNestedFieldsMultiJoinChainExpected">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    d.developer_id,
+    d.person.info.name AS developer_name,
+    s.ticket_id,
+    s.reporter.info.priority AS ticket_priority,
+    f.feedback_id,
+    f.author.info.rating AS feedback_rating,
+    sub.sub_id,
+    sub.subscriber.info.plan AS subscription_plan
+FROM Developers AS d
+LEFT JOIN SupportTickets AS s
+    ON d.person.info.id = s.reporter.info.id
+LEFT JOIN Feedback AS f
+    ON d.person.info.id = f.author.info.id
+LEFT JOIN Subscriptions AS sub
+    ON d.person.info.id = sub.subscriber.info.id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(developer_id=[$0], developer_name=[$1.info.name], ticket_id=[$3], ticket_priority=[$4.info.priority], feedback_id=[$6], feedback_rating=[$7.info.rating], sub_id=[$9], subscription_plan=[$10.info.plan])
++- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], feedback_id=[$6], author=[$7], message=[$8], sub_id=[$10], subscriber=[$11], active=[$12])
+   +- LogicalJoin(condition=[=($9, $13)], joinType=[left])
+      :- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], feedback_id=[$6], author=[$7], message=[$8], $f9=[$1.info.id])
+      :  +- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], feedback_id=[$7], author=[$8], message=[$9])
+      :     +- LogicalJoin(condition=[=($6, $10)], joinType=[left])
+      :        :- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$3], reporter=[$4], issue=[$5], $f6=[$1.info.id])
+      :        :  +- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], ticket_id=[$4], reporter=[$5], issue=[$6])
+      :        :     +- LogicalJoin(condition=[=($3, $7)], joinType=[left])
+      :        :        :- LogicalProject(developer_id=[$0], person=[$1], experience_years=[$2], $f3=[$1.info.id])
+      :        :        :  +- LogicalTableScan(table=[[default_catalog, default_database, Developers]])
+      :        :        +- LogicalProject(ticket_id=[$0], reporter=[$1], issue=[$2], $f3=[$1.info.id])
+      :        :           +- LogicalTableScan(table=[[default_catalog, default_database, SupportTickets]])
+      :        +- LogicalProject(feedback_id=[$0], author=[$1], message=[$2], $f3=[$1.info.id])
+      :           +- LogicalTableScan(table=[[default_catalog, default_database, Feedback]])
+      +- LogicalProject(sub_id=[$0], subscriber=[$1], active=[$2], $f3=[$1.info.id])
+         +- LogicalTableScan(table=[[default_catalog, default_database, Subscriptions]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[developer_id, person.info.name AS developer_name, ticket_id, reporter.info.priority AS ticket_priority, feedback_id, author.info.rating AS feedback_rating, sub_id, subscriber.info.plan AS subscription_plan])
++- MultiJoin(commonJoinKey=[$f9], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (sub_id)], joinConditions=[=($f9, $f3)], select=[developer_id,person,ticket_id,reporter,feedback_id,author,$f9,sub_id,subscriber,$f3], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCHAR(2147483647) name, VARCHAR(2147483647) region) info) person, VARCHAR(2147483647) ticket_id, RecordType:peek_no_expand(RecordType:peek_no_e [...]
+   :- Exchange(distribution=[hash[$f9]])
+   :  +- Calc(select=[developer_id, person, ticket_id, reporter, feedback_id, author, person.info.id AS $f9])
+   :     +- MultiJoin(commonJoinKey=[$f6], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (feedback_id)], joinConditions=[=($f6, $f3)], select=[developer_id,person,ticket_id,reporter,$f6,feedback_id,author,$f3], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCHAR(2147483647) name, VARCHAR(2147483647) region) info) person, VARCHAR(2147483647) ticket_id, RecordType:peek_no_expand(RecordType:peek_no_expan [...]
+   :        :- Exchange(distribution=[hash[$f6]])
+   :        :  +- Calc(select=[developer_id, person, ticket_id, reporter, person.info.id AS $f6])
+   :        :     +- MultiJoin(commonJoinKey=[$f3], joinTypes=[LEFT], inputUniqueKeys=[(developer_id), (ticket_id)], joinConditions=[=($f3, $f30)], select=[developer_id,person,$f3,ticket_id,reporter,$f30], rowType=[RecordType(VARCHAR(2147483647) developer_id, RecordType:peek_no_expand(RecordType:peek_no_expand(VARCHAR(2147483647) id, VARCHAR(2147483647) name, VARCHAR(2147483647) region) info) person, VARCHAR(2147483647) $f3, VARCHAR(2147483647) ticket_id, RecordType:peek_no_expand(Record [...]
+   :        :        :- Exchange(distribution=[hash[$f3]])
+   :        :        :  +- Calc(select=[developer_id, person, person.info.id AS $f3])
+   :        :        :     +- ChangelogNormalize(key=[developer_id])
+   :        :        :        +- Exchange(distribution=[hash[developer_id]])
+   :        :        :           +- TableSourceScan(table=[[default_catalog, default_database, Developers, project=[developer_id, person], metadata=[]]], fields=[developer_id, person])
+   :        :        +- Exchange(distribution=[hash[$f3]])
+   :        :           +- Calc(select=[ticket_id, reporter, reporter.info.id AS $f3])
+   :        :              +- ChangelogNormalize(key=[ticket_id])
+   :        :                 +- Exchange(distribution=[hash[ticket_id]])
+   :        :                    +- TableSourceScan(table=[[default_catalog, default_database, SupportTickets, project=[ticket_id, reporter], metadata=[]]], fields=[ticket_id, reporter])
+   :        +- Exchange(distribution=[hash[$f3]])
+   :           +- Calc(select=[feedback_id, author, author.info.id AS $f3])
+   :              +- ChangelogNormalize(key=[feedback_id])
+   :                 +- Exchange(distribution=[hash[feedback_id]])
+   :                    +- TableSourceScan(table=[[default_catalog, default_database, Feedback, project=[feedback_id, author], metadata=[]]], fields=[feedback_id, author])
+   +- Exchange(distribution=[hash[$f3]])
+      +- Calc(select=[sub_id, subscriber, subscriber.info.id AS $f3])
+         +- ChangelogNormalize(key=[sub_id])
+            +- Exchange(distribution=[hash[sub_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, Subscriptions, project=[sub_id, subscriber], metadata=[]]], fields=[sub_id, subscriber])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinOfProjections">
+    <Resource name="sql">
+      <![CDATA[
+SELECT u.user_id, o.order_id, o.product, p.price, s.location
+FROM (SELECT user_id, name, cash FROM Users WHERE cash > 100) AS u
+JOIN (SELECT user_id, order_id, product FROM Orders WHERE product IS NOT NULL) AS o
+    ON u.user_id = o.user_id
+LEFT JOIN (SELECT user_id, price FROM Payments WHERE price > 50) AS p
+    ON u.user_id = p.user_id
+LEFT JOIN (SELECT user_id, location FROM Shipments WHERE location IS NOT NULL) AS s
+    ON u.user_id = s.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], order_id=[$4], product=[$5], price=[$7], location=[$9])
++- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+   :- LogicalJoin(condition=[=($0, $6)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
+   :  :  :- LogicalProject(user_id=[$0], name=[$1], cash=[$2])
+   :  :  :  +- LogicalFilter(condition=[>($2, 100)])
+   :  :  :     +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalProject(user_id=[$1], order_id=[$0], product=[$2])
+   :  :     +- LogicalFilter(condition=[IS NOT NULL($2)])
+   :  :        +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalProject(user_id=[$2], price=[$1])
+   :     +- LogicalFilter(condition=[>($1, 50)])
+   :        +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalProject(user_id=[$1], location=[$0])
+      +- LogicalFilter(condition=[IS NOT NULL($0)])
+         +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, order_id, product, price, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1), =(user_id, user_id2)], select=[user_id,user_id0,order_id,product,user_id1,price,user_id2,location], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) order_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id1, INTEGER price, VARCHAR(21474 [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id])
+   :     +- ChangelogNormalize(key=[user_id], condition=[>(cash, 100)])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, cash], metadata=[]]], fields=[user_id, cash])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id, order_id, product], where=[IS NOT NULL(product)])
+   :     +- TableSourceScan(table=[[default_catalog, default_database, Orders, filter=[]]], fields=[order_id, user_id, product])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id, price], where=[>(price, 50)])
+   :     +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[], project=[price, user_id], metadata=[]]], fields=[price, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- Calc(select=[user_id, location], where=[IS NOT NULL(location)])
+         +- TableSourceScan(table=[[default_catalog, default_database, Shipments, filter=[]]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinWithNestedSubqueryMultiJoinChainExpected">
+    <Resource name="sql">
+      <![CDATA[
+SELECT *
+FROM Users u
+JOIN (
+    SELECT o.user_id, o.order_id, p.payment_id, p.price
+    FROM Orders o
+    JOIN (
+        SELECT payment_id, user_id, price
+        FROM Payments
+        WHERE price > 100
+    ) AS p
+    ON o.user_id = p.user_id
+) AS op
+ON u.user_id = op.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], cash=[$2], user_id0=[$3], order_id=[$4], payment_id=[$5], price=[$6])
++- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
+   :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   +- LogicalProject(user_id=[$1], order_id=[$0], payment_id=[$3], price=[$5])
+      +- LogicalJoin(condition=[=($1, $4)], joinType=[inner])
+         :- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+         +- LogicalProject(payment_id=[$0], user_id=[$2], price=[$1])
+            +- LogicalFilter(condition=[>($1, 100)])
+               +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), noUniqueKey], joinConditions=[=(user_id, user_id0)], select=[user_id,name,cash,user_id0,order_id,payment_id,price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price)])
+:- Exchange(distribution=[hash[user_id]])
+:  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
++- Exchange(distribution=[hash[user_id]])
+   +- Calc(select=[user_id, order_id, payment_id, price])
+      +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(order_id), (payment_id)], joinConditions=[=(user_id, user_id0)], select=[order_id,user_id,payment_id,user_id0,price], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id0, INTEGER price)])
+         :- Exchange(distribution=[hash[user_id]])
+         :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+         +- Exchange(distribution=[hash[user_id]])
+            +- Calc(select=[payment_id, user_id, price], where=[>(price, 100)])
+               +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testMultiSinkOnMultiJoinedView">
+    <Resource name="optimized rel plan with advice">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.sink1], fields=[a, day, EXPR$2, EXPR$3])
++- GroupAggregate(advice=[1], groupBy=[a, day], select=[a, day, SUM_RETRACT(b) AS EXPR$2, COUNT_RETRACT(DISTINCT c) AS EXPR$3])
+   +- Exchange(distribution=[hash[a, day]])
+      +- Calc(select=[a, day, b0 AS b, c])
+         +- MultiJoin(commonJoinKey=[a], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(a, d)], select=[a,day,b0,c,d], rowType=[RecordType(INTEGER a, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) c, INTEGER d)])
+            :- Exchange(distribution=[hash[a]])
+            :  +- Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
+            :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a])
+            +- Exchange(distribution=[hash[d]])
+               +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
+
+Sink(table=[default_catalog.default_database.sink2], fields=[a, day, b, c])
++- Calc(select=[a, day, b0 AS b, c])
+   +- MultiJoin(commonJoinKey=[a], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(a, d)], select=[a,day,b0,c,d], rowType=[RecordType(INTEGER a, VARCHAR(2147483647) day, BIGINT b0, VARCHAR(2147483647) c, INTEGER d)])
+      :- Exchange(distribution=[hash[a]])
+      :  +- Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), 'yyMMdd') AS day])
+      :     +- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a])
+      +- Exchange(distribution=[hash[d]])
+         +- Calc(select=[b, c, d], where=[>(b, 100)])
+            +- TableSourceScan(table=[[default_catalog, default_database, src2, project=[b, c, d], metadata=[]]], fields=[b, c, d])
+
+advice[1]: [ADVICE] You might want to enable local-global two-phase optimization by configuring ('table.exec.mini-batch.enabled' to 'true', 'table.exec.mini-batch.allow-latency' to a positive long value, 'table.exec.mini-batch.size' to a positive long value).
+advice[2]: [WARNING] The column(s): day(generated by non-deterministic function: CURRENT_TIMESTAMP ) can not satisfy the determinism requirement for correctly processing update message('UB'/'UA'/'D' in changelogMode, not 'I' only), this usually happens when input node has no upsertKey(upsertKeys=[{}]) or current node outputs non-deterministic update messages. Please consider removing these non-deterministic columns or making them deterministic by using deterministic functions.
+
+related rel plan:
+Calc(select=[a, DATE_FORMAT(CURRENT_TIMESTAMP(), _UTF-16LE'yyMMdd') AS day], changelogMode=[I,UB,UA,D])
++- TableSourceScan(table=[[default_catalog, default_database, src1, project=[a], metadata=[]]], fields=[a], changelogMode=[I,UB,UA,D])
+
+
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPreservesUpsertKeyFourWayComplex">
+    <Resource name="sql">
+      <![CDATA[
+INSERT INTO sink_four_way
+SELECT
+    u.user_id,
+    o.order_id,
+    o.user_id,
+    p.payment_id,
+    p.user_id,
+    u.name,
+    a.location
+FROM UsersPK u
+JOIN OrdersPK o
+    ON u.user_id = o.user_id
+    AND o.product IS NOT NULL
+JOIN PaymentsPK p
+    ON u.user_id = p.user_id
+    AND p.price >= 0
+JOIN AddressPK a
+    ON u.user_id = a.user_id
+    AND a.location IS NOT NULL]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalSink(table=[default_catalog.default_database.sink_four_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, name, location])
++- LogicalProject(user_id=[$0], order_id=[$4], user_id0=[$5], payment_id=[$7], user_id1=[$8], name=[$1], location=[$11])
+   +- LogicalJoin(condition=[AND(=($0, $10), IS NOT NULL($11))], joinType=[inner])
+      :- LogicalJoin(condition=[AND(=($0, $8), >=($9, 0))], joinType=[inner])
+      :  :- LogicalJoin(condition=[AND(=($0, $5), IS NOT NULL($6))], joinType=[inner])
+      :  :  :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
+      :  :  +- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
+      :  +- LogicalTableScan(table=[[default_catalog, default_database, PaymentsPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, AddressPK]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Sink(table=[default_catalog.default_database.sink_four_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, name, location])
++- Calc(select=[user_id, order_id, user_id0, payment_id, user_id1, name, location])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER, INNER], inputUniqueKeys=[(user_id), (order_id, user_id), (payment_id, user_id), (user_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1), =(user_id, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,user_id2,location], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCH [...]
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- Calc(select=[order_id, user_id])
+      :     +- ChangelogNormalize(key=[order_id, user_id], condition=[IS NOT NULL(product)])
+      :        +- Exchange(distribution=[hash[order_id, user_id]])
+      :           +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK, filter=[]]], fields=[order_id, user_id, product])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- Calc(select=[payment_id, user_id])
+      :     +- ChangelogNormalize(key=[payment_id, user_id], condition=[>=(price, 0)])
+      :        +- Exchange(distribution=[hash[payment_id, user_id]])
+      :           +- TableSourceScan(table=[[default_catalog, default_database, PaymentsPK, filter=[]]], fields=[payment_id, user_id, price])
+      +- Exchange(distribution=[hash[user_id]])
+         +- ChangelogNormalize(key=[user_id], condition=[IS NOT NULL(location)])
+            +- Exchange(distribution=[hash[user_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, AddressPK, filter=[]]], fields=[user_id, location])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testFourWayComplexJoinRelPlan">
+  <TestCase name="testPreservesUpsertKeyThreeWayJoin">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 AND (u.cash >= p.price OR p.price < 0) LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3]]>
+      <![CDATA[
+INSERT INTO sink_three_way
+SELECT
+    o.user_id,
+    o.order_id,
+    p.user_id,
+    p.payment_id,
+    u.user_id,
+    u.description
+FROM UsersPK u
+JOIN OrdersPK o
+    ON o.user_id = u.user_id
+JOIN PaymentsPK p
+    ON o.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($8, $10)], joinType=[left])
-   :- LogicalJoin(condition=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinType=[inner])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+LogicalSink(table=[default_catalog.default_database.sink_three_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, description])
++- LogicalProject(user_id=[$5], order_id=[$4], user_id0=[$8], payment_id=[$7], user_id1=[$0], description=[$3])
+   +- LogicalJoin(condition=[=($5, $8)], joinType=[inner])
+      :- LogicalJoin(condition=[=($5, $0)], joinType=[inner])
+      :  :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
+      :  +- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, PaymentsPK]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id, location])
-+- MultiJoin(joinFilter=[AND(=($0, $8), OR(>=($2, $7), <($7, 0)))], joinTypes=[[INNER, LEFT, INNER, LEFT]], joinConditions=[[true, =($0, $4), AND(=($0, $8), OR(>=($2, $7), <($7, 0))), =($8, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;], 3=[LeftInputId:2;LeftFieldIndex:2;RightInputId:3;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_ [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   :- Exchange(distribution=[hash[user_id_2]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-   +- Exchange(distribution=[hash[user_id_3]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])
+Sink(table=[default_catalog.default_database.sink_three_way], fields=[user_id, order_id, user_id0, payment_id, user_id1, description])
++- Calc(select=[user_id0 AS user_id, order_id, user_id1 AS user_id0, payment_id, user_id AS user_id1, description])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id, user_id), (payment_id, user_id)], joinConditions=[=(user_id0, user_id), =(user_id0, user_id1)], select=[user_id,description,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) description, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, description], metadata=[]]], fields=[user_id, description])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+      +- Exchange(distribution=[hash[user_id]])
+         +- TableSourceScan(table=[[default_catalog, default_database, PaymentsPK, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testFourWayJoinNoCommonJoinKeyRelPlan">
+  <TestCase name="testPreservesUpsertKeyTwoWayInnerJoinOrders">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN Payments p ON u.user_id_0 = p.user_id_2 LEFT JOIN Shipments s ON p.payment_id = s.user_id_3]]>
+      <![CDATA[
+INSERT INTO sink_two_way
+SELECT
+    o.user_id,
+    o.order_id,
+    o.product,
+    u.region_id
+FROM UsersPK u
+INNER JOIN OrdersPK o
+    ON u.user_id = o.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($6, $10)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+LogicalSink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
++- LogicalProject(user_id=[$5], order_id=[$4], product=[$6], region_id=[$2])
+   +- LogicalJoin(condition=[=($0, $5)], joinType=[inner])
+      :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id, location])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, =($6, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:6;RightInputId:1;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2,location,user_id_3], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product, VARCHAR(2147483647) p [...]
-   :- Exchange(distribution=[hash[payment_id]])
-   :  +- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VA [...]
-   :     :- Exchange(distribution=[hash[user_id_0]])
-   :     :  +- ChangelogNormalize(key=[user_id_0])
-   :     :     +- Exchange(distribution=[hash[user_id_0]])
-   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :     :- Exchange(distribution=[hash[user_id_1]])
-   :     :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   :     +- Exchange(distribution=[hash[user_id_2]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-   +- Exchange(distribution=[hash[user_id_3]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])
+Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
++- Calc(select=[user_id0 AS user_id, order_id, product, region_id])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), (order_id, user_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,region_id,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, INTEGER region_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, region_id], metadata=[]]], fields=[user_id, region_id])
+      +- Exchange(distribution=[hash[user_id]])
+         +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testFourWayJoinTransitiveCommonJoinKeyRelPlan">
+  <TestCase name="testPreservesUpsertKeyTwoWayInnerJoinOrdersDoesNot">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id, s.location FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 LEFT JOIN Payments p ON o.user_id_1 = p.user_id_2 LEFT JOIN Shipments s ON p.user_id_2 = s.user_id_3]]>
+      <![CDATA[
+INSERT INTO sink_two_way
+SELECT
+    o.user_id,
+    o.order_id,
+    o.product,
+    u.region_id
+FROM UsersPK u
+INNER JOIN OrdersSimplePK o
+    ON u.user_id = o.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
-+- LogicalJoin(condition=[=($8, $10)], joinType=[left])
-   :- LogicalJoin(condition=[=($4, $8)], joinType=[left])
-   :  :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+LogicalSink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
++- LogicalProject(user_id=[$5], order_id=[$4], product=[$6], region_id=[$2])
+   +- LogicalJoin(condition=[=($0, $5)], joinType=[inner])
+      :- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, OrdersSimplePK]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id, location])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($4, $8), =($8, $10)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:1;LeftFieldIndex:1;RightInputId:2;RightFieldIndex:2;], 3=[LeftInputId:2;LeftFieldIndex:2;RightInputId:3;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2,location,user_id_3], rowType=[RecordType(VARCHAR( [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   :- Exchange(distribution=[hash[user_id_2]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-   +- Exchange(distribution=[hash[user_id_3]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id_3])
+Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id], upsertMaterialize=[true])
++- Calc(select=[user_id0 AS user_id, order_id, product, region_id])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,region_id,order_id,user_id0,product], rowType=[RecordType(VARCHAR(2147483647) user_id, INTEGER region_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) product)])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- ChangelogNormalize(key=[user_id])
+      :     +- Exchange(distribution=[hash[user_id]])
+      :        +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, region_id], metadata=[]]], fields=[user_id, region_id])
+      +- Exchange(distribution=[hash[user_id]])
+         +- ChangelogNormalize(key=[order_id])
+            +- Exchange(distribution=[hash[order_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, OrdersSimplePK]], fields=[order_id, user_id, product])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testIntervalJoinExcludedFromMultiJoin">
+  <TestCase name="testPreservesUpsertKeyTwoWayLeftJoinOrders">
     <Resource name="sql">
-      <![CDATA[SELECT e1.id, e1.val, e2.price FROM EventTable1 e1 JOIN EventTable2 e2 ON e1.id = e2.id AND e1.rowtime BETWEEN e2.rowtime - INTERVAL '1' MINUTE AND e2.rowtime + INTERVAL '1' MINUTE]]>
+      <![CDATA[
+INSERT INTO sink_two_way
+SELECT
+    o.user_id,
+    o.order_id,
+    o.product,
+    u.region_id
+FROM OrdersPK o
+LEFT JOIN UsersPK u
+    ON u.user_id = o.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(id=[$0], val=[$1], price=[$4])
-+- LogicalJoin(condition=[AND(=($0, $3), >=($2, -($5, 60000:INTERVAL MINUTE)), <=($2, +($5, 60000:INTERVAL MINUTE)))], joinType=[inner])
-   :- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
-   +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-      +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
+LogicalSink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
++- LogicalProject(user_id=[$1], order_id=[$0], product=[$2], region_id=[$5])
+   +- LogicalJoin(condition=[=($3, $1)], joinType=[left])
+      :- LogicalTableScan(table=[[default_catalog, default_database, OrdersPK]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, UsersPK]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[id, val, price])
-+- IntervalJoin(joinType=[InnerJoin], windowBounds=[isRowTime=true, leftLowerBound=-60000, leftUpperBound=60000, leftTimeIndex=2, rightTimeIndex=2], where=[AND(=(id, id0), >=(rowtime, -(rowtime0, 60000:INTERVAL MINUTE)), <=(rowtime, +(rowtime0, 60000:INTERVAL MINUTE)))], select=[id, val, rowtime, id0, price, rowtime0])
-   :- Exchange(distribution=[hash[id]])
-   :  +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime, 5000:INTERVAL SECOND)])
-   :     +- TableSourceScan(table=[[default_catalog, default_database, EventTable1]], fields=[id, val, rowtime])
-   +- Exchange(distribution=[hash[id]])
-      +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime, 5000:INTERVAL SECOND)])
-         +- TableSourceScan(table=[[default_catalog, default_database, EventTable2]], fields=[id, price, rowtime])
+Sink(table=[default_catalog.default_database.sink_two_way], fields=[user_id, order_id, product, region_id])
++- Calc(select=[user_id, order_id, product, region_id])
+   +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(order_id, user_id), (user_id)], joinConditions=[=(user_id0, user_id)], select=[order_id,user_id,product,user_id0,region_id], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id0, INTEGER region_id)])
+      :- Exchange(distribution=[hash[user_id]])
+      :  +- TableSourceScan(table=[[default_catalog, default_database, OrdersPK]], fields=[order_id, user_id, product])
+      +- Exchange(distribution=[hash[user_id]])
+         +- TableSourceScan(table=[[default_catalog, default_database, UsersPK, project=[user_id, region_id], metadata=[]]], fields=[user_id, region_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testRegularJoinsAreMergedApartFromTemporalJoin">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, temporal.age FROM Users u INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN (  SELECT s.user_id, l.age   FROM StreamTable s   JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l   ON s.user_id = l.id) temporal ON u.user_id_0 = temporal.user_id]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    temporal.age FROM Users u
+INNER JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN (
+    SELECT s.user_id, l.age
+    FROM StreamTable s
+    JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l
+        ON s.user_id = l.id
+) temporal ON u.user_id = temporal.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], age=[$7])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], age=[$7])
 +- LogicalJoin(condition=[=($0, $6)], joinType=[inner])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
@@ -268,12 +1605,12 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], age=[$7])
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, age])
-+- MultiJoin(joinFilter=[AND(=($0, $6), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $6)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:0;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,user_id,age], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHA [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
+Calc(select=[user_id, name, order_id, age])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,user_id1,age], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) user_id1, INTEGER age)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
    +- Exchange(distribution=[hash[user_id]])
       +- Calc(select=[user_id, age])
          +- LookupJoin(table=[default_catalog.default_database.LookupTable], joinType=[InnerJoin], lookup=[id=user_id], select=[user_id, id, age])
@@ -283,11 +1620,21 @@ Calc(select=[user_id_0, name, order_id, age])
   </TestCase>
   <TestCase name="testRightJoinNotSupported">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u RIGHT JOIN Orders o ON u.user_id_0 = o.user_id_1 RIGHT JOIN Payments p ON o.user_id_1 = p.user_id_2]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+RIGHT JOIN Orders o
+    ON u.user_id = o.user_id
+RIGHT JOIN Payments p
+    ON o.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalJoin(condition=[=($4, $8)], joinType=[right])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[right])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
@@ -297,24 +1644,70 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- Join(joinType=[RightOuterJoin], where=[=(user_id_1, user_id_2)], select=[user_id_0, name, order_id, user_id_1, payment_id, user_id_2], leftInputSpec=[NoUniqueKey], rightInputSpec=[HasUniqueKey])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- Join(joinType=[RightOuterJoin], where=[=(user_id_0, user_id_1)], select=[user_id_0, name, order_id, user_id_1], leftInputSpec=[JoinKeyContainsUniqueKey], rightInputSpec=[HasUniqueKey])
-   :     :- Exchange(distribution=[hash[user_id_0]])
-   :     :  +- ChangelogNormalize(key=[user_id_0])
-   :     :     +- Exchange(distribution=[hash[user_id_0]])
-   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id_0, name], metadata=[]]], fields=[user_id_0, name])
-   :     +- Exchange(distribution=[hash[user_id_1]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id_1], metadata=[]]], fields=[order_id, user_id_1])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id_2], metadata=[]]], fields=[payment_id, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- Join(joinType=[RightOuterJoin], where=[=(user_id0, user_id1)], select=[user_id, name, order_id, user_id0, payment_id, user_id1], leftInputSpec=[NoUniqueKey], rightInputSpec=[HasUniqueKey])
+   :- Exchange(distribution=[hash[user_id0]])
+   :  +- Join(joinType=[RightOuterJoin], where=[=(user_id, user_id0)], select=[user_id, name, order_id, user_id0], leftInputSpec=[JoinKeyContainsUniqueKey], rightInputSpec=[HasUniqueKey])
+   :     :- Exchange(distribution=[hash[user_id]])
+   :     :  +- ChangelogNormalize(key=[user_id])
+   :     :     +- Exchange(distribution=[hash[user_id]])
+   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSameTableMultipleAliases">
+    <Resource name="sql">
+      <![CDATA[
+SELECT *
+FROM Users u
+LEFT JOIN Users u1
+    ON u.user_id = u1.user_id
+LEFT JOIN Users u2
+    ON u1.user_id = u2.user_id
+LEFT JOIN Users u3
+    ON u2.user_id = u3.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], cash=[$2], user_id0=[$3], name0=[$4], cash0=[$5], user_id1=[$6], name1=[$7], cash1=[$8], user_id2=[$9], name2=[$10], cash2=[$11])
++- LogicalJoin(condition=[=($6, $9)], joinType=[left])
+   :- LogicalJoin(condition=[=($3, $6)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($0, $3)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (user_id), (user_id), (user_id)], joinConditions=[=(user_id, user_id0), =(user_id0, user_id1), =(user_id1, user_id2)], select=[user_id,name,cash,user_id0,name0,cash0,user_id1,name1,cash1,user_id2,name2,cash2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name0, INTEGER cash0, VARCHAR(2147483647) user_id1 [...]
+:- Exchange(distribution=[hash[user_id]])
+:  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+:- Exchange(distribution=[hash[user_id]])
+:  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+:- Exchange(distribution=[hash[user_id]])
+:  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
++- Exchange(distribution=[hash[user_id]])
+   +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testTemporalJoinExcludedFromMultiJoin">
     <Resource name="sql">
-      <![CDATA[SELECT s.user_id, s.amount, l.name, l.age FROM StreamTable s JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l ON s.user_id = l.id]]>
+      <![CDATA[
+SELECT
+    s.user_id,
+    s.amount,
+    l.name,
+    l.age
+FROM StreamTable s
+JOIN LookupTable FOR SYSTEM_TIME AS OF s.proctime AS l
+    ON s.user_id = l.id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
@@ -337,11 +1730,21 @@ Calc(select=[user_id, amount, name, age])
   </TestCase>
   <TestCase name="testThreeWayInnerJoinExecPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN Payments p ON u.user_id_0 = p.user_id_2]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+INNER JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
@@ -351,21 +1754,21 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
     </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) o [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayInnerJoinExplain">
     <Resource name="explain">
       <![CDATA[== Abstract Syntax Tree ==
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
@@ -373,35 +1776,45 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
    +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
 
 == Optimized Physical Plan ==
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) o [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 
 == Optimized Execution Plan ==
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) o [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayInnerJoinNoCommonJoinKeyRelPlan">
+  <TestCase name="testThreeWayInnerJoinRelPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u INNER JOIN Orders o ON u.user_id_0 = o.user_id_1 INNER JOIN Payments p ON u.cash = p.price]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+INNER JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($2, $7)], joinType=[inner])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
    :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
@@ -410,91 +1823,150 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[=($2, $7)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($2, $7)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:2;RightInputId:1;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INT [...]
-   :- Exchange(distribution=[hash[cash]])
-   :  +- MultiJoin(joinFilter=[=($0, $4)], joinTypes=[[INNER, INNER]], joinConditions=[[true, =($0, $4)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;]}], select=[user_id_0,name,cash,order_id,user_id_1,product], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product)])
-   :     :- Exchange(distribution=[hash[user_id_0]])
-   :     :  +- ChangelogNormalize(key=[user_id_0])
-   :     :     +- Exchange(distribution=[hash[user_id_0]])
-   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :     +- Exchange(distribution=[hash[user_id_1]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[price]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayLeftOuterJoinExplain">
-    <Resource name="explain">
-      <![CDATA[== Abstract Syntax Tree ==
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+  <TestCase name="testThreeWayInnerJoinRelPlanNoCommonJoinKey">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+INNER JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.cash = p.price]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalJoin(condition=[=($2, $7)], joinType=[inner])
+   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
    :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
    +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-
-== Optimized Physical Plan ==
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(214748 [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
-
-== Optimized Execution Plan ==
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(214748 [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[cash], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[=(cash, price)], select=[user_id,name,cash,order_id,payment_id,price], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, INTEGER price)])
+   :- Exchange(distribution=[hash[cash]])
+   :  +- Calc(select=[user_id, name, cash, order_id])
+   :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id, user_id0)], select=[user_id,name,cash,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
+   :        :- Exchange(distribution=[hash[user_id]])
+   :        :  +- ChangelogNormalize(key=[user_id])
+   :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[price]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, price], metadata=[]]], fields=[payment_id, price])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testFullOuterNotSupported">
+  <TestCase name="testThreeWayInnerJoinWithSingleTttlHint">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u FULL OUTER JOIN Orders o ON u.user_id_0 = o.user_id_1 FULL OUTER JOIN Payments p ON o.user_id_1 = p.user_id_2]]>
+      <![CDATA[
+SELECT
+    /*+ STaTE_tTL(o='2d') */
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+INNER JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalJoin(condition=[=($4, $8)], joinType=[full])
-   :- LogicalJoin(condition=[=($0, $4)], joinType=[full])
-   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalJoin(condition=[=($0, $8)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0] options:{o=2d}]]])
+   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0, 0] options:{o=2d}]]])
+   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]], hints=[[[ALIAS inheritPath:[] options:[u]]]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]], hints=[[[ALIAS inheritPath:[] options:[o]]]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]], hints=[[[ALIAS inheritPath:[] options:[p]]]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], stateTtlHints=[[[STATE_TTL options:[0s, 2d, 0s]]]], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testThreeWayInnerJoinWithTttlHints">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    /*+ STATE_TTL(u='1d', o='2d', p='1h') */
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+INNER JOIN Orders o
+    ON u.user_id = o.user_id
+INNER JOIN Payments p
+    ON u.user_id = p.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalJoin(condition=[=($0, $8)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0] options:{p=1h, u=1d, o=2d}]]])
+   :- LogicalJoin(condition=[=($0, $4)], joinType=[inner], stateTtlHints=[[[STATE_TTL inheritPath:[0, 0] options:{p=1h, u=1d, o=2d}]]])
+   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]], hints=[[[ALIAS inheritPath:[] options:[u]]]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]], hints=[[[ALIAS inheritPath:[] options:[o]]]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]], hints=[[[ALIAS inheritPath:[] options:[p]]]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- Join(joinType=[FullOuterJoin], where=[=(user_id_1, user_id_2)], select=[user_id_0, name, order_id, user_id_1, payment_id, user_id_2], leftInputSpec=[NoUniqueKey], rightInputSpec=[HasUniqueKey])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- Join(joinType=[FullOuterJoin], where=[=(user_id_0, user_id_1)], select=[user_id_0, name, order_id, user_id_1], leftInputSpec=[JoinKeyContainsUniqueKey], rightInputSpec=[HasUniqueKey])
-   :     :- Exchange(distribution=[hash[user_id_0]])
-   :     :  +- ChangelogNormalize(key=[user_id_0])
-   :     :     +- Exchange(distribution=[hash[user_id_0]])
-   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id_0, name], metadata=[]]], fields=[user_id_0, name])
-   :     +- Exchange(distribution=[hash[user_id_1]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id_1], metadata=[]]], fields=[order_id, user_id_1])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id_2], metadata=[]]], fields=[payment_id, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], stateTtlHints=[[[STATE_TTL options:[1d, 2d, 1h]]]], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayJoinNoJoinKeyExecPlan">
+  <TestCase name="testThreeWayJoinExecPlanNoCommonJoinKey">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u LEFT JOIN Orders o ON TRUE INNER JOIN Payments p ON TRUE ]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+LEFT JOIN Orders o ON TRUE
+INNER JOIN Payments p ON TRUE]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalJoin(condition=[true], joinType=[inner])
    :- LogicalJoin(condition=[true], joinType=[left])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
@@ -504,118 +1976,148 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
     </Resource>
     <Resource name="optimized exec plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, INNER]], joinConditions=[[true, true]], joinAttributeMap=[{}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product, VARCHAR(2147483647) payment_id, INTEGER price, VARCHAR(2147483647) user_id_2)])
-   :- Exchange(distribution=[single])
-   :  +- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT]], joinConditions=[[true, true]], joinAttributeMap=[{}], select=[user_id_0,name,cash,order_id,user_id_1,product], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product)])
-   :     :- Exchange(distribution=[single])
-   :     :  +- ChangelogNormalize(key=[user_id_0])
-   :     :     +- Exchange(distribution=[hash[user_id_0]])
-   :     :        +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :     +- Exchange(distribution=[single])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[single])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+MultiJoin(commonJoinKey=[noCommonJoinKey], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[true], select=[user_id,name,order_id,payment_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id)])
+:- Exchange(distribution=[single])
+:  +- MultiJoin(commonJoinKey=[noCommonJoinKey], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[true], select=[user_id,name,order_id], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id)])
+:     :- Exchange(distribution=[single])
+:     :  +- ChangelogNormalize(key=[user_id])
+:     :     +- Exchange(distribution=[hash[user_id]])
+:     :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+:     +- Exchange(distribution=[single])
+:        +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id], metadata=[]]], fields=[order_id])
++- Exchange(distribution=[single])
+   +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id], metadata=[]]], fields=[payment_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayJoinWithTimeAttributesMaterialization">
     <Resource name="sql">
-      <![CDATA[SELECT u.name, u.proctime, o.rowtime, p.price FROM UsersWithProctime u JOIN OrdersWithRowtime o ON u.user_id_0 = o.user_id_1 JOIN Payments p ON u.user_id_0 = p.user_id_2]]>
+      <![CDATA[
+SELECT
+    u.name,
+    u.proctime,
+    o.`$rowtime`,
+    p.price
+FROM UsersWithProctime u
+JOIN OrdersWithRowtime o
+    ON u.user_id = o.user_id
+JOIN Payments p
+    ON u.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(name=[$1], proctime=[$2], rowtime=[$5], price=[$7])
+LogicalProject(name=[$1], proctime=[$2], $rowtime=[$5], price=[$7])
 +- LogicalJoin(condition=[=($0, $8)], joinType=[inner])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[inner])
-   :  :- LogicalProject(user_id_0=[$0], name=[$1], proctime=[PROCTIME()])
+   :  :- LogicalProject(user_id=[$0], name=[$1], proctime=[PROCTIME()])
    :  :  +- LogicalTableScan(table=[[default_catalog, default_database, UsersWithProctime]])
-   :  +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[$2])
+   :  +- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[$2])
    :     +- LogicalTableScan(table=[[default_catalog, default_database, OrdersWithRowtime]])
    +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[name, proctime, rowtime, price])
-+- MultiJoin(joinFilter=[AND(=($0, $8), =($0, $4))], joinTypes=[[INNER, INNER, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,proctime,order_id,user_id_1,rowtime,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, TIMESTAMP_WITH_LOCAL_TIME_ZONE( [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- Calc(select=[user_id_0, name, PROCTIME_MATERIALIZE(PROCTIME()) AS proctime])
-   :     +- TableSourceScan(table=[[default_catalog, default_database, UsersWithProctime]], fields=[user_id_0, name])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- Calc(select=[order_id, user_id_1, CAST(rowtime AS TIMESTAMP(3)) AS rowtime])
-   :     +- WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersWithRowtime]], fields=[order_id, user_id_1, rowtime])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[name, proctime, $rowtime, price])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[INNER, INNER], inputUniqueKeys=[(user_id), noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,proctime,user_id0,$rowtime,price,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, TIMESTAMP_WITH_LOCAL_TIME_ZONE(3) proctime, VARCHAR(2147483647) user_id0, TIMESTAMP(3) $rowtime, INTEGER price, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id, name, PROCTIME_MATERIALIZE(PROCTIME()) AS proctime])
+   :     +- TableSourceScan(table=[[default_catalog, default_database, UsersWithProctime]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id, CAST($rowtime AS TIMESTAMP(3)) AS $rowtime])
+   :     +- WatermarkAssigner(rowtime=[$rowtime], watermark=[$rowtime])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, OrdersWithRowtime, project=[user_id, $rowtime], metadata=[]]], fields=[user_id, $rowtime])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[price, user_id], metadata=[]]], fields=[price, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseRelPlan">
+  <TestCase name="testThreeWayLeftOuterJoinExecPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2 WHERE u.name = 'Gus' AND p.price > 10]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
-+- LogicalFilter(condition=[AND(=($1, _UTF-16LE'Gus'), >($7, 10))])
-   +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
-      :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
-      :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
-      :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-      +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+   :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+   :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
 ]]>
     </Resource>
-    <Resource name="optimized rel plan">
+    <Resource name="optimized exec plan">
       <![CDATA[
-Calc(select=[user_id_0, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
-+- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR( [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0], condition=[=(name, 'Gus')])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- Calc(select=[payment_id, price, user_id_2], where=[>(price, 10)])
-         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayLeftOuterJoinExecPlan">
-    <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2]]>
-    </Resource>
-    <Resource name="ast">
-      <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+  <TestCase name="testThreeWayLeftOuterJoinExplain">
+    <Resource name="explain">
+      <![CDATA[== Abstract Syntax Tree ==
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
    :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
    +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
-]]>
-    </Resource>
-    <Resource name="optimized exec plan">
-      <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(214748 [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+
+== Optimized Physical Plan ==
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+
+== Optimized Execution Plan ==
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayLeftOuterJoinRelPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
    :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
    :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
@@ -625,21 +2127,64 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, name, order_id, payment_id])
-+- MultiJoin(joinFilter=[true], joinTypes=[[INNER, LEFT, LEFT]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR(214748 [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- TableSourceScan(table=[[default_catalog, default_database, Payments]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, name, order_id, payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,name,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseExecPlan">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+WHERE u.name = 'Gus' AND p.price > 10]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
++- LogicalFilter(condition=[AND(=($1, _UTF-16LE'Gus'), >($7, 10))])
+   +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+      :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
+      :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+      :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+      +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+]]>
+    </Resource>
+    <Resource name="optimized exec plan">
+      <![CDATA[
+Calc(select=[user_id, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id])
+   :     +- ChangelogNormalize(key=[user_id], condition=[(name = 'Gus')])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- Calc(select=[payment_id, user_id], where=[(price > 10)])
+         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseExplain">
     <Resource name="explain">
       <![CDATA[== Abstract Syntax Tree ==
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalFilter(condition=[AND(=($1, _UTF-16LE'Gus'), >($7, 10))])
    +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
       :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
@@ -648,40 +2193,53 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
       +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
 
 == Optimized Physical Plan ==
-Calc(select=[user_id_0, CAST(_UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE" AS VARCHAR(2147483647) CHARACTER SET "UTF-16LE") AS name, order_id, CAST(payment_id AS VARCHAR(2147483647) CHARACTER SET "UTF-16LE") AS payment_id])
-+- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR( [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0], condition=[=(name, _UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE")])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- Calc(select=[payment_id, price, user_id_2], where=[>(price, 10)])
-         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, CAST(_UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE" AS VARCHAR(2147483647) CHARACTER SET "UTF-16LE") AS name, order_id, CAST(payment_id AS VARCHAR(2147483647) CHARACTER SET "UTF-16LE") AS payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id])
+   :     +- ChangelogNormalize(key=[user_id], condition=[=(name, _UTF-16LE'Gus':VARCHAR(2147483647) CHARACTER SET "UTF-16LE")])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- Calc(select=[payment_id, user_id], where=[>(price, 10)])
+         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 
 == Optimized Execution Plan ==
-Calc(select=[user_id_0, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
-+- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR( [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0], condition=[(name = 'Gus')])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- Calc(select=[payment_id, price, user_id_2], where=[(price > 10)])
-         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id_2])
+Calc(select=[user_id, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[(user_id = user_id0), (user_id = user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id])
+   :     +- ChangelogNormalize(key=[user_id], condition=[(name = 'Gus')])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- Calc(select=[payment_id, user_id], where=[(price > 10)])
+         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseExecPlan">
+  <TestCase name="testThreeWayLeftOuterJoinWithWhereClauseRelPlan">
     <Resource name="sql">
-      <![CDATA[SELECT u.user_id_0, u.name, o.order_id, p.payment_id FROM Users u LEFT JOIN Orders o ON u.user_id_0 = o.user_id_1 LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2 WHERE u.name = 'Gus' AND p.price > 10]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id
+FROM Users u
+LEFT JOIN Orders o
+    ON u.user_id = o.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id
+WHERE u.name = 'Gus' AND p.price > 10]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6])
 +- LogicalFilter(condition=[AND(=($1, _UTF-16LE'Gus'), >($7, 10))])
    +- LogicalJoin(condition=[=($0, $8)], joinType=[left])
       :- LogicalJoin(condition=[=($0, $4)], joinType=[left])
@@ -690,81 +2248,312 @@ LogicalProject(user_id_0=[$0], name=[$1], order_id=[$3], payment_id=[$6])
       +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
 ]]>
     </Resource>
-    <Resource name="optimized exec plan">
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, INNER], inputUniqueKeys=[(user_id), (order_id), (payment_id)], joinConditions=[=(user_id, user_id0), =(user_id, user_id1)], select=[user_id,order_id,user_id0,payment_id,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id])
+   :     +- ChangelogNormalize(key=[user_id], condition=[=(name, 'Gus')])
+   :        +- Exchange(distribution=[hash[user_id]])
+   :           +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[], project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- Calc(select=[payment_id, user_id], where=[>(price, 10)])
+         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testTwoWayJoinWithRank">
+    <Resource name="sql">
+      <![CDATA[
+WITH JoinedEvents as (
+SELECT
+    e1.id as id,
+    e1.val,
+    e1.`$rowtime` as `$rowtime`,
+    e2.price
+FROM EventTable1 e1
+JOIN EventTable2 e2
+    ON e1.id = e2.id)
+SELECT
+    id,
+    val,
+    `$rowtime`
+FROM (
+    SELECT
+        *,
+        ROW_NUMBER() OVER (PARTITION BY id ORDER BY `$rowtime` DESC) as ts
+    FROM JoinedEvents)
+WHERE ts = 1]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], val=[$1], $rowtime=[$2])
++- LogicalFilter(condition=[=($4, 1)])
+   +- LogicalProject(id=[$0], val=[$1], $rowtime=[$2], price=[$3], ts=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
+      +- LogicalProject(id=[$0], val=[$1], $rowtime=[$2], price=[$4])
+         +- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
+            :- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
+            :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
+            +- LogicalWatermarkAssigner(rowtime=[$rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
+               +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
       <![CDATA[
-Calc(select=[user_id_0, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CAST(payment_id AS VARCHAR(2147483647)) AS payment_id])
-+- MultiJoin(joinFilter=[=($0, $8)], joinTypes=[[INNER, LEFT, INNER]], joinConditions=[[true, =($0, $4), =($0, $8)]], joinAttributeMap=[{1=[LeftInputId:0;LeftFieldIndex:0;RightInputId:1;RightFieldIndex:1;], 2=[LeftInputId:0;LeftFieldIndex:0;RightInputId:2;RightFieldIndex:2;]}], select=[user_id_0,name,cash,order_id,user_id_1,product,payment_id,price,user_id_2], rowType=[RecordType(VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash, VARCHAR(2147483647) order_id, VARCHAR( [...]
-   :- Exchange(distribution=[hash[user_id_0]])
-   :  +- ChangelogNormalize(key=[user_id_0], condition=[(name = 'Gus')])
-   :     +- Exchange(distribution=[hash[user_id_0]])
-   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, filter=[]]], fields=[user_id_0, name, cash])
-   :- Exchange(distribution=[hash[user_id_1]])
-   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-   +- Exchange(distribution=[hash[user_id_2]])
-      +- Calc(select=[payment_id, price, user_id_2], where=[(price > 10)])
-         +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id_2])
+Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], rankRange=[rankStart=1, rankEnd=1], partitionBy=[id], orderBy=[$rowtime DESC], select=[id, val, $rowtime])
++- Exchange(distribution=[hash[id]])
+   +- Calc(select=[id, val, $rowtime])
+      +- MultiJoin(commonJoinKey=[id], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(id, id0)], select=[id,val,$rowtime,id0,price,$rowtime0], rowType=[RecordType(VARCHAR(2147483647) id, INTEGER val, TIMESTAMP(3) $rowtime, VARCHAR(2147483647) id0, DOUBLE price, TIMESTAMP(3) $rowtime0)])
+         :- Exchange(distribution=[hash[id]])
+         :  +- Calc(select=[id, val, CAST($rowtime AS TIMESTAMP(3)) AS $rowtime])
+         :     +- WatermarkAssigner(rowtime=[$rowtime], watermark=[-($rowtime, 5000:INTERVAL SECOND)])
+         :        +- TableSourceScan(table=[[default_catalog, default_database, EventTable1]], fields=[id, val, $rowtime])
+         +- Exchange(distribution=[hash[id]])
+            +- Calc(select=[id, price, CAST($rowtime AS TIMESTAMP(3)) AS $rowtime])
+               +- WatermarkAssigner(rowtime=[$rowtime], watermark=[-($rowtime, 5000:INTERVAL SECOND)])
+                  +- TableSourceScan(table=[[default_catalog, default_database, EventTable2]], fields=[id, price, $rowtime])
 ]]>
     </Resource>
   </TestCase>
   <TestCase name="testTwoWayJoinWithUnion">
     <Resource name="sql">
-      <![CDATA[WITH OrdersUnion as (SELECT * FROM Orders UNION ALL SELECT * FROM Orders2) SELECT * FROM OrdersUnion o LEFT JOIN Users u ON o.user_id_1 = u.user_id_0]]>
+      <![CDATA[
+WITH OrdersUnion as (
+SELECT * FROM Orders
+UNION ALL
+SELECT * FROM Orders2
+)
+SELECT * FROM OrdersUnion o
+LEFT JOIN Users u
+    ON o.user_id = u.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(order_id=[$0], user_id_1=[$1], product=[$2], user_id_0=[$3], name=[$4], cash=[$5])
+LogicalProject(order_id=[$0], user_id=[$1], product=[$2], user_id0=[$3], name=[$4], cash=[$5])
 +- LogicalJoin(condition=[=($1, $3)], joinType=[left])
    :- LogicalUnion(all=[true])
-   :  :- LogicalProject(order_id=[$0], user_id_1=[$1], product=[$2])
+   :  :- LogicalProject(order_id=[$0], user_id=[$1], product=[$2])
    :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
-   :  +- LogicalProject(order_id=[$0], user_id_1=[$1], product=[$2])
+   :  +- LogicalProject(order_id=[$0], user_id=[$1], product=[$2])
    :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders2]])
    +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-MultiJoin(commonJoinKey=[user_id_1], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (user_id_0)], joinConditions=[=(user_id_1, user_id_0)], select=[order_id,user_id_1,product,user_id_0,name,cash], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash)])
-:- Exchange(distribution=[hash[user_id_1]])
-:  +- Union(all=[true], union=[order_id, user_id_1, product])
-:     :- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
-:     +- TableSourceScan(table=[[default_catalog, default_database, Orders2]], fields=[order_id, user_id_1, product])
-+- Exchange(distribution=[hash[user_id_0]])
-   +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
+MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (user_id)], joinConditions=[=(user_id, user_id0)], select=[order_id,user_id,product,user_id0,name,cash], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) name, INTEGER cash)])
+:- Exchange(distribution=[hash[user_id]])
+:  +- Union(all=[true], union=[order_id, user_id, product])
+:     :- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id, product])
+:     +- TableSourceScan(table=[[default_catalog, default_database, Orders2]], fields=[order_id, user_id, product])
++- Exchange(distribution=[hash[user_id]])
+   +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id, name, cash])
 ]]>
     </Resource>
   </TestCase>
-  <TestCase name="testTwoWayJoinWithRank">
+  <TestCase name="testWithCastCommonJoinKeyToIntegerMultiJoinChainExpected">
     <Resource name="sql">
-      <![CDATA[WITH JoinedEvents as (SELECT e1.id as id, e1.val, e1.rowtime as `rowtime`, e2.price FROM EventTable1 e1 JOIN EventTable2 e2 ON e1.id = e2.id) SELECT id, val, `rowtime` FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY `rowtime` DESC) as ts FROM JoinedEvents) WHERE ts = 1]]>
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON o.user_id = u.user_id
+LEFT JOIN Payments p
+    ON CAST(u.user_id as INTEGER) = CAST(p.user_id as INTEGER)
+LEFT JOIN Shipments s
+    ON u.user_id = s.user_id]]>
     </Resource>
     <Resource name="ast">
       <![CDATA[
-LogicalProject(id=[$0], val=[$1], rowtime=[$2])
-+- LogicalFilter(condition=[=($4, 1)])
-   +- LogicalProject(id=[$0], val=[$1], rowtime=[$2], price=[$3], ts=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
-      +- LogicalProject(id=[$0], val=[$1], rowtime=[$2], price=[$4])
-         +- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
-            :- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-            :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
-            +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
-               +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
++- LogicalJoin(condition=[=($0, $10)], joinType=[left])
+   :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], payment_id=[$7], price=[$8], user_id2=[$9])
+   :  +- LogicalJoin(condition=[=($6, $10)], joinType=[left])
+   :     :- LogicalProject(user_id=[$0], name=[$1], cash=[$2], order_id=[$3], user_id0=[$4], product=[$5], user_id1=[CAST($0):INTEGER NOT NULL])
+   :     :  +- LogicalJoin(condition=[=($4, $0)], joinType=[left])
+   :     :     :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :     :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :     +- LogicalProject(payment_id=[$0], price=[$1], user_id=[$2], user_id0=[CAST($2):INTEGER])
+   :        +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
 ]]>
     </Resource>
     <Resource name="optimized rel plan">
       <![CDATA[
-Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], rankRange=[rankStart=1, rankEnd=1], partitionBy=[id], orderBy=[rowtime DESC], select=[id, val, rowtime])
-+- Exchange(distribution=[hash[id]])
-   +- Calc(select=[id, val, rowtime])
-      +- MultiJoin(commonJoinKey=[id], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(id, id0)], select=[id,val,rowtime,id0,price,rowtime0], rowType=[RecordType(VARCHAR(2147483647) id, INTEGER val, TIMESTAMP(3) rowtime, VARCHAR(2147483647) id0, DOUBLE price, TIMESTAMP(3) rowtime0)])
-         :- Exchange(distribution=[hash[id]])
-         :  +- Calc(select=[id, val, CAST(rowtime AS TIMESTAMP(3)) AS rowtime])
-         :     +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime, 5000:INTERVAL SECOND)])
-         :        +- TableSourceScan(table=[[default_catalog, default_database, EventTable1]], fields=[id, val, rowtime])
-         +- Exchange(distribution=[hash[id]])
-            +- Calc(select=[id, price, CAST(rowtime AS TIMESTAMP(3)) AS rowtime])
-               +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime, 5000:INTERVAL SECOND)])
-                  +- TableSourceScan(table=[[default_catalog, default_database, EventTable2]], fields=[id, price, rowtime])
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(user_id, user_id1)], select=[user_id,name,order_id,payment_id,location,user_id1], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) location, VARCHAR(2147483647) user_id1)])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- Calc(select=[user_id, name, order_id, payment_id])
+   :     +- MultiJoin(commonJoinKey=[user_id1], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (payment_id)], joinConditions=[=(user_id1, user_id00)], select=[user_id,name,order_id,user_id1,payment_id,user_id00], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, INTEGER user_id1, VARCHAR(2147483647) payment_id, INTEGER user_id00)])
+   :        :- Exchange(distribution=[hash[user_id1]])
+   :        :  +- Calc(select=[user_id, name, order_id, CAST(user_id AS INTEGER) AS user_id1])
+   :        :     +- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT], inputUniqueKeys=[(user_id), (order_id)], joinConditions=[=(user_id0, user_id)], select=[user_id,name,order_id,user_id0], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0)])
+   :        :        :- Exchange(distribution=[hash[user_id]])
+   :        :        :  +- ChangelogNormalize(key=[user_id])
+   :        :        :     +- Exchange(distribution=[hash[user_id]])
+   :        :        :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :        :        +- Exchange(distribution=[hash[user_id]])
+   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :        +- Exchange(distribution=[hash[user_id0]])
+   :           +- Calc(select=[payment_id, CAST(user_id AS INTEGER) AS user_id0])
+   :              +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testWithCastCommonJoinKeyToVarchar">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON o.user_id = u.user_id
+LEFT JOIN Payments p
+    ON CAST(u.user_id as VARCHAR) = CAST(p.user_id as VARCHAR)
+LEFT JOIN Shipments s
+    ON u.user_id = s.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
++- LogicalJoin(condition=[=($0, $10)], joinType=[left])
+   :- LogicalJoin(condition=[=($0, $8)], joinType=[left])
+   :  :- LogicalJoin(condition=[=($4, $0)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id0, user_id), =(user_id, user_id1), =(user_id, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, VARCHAR(2147483647) user_id [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testWithExpressionInJoinConditionMultiJoinChainExpected">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    p.product_id,
+    (p.price - p.discount) AS net_price,
+    s.quantity,
+    pr.promo_text
+FROM Products AS p
+LEFT JOIN Sales AS s
+    ON (p.price - p.discount) = s.product_key
+LEFT JOIN Promotions AS pr
+    ON (p.price - p.discount) = pr.product_key
+WHERE (p.price - p.discount) > 100]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(product_id=[$0], net_price=[-($1, $2)], quantity=[$5], promo_text=[$8])
++- LogicalFilter(condition=[>(-($1, $2), 100)])
+   +- LogicalProject(product_id=[$0], price=[$1], discount=[$2], sale_id=[$3], product_key=[$4], quantity=[$5], promo_id=[$7], product_key0=[$8], promo_text=[$9])
+      +- LogicalJoin(condition=[=($6, $8)], joinType=[left])
+         :- LogicalProject(product_id=[$0], price=[$1], discount=[$2], sale_id=[$3], product_key=[$4], quantity=[$5], $f6=[-($1, $2)])
+         :  +- LogicalProject(product_id=[$0], price=[$1], discount=[$2], sale_id=[$4], product_key=[$5], quantity=[$6])
+         :     +- LogicalJoin(condition=[=($3, $5)], joinType=[left])
+         :        :- LogicalProject(product_id=[$0], price=[$1], discount=[$2], $f3=[-($1, $2)])
+         :        :  +- LogicalTableScan(table=[[default_catalog, default_database, Products]])
+         :        +- LogicalTableScan(table=[[default_catalog, default_database, Sales]])
+         +- LogicalTableScan(table=[[default_catalog, default_database, Promotions]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[product_id, -(price, discount) AS net_price, quantity, promo_text])
++- MultiJoin(commonJoinKey=[$f6], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=($f6, product_key0)], select=[product_id,price,discount,quantity,$f6,product_key0,promo_text], rowType=[RecordType(VARCHAR(2147483647) product_id, DOUBLE price, DOUBLE discount, INTEGER quantity, DOUBLE $f6, DOUBLE product_key0, VARCHAR(2147483647) promo_text)])
+   :- Exchange(distribution=[hash[$f6]])
+   :  +- Calc(select=[product_id, price, discount, quantity, -(price, discount) AS $f6])
+   :     +- MultiJoin(commonJoinKey=[$f3], joinTypes=[LEFT], inputUniqueKeys=[(product_id), noUniqueKey], joinConditions=[=($f3, product_key)], select=[product_id,price,discount,$f3,product_key,quantity], rowType=[RecordType(VARCHAR(2147483647) product_id, DOUBLE price, DOUBLE discount, DOUBLE $f3, DOUBLE product_key, INTEGER quantity)])
+   :        :- Exchange(distribution=[hash[$f3]])
+   :        :  +- Calc(select=[product_id, price, discount, -(price, discount) AS $f3])
+   :        :     +- ChangelogNormalize(key=[product_id], condition=[>(-(price, discount), 100)])
+   :        :        +- Exchange(distribution=[hash[product_id]])
+   :        :           +- TableSourceScan(table=[[default_catalog, default_database, Products, filter=[]]], fields=[product_id, price, discount])
+   :        +- Exchange(distribution=[hash[product_key]])
+   :           +- Calc(select=[product_key, quantity])
+   :              +- ChangelogNormalize(key=[sale_id])
+   :                 +- Exchange(distribution=[hash[sale_id]])
+   :                    +- TableSourceScan(table=[[default_catalog, default_database, Sales]], fields=[sale_id, product_key, quantity])
+   +- Exchange(distribution=[hash[product_key]])
+      +- Calc(select=[product_key, promo_text])
+         +- ChangelogNormalize(key=[promo_id])
+            +- Exchange(distribution=[hash[promo_id]])
+               +- TableSourceScan(table=[[default_catalog, default_database, Promotions]], fields=[promo_id, product_key, promo_text])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testWithOrInJoinCondition">
+    <Resource name="sql">
+      <![CDATA[
+SELECT
+    u.user_id,
+    u.name,
+    o.order_id,
+    p.payment_id,
+    s.location
+FROM Users u
+LEFT JOIN Orders o
+    ON o.user_id = u.user_id
+LEFT JOIN Payments p
+    ON u.user_id = p.user_id OR u.name = p.payment_id
+LEFT JOIN Shipments s
+    ON p.user_id = s.user_id]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(user_id=[$0], name=[$1], order_id=[$3], payment_id=[$6], location=[$9])
++- LogicalJoin(condition=[=($8, $10)], joinType=[left])
+   :- LogicalJoin(condition=[OR(=($0, $8), =($1, $6))], joinType=[left])
+   :  :- LogicalJoin(condition=[=($4, $0)], joinType=[left])
+   :  :  :- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalTableScan(table=[[default_catalog, default_database, Payments]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Shipments]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Calc(select=[user_id, name, order_id, payment_id, location])
++- MultiJoin(commonJoinKey=[user_id], joinTypes=[LEFT, LEFT, LEFT], inputUniqueKeys=[(user_id), (order_id), (payment_id), noUniqueKey], joinConditions=[=(user_id0, user_id), OR(=(user_id, user_id1), =(name, payment_id)), =(user_id1, user_id2)], select=[user_id,name,order_id,user_id0,payment_id,user_id1,location,user_id2], rowType=[RecordType(VARCHAR(2147483647) user_id, VARCHAR(2147483647) name, VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id0, VARCHAR(2147483647) payment_id, V [...]
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- ChangelogNormalize(key=[user_id])
+   :     +- Exchange(distribution=[hash[user_id]])
+   :        +- TableSourceScan(table=[[default_catalog, default_database, Users, project=[user_id, name], metadata=[]]], fields=[user_id, name])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Orders, project=[order_id, user_id], metadata=[]]], fields=[order_id, user_id])
+   :- Exchange(distribution=[hash[user_id]])
+   :  +- TableSourceScan(table=[[default_catalog, default_database, Payments, project=[payment_id, user_id], metadata=[]]], fields=[payment_id, user_id])
+   +- Exchange(distribution=[hash[user_id]])
+      +- TableSourceScan(table=[[default_catalog, default_database, Shipments]], fields=[location, user_id])
 ]]>
     </Resource>
   </TestCase>
diff --git a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
index 9566dd91186..10a2a9f3e0c 100644
--- a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
+++ b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/stream/keyselector/AttributeBasedJoinKeyExtractor.java
@@ -22,6 +22,7 @@ import org.apache.flink.table.data.GenericRowData;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.types.logical.LogicalType;
 import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.table.utils.NoCommonJoinKeyException;
 
 import javax.annotation.Nullable;
 
@@ -420,7 +421,7 @@ public class AttributeBasedJoinKeyExtractor implements JoinKeyExtractor, Seriali
             if (commonAttrsForThisInput.isEmpty()) {
                 // This indicates that there is no common join key among all inputs.
                 // In this case, we cannot use a multi-join, so throw an exception.
-                throw new IllegalStateException(
+                throw new NoCommonJoinKeyException(
                         "All inputs in a multi-way join must share a common join key. Input #"
                                 + currentInputId
                                 + " does not share a join key with the other inputs. Please ensure all join"


(flink) 02/05: [FLINK-38379][table] Adjust `visitMultiJoin` in `RelTimeIndicatorConverter` to use fields and not rows

Posted by fp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 0e0a9d904074055c1e5ca9089d5dfda3f371def0
Author: Gustavo de Morais <gd...@confluent.io>
AuthorDate: Fri Sep 19 15:33:41 2025 +0100

    [FLINK-38379][table] Adjust `visitMultiJoin` in `RelTimeIndicatorConverter` to use fields and not rows
---
 .claude/CLAUDE.local.md                            |  8 ++
 .../planner/calcite/RelTimeIndicatorConverter.java |  4 +-
 .../nodes/exec/stream/MultiJoinSemanticTests.java  |  4 +-
 .../nodes/exec/stream/MultiJoinTestPrograms.java   | 97 ++++++++++++++++++++++
 4 files changed, 111 insertions(+), 2 deletions(-)

diff --git a/.claude/CLAUDE.local.md b/.claude/CLAUDE.local.md
new file mode 100644
index 00000000000..8d9ff2b2c09
--- /dev/null
+++ b/.claude/CLAUDE.local.md
@@ -0,0 +1,8 @@
+- You're a respected open source apache flink maintainer.
+
+- Test your changes with one appropriate existing test using this format just to check if we have no compilation errors "cd /Users/gdemorais/qdev/flink2 && ./mvnw test -Dtest="MultiJoinTest#testTwoWayJoinWithUnion" -pl flink-table/flink-table-planner -q -Dcheckstyle.skip -Drat.skip -Dscalastyle.skip -Denforcer.skip=true -Pgenerate-config-docs -Dspotless.check.skip=true"
+- If you cannot run your tests because they get stuck downloading dependencies. You can run "./mvnw clean install -T1C -DskipTests -Pfast -Dcheckstyle.skip -Drat.skip -Dscalastyle.skip -Denforcer.skip=true -Pgenerate-config-docs -Dspotless.check.skip=true -DskipITs=true -Dmaven.javadoc.skip=true -Djapicmp.skip=true -Pskip-webui-build -T4 -fn" to build first. Just do this once and if necessary because it takes over 5 minutes!
+- If you have to install only one module, also use all the available flags to speed up the process. E.g. "./mvnw install -pl flink-table/flink-table-common -T1C -DskipTests -Pfast -Dcheckstyle.skip -Drat.skip -Dscalastyle.skip -Denforcer.skip=true -Pgenerate-config-docs -Dspotless.check.skip=true -DskipITs=true -Dmaven.javadoc.skip=true -Djapicmp.skip=true -Pskip-webui-build -T4"
+
+- We always want to keep the things we do in "eval" for functions and in the processRecord methods in operators to a minimum since this is a hot path for flink which is executed millions of times. Always do what you can in the constructor, intialize things else where. 
+    
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/calcite/RelTimeIndicatorConverter.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/calcite/RelTimeIndicatorConverter.java
index cffe954de32..305296e5aff 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/calcite/RelTimeIndicatorConverter.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/calcite/RelTimeIndicatorConverter.java
@@ -580,7 +580,9 @@ public final class RelTimeIndicatorConverter extends RelHomogeneousShuttle {
                         .collect(Collectors.toList());
 
         final List<RelDataType> allFields =
-                newInputs.stream().map(RelNode::getRowType).collect(Collectors.toList());
+                newInputs.stream()
+                        .flatMap(input -> RelOptUtil.getFieldTypeList(input.getRowType()).stream())
+                        .collect(Collectors.toList());
 
         RexTimeIndicatorMaterializer materializer = new RexTimeIndicatorMaterializer(allFields);
 
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
index 42db5806102..fb24e92e1d7 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinSemanticTests.java
@@ -40,6 +40,8 @@ public class MultiJoinSemanticTests extends SemanticTestBase {
                 MultiJoinTestPrograms.MULTI_JOIN_THREE_WAY_LEFT_OUTER_JOIN_WITH_CTE,
                 MultiJoinTestPrograms.MULTI_JOIN_LEFT_OUTER_WITH_NULL_KEYS,
                 MultiJoinTestPrograms.MULTI_JOIN_NULL_SAFE_JOIN_WITH_NULL_KEYS,
-                MultiJoinTestPrograms.MULTI_JOIN_MIXED_CHANGELOG_MODES);
+                MultiJoinTestPrograms.MULTI_JOIN_MIXED_CHANGELOG_MODES,
+                MultiJoinTestPrograms
+                        .MULTI_JOIN_WITH_TIME_ATTRIBUTES_IN_CONDITIONS_MATERIALIZATION);
     }
 }
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
index ffc47c62a20..3e8e27e9c30 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/MultiJoinTestPrograms.java
@@ -1102,4 +1102,101 @@ public class MultiJoinTestPrograms {
                                     + "LEFT JOIN RetractTable r ON a.id = r.ref_id "
                                     + "LEFT JOIN UpsertTable u ON a.id = u.key_id")
                     .build();
+
+    public static final TableTestProgram
+            MULTI_JOIN_WITH_TIME_ATTRIBUTES_IN_CONDITIONS_MATERIALIZATION =
+                    TableTestProgram.of(
+                                    "three-way-join-with-time-attributes-in-join-conditions",
+                                    "A query from the nexmark benchmark: "
+                                            + "auction and bid aggregation with time-based filtering")
+                            .setupTableSource(
+                                    SourceTestStep.newBuilder("auctions")
+                                            .addSchema(
+                                                    "id BIGINT PRIMARY KEY NOT ENFORCED",
+                                                    "category STRING",
+                                                    "auctionTimestamp STRING",
+                                                    "expiresTimestamp STRING",
+                                                    "auctionDateTime AS TO_TIMESTAMP(auctionTimestamp)",
+                                                    "expires AS TO_TIMESTAMP(expiresTimestamp)",
+                                                    "WATERMARK FOR auctionDateTime AS auctionDateTime - INTERVAL '1' SECOND")
+                                            .addOption("changelog-mode", "I")
+                                            .producedValues(
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            1L,
+                                                            "Electronics",
+                                                            "2024-01-01 12:00:00",
+                                                            "2024-01-01 12:30:00"),
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            3L,
+                                                            "Electronics",
+                                                            "2024-01-01 12:10:00",
+                                                            "2024-01-01 12:40:00"),
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            2L,
+                                                            "Books",
+                                                            "2024-01-01 12:05:00",
+                                                            "2024-01-01 12:35:00"))
+                                            .build())
+                            .setupTableSource(
+                                    SourceTestStep.newBuilder("bids")
+                                            .addSchema(
+                                                    "auction BIGINT",
+                                                    "price DOUBLE",
+                                                    "bidTimestamp STRING",
+                                                    "bidDateTime AS TO_TIMESTAMP(bidTimestamp)",
+                                                    "WATERMARK FOR bidDateTime AS bidDateTime - INTERVAL '1' SECOND")
+                                            .addOption("changelog-mode", "I")
+                                            .producedValues(
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            1L,
+                                                            12.0,
+                                                            "2024-01-01 12:15:00"),
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            1L,
+                                                            15.0,
+                                                            "2024-01-01 12:20:00"),
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            2L,
+                                                            25.0,
+                                                            "2024-01-01 12:25:00"),
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            3L,
+                                                            18.0,
+                                                            "2024-01-01 12:30:00"),
+                                                    Row.ofKind(
+                                                            RowKind.INSERT,
+                                                            1L,
+                                                            20.0,
+                                                            "2024-01-01 12:45:00"))
+                                            .build())
+                            .setupTableSink(
+                                    SinkTestStep.newBuilder("sink")
+                                            .addSchema("category STRING", "avg_final DOUBLE")
+                                            .consumedValues(
+                                                    // Electronics: AVG(MAX(12.0, 15.0), MAX(18.0))
+                                                    // =
+                                                    // AVG(15.0, 18.0) = 16.5
+                                                    "+I[Electronics, 16.5]",
+                                                    // Books: MAX(25.0) = 25.0, AVG(25.0) = 25.0
+                                                    "+I[Books, 25.0]")
+                                            .testMaterializedData()
+                                            .build())
+                            .runSql(
+                                    "INSERT INTO sink "
+                                            + "SELECT Q.category, AVG(Q.final) "
+                                            + "FROM ( "
+                                            + "    SELECT MAX(B.price) AS final, A.category "
+                                            + "    FROM auctions A, bids B "
+                                            + "    WHERE A.id = B.auction AND B.bidDateTime BETWEEN A.auctionDateTime AND A.expires "
+                                            + "    GROUP BY A.id, A.category "
+                                            + ") Q "
+                                            + "GROUP BY Q.category")
+                            .build();
 }


(flink) 03/05: [FLINK-38554][table] Fix rowCount cost for FlinkLogicalMultiJoin

Posted by fp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 74404f8e8bc8e711114b01b351cbd0f81e08e6ce
Author: Gustavo de Morais <gd...@confluent.io>
AuthorDate: Fri Oct 24 11:57:15 2025 +0200

    [FLINK-38554][table] Fix rowCount cost for FlinkLogicalMultiJoin
---
 .../plan/nodes/logical/FlinkLogicalMultiJoin.java  |  2 +-
 .../planner/plan/stream/sql/MultiJoinTest.java     | 39 ++++++++++++++
 .../planner/plan/stream/sql/MultiJoinTest.xml      | 62 ++++++++++++++++++++++
 3 files changed, 102 insertions(+), 1 deletion(-)

diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/logical/FlinkLogicalMultiJoin.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/logical/FlinkLogicalMultiJoin.java
index 5429629f991..21bb14c73a0 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/logical/FlinkLogicalMultiJoin.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/logical/FlinkLogicalMultiJoin.java
@@ -153,7 +153,7 @@ public class FlinkLogicalMultiJoin extends AbstractRelNode implements FlinkLogic
 
             final Double averageRowSize = mq.getAverageRowSize(input);
             final double dAverageRowSize = averageRowSize == null ? 100.0 : averageRowSize;
-            rowCount *= inputRowCount;
+            rowCount += inputRowCount;
             cpu += inputRowCount;
             io += inputRowCount * dAverageRowSize;
         }
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
index a6a488d3db3..8121d763d84 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.java
@@ -171,6 +171,45 @@ public class MultiJoinTest extends TableTestBase {
                         + "LEFT JOIN Payments p ON u.user_id_0 = p.user_id_2");
     }
 
+    @Test
+    void testTwoWayJoinWithUnion() {
+        util.tableEnv()
+                .executeSql(
+                        "CREATE TABLE Orders2 ("
+                                + "  order_id STRING PRIMARY KEY NOT ENFORCED,"
+                                + "  user_id_1 STRING,"
+                                + "  product STRING"
+                                + ") WITH ('connector' = 'values', 'changelog-mode' = 'I,D')");
+
+        util.verifyRelPlan(
+                "WITH OrdersUnion as ("
+                        + "SELECT * FROM Orders "
+                        + "UNION ALL "
+                        + "SELECT * FROM Orders2"
+                        + ") "
+                        + "SELECT * FROM OrdersUnion o "
+                        + "LEFT JOIN Users u "
+                        + "ON o.user_id_1 = u.user_id_0");
+    }
+
+    @Test
+    void testTwoWayJoinWithRank() {
+        util.getTableEnv()
+                .getConfig()
+                .set(OptimizerConfigOptions.TABLE_OPTIMIZER_MULTI_JOIN_ENABLED, true);
+
+        util.verifyRelPlan(
+                "WITH JoinedEvents as ("
+                        + "SELECT e1.id as id, e1.val, e1.rowtime as `rowtime`, e2.price "
+                        + "FROM EventTable1 e1 "
+                        + "JOIN EventTable2 e2 ON e1.id = e2.id) "
+                        + "SELECT id, val, `rowtime` FROM ("
+                        + "SELECT *, "
+                        + "ROW_NUMBER() OVER (PARTITION BY id ORDER BY `rowtime` DESC) as ts "
+                        + "FROM JoinedEvents) "
+                        + "WHERE ts = 1");
+    }
+
     @Test
     void testFourWayComplexJoinRelPlan() {
         util.verifyRelPlan(
diff --git a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
index ba05d7b6778..7df8d36563a 100644
--- a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
+++ b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/MultiJoinTest.xml
@@ -703,6 +703,68 @@ Calc(select=[user_id_0, CAST('Gus' AS VARCHAR(2147483647)) AS name, order_id, CA
    +- Exchange(distribution=[hash[user_id_2]])
       +- Calc(select=[payment_id, price, user_id_2], where=[(price > 10)])
          +- TableSourceScan(table=[[default_catalog, default_database, Payments, filter=[]]], fields=[payment_id, price, user_id_2])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testTwoWayJoinWithUnion">
+    <Resource name="sql">
+      <![CDATA[WITH OrdersUnion as (SELECT * FROM Orders UNION ALL SELECT * FROM Orders2) SELECT * FROM OrdersUnion o LEFT JOIN Users u ON o.user_id_1 = u.user_id_0]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(order_id=[$0], user_id_1=[$1], product=[$2], user_id_0=[$3], name=[$4], cash=[$5])
++- LogicalJoin(condition=[=($1, $3)], joinType=[left])
+   :- LogicalUnion(all=[true])
+   :  :- LogicalProject(order_id=[$0], user_id_1=[$1], product=[$2])
+   :  :  +- LogicalTableScan(table=[[default_catalog, default_database, Orders]])
+   :  +- LogicalProject(order_id=[$0], user_id_1=[$1], product=[$2])
+   :     +- LogicalTableScan(table=[[default_catalog, default_database, Orders2]])
+   +- LogicalTableScan(table=[[default_catalog, default_database, Users]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+MultiJoin(commonJoinKey=[user_id_1], joinTypes=[LEFT], inputUniqueKeys=[noUniqueKey, (user_id_0)], joinConditions=[=(user_id_1, user_id_0)], select=[order_id,user_id_1,product,user_id_0,name,cash], rowType=[RecordType(VARCHAR(2147483647) order_id, VARCHAR(2147483647) user_id_1, VARCHAR(2147483647) product, VARCHAR(2147483647) user_id_0, VARCHAR(2147483647) name, INTEGER cash)])
+:- Exchange(distribution=[hash[user_id_1]])
+:  +- Union(all=[true], union=[order_id, user_id_1, product])
+:     :- TableSourceScan(table=[[default_catalog, default_database, Orders]], fields=[order_id, user_id_1, product])
+:     +- TableSourceScan(table=[[default_catalog, default_database, Orders2]], fields=[order_id, user_id_1, product])
++- Exchange(distribution=[hash[user_id_0]])
+   +- TableSourceScan(table=[[default_catalog, default_database, Users]], fields=[user_id_0, name, cash])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testTwoWayJoinWithRank">
+    <Resource name="sql">
+      <![CDATA[WITH JoinedEvents as (SELECT e1.id as id, e1.val, e1.rowtime as `rowtime`, e2.price FROM EventTable1 e1 JOIN EventTable2 e2 ON e1.id = e2.id) SELECT id, val, `rowtime` FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY `rowtime` DESC) as ts FROM JoinedEvents) WHERE ts = 1]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], val=[$1], rowtime=[$2])
++- LogicalFilter(condition=[=($4, 1)])
+   +- LogicalProject(id=[$0], val=[$1], rowtime=[$2], price=[$3], ts=[ROW_NUMBER() OVER (PARTITION BY $0 ORDER BY $2 DESC NULLS LAST)])
+      +- LogicalProject(id=[$0], val=[$1], rowtime=[$2], price=[$4])
+         +- LogicalJoin(condition=[=($0, $3)], joinType=[inner])
+            :- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
+            :  +- LogicalTableScan(table=[[default_catalog, default_database, EventTable1]])
+            +- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[-($2, 5000:INTERVAL SECOND)])
+               +- LogicalTableScan(table=[[default_catalog, default_database, EventTable2]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], rankRange=[rankStart=1, rankEnd=1], partitionBy=[id], orderBy=[rowtime DESC], select=[id, val, rowtime])
++- Exchange(distribution=[hash[id]])
+   +- Calc(select=[id, val, rowtime])
+      +- MultiJoin(commonJoinKey=[id], joinTypes=[INNER], inputUniqueKeys=[noUniqueKey, noUniqueKey], joinConditions=[=(id, id0)], select=[id,val,rowtime,id0,price,rowtime0], rowType=[RecordType(VARCHAR(2147483647) id, INTEGER val, TIMESTAMP(3) rowtime, VARCHAR(2147483647) id0, DOUBLE price, TIMESTAMP(3) rowtime0)])
+         :- Exchange(distribution=[hash[id]])
+         :  +- Calc(select=[id, val, CAST(rowtime AS TIMESTAMP(3)) AS rowtime])
+         :     +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime, 5000:INTERVAL SECOND)])
+         :        +- TableSourceScan(table=[[default_catalog, default_database, EventTable1]], fields=[id, val, rowtime])
+         +- Exchange(distribution=[hash[id]])
+            +- Calc(select=[id, price, CAST(rowtime AS TIMESTAMP(3)) AS rowtime])
+               +- WatermarkAssigner(rowtime=[rowtime], watermark=[-(rowtime, 5000:INTERVAL SECOND)])
+                  +- TableSourceScan(table=[[default_catalog, default_database, EventTable2]], fields=[id, price, rowtime])
 ]]>
     </Resource>
   </TestCase>