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

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 65492bc162c [fix](rewrite rule) Reject mismatched compare plan in 
PullUpJoinFromUnionAll (#65472)
65492bc162c is described below

commit 65492bc162cfe8c0d73672a48e4db1ac5f919ae5
Author: yujun <[email protected]>
AuthorDate: Fri Jul 24 14:36:57 2026 +0800

    [fix](rewrite rule) Reject mismatched compare plan in 
PullUpJoinFromUnionAll (#65472)
    
    Avoid comparing and remapping join inputs as equivalent when the
    common-side projects expose different output arity. This keeps the union
    pull-up rewrite aligned with the actual plan shape and adds focused
    coverage for the comparator and rewrite guard.
    
    Key changes:
    - Add an output-size guard before comparing plan nodes in
    PullUpJoinFromUnionAll.
    - Add a FE unit test that covers both the comparator and the rewrite
    path.
    
    Unit Test:
    - PullUpJoinFromUnionAllTest
    
    relate PR:  #28682
---
 .../catalog/stream/OlapTableStreamWrapper.java     |   4 +
 .../rules/rewrite/PullUpJoinFromUnionAll.java      |  11 +-
 .../plans/logical/LogicalCatalogRelation.java      |  42 ++
 .../trees/plans/logical/LogicalFileScan.java       |  29 ++
 .../trees/plans/logical/LogicalHudiScan.java       |  10 +
 .../trees/plans/logical/LogicalOdbcScan.java       |   5 +
 .../trees/plans/logical/LogicalOlapScan.java       |  25 +
 .../plans/logical/LogicalOlapTableStreamScan.java  |  25 +
 .../trees/plans/logical/LogicalSchemaScan.java     |  13 +
 .../trees/plans/logical/LogicalTestScan.java       |   5 +
 .../java/org/apache/doris/nereids/util/Utils.java  |   7 +
 .../rules/rewrite/PullUpJoinFromUnionAllTest.java  | 509 +++++++++++++++++++++
 12 files changed, 679 insertions(+), 6 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStreamWrapper.java
 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStreamWrapper.java
index 92c9701884a..b6f5bcd0f8b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStreamWrapper.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStreamWrapper.java
@@ -213,6 +213,10 @@ public class OlapTableStreamWrapper extends OlapTable {
         return baseTable;
     }
 
+    public KeysType getStreamKeysType() {
+        return keysType;
+    }
+
     public BaseTableStream.StreamScanType getStreamScanType() {
         if (keysType == KeysType.DUP_KEYS) {
             return BaseTableStream.StreamScanType.APPEND_ONLY;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAll.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAll.java
index f7197516011..803749366bd 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAll.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAll.java
@@ -17,7 +17,6 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
-import org.apache.doris.catalog.constraint.TableIdentifier;
 import org.apache.doris.common.Pair;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
@@ -577,14 +576,14 @@ public class PullUpJoinFromUnionAll extends 
OneRewriteRuleFactory {
         }
 
         boolean comparePlan(Plan plan1, Plan plan2) {
+            if (plan1.getOutput().size() != plan2.getOutput().size()) {
+                return false;
+            }
             boolean isEqual = true;
             if (plan1 instanceof LogicalCatalogRelation && plan2 instanceof 
LogicalCatalogRelation) {
-                isEqual = new TableIdentifier(((LogicalCatalogRelation) 
plan1).getTable())
-                        .equals(new TableIdentifier(((LogicalCatalogRelation) 
plan2).getTable()));
+                isEqual = ((LogicalCatalogRelation) plan1)
+                        .hasSameScanSemantics((LogicalCatalogRelation) plan2);
             } else if (plan1 instanceof LogicalProject && plan2 instanceof 
LogicalProject) {
-                if (plan1.getOutput().size() != plan2.getOutput().size()) {
-                    isEqual = false;
-                }
                 for (int i = 0; isEqual && i < plan2.getOutput().size(); i++) {
                     Expression expr1 = ((LogicalProject<?>) 
plan1).getProjects().get(i);
                     Expression expr2 = ((LogicalProject<?>) 
plan2).getProjects().get(i);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCatalogRelation.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCatalogRelation.java
index 1fc43325d18..61a6dd54fbd 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCatalogRelation.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCatalogRelation.java
@@ -23,6 +23,7 @@ import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.catalog.constraint.ConstraintManager;
 import org.apache.doris.catalog.constraint.PrimaryKeyConstraint;
+import org.apache.doris.catalog.constraint.TableIdentifier;
 import org.apache.doris.catalog.constraint.UniqueConstraint;
 import org.apache.doris.catalog.info.TableNameInfo;
 import org.apache.doris.catalog.stream.StreamReadMode;
@@ -251,6 +252,47 @@ public abstract class LogicalCatalogRelation extends 
LogicalRelation implements
         return this;
     }
 
+    /** Compare whether two catalog relations read the same data with the same 
output semantics. */
+    public final boolean hasSameScanSemantics(LogicalCatalogRelation other) {
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+        if (!hasSameTableIdentity(other)) {
+            return false;
+        }
+        if (getOutput().size() != other.getOutput().size()) {
+            return false;
+        }
+        for (int i = 0; i < getOutput().size(); i++) {
+            if (!hasSameOutputSlotSemantics(getOutput().get(i), 
other.getOutput().get(i))) {
+                return false;
+            }
+        }
+        return hasSameScanState(other);
+    }
+
+    protected boolean hasSameTableIdentity(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        return new TableIdentifier(table).equals(new 
TableIdentifier(other.table));
+    }
+
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        return false;
+    }
+
+    private boolean hasSameOutputSlotSemantics(Slot left, Slot right) {
+        if (!(left instanceof SlotReference) || !(right instanceof 
SlotReference)) {
+            return false;
+        }
+        SlotReference leftSlot = (SlotReference) left;
+        SlotReference rightSlot = (SlotReference) right;
+        return Objects.equals(left.getClass(), right.getClass())
+                && Objects.equals(leftSlot.getName(), rightSlot.getName())
+                && Objects.equals(leftSlot.getSubPath(), 
rightSlot.getSubPath());
+    }
+
     public abstract LogicalCatalogRelation withRelationId(RelationId 
relationId);
 
     /**
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
index 29e2f84feac..341818e6da6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
@@ -198,6 +198,18 @@ public class LogicalFileScan extends 
LogicalCatalogRelation implements SupportPr
         return super.equals(o) && Objects.equals(selectedPartitions, 
((LogicalFileScan) o).selectedPartitions);
     }
 
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        LogicalFileScan that = (LogicalFileScan) other;
+        return Objects.equals(selectedPartitions, that.selectedPartitions)
+                && Objects.equals(tableSample, that.tableSample)
+                && hasSameSnapshot(tableSnapshot, that.tableSnapshot)
+                && hasSameScanParams(scanParams, that.scanParams);
+    }
+
     @Override
     public List<Slot> computeOutput() {
         if (cachedOutputs.isPresent()) {
@@ -266,6 +278,23 @@ public class LogicalFileScan extends 
LogicalCatalogRelation implements SupportPr
         return false;
     }
 
+    private boolean hasSameSnapshot(Optional<TableSnapshot> left, 
Optional<TableSnapshot> right) {
+        if (!left.isPresent() || !right.isPresent()) {
+            return left.isPresent() == right.isPresent();
+        }
+        return left.get().getType() == right.get().getType()
+                && Objects.equals(left.get().getValue(), 
right.get().getValue());
+    }
+
+    private boolean hasSameScanParams(Optional<TableScanParams> left, 
Optional<TableScanParams> right) {
+        if (!left.isPresent() || !right.isPresent()) {
+            return left.isPresent() == right.isPresent();
+        }
+        return Objects.equals(left.get().getParamType(), 
right.get().getParamType())
+                && Objects.equals(left.get().getMapParams(), 
right.get().getMapParams())
+                && Objects.equals(left.get().getListParams(), 
right.get().getListParams());
+    }
+
     /**
      * SelectedPartitions contains the selected partitions and the total 
partition number.
      * Mainly for hive table partition pruning.
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalHudiScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalHudiScan.java
index 5ff0f937444..97b2d94e27e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalHudiScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalHudiScan.java
@@ -121,6 +121,16 @@ public class LogicalHudiScan extends LogicalFileScan {
         return incrementalRelation;
     }
 
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        LogicalHudiScan that = (LogicalHudiScan) other;
+        // IncrementalRelation contains the resolved Hudi timeline and split 
state and has no value equality.
+        return super.hasSameScanState(other) && 
Objects.equals(incrementalRelation, that.incrementalRelation);
+    }
+
     /**
      * replace incremental params as AND expression
      * incr('beginTime'='20240308110257169', 'endTime'='20240308110677278') =>
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOdbcScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOdbcScan.java
index a4d66919283..e0b64133cef 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOdbcScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOdbcScan.java
@@ -95,6 +95,11 @@ public class LogicalOdbcScan extends LogicalCatalogRelation {
                 Optional.of(getLogicalProperties()), tableAlias));
     }
 
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        return Utils.isSameClass(this, other);
+    }
+
     @Override
     public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
         return visitor.visitLogicalOdbcScan(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
index 71fb7589c47..94bed913bcb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
@@ -1207,6 +1207,22 @@ public class LogicalOlapScan extends 
LogicalCatalogRelation implements OlapScan,
                         Optional.of(scanParams)));
     }
 
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        LogicalOlapScan that = (LogicalOlapScan) other;
+        return selectedIndexId == that.selectedIndexId
+                && indexSelected == that.indexSelected
+                && Objects.equals(selectedPartitionIds, 
that.selectedPartitionIds)
+                && Objects.equals(manuallySpecifiedPartitions, 
that.manuallySpecifiedPartitions)
+                && Objects.equals(selectedTabletIds, that.selectedTabletIds)
+                && Objects.equals(manuallySpecifiedTabletIds, 
that.manuallySpecifiedTabletIds)
+                && Objects.equals(tableSample, that.tableSample)
+                && hasSameScanParams(scanParams, that.scanParams);
+    }
+
     @Override
     public boolean supportPruneNestedColumn() {
         return true;
@@ -1215,4 +1231,13 @@ public class LogicalOlapScan extends 
LogicalCatalogRelation implements OlapScan,
     public Optional<TableScanParams> getScanParams() {
         return scanParams;
     }
+
+    private boolean hasSameScanParams(Optional<TableScanParams> left, 
Optional<TableScanParams> right) {
+        if (!left.isPresent() || !right.isPresent()) {
+            return left.isPresent() == right.isPresent();
+        }
+        return Objects.equals(left.get().getParamType(), 
right.get().getParamType())
+                && Objects.equals(left.get().getMapParams(), 
right.get().getMapParams())
+                && Objects.equals(left.get().getListParams(), 
right.get().getListParams());
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
index 73a4d0441b4..47ba78d7a61 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapTableStreamScan.java
@@ -21,6 +21,7 @@ import org.apache.doris.analysis.TableScanParams;
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.Table;
+import org.apache.doris.catalog.constraint.TableIdentifier;
 import org.apache.doris.catalog.stream.OlapTableStreamWrapper;
 import org.apache.doris.catalog.stream.StreamReadMode;
 import org.apache.doris.common.IdGenerator;
@@ -464,6 +465,30 @@ public class LogicalOlapTableStreamScan extends 
LogicalOlapScan {
                         scanParams, readMode));
     }
 
+    @Override
+    protected boolean hasSameTableIdentity(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        LogicalOlapTableStreamScan that = (LogicalOlapTableStreamScan) other;
+        return Objects.equals(getTable().getStreamDbId(), 
that.getTable().getStreamDbId())
+                && Objects.equals(getTable().getStreamId(), 
that.getTable().getStreamId())
+                && new TableIdentifier(getTable().getBaseTable())
+                .equals(new TableIdentifier(that.getTable().getBaseTable()));
+    }
+
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        LogicalOlapTableStreamScan that = (LogicalOlapTableStreamScan) other;
+        return super.hasSameScanState(other)
+                && readMode == that.readMode
+                && getTable().getStreamKeysType() == 
that.getTable().getStreamKeysType()
+                && Objects.equals(getTable().getOutputUpdateMap(), 
that.getTable().getOutputUpdateMap());
+    }
+
     @Override
     public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
         return visitor.visitLogicalOlapTableStreamScan(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSchemaScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSchemaScan.java
index c441cfbb15b..f42ee2640fe 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSchemaScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSchemaScan.java
@@ -113,6 +113,19 @@ public class LogicalSchemaScan extends 
LogicalCatalogRelation {
         return frontendConjuncts;
     }
 
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        if (!Utils.isSameClass(this, other)) {
+            return false;
+        }
+        LogicalSchemaScan that = (LogicalSchemaScan) other;
+        return filterPushed == that.filterPushed
+                && Objects.equals(schemaCatalog, that.schemaCatalog)
+                && Objects.equals(schemaDatabase, that.schemaDatabase)
+                && Objects.equals(schemaTable, that.schemaTable)
+                && Objects.equals(frontendConjuncts, that.frontendConjuncts);
+    }
+
     @Override
     public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
         return visitor.visitLogicalSchemaScan(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTestScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTestScan.java
index e49df118b9c..58bca515fdb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTestScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTestScan.java
@@ -85,6 +85,11 @@ public class LogicalTestScan extends LogicalCatalogRelation {
                 Optional.of(getLogicalProperties()), tableAlias));
     }
 
+    @Override
+    protected boolean hasSameScanState(LogicalCatalogRelation other) {
+        return Utils.isSameClass(this, other);
+    }
+
     @Override
     public LogicalTestScan withRelationId(RelationId relationId) {
         throw new RuntimeException("should not call LogicalTestScan's 
withRelationId method");
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java
index c268f01f73c..31d3f6fc29d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java
@@ -62,6 +62,13 @@ import java.util.stream.Stream;
 public class Utils {
     public static final boolean enableAssert;
 
+    /**
+     * Check whether two objects are non-null and have the same concrete class.
+     */
+    public static boolean isSameClass(Object left, Object right) {
+        return left != null && right != null && left.getClass() == 
right.getClass();
+    }
+
     static {
         boolean enabled = false;
         // if run jvm with -ea or -enableassertions, the assert statement will 
be executed
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAllTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAllTest.java
new file mode 100644
index 00000000000..e10965911c9
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PullUpJoinFromUnionAllTest.java
@@ -0,0 +1,509 @@
+// 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.doris.nereids.rules.rewrite;
+
+import org.apache.doris.analysis.TableScanParams;
+import org.apache.doris.analysis.TableSnapshot;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.OdbcTable;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.catalog.stream.OlapTableStreamWrapper;
+import org.apache.doris.catalog.stream.StreamReadMode;
+import org.apache.doris.common.Pair;
+import org.apache.doris.datasource.ExternalTable;
+import org.apache.doris.datasource.hudi.source.IncrementalRelation;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFileScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalHudiScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOdbcScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapTableStreamScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSchemaScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalTestScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.nereids.util.PlanConstructor;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+class PullUpJoinFromUnionAllTest {
+
+    @Test
+    void comparatorRejectsDifferentProjectOutputSizes() {
+        LogicalOlapScan smallScan = newScan(1, "common_small");
+        LogicalOlapScan largeScan = newScan(1, "common_large");
+        LogicalProject<LogicalOlapScan> smallProject = 
project(selectSlots(smallScan.getOutput(), 0), smallScan);
+        LogicalProject<LogicalOlapScan> largeProject = 
project(selectSlots(largeScan.getOutput(), 0, 1), largeScan);
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(largeProject, 
smallProject));
+    }
+
+    @Test
+    void comparatorRejectsDifferentFilterChildOutputSizes() {
+        LogicalFilter<LogicalOlapScan> smallFilter = 
filter(newCachedOutputScan(1, "common_filter", 0));
+        LogicalFilter<LogicalOlapScan> largeFilter = 
filter(newCachedOutputScan(1, "common_filter", 0, 1));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(largeFilter, 
smallFilter));
+    }
+
+    @Test
+    void comparatorRejectsDifferentMaterializedIndexSelections() {
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        LogicalOlapScan baseScan = newScanWithExtraIndex(11, "common_index", 
101);
+        LogicalOlapScan indexScan = 
baseScan.withMaterializedIndexSelected(101);
+
+        Assertions.assertFalse(comparator.isLogicalEqual(baseScan, indexScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentSelectedPartitions() {
+        OlapTable table = Mockito.mock(OlapTable.class);
+        Mockito.when(table.getId()).thenReturn(12L);
+        Mockito.when(table.getName()).thenReturn("common_partition");
+        Mockito.when(table.getDatabase()).thenReturn(null);
+        Mockito.when(table.getBaseIndexId()).thenReturn(1L);
+        Mockito.when(table.getBaseSchema()).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        
Mockito.when(table.getPartition(1L)).thenReturn(Mockito.mock(Partition.class));
+        
Mockito.when(table.getPartition(2L)).thenReturn(Mockito.mock(Partition.class));
+
+        LogicalOlapScan firstPartitionScan = newPartitionedScan(table)
+                .withSelectedPartitionIds(ImmutableList.of(1L), true);
+        LogicalOlapScan secondPartitionScan = newPartitionedScan(table)
+                .withSelectedPartitionIds(ImmutableList.of(2L), true);
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(firstPartitionScan, 
secondPartitionScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentHudiIncrementalRelations() {
+        ExternalTable table = Mockito.mock(ExternalTable.class);
+        Mockito.when(table.getId()).thenReturn(18L);
+        Mockito.when(table.getName()).thenReturn("common_hudi");
+        Mockito.when(table.getDatabase()).thenReturn(null);
+        Mockito.when(table.getBaseSchema()).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+
+        IncrementalRelation relation = Mockito.mock(IncrementalRelation.class);
+        IncrementalRelation differentRelation = 
Mockito.mock(IncrementalRelation.class);
+        LogicalHudiScan scan = newHudiScan(table, relation);
+        LogicalHudiScan sameRelationScan = newHudiScan(table, relation);
+        LogicalHudiScan differentRelationScan = newHudiScan(table, 
differentRelation);
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertTrue(comparator.isLogicalEqual(scan, 
sameRelationScan));
+        Assertions.assertFalse(comparator.isLogicalEqual(scan, 
differentRelationScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentTableSamples() {
+        LogicalOlapScan sampleOneScan = newScanWithTableSample(43, 
"common_sample",
+                new org.apache.doris.nereids.trees.TableSample(10, true, 0));
+        LogicalOlapScan sampleTwoScan = newScanWithTableSample(43, 
"common_sample",
+                new org.apache.doris.nereids.trees.TableSample(20, true, 0));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(sampleOneScan, 
sampleTwoScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentOlapScanParams() {
+        LogicalOlapScan incrementalScan = newScanWithScanParams(44, 
"common_params",
+                new TableScanParams(TableScanParams.INCREMENTAL_READ,
+                        ImmutableMap.of("end_ts", "10"), ImmutableList.of()));
+        LogicalOlapScan differentIncrementalScan = newScanWithScanParams(44, 
"common_params",
+                new TableScanParams(TableScanParams.INCREMENTAL_READ,
+                        ImmutableMap.of("end_ts", "20"), ImmutableList.of()));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(incrementalScan, 
differentIncrementalScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentStreamReadModes() {
+        LogicalOlapTableStreamScan snapshotScan = newStreamScanWithOffsets(15, 
"common_stream", 1000L, 2000L,
+                KeysType.UNIQUE_KEYS, ImmutableMap.of(1L, Pair.of(10L, 20L)))
+                .withReadMode(StreamReadMode.SNAPSHOT);
+        LogicalOlapTableStreamScan resetScan = 
snapshotScan.withReadMode(StreamReadMode.RESET);
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(snapshotScan, 
resetScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentStreamOffsets() {
+        LogicalOlapTableStreamScan left = newStreamScanWithOffsets(45, 
"common_stream_offset", 1000L, 2000L,
+                KeysType.UNIQUE_KEYS, ImmutableMap.of(1L, Pair.of(10L, 20L)));
+        LogicalOlapTableStreamScan right = newStreamScanWithOffsets(45, 
"common_stream_offset", 1000L, 2000L,
+                KeysType.UNIQUE_KEYS, ImmutableMap.of(1L, Pair.of(30L, 40L)));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(left, right));
+    }
+
+    @Test
+    void comparatorRejectsDifferentStreamKeysTypes() {
+        LogicalOlapTableStreamScan uniqueKeyScan = 
newStreamScanWithOffsets(46, "common_stream_keys", 1000L, 2000L,
+                KeysType.UNIQUE_KEYS, ImmutableMap.of(1L, Pair.of(10L, 20L)));
+        LogicalOlapTableStreamScan dupKeyScan = newStreamScanWithOffsets(46, 
"common_stream_keys", 1000L, 2000L,
+                KeysType.DUP_KEYS, ImmutableMap.of(1L, Pair.of(10L, 20L)));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(uniqueKeyScan, 
dupKeyScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentStreamIdentities() {
+        LogicalOlapTableStreamScan left = newStreamScanWithOffsets(47, 
"common_stream_identity", 1000L, 2000L,
+                KeysType.UNIQUE_KEYS, ImmutableMap.of(1L, Pair.of(10L, 20L)));
+        LogicalOlapTableStreamScan right = newStreamScanWithOffsets(47, 
"common_stream_identity", 1000L, 3000L,
+                KeysType.UNIQUE_KEYS, ImmutableMap.of(1L, Pair.of(10L, 20L)));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(left, right));
+    }
+
+    @Test
+    void comparatorRejectsDifferentFileScanSnapshots() {
+        LogicalFileScan versionOneScan = newFileScan(17L, 
TableSnapshot.versionOf("1"));
+        LogicalFileScan versionTwoScan = newFileScan(17L, 
TableSnapshot.versionOf("2"));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(versionOneScan, 
versionTwoScan));
+    }
+
+    @Test
+    void comparatorRejectsDifferentSchemaScanConjuncts() {
+        LogicalSchemaScan baseScan = newSchemaScan(16, "schema_common");
+        LogicalSchemaScan filteredScan = 
baseScan.withFrontendConjuncts(Optional.of("ctl"),
+                Optional.of("db"), Optional.of("tbl"),
+                ImmutableList.of(new EqualTo(baseScan.getOutput().get(0), 
baseScan.getOutput().get(1))));
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertFalse(comparator.isLogicalEqual(baseScan, 
filteredScan));
+    }
+
+    @Test
+    void comparatorAcceptsSameOdbcScanWithDifferentRelationIds() {
+        LogicalOdbcScan left = newOdbcScan(18L, "odbc_common");
+        LogicalOdbcScan right = 
left.withRelationId(PlanConstructor.getNextRelationId());
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertTrue(comparator.isLogicalEqual(left, right));
+    }
+
+    @Test
+    void comparatorAcceptsSameTestScan() {
+        LogicalTestScan left = newTestScan(19L, "test_common");
+        LogicalTestScan right = newTestScan(19L, "test_common");
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertTrue(comparator.isLogicalEqual(left, right));
+    }
+
+    @Test
+    void comparatorAcceptsSameScanWithDifferentRelationIds() {
+        LogicalOlapScan left = newScan(13, "common_relation_id");
+        LogicalOlapScan right = 
left.withRelationId(PlanConstructor.getNextRelationId());
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertTrue(comparator.isLogicalEqual(left, right));
+    }
+
+    @Test
+    void comparatorAcceptsSameScanWithDifferentTableAliases() {
+        LogicalOlapScan left = newScan(14, 
"common_alias").withTableAlias("left_alias");
+        LogicalOlapScan right = newScan(14, 
"common_alias").withTableAlias("right_alias");
+
+        PullUpJoinFromUnionAll.LogicalPlanComparator comparator =
+                new PullUpJoinFromUnionAll().new LogicalPlanComparator();
+        Assertions.assertTrue(comparator.isLogicalEqual(left, right));
+    }
+
+    @Test
+    void ruleSkipsJoinChildrenWithDifferentFilteredCommonSideOutputs() {
+        LogicalOlapScan commonSmallScan = newScan(10, "common_small");
+        LogicalOlapScan commonLargeScan = newScan(10, "common_large");
+        LogicalFilter<LogicalOlapScan> commonSmall = 
filter(commonSmallScan.withCachedOutput(
+                selectSlotsAsSlots(commonSmallScan.getOutput(), 0)));
+        LogicalFilter<LogicalOlapScan> commonLarge = 
filter(commonLargeScan.withCachedOutput(
+                selectSlotsAsSlots(commonLargeScan.getOutput(), 0, 1)));
+        LogicalOlapScan otherLeft = newScan(20, "other_left");
+        LogicalOlapScan otherRight = newScan(30, "other_right");
+
+        LogicalProject<Plan> unionChild1 = outputProject(
+                join(commonSmall, otherLeft),
+                "x", commonSmall.getOutput().get(0),
+                "y", otherLeft.getOutput().get(1));
+        LogicalProject<Plan> unionChild2 = outputProject(
+                join(commonLarge, otherRight),
+                "x", commonLarge.getOutput().get(0),
+                "y", otherRight.getOutput().get(1));
+
+        LogicalUnion union = union(unionChild1, unionChild2);
+
+        Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), union)
+                .applyTopDown(new PullUpJoinFromUnionAll())
+                .getPlan();
+
+        Assertions.assertInstanceOf(LogicalUnion.class, rewritten);
+        LogicalUnion rewrittenUnion = (LogicalUnion) rewritten;
+        Assertions.assertEquals(2, rewrittenUnion.children().size());
+        Assertions.assertInstanceOf(LogicalProject.class, 
rewrittenUnion.child(0));
+        Assertions.assertInstanceOf(LogicalProject.class, 
rewrittenUnion.child(1));
+    }
+
+    @Test
+    void ruleSkipsJoinChildrenWithDifferentCommonSideTabletScopes() {
+        LogicalOlapScan commonLeft = newScan(40, 
"common_tablet").withSelectedTabletIds(ImmutableList.of(1L));
+        LogicalOlapScan commonRight = newScan(40, 
"common_tablet").withSelectedTabletIds(ImmutableList.of(2L));
+        LogicalOlapScan otherLeft = newScan(41, "other_left_tablet");
+        LogicalOlapScan otherRight = newScan(42, "other_right_tablet");
+
+        LogicalProject<Plan> unionChild1 = outputProject(
+                join(commonLeft, otherLeft),
+                "x", commonLeft.getOutput().get(0),
+                "y", otherLeft.getOutput().get(1));
+        LogicalProject<Plan> unionChild2 = outputProject(
+                join(commonRight, otherRight),
+                "x", commonRight.getOutput().get(0),
+                "y", otherRight.getOutput().get(1));
+
+        LogicalUnion union = union(unionChild1, unionChild2);
+
+        Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), union)
+                .applyTopDown(new PullUpJoinFromUnionAll())
+                .getPlan();
+
+        Assertions.assertInstanceOf(LogicalUnion.class, rewritten);
+    }
+
+    private static LogicalOlapScan newScan(long tableId, String tableName) {
+        return PlanConstructor.newLogicalOlapScan(tableId, tableName, 0);
+    }
+
+    private static LogicalOlapScan newPartitionedScan(OlapTable table) {
+        return new LogicalOlapScan(PlanConstructor.getNextRelationId(), table, 
ImmutableList.of("db"),
+                ImmutableList.of(), ImmutableList.of(), Optional.empty(), 
ImmutableList.of());
+    }
+
+    private static LogicalHudiScan newHudiScan(ExternalTable table, 
IncrementalRelation incrementalRelation) {
+        return new TestLogicalHudiScan(PlanConstructor.getNextRelationId(), 
table, incrementalRelation);
+    }
+
+    private static LogicalOlapScan newScanWithTableSample(long tableId, String 
tableName,
+            org.apache.doris.nereids.trees.TableSample tableSample) {
+        return new LogicalOlapScan(PlanConstructor.getNextRelationId(),
+                PlanConstructor.newOlapTable(tableId, tableName, 0), 
ImmutableList.of("db"),
+                ImmutableList.of(), ImmutableList.of(), 
Optional.of(tableSample), ImmutableList.of());
+    }
+
+    private static LogicalOlapScan newScanWithScanParams(long tableId, String 
tableName, TableScanParams scanParams) {
+        return new LogicalOlapScan(PlanConstructor.getNextRelationId(),
+                PlanConstructor.newOlapTable(tableId, tableName, 0), 
ImmutableList.of("db"),
+                ImmutableList.of(), ImmutableList.of(), Optional.empty(), 
ImmutableList.of(),
+                Optional.of(scanParams));
+    }
+
+    private static LogicalOlapTableStreamScan newStreamScan(long tableId, 
String tableName) {
+        OlapTableStreamWrapper table = 
Mockito.mock(OlapTableStreamWrapper.class);
+        Mockito.when(table.getId()).thenReturn(tableId);
+        Mockito.when(table.getDatabase()).thenReturn(null);
+        Mockito.when(table.getName()).thenReturn(tableName);
+        
Mockito.when(table.getBaseSchema(true)).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        
Mockito.when(table.getBaseSchema(false)).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        Mockito.when(table.getBaseSchema()).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        return new 
LogicalOlapTableStreamScan(PlanConstructor.getNextRelationId(),
+                table, ImmutableList.of("db"),
+                ImmutableList.of(), ImmutableList.of(), Optional.empty(), 
ImmutableList.of());
+    }
+
+    private static LogicalOlapTableStreamScan newStreamScanWithOffsets(long 
tableId, String tableName,
+            long streamDbId, long streamId, KeysType keysType, 
ImmutableMap<Long, Pair<Long, Long>> offsets) {
+        OlapTable baseTable = Mockito.mock(OlapTable.class);
+        Mockito.when(baseTable.getId()).thenReturn(tableId);
+        Mockito.when(baseTable.getDatabase()).thenReturn(null);
+        Mockito.when(baseTable.getName()).thenReturn(tableName);
+
+        OlapTableStreamWrapper table = 
Mockito.mock(OlapTableStreamWrapper.class);
+        Mockito.when(table.getId()).thenReturn(tableId);
+        Mockito.when(table.getDatabase()).thenReturn(null);
+        Mockito.when(table.getName()).thenReturn(tableName);
+        Mockito.when(table.getBaseTable()).thenReturn(baseTable);
+        Mockito.when(table.getStreamDbId()).thenReturn(streamDbId);
+        Mockito.when(table.getStreamId()).thenReturn(streamId);
+        Mockito.when(table.getStreamKeysType()).thenReturn(keysType);
+        
Mockito.when(table.getBaseSchema(true)).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        
Mockito.when(table.getBaseSchema(false)).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        Mockito.when(table.getBaseSchema()).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        Mockito.when(table.getOutputUpdateMap()).thenReturn(offsets);
+        return new 
LogicalOlapTableStreamScan(PlanConstructor.getNextRelationId(),
+                table, ImmutableList.of("db"), ImmutableList.of(1L),
+                ImmutableList.of(), ImmutableList.of(), Optional.empty(), 
ImmutableList.of());
+    }
+
+    private static LogicalFileScan newFileScan(long tableId, TableSnapshot 
snapshot) {
+        ExternalTable table = Mockito.mock(ExternalTable.class);
+        Mockito.when(table.getId()).thenReturn(tableId);
+        Mockito.when(table.getDatabase()).thenReturn(null);
+        Mockito.when(table.getName()).thenReturn("ext_common");
+        Mockito.when(table.initSelectedPartitions(Mockito.any()))
+                .thenReturn(LogicalFileScan.SelectedPartitions.NOT_PRUNED);
+        Mockito.when(table.getBaseSchema()).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        return new LogicalFileScan(new RelationId(1), table, 
Collections.singletonList("db"),
+                Collections.emptyList(), Optional.empty(), 
Optional.of(snapshot), Optional.empty(), Optional.empty());
+    }
+
+    private static LogicalSchemaScan newSchemaScan(long tableId, String 
tableName) {
+        return new LogicalSchemaScan(PlanConstructor.getNextRelationId(),
+                PlanConstructor.newOlapTable(tableId, tableName, 0), 
ImmutableList.of("db"));
+    }
+
+    private static LogicalOdbcScan newOdbcScan(long tableId, String tableName) 
{
+        OdbcTable table = Mockito.mock(OdbcTable.class);
+        Mockito.when(table.getId()).thenReturn(tableId);
+        Mockito.when(table.getDatabase()).thenReturn(null);
+        Mockito.when(table.getName()).thenReturn(tableName);
+        Mockito.when(table.getBaseSchema()).thenReturn(ImmutableList.of(new 
Column("id", Type.INT, true)));
+        return new LogicalOdbcScan(PlanConstructor.getNextRelationId(), table, 
ImmutableList.of("db"));
+    }
+
+    private static LogicalTestScan newTestScan(long tableId, String tableName) 
{
+        return new LogicalTestScan(PlanConstructor.getNextRelationId(),
+                PlanConstructor.newOlapTable(tableId, tableName, 0), 
ImmutableList.of("db"));
+    }
+
+    private static LogicalOlapScan newScanWithExtraIndex(long tableId, String 
tableName, long indexId) {
+        OlapTable table = PlanConstructor.newOlapTable(tableId, tableName, 0, 
KeysType.DUP_KEYS);
+        table.setIndexMeta(indexId, tableName + "_mv", table.getFullSchema(),
+                0, 0, (short) 0, org.apache.doris.thrift.TStorageType.COLUMN, 
KeysType.DUP_KEYS);
+        return new LogicalOlapScan(PlanConstructor.getNextRelationId(), table, 
ImmutableList.of("db"));
+    }
+
+    private static LogicalOlapScan newCachedOutputScan(long tableId, String 
tableName, int... indexes) {
+        LogicalOlapScan scan = newScan(tableId, tableName);
+        return scan.withCachedOutput(selectSlotsAsSlots(scan.getOutput(), 
indexes));
+    }
+
+    private static LogicalFilter<LogicalOlapScan> filter(LogicalOlapScan 
child) {
+        return new LogicalFilter<>(ImmutableSet.of(new 
EqualTo(child.getOutput().get(0), child.getOutput().get(0))),
+                child);
+    }
+
+    private static LogicalJoin<Plan, LogicalOlapScan> join(Plan commonSide,
+            LogicalOlapScan otherSide) {
+        Expression joinCondition = new EqualTo(commonSide.getOutput().get(0), 
otherSide.getOutput().get(0));
+        return new LogicalJoin<>(JoinType.INNER_JOIN, 
ImmutableList.of(joinCondition), ImmutableList.of(),
+                commonSide, otherSide, null);
+    }
+
+    private static LogicalProject<Plan> outputProject(Plan child, String 
leftAlias, Slot leftSlot,
+            String rightAlias, Slot rightSlot) {
+        return new LogicalProject<>(ImmutableList.of(
+                new Alias(leftSlot, leftAlias),
+                new Alias(rightSlot, rightAlias)), child);
+    }
+
+    private static LogicalUnion union(LogicalProject<Plan> left, 
LogicalProject<Plan> right) {
+        ImmutableList.Builder<NamedExpression> outputs = 
ImmutableList.builder();
+        for (Slot slot : left.getOutput()) {
+            outputs.add(new SlotReference(slot.getName(), slot.getDataType(), 
slot.nullable()));
+        }
+        return new LogicalUnion(Qualifier.ALL, outputs.build(), 
ImmutableList.of(
+                toSlotReferences(left.getOutput()),
+                toSlotReferences(right.getOutput())), ImmutableList.of(), 
false, ImmutableList.of(left, right));
+    }
+
+    private static LogicalProject<LogicalOlapScan> 
project(List<NamedExpression> outputs, LogicalOlapScan child) {
+        return new LogicalProject<>(outputs, child);
+    }
+
+    private static List<NamedExpression> selectSlots(List<Slot> output, int... 
indexes) {
+        ImmutableList.Builder<NamedExpression> selected = 
ImmutableList.builder();
+        for (int index : indexes) {
+            selected.add(output.get(index));
+        }
+        return selected.build();
+    }
+
+    private static List<Slot> selectSlotsAsSlots(List<Slot> output, int... 
indexes) {
+        ImmutableList.Builder<Slot> selected = ImmutableList.builder();
+        for (int index : indexes) {
+            selected.add(output.get(index));
+        }
+        return selected.build();
+    }
+
+    private static List<SlotReference> toSlotReferences(List<Slot> slots) {
+        ImmutableList.Builder<SlotReference> references = 
ImmutableList.builder();
+        for (Slot slot : slots) {
+            references.add((SlotReference) slot);
+        }
+        return references.build();
+    }
+
+    private static class TestLogicalHudiScan extends LogicalHudiScan {
+        private TestLogicalHudiScan(RelationId id, ExternalTable table, 
IncrementalRelation incrementalRelation) {
+            super(id, table, ImmutableList.of("db"), 
LogicalFileScan.SelectedPartitions.NOT_PRUNED,
+                    Optional.empty(), Optional.empty(), Optional.empty(), 
Optional.of(incrementalRelation),
+                    ImmutableList.of(), ImmutableList.of(), Optional.empty(), 
Optional.empty(), "", Optional.empty());
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to