This is an automated email from the ASF dual-hosted git repository.
jackietien pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new fbcca989b37 Support Union in TableModel
fbcca989b37 is described below
commit fbcca989b37d1f3236510417259575b2db49fa0e
Author: Weihao Li <[email protected]>
AuthorDate: Thu Aug 21 16:28:52 2025 +0800
Support Union in TableModel
---
.../it/query/recent/IoTDBUnionTableIT.java | 127 +++++++++++++++++++
.../plan/planner/TableOperatorGenerator.java | 17 +++
.../plan/planner/plan/node/PlanGraphPrinter.java | 9 ++
.../plan/planner/plan/node/PlanNodeType.java | 4 +
.../plan/planner/plan/node/PlanVisitor.java | 5 +
.../relational/analyzer/StatementAnalyzer.java | 6 +-
.../plan/relational/planner/QueryPlanner.java | 34 +++++
.../plan/relational/planner/RelationPlanner.java | 90 ++++++++++++-
.../distribute/TableDistributedPlanGenerator.java | 7 ++
.../iterative/rule/PruneDistinctAggregation.java | 6 +
.../planner/iterative/rule/PruneUnionColumns.java | 80 ++++++++++++
.../iterative/rule/PruneUnionSourceColumns.java | 52 ++++++++
.../plan/relational/planner/node/Patterns.java | 4 +
.../relational/planner/node/SetOperationNode.java | 140 +++++++++++++++++++++
.../plan/relational/planner/node/UnionNode.java | 97 ++++++++++++++
.../optimizations/LogicalOptimizeFactory.java | 6 +-
.../optimizations/PushPredicateIntoTableScan.java | 27 ++++
.../TransformAggregationToStreamable.java | 6 +
.../optimizations/TransformSortToStreamSort.java | 7 ++
.../optimizations/UnaliasSymbolReferences.java | 54 ++++++++
.../plan/relational/type/CompatibleResolver.java | 108 ++++++++++++++++
.../plan/relational/analyzer/UnionTest.java | 65 ++++++++++
.../planner/assertions/PlanMatchPattern.java | 5 +
23 files changed, 949 insertions(+), 7 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBUnionTableIT.java
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBUnionTableIT.java
new file mode 100644
index 00000000000..90ed5f04332
--- /dev/null
+++
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBUnionTableIT.java
@@ -0,0 +1,127 @@
+/*
+ * 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.iotdb.relational.it.query.recent;
+
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData;
+import static org.apache.iotdb.db.it.utils.TestUtils.tableAssertTestFail;
+import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({TableLocalStandaloneIT.class, TableClusterIT.class})
+public class IoTDBUnionTableIT {
+ protected static final String DATABASE_NAME = "test";
+ protected static final String[] createSqls =
+ new String[] {
+ "CREATE DATABASE " + DATABASE_NAME,
+ "USE " + DATABASE_NAME,
+ "create table table1(device STRING TAG, s1 INT32 FIELD, s2 INT32
FIELD)",
+ "insert into table1 values (1, 'd1', 1, 1)",
+ "insert into table1 values (2, 'd1', 2, 2)",
+ "create table table2(device STRING TAG, s1 INT64 FIELD, s2 DOUBLE
FIELD)",
+ "insert into table2 values (1, 'd1', 1, 1.0)",
+ "insert into table2 values (3, 'd1', 3, 3.0)",
+ "create table table3(device STRING TAG, s1_testName INT64 FIELD,
s2_testName DOUBLE FIELD)",
+ "insert into table3 values (1, 'd1', 1, 1.0)",
+ "insert into table3 values (3, 'd1', 3, 3.0)",
+ "create table table4(device STRING TAG, s1 TEXT FIELD, s2 DOUBLE
FIELD)"
+ };
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ EnvFactory.getEnv().initClusterEnvironment();
+ prepareTableData(createSqls);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ EnvFactory.getEnv().cleanClusterEnvironment();
+ }
+
+ @Test
+ public void normalTest() {
+ String[] expectedHeader = new String[] {"time", "device", "s1", "s2"};
+ String[] retArray =
+ new String[] {
+ "1970-01-01T00:00:00.001Z,d1,1,1.0,",
+ "1970-01-01T00:00:00.002Z,d1,2,2.0,",
+ "1970-01-01T00:00:00.003Z,d1,3,3.0,"
+ };
+ tableResultSetEqualTest(
+ "(select * from table1) union (select * from table2) order by time",
+ expectedHeader,
+ retArray,
+ DATABASE_NAME);
+ tableResultSetEqualTest(
+ "(select * from table1) union distinct (select * from table2) order by
time",
+ expectedHeader,
+ retArray,
+ DATABASE_NAME);
+
+ retArray =
+ new String[] {
+ "1970-01-01T00:00:00.001Z,d1,1,1.0,",
+ "1970-01-01T00:00:00.001Z,d1,1,1.0,",
+ "1970-01-01T00:00:00.002Z,d1,2,2.0,",
+ "1970-01-01T00:00:00.003Z,d1,3,3.0,"
+ };
+ tableResultSetEqualTest(
+ "(select * from table1) union all (select * from table2) order by
time",
+ expectedHeader,
+ retArray,
+ DATABASE_NAME);
+ tableResultSetEqualTest(
+ "(select * from table1) union all (select * from table3) order by
time",
+ expectedHeader,
+ retArray,
+ DATABASE_NAME);
+
+ // result correction test for union with predicate
+ retArray =
+ new String[] {"1970-01-01T00:00:00.002Z,d1,2,2.0,",
"1970-01-01T00:00:00.003Z,d1,3,3.0,"};
+ tableResultSetEqualTest(
+ "select * from ((select * from table1) union all (select * from
table2)) where s1>1 order by time",
+ expectedHeader,
+ retArray,
+ DATABASE_NAME);
+ }
+
+ @Test
+ public void exceptionTest() {
+ tableAssertTestFail(
+ "(select * from table1) union all (select * from table4)",
+ "has incompatible types: INT32, TEXT",
+ DATABASE_NAME);
+ tableAssertTestFail(
+ "(select * from table1) union all (select time from table4)",
+ "UNION query has different number of fields: 4, 1",
+ DATABASE_NAME);
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
index 475732a6af5..0dd59d84d6b 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/TableOperatorGenerator.java
@@ -210,6 +210,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableDeviceFetchNode;
@@ -4060,4 +4061,20 @@ public class TableOperatorGenerator extends
PlanVisitor<Operator, LocalExecution
return new WindowAggregator(
accumulator, getTSDataType(typeProvider.getTableModelType(symbol)),
argumentChannels);
}
+
+ @Override
+ public Operator visitUnion(UnionNode node, LocalExecutionPlanContext
context) {
+ List<Operator> children =
+ node.getChildren().stream()
+ .map(child -> child.accept(this, context))
+ .collect(Collectors.toList());
+ OperatorContext operatorContext =
+ context
+ .getDriverContext()
+ .addOperatorContext(
+ context.getNextOperatorId(),
+ node.getPlanNodeId(),
+ CollectOperator.class.getSimpleName());
+ return new CollectOperator(operatorContext, children);
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
index 776adf96cb7..b07cc767910 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
@@ -82,6 +82,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.SemiJoinNode
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
@@ -1094,6 +1095,14 @@ public class PlanGraphPrinter extends
PlanVisitor<List<String>, PlanGraphPrinter
: String.valueOf(regionReplicaSet.getRegionId().id));
}
+ @Override
+ public List<String> visitUnion(UnionNode node, GraphContext context) {
+ List<String> boxValue = new ArrayList<>();
+ boxValue.add(String.format("Union-%s", node.getPlanNodeId().getId()));
+ boxValue.add(String.format("OutputSymbols: %s", node.getOutputSymbols()));
+ return render(node, boxValue, context);
+ }
+
private List<String> render(PlanNode node, List<String> nodeBoxString,
GraphContext context) {
Box box = new Box(nodeBoxString);
List<List<String>> children = new ArrayList<>();
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
index f7459a0eb57..a834f7e076d 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanNodeType.java
@@ -130,6 +130,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctio
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.ConstructTableDevicesBlackListNode;
@@ -308,6 +309,7 @@ public enum PlanNodeType {
TABLE_PATTERN_RECOGNITION_NODE((short) 1031),
TABLE_WINDOW_FUNCTION((short) 1032),
TABLE_INTO_NODE((short) 1033),
+ TABLE_UNION_NODE((short) 1034),
RELATIONAL_INSERT_TABLET((short) 2000),
RELATIONAL_INSERT_ROW((short) 2001),
@@ -693,6 +695,8 @@ public enum PlanNodeType {
case 1033:
return
org.apache.iotdb.db.queryengine.plan.relational.planner.node.IntoNode.deserialize(
buffer);
+ case 1034:
+ return UnionNode.deserialize(buffer);
case 2000:
return RelationalInsertTabletNode.deserialize(buffer);
case 2001:
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
index 9ac845015e3..b884ac01935 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java
@@ -136,6 +136,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNod
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.ConstructTableDevicesBlackListNode;
@@ -830,4 +831,8 @@ public abstract class PlanVisitor<R, C> {
public R visitPatternRecognition(PatternRecognitionNode node, C context) {
return visitPlan(node, context);
}
+
+ public R visitUnion(UnionNode node, C context) {
+ return visitPlan(node, context);
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
index 382895122a7..8a07a18f91b 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
@@ -190,6 +190,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.WindowSpecificati
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.With;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.WithQuery;
import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.WrappedInsertStatement;
+import org.apache.iotdb.db.queryengine.plan.relational.type.CompatibleResolver;
import
org.apache.iotdb.db.queryengine.plan.relational.type.InternalTypeManager;
import org.apache.iotdb.db.queryengine.plan.relational.type.TypeManager;
import org.apache.iotdb.db.queryengine.plan.statement.component.FillPolicy;
@@ -2974,7 +2975,9 @@ public class StatementAnalyzer {
}
for (int i = 0; i < descFieldSize; i++) {
Type descFieldType = relationType.getFieldByIndex(i).getType();
- if (descFieldType != outputFieldTypes[i]) {
+ Optional<Type> commonSuperType =
+ CompatibleResolver.getCommonSuperType(outputFieldTypes[i],
descFieldType);
+ if (!commonSuperType.isPresent()) {
throw new SemanticException(
String.format(
"column %d in %s query has incompatible types: %s, %s",
@@ -2983,6 +2986,7 @@ public class StatementAnalyzer {
outputFieldTypes[i].getDisplayName(),
descFieldType.getDisplayName()));
}
+ outputFieldTypes[i] = commonSuperType.get();
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
index a8b5f0a80e9..106b406257f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/QueryPlanner.java
@@ -1101,6 +1101,32 @@ public class QueryPlanner {
return new PlanAndMappings(subPlan, mappings);
}
+ public static NodeAndMappings coerce(
+ RelationPlan plan, List<Type> types, SymbolAllocator symbolAllocator,
QueryId idAllocator) {
+ List<Symbol> visibleFields = visibleFields(plan);
+ checkArgument(visibleFields.size() == types.size());
+
+ Assignments.Builder assignments = Assignments.builder();
+ ImmutableList.Builder<Symbol> mappings = ImmutableList.builder();
+ for (int i = 0; i < types.size(); i++) {
+ Symbol input = visibleFields.get(i);
+ Type type = types.get(i);
+
+ if (!symbolAllocator.getTypes().getTableModelType(input).equals(type)) {
+ Symbol coerced = symbolAllocator.newSymbol(input.getName(), type);
+ assignments.put(coerced, new Cast(input.toSymbolReference(),
toSqlType(type)));
+ mappings.add(coerced);
+ } else {
+ assignments.putIdentity(input);
+ mappings.add(input);
+ }
+ }
+
+ ProjectNode coerced =
+ new ProjectNode(idAllocator.genPlanNodeId(), plan.getRoot(),
assignments.build());
+ return new NodeAndMappings(coerced, mappings.build());
+ }
+
public static List<Symbol> visibleFields(RelationPlan subPlan) {
RelationType descriptor = subPlan.getDescriptor();
return descriptor.getAllFields().stream()
@@ -1110,6 +1136,14 @@ public class QueryPlanner {
.collect(toImmutableList());
}
+ public static NodeAndMappings pruneInvisibleFields(RelationPlan plan,
QueryId idAllocator) {
+ List<Symbol> visibleFields = visibleFields(plan);
+ ProjectNode pruned =
+ new ProjectNode(
+ idAllocator.genPlanNodeId(), plan.getRoot(),
Assignments.identity(visibleFields));
+ return new NodeAndMappings(pruned, visibleFields);
+ }
+
public static OrderingScheme translateOrderingScheme(
List<SortItem> items, Function<Expression, Symbol> coercions) {
List<Symbol> coerced =
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
index 55f65db3d53..b69a03e26e1 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java
@@ -66,6 +66,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.SkipToPositi
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.AggregationLabelSet;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.AggregationValuePointer;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.ClassifierValuePointer;
@@ -105,7 +106,9 @@ import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.PipeEnriched;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.QualifiedName;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Query;
import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.QuerySpecification;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Relation;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RowPattern;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SetOperation;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SkipTo;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SortItem;
import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SubqueryExpression;
@@ -121,9 +124,12 @@ import
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement;
import
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
import org.apache.tsfile.read.common.type.Type;
import org.apache.tsfile.write.schema.MeasurementSchema;
@@ -152,7 +158,10 @@ import static
org.apache.iotdb.db.queryengine.plan.relational.planner.PlanBuilde
import static
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.coerce;
import static
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.coerceIfNecessary;
import static
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.extractPatternRecognitionExpressions;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.QueryPlanner.pruneInvisibleFields;
import static
org.apache.iotdb.db.queryengine.plan.relational.planner.ir.IrUtils.extractPredicates;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode.singleAggregation;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode.singleGroupingSet;
import static
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Join.Type.CROSS;
import static
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Join.Type.FULL;
import static
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Join.Type.IMPLICIT;
@@ -1094,6 +1103,64 @@ public class RelationPlanner extends
AstVisitor<RelationPlan, Void> {
}
}
+ @Override
+ protected RelationPlan visitUnion(Union node, Void context) {
+ Preconditions.checkArgument(!node.getRelations().isEmpty(), "No relations
specified for UNION");
+
+ SetOperationPlan setOperationPlan = process(node);
+
+ PlanNode planNode =
+ new UnionNode(
+ idAllocator.genPlanNodeId(),
+ setOperationPlan.getChildren(),
+ setOperationPlan.getSymbolMapping(),
+
ImmutableList.copyOf(setOperationPlan.getSymbolMapping().keySet()));
+ if (node.isDistinct()) {
+ planNode = distinct(planNode);
+ }
+ return new RelationPlan(
+ planNode, analysis.getScope(node), planNode.getOutputSymbols(),
outerContext);
+ }
+
+ private SetOperationPlan process(SetOperation node) {
+ RelationType outputFields = analysis.getOutputDescriptor(node);
+ List<Symbol> outputs =
+ outputFields.getAllFields().stream()
+ .map(symbolAllocator::newSymbol)
+ .collect(toImmutableList());
+
+ ImmutableListMultimap.Builder<Symbol, Symbol> symbolMapping =
ImmutableListMultimap.builder();
+ ImmutableList.Builder<PlanNode> children = ImmutableList.builder();
+
+ for (Relation child : node.getRelations()) {
+ RelationPlan plan = process(child, null);
+
+ NodeAndMappings planAndMappings;
+ List<Type> types = analysis.getRelationCoercion(child);
+ if (types == null) {
+ // no coercion required, only prune invisible fields from child outputs
+ planAndMappings = pruneInvisibleFields(plan, idAllocator);
+ } else {
+ // apply required coercion and prune invisible fields from child
outputs
+ planAndMappings = coerce(plan, types, symbolAllocator, idAllocator);
+ }
+ for (int i = 0; i < outputFields.getAllFields().size(); i++) {
+ symbolMapping.put(outputs.get(i), planAndMappings.getFields().get(i));
+ }
+
+ children.add(planAndMappings.getNode());
+ }
+ return new SetOperationPlan(children.build(), symbolMapping.build());
+ }
+
+ private PlanNode distinct(PlanNode node) {
+ return singleAggregation(
+ idAllocator.genPlanNodeId(),
+ node,
+ ImmutableMap.of(),
+ singleGroupingSet(node.getOutputSymbols()));
+ }
+
// ================================ Implemented later
=====================================
@Override
@@ -1106,11 +1173,6 @@ public class RelationPlanner extends
AstVisitor<RelationPlan, Void> {
throw new IllegalStateException("Intersect is not supported in current
version.");
}
- @Override
- protected RelationPlan visitUnion(Union node, Void context) {
- throw new IllegalStateException("Union is not supported in current
version.");
- }
-
@Override
protected RelationPlan visitExcept(Except node, Void context) {
throw new IllegalStateException("Except is not supported in current
version.");
@@ -1384,6 +1446,24 @@ public class RelationPlanner extends
AstVisitor<RelationPlan, Void> {
}
}
+ private static final class SetOperationPlan {
+ private final List<PlanNode> children;
+ private final ListMultimap<Symbol, Symbol> symbolMapping;
+
+ private SetOperationPlan(List<PlanNode> children, ListMultimap<Symbol,
Symbol> symbolMapping) {
+ this.children = children;
+ this.symbolMapping = symbolMapping;
+ }
+
+ public List<PlanNode> getChildren() {
+ return children;
+ }
+
+ public ListMultimap<Symbol, Symbol> getSymbolMapping() {
+ return symbolMapping;
+ }
+ }
+
public static class PatternRecognitionComponents {
private final Map<Symbol, Measure> measures;
private final List<Symbol> measureOutputs;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
index 2d633eacd64..7d569b804fa 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableDistributedPlanGenerator.java
@@ -82,6 +82,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.AbstractTableDeviceQueryNode;
@@ -1758,6 +1759,12 @@ public class TableDistributedPlanGenerator
}
}
+ @Override
+ public List<PlanNode> visitUnion(UnionNode node, PlanContext context) {
+ context.clearExpectedOrderingScheme();
+ return visitMultiChildProcess(node, context);
+ }
+
public static class PlanContext {
final Map<PlanNodeId, NodeDistribution> nodeDistributionMap;
boolean hasExchangeNode = false;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
index 0e9f55c025e..5b3a4101478 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneDistinctAggregation.java
@@ -24,6 +24,7 @@ import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Lookup;
import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
@@ -94,6 +95,11 @@ public class PruneDistinctAggregation implements
Rule<AggregationNode> {
return rewriteChildren(node, false);
}
+ @Override
+ public PlanNode visitUnion(UnionNode node, Boolean context) {
+ return rewriteChildren(node, context);
+ }
+
/*@Override
public PlanNode visitUnion(UnionNode node, Boolean context)
{
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionColumns.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionColumns.java
new file mode 100644
index 00000000000..d945ce117af
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionColumns.java
@@ -0,0 +1,80 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static
com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns.union;
+
+/**
+ * Transforms
+ *
+ * <pre>
+ * - Project (a)
+ * - Union
+ * output mappings: {a->c, a->e, b->d, b->f}
+ * - Source (c, d)
+ * - Source (e, f)
+ * </pre>
+ *
+ * into:
+ *
+ * <pre>
+ * - Project (a)
+ * - Union
+ * output mappings: {a->c, a->e}
+ * - Source (c, d)
+ * - Source (e, f)
+ * </pre>
+ *
+ * Note: as a result of this rule, the UnionNode's sources are eligible for
pruning outputs. This is
+ * accomplished by PruneUnionSourceColumns rule.
+ */
+public class PruneUnionColumns extends ProjectOffPushDownRule<UnionNode> {
+ public PruneUnionColumns() {
+ super(union());
+ }
+
+ @Override
+ protected Optional<PlanNode> pushDownProjectOff(
+ Context context, UnionNode unionNode, Set<Symbol> referencedOutputs) {
+ ImmutableListMultimap<Symbol, Symbol> prunedOutputMappings =
+ unionNode.getSymbolMapping().entries().stream()
+ .filter(entry -> referencedOutputs.contains(entry.getKey()))
+ .collect(toImmutableListMultimap(Map.Entry::getKey,
Map.Entry::getValue));
+
+ return Optional.of(
+ new UnionNode(
+ unionNode.getPlanNodeId(),
+ unionNode.getChildren(),
+ prunedOutputMappings,
+ ImmutableList.copyOf(prunedOutputMappings.keySet())));
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionSourceColumns.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionSourceColumns.java
new file mode 100644
index 00000000000..71760f58aaa
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneUnionSourceColumns.java
@@ -0,0 +1,52 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.iterative.rule;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.Rule;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Captures;
+import org.apache.iotdb.db.queryengine.plan.relational.utils.matching.Pattern;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Util.restrictChildOutputs;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.node.Patterns.union;
+
+public class PruneUnionSourceColumns implements Rule<UnionNode> {
+ @Override
+ public Pattern<UnionNode> getPattern() {
+ return union();
+ }
+
+ @Override
+ public Result apply(UnionNode node, Captures captures, Context context) {
+ @SuppressWarnings("unchecked")
+ Set<Symbol>[] referencedInputs = new Set[node.getChildren().size()];
+ for (int i = 0; i < node.getChildren().size(); i++) {
+ referencedInputs[i] = ImmutableSet.copyOf(node.sourceOutputLayout(i));
+ }
+ return restrictChildOutputs(context.getIdAllocator(), node,
referencedInputs)
+ .map(Rule.Result::ofPlanNode)
+ .orElse(Rule.Result.empty());
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
index b826b00ea93..3ebd933d28c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/Patterns.java
@@ -185,6 +185,10 @@ public final class Patterns {
return typeOf(PatternRecognitionNode.class);
}
+ public static Pattern<UnionNode> union() {
+ return typeOf(UnionNode.class);
+ }
+
/*public static Pattern<TableWriterNode> tableWriterNode()
{
return typeOf(TableWriterNode.class);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/SetOperationNode.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/SetOperationNode.java
new file mode 100644
index 00000000000..a23ba5f6cd4
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/SetOperationNode.java
@@ -0,0 +1,140 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.node;
+
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeId;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.MultiChildProcessNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SymbolReference;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
+
+public abstract class SetOperationNode extends MultiChildProcessNode {
+ // Corresponding is not supported in UNION now, this field can be used for
future expansion.
+ // We don't need to serialize this field now, consider it when support
Corresponding.
+ private final transient ListMultimap<Symbol, Symbol> outputToInputs;
+ private final List<Symbol> outputs;
+
+ protected SetOperationNode(
+ PlanNodeId id,
+ List<PlanNode> children,
+ ListMultimap<Symbol, Symbol> outputToInputs,
+ List<Symbol> outputs) {
+ super(id);
+
+ requireNonNull(children, "children is null");
+ checkArgument(!children.isEmpty(), "Must have at least one source");
+ requireNonNull(outputToInputs, "outputToInputs is null");
+ requireNonNull(outputs, "outputs is null");
+
+ this.children = ImmutableList.copyOf(children);
+ this.outputToInputs = ImmutableListMultimap.copyOf(outputToInputs);
+ this.outputs = ImmutableList.copyOf(outputs);
+
+ for (Collection<Symbol> inputs : this.outputToInputs.asMap().values()) {
+ checkArgument(
+ inputs.size() == this.children.size(),
+ "Every child needs to map its symbols to an output %s operation
symbol",
+ this.getClass().getSimpleName());
+ }
+
+ // Make sure each child positionally corresponds to their Symbol values in
the Multimap
+ for (int i = 0; i < children.size(); i++) {
+ Set<Symbol> childSymbols =
ImmutableSet.copyOf(children.get(i).getOutputSymbols());
+ for (Collection<Symbol> expectedInputs :
this.outputToInputs.asMap().values()) {
+ checkArgument(
+ childSymbols.contains(Iterables.get(expectedInputs, i)),
+ "Child does not provide required symbols");
+ }
+ }
+ }
+
+ // used for clone(), we needn't check arguments again
+ protected SetOperationNode(
+ PlanNodeId id, ListMultimap<Symbol, Symbol> outputToInputs, List<Symbol>
outputs) {
+ super(id);
+
+ this.outputToInputs = ImmutableListMultimap.copyOf(outputToInputs);
+ this.outputs = ImmutableList.copyOf(outputs);
+ }
+
+ @Override
+ public List<Symbol> getOutputSymbols() {
+ return outputs;
+ }
+
+ public ListMultimap<Symbol, Symbol> getSymbolMapping() {
+ return outputToInputs;
+ }
+
+ public List<Symbol> sourceOutputLayout(int sourceIndex) {
+ // Make sure the sourceOutputLayout symbols are listed in the same order
as the corresponding
+ // output symbols
+ return getOutputSymbols().stream()
+ .map(symbol -> outputToInputs.get(symbol).get(sourceIndex))
+ .collect(toImmutableList());
+ }
+
+ /** Returns the output to input symbol mapping for the given source channel
*/
+ public Map<Symbol, SymbolReference> sourceSymbolMap(int sourceIndex) {
+ ImmutableMap.Builder<Symbol, SymbolReference> builder =
ImmutableMap.builder();
+ for (Map.Entry<Symbol, Collection<Symbol>> entry :
outputToInputs.asMap().entrySet()) {
+ builder.put(entry.getKey(), Iterables.get(entry.getValue(),
sourceIndex).toSymbolReference());
+ }
+
+ return builder.buildOrThrow();
+ }
+
+ /**
+ * Returns the input to output symbol mapping for the given source channel.
A single input symbol
+ * can map to multiple output symbols, thus requiring a Multimap.
+ */
+ public Multimap<Symbol, SymbolReference> outputSymbolMap(int sourceIndex) {
+ return Multimaps.transformValues(
+ FluentIterable.from(getOutputSymbols())
+ .toMap(outputToSourceSymbolFunction(sourceIndex))
+ .asMultimap()
+ .inverse(),
+ Symbol::toSymbolReference);
+ }
+
+ private Function<Symbol, Symbol> outputToSourceSymbolFunction(int
sourceIndex) {
+ return outputSymbol -> outputToInputs.get(outputSymbol).get(sourceIndex);
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/UnionNode.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/UnionNode.java
new file mode 100644
index 00000000000..59fa1674368
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/UnionNode.java
@@ -0,0 +1,97 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.node;
+
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanNodeType;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.Symbol;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UnionNode extends SetOperationNode {
+ public UnionNode(
+ PlanNodeId id,
+ List<PlanNode> children,
+ ListMultimap<Symbol, Symbol> outputToInputs,
+ List<Symbol> outputs) {
+ super(id, children, outputToInputs, outputs);
+ }
+
+ private UnionNode(
+ PlanNodeId id, ListMultimap<Symbol, Symbol> outputToInputs, List<Symbol>
outputs) {
+ super(id, outputToInputs, outputs);
+ }
+
+ @Override
+ public PlanNode clone() {
+ return new UnionNode(getPlanNodeId(), getSymbolMapping(),
getOutputSymbols());
+ }
+
+ @Override
+ public List<String> getOutputColumnNames() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+ return visitor.visitUnion(this, context);
+ }
+
+ @Override
+ protected void serializeAttributes(ByteBuffer byteBuffer) {
+ PlanNodeType.TABLE_UNION_NODE.serialize(byteBuffer);
+ ReadWriteIOUtils.write(getOutputSymbols().size(), byteBuffer);
+ getOutputSymbols().forEach(symbol -> Symbol.serialize(symbol, byteBuffer));
+ }
+
+ @Override
+ protected void serializeAttributes(DataOutputStream stream) throws
IOException {
+ PlanNodeType.TABLE_UNION_NODE.serialize(stream);
+ ReadWriteIOUtils.write(getOutputSymbols().size(), stream);
+ for (Symbol symbol : getOutputSymbols()) {
+ Symbol.serialize(symbol, stream);
+ }
+ }
+
+ public static UnionNode deserialize(ByteBuffer byteBuffer) {
+ int size = ReadWriteIOUtils.readInt(byteBuffer);
+ List<Symbol> outputs = new ArrayList<>(size);
+ while (size-- > 0) {
+ outputs.add(Symbol.deserialize(byteBuffer));
+ }
+ PlanNodeId planNodeId = PlanNodeId.deserialize(byteBuffer);
+ return new UnionNode(planNodeId, ImmutableListMultimap.of(), outputs);
+ }
+
+ @Override
+ public PlanNode replaceChildren(List<PlanNode> newChildren) {
+ return new UnionNode(getPlanNodeId(), newChildren, getSymbolMapping(),
getOutputSymbols());
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
index 1112ffd978b..3af300db979 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
@@ -62,6 +62,8 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.Pr
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTableFunctionProcessorSourceColumns;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTableScanColumns;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneTopKColumns;
+import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneUnionColumns;
+import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneUnionSourceColumns;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PruneWindowColumns;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PushLimitThroughOffset;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.iterative.rule.PushLimitThroughProject;
@@ -132,7 +134,9 @@ public class LogicalOptimizeFactory {
new PruneWindowColumns(),
new PruneJoinColumns(),
new PruneJoinChildrenColumns(),
- new PrunePatternRecognitionSourceColumns());
+ new PrunePatternRecognitionSourceColumns(),
+ new PruneUnionColumns(),
+ new PruneUnionSourceColumns());
IterativeOptimizer columnPruningOptimizer =
new IterativeOptimizer(plannerContext, ruleStats, columnPruningRules);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
index 8585eb4ebcb..fab09a2e3f5 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/PushPredicateIntoTableScan.java
@@ -61,6 +61,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.SemiJoinNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ComparisonExpression;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Expression;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.FunctionCall;
@@ -1155,6 +1156,32 @@ public class PushPredicateIntoTableScan implements
PlanOptimizer {
return node;
}
+ @Override
+ public PlanNode visitUnion(UnionNode node, RewriteContext context) {
+ boolean modified = false;
+ ImmutableList.Builder<PlanNode> builder = ImmutableList.builder();
+ for (int i = 0; i < node.getChildren().size(); i++) {
+ Expression sourcePredicate =
+ inlineSymbols(node.sourceSymbolMap(i), context.inheritedPredicate);
+ PlanNode child = node.getChildren().get(i);
+ PlanNode rewritten = child.accept(this, new
RewriteContext(sourcePredicate));
+ if (rewritten != child) {
+ modified = true;
+ }
+ builder.add(rewritten);
+ }
+
+ if (modified) {
+ return new UnionNode(
+ node.getPlanNodeId(),
+ builder.build(),
+ node.getSymbolMapping(),
+ node.getOutputSymbols());
+ }
+
+ return node;
+ }
+
private DataPartition fetchDataPartitionByDevices(
final String
database, // for tree view, database should be the real tree db
name with `root.` prefix
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
index dbe3735bfca..1d64e28f8e3 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformAggregationToStreamable.java
@@ -33,6 +33,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.MergeSortNod
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ProjectNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import com.google.common.collect.ImmutableList;
@@ -144,6 +145,11 @@ public class TransformAggregationToStreamable implements
PlanOptimizer {
return node.getChild().accept(this, context);
}
+ @Override
+ public List<Symbol> visitUnion(UnionNode node, GroupContext context) {
+ return ImmutableList.of();
+ }
+
@Override
public List<Symbol> visitSort(SortNode node, GroupContext context) {
return getMatchedPrefixSymbols(context, node.getOrderingScheme());
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
index 38d3d287853..3eb338a650c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
@@ -34,6 +34,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.GroupNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.InformationSchemaTableScanNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.SortNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.StreamSortNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import java.util.Map;
@@ -154,6 +155,12 @@ public class TransformSortToStreamSort implements
PlanOptimizer {
context.setCanTransform(false);
return visitTableScan(node, context);
}
+
+ @Override
+ public PlanNode visitUnion(UnionNode node, Context context) {
+ context.setCanTransform(false);
+ return visitMultiChildProcess(node, context);
+ }
}
/**
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
index f94c78a295f..96ab8434475 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
@@ -57,6 +57,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctio
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ValueFillNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Expression;
@@ -64,13 +65,17 @@ import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.NullLiteral;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.SymbolReference;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import java.util.AbstractMap.SimpleEntry;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -972,6 +977,55 @@ public class UnaliasSymbolReferences implements
PlanOptimizer {
return new PlanAndMappings(rewrittenPatternRecognition, mapping);
}
+
+ @Override
+ public PlanAndMappings visitUnion(UnionNode node, UnaliasContext context) {
+ List<PlanAndMappings> rewrittenSources =
+ node.getChildren().stream()
+ .map(source -> source.accept(this, context))
+ .collect(toImmutableList());
+
+ List<SymbolMapper> inputMappers =
+ rewrittenSources.stream()
+ .map(source -> symbolMapper(new HashMap<>(source.getMappings())))
+ .collect(toImmutableList());
+
+ Map<Symbol, Symbol> mapping = new
HashMap<>(context.getCorrelationMapping());
+ SymbolMapper outputMapper = symbolMapper(mapping);
+
+ ListMultimap<Symbol, Symbol> newOutputToInputs =
+ rewriteOutputToInputsMap(node.getSymbolMapping(), outputMapper,
inputMappers);
+ List<Symbol> newOutputs =
outputMapper.mapAndDistinct(node.getOutputSymbols());
+
+ return new PlanAndMappings(
+ new UnionNode(
+ node.getPlanNodeId(),
+
rewrittenSources.stream().map(PlanAndMappings::getRoot).collect(toImmutableList()),
+ newOutputToInputs,
+ newOutputs),
+ mapping);
+ }
+
+ private ListMultimap<Symbol, Symbol> rewriteOutputToInputsMap(
+ ListMultimap<Symbol, Symbol> oldMapping,
+ SymbolMapper outputMapper,
+ List<SymbolMapper> inputMappers) {
+ ImmutableListMultimap.Builder<Symbol, Symbol> newMappingBuilder =
+ ImmutableListMultimap.builder();
+ Set<Symbol> addedSymbols = new HashSet<>();
+ for (Map.Entry<Symbol, Collection<Symbol>> entry :
oldMapping.asMap().entrySet()) {
+ Symbol rewrittenOutput = outputMapper.map(entry.getKey());
+ if (addedSymbols.add(rewrittenOutput)) {
+ List<Symbol> inputs = ImmutableList.copyOf(entry.getValue());
+ ImmutableList.Builder<Symbol> rewrittenInputs =
ImmutableList.builder();
+ for (int i = 0; i < inputs.size(); i++) {
+ rewrittenInputs.add(inputMappers.get(i).map(inputs.get(i)));
+ }
+ newMappingBuilder.putAll(rewrittenOutput, rewrittenInputs.build());
+ }
+ }
+ return newMappingBuilder.build();
+ }
}
private static class UnaliasContext {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/type/CompatibleResolver.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/type/CompatibleResolver.java
new file mode 100644
index 00000000000..de7ac3327fb
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/type/CompatibleResolver.java
@@ -0,0 +1,108 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.type;
+
+import org.apache.tsfile.read.common.type.Type;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.apache.tsfile.read.common.type.BinaryType.TEXT;
+import static org.apache.tsfile.read.common.type.BlobType.BLOB;
+import static org.apache.tsfile.read.common.type.BooleanType.BOOLEAN;
+import static org.apache.tsfile.read.common.type.DateType.DATE;
+import static org.apache.tsfile.read.common.type.DoubleType.DOUBLE;
+import static org.apache.tsfile.read.common.type.FloatType.FLOAT;
+import static org.apache.tsfile.read.common.type.IntType.INT32;
+import static org.apache.tsfile.read.common.type.LongType.INT64;
+import static org.apache.tsfile.read.common.type.StringType.STRING;
+import static org.apache.tsfile.read.common.type.TimestampType.TIMESTAMP;
+import static org.apache.tsfile.read.common.type.UnknownType.UNKNOWN;
+
+public class CompatibleResolver {
+
+ private static final Map<Type, Map<Type, Type>> CONDITION_MAP = new
HashMap<>();
+
+ static {
+ addCondition(INT32, INT32, INT32);
+ addCondition(INT32, INT64, INT64);
+ addCondition(INT32, FLOAT, FLOAT);
+ addCondition(INT32, DOUBLE, DOUBLE);
+ addCondition(INT32, UNKNOWN, INT32);
+
+ addCondition(INT64, INT32, INT64);
+ addCondition(INT64, INT64, INT64);
+ addCondition(INT64, FLOAT, FLOAT);
+ addCondition(INT64, DOUBLE, DOUBLE);
+ addCondition(INT64, TIMESTAMP, TIMESTAMP);
+ addCondition(INT64, UNKNOWN, INT64);
+
+ addCondition(FLOAT, INT32, FLOAT);
+ addCondition(FLOAT, INT64, FLOAT);
+ addCondition(FLOAT, FLOAT, FLOAT);
+ addCondition(FLOAT, DOUBLE, DOUBLE);
+ addCondition(FLOAT, UNKNOWN, FLOAT);
+
+ addCondition(DOUBLE, INT32, DOUBLE);
+ addCondition(DOUBLE, INT64, DOUBLE);
+ addCondition(DOUBLE, FLOAT, DOUBLE);
+ addCondition(DOUBLE, DOUBLE, DOUBLE);
+ addCondition(DOUBLE, UNKNOWN, DOUBLE);
+
+ addCondition(DATE, DATE, DATE);
+ addCondition(DATE, UNKNOWN, DATE);
+
+ addCondition(TIMESTAMP, TIMESTAMP, TIMESTAMP);
+ addCondition(TIMESTAMP, INT64, TIMESTAMP);
+ addCondition(TIMESTAMP, UNKNOWN, TIMESTAMP);
+
+ addCondition(BOOLEAN, BOOLEAN, BOOLEAN);
+ addCondition(BOOLEAN, UNKNOWN, BOOLEAN);
+
+ addCondition(TEXT, TEXT, TEXT);
+ addCondition(TEXT, STRING, STRING);
+ addCondition(TEXT, UNKNOWN, TEXT);
+
+ addCondition(STRING, STRING, STRING);
+ addCondition(STRING, TEXT, STRING);
+ addCondition(STRING, UNKNOWN, STRING);
+
+ addCondition(BLOB, BLOB, BLOB);
+ addCondition(BLOB, UNKNOWN, BLOB);
+
+ addCondition(UNKNOWN, INT32, INT32);
+ addCondition(UNKNOWN, INT64, INT64);
+ addCondition(UNKNOWN, FLOAT, FLOAT);
+ addCondition(UNKNOWN, DOUBLE, DOUBLE);
+ addCondition(UNKNOWN, DATE, DATE);
+ addCondition(UNKNOWN, TIMESTAMP, TIMESTAMP);
+ }
+
+ private static void addCondition(Type condition1, Type condition2, Type
result) {
+ CONDITION_MAP.computeIfAbsent(condition1, k -> new
HashMap<>()).put(condition2, result);
+ }
+
+ public static Optional<Type> getCommonSuperType(Type type1, Type type2) {
+ return Optional.ofNullable(
+ CONDITION_MAP.getOrDefault(type1,
Collections.emptyMap()).getOrDefault(type2, null));
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/UnionTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/UnionTest.java
new file mode 100644
index 00000000000..f0fae2654b7
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/UnionTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.analyzer;
+
+import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+
+import org.junit.Test;
+
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanAssert.assertPlan;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.aggregation;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.output;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.project;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.tableScan;
+import static
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern.union;
+
+public class UnionTest {
+ @Test
+ public void simpleTest() {
+ PlanTester planTester = new PlanTester();
+
+ assertPlan(
+ planTester.createPlan("(select * from table2) union all (select * from
table3)"),
+ output(union(tableScan("testdb.table2"), tableScan("testdb.table3"))));
+
+ // use Aggregation to process distinct
+ assertPlan(
+ planTester.createPlan("(select * from table2) union (select * from
table3)"),
+ output(aggregation(union(tableScan("testdb.table2"),
tableScan("testdb.table3")))));
+
+ // use CAST if types of according columns is not compatible
+ // s1 is INT64, s3 is DOUBLE
+ assertPlan(
+ planTester.createPlan("(select s1, s3 from table2) union (select s1,
s1 from table3)"),
+ output(
+ aggregation(union(tableScan("testdb.table2"),
project(tableScan("testdb.table3"))))));
+ }
+
+ @Test
+ public void optimizerTest() {
+ PlanTester planTester = new PlanTester();
+
+ // The predicate will be push down into TableScanNode
+ assertPlan(
+ planTester.createPlan(
+ "select * from ((select * from table2) union all (select * from
table3)) where s1 > 1"),
+ output(union(tableScan("testdb.table2"), tableScan("testdb.table3"))));
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
index d7ca50b1404..9146f46eeca 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/assertions/PlanMatchPattern.java
@@ -53,6 +53,7 @@ import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TopKNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeAlignedDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeDeviceViewScanNode;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.TreeNonAlignedDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.UnionNode;
import org.apache.iotdb.db.queryengine.plan.relational.planner.node.WindowNode;
import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ComparisonExpression;
import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DataType;
@@ -768,6 +769,10 @@ public final class PlanMatchPattern {
return node(ExchangeNode.class).with(new ExchangeNodeMatcher());
}
+ public static PlanMatchPattern union(PlanMatchPattern... sources) {
+ return node(UnionNode.class, sources);
+ }
+
public static PlanMatchPattern enforceSingleRow(PlanMatchPattern source) {
return node(EnforceSingleRowNode.class, source);
}