This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.0 by this push:
new 2f19e95bc08 branch-4.0: [feat](load) support merge into #57044 #58091
(#58219)
2f19e95bc08 is described below
commit 2f19e95bc0824bfad5df018a1adb1bb9ca4a724c
Author: morrySnow <[email protected]>
AuthorDate: Sat Nov 22 07:19:31 2025 +0800
branch-4.0: [feat](load) support merge into #57044 #58091 (#58219)
picked from #57044 #58091
---
.../antlr4/org/apache/doris/nereids/DorisLexer.g4 | 1 +
.../antlr4/org/apache/doris/nereids/DorisParser.g4 | 15 +
.../java/org/apache/doris/analysis/StmtType.java | 1 +
.../main/java/org/apache/doris/catalog/Column.java | 4 +
.../java/org/apache/doris/catalog/OlapTable.java | 4 +
.../doris/nereids/parser/LogicalPlanBuilder.java | 51 +-
.../doris/nereids/rules/analysis/BindSink.java | 4 +-
.../apache/doris/nereids/trees/plans/PlanType.java | 1 +
.../trees/plans/commands/UpdateCommand.java | 22 +-
.../plans/commands/merge/MergeIntoCommand.java | 530 ++++++++++++
.../plans/commands/merge/MergeMatchedClause.java | 55 ++
.../commands/merge/MergeNotMatchedClause.java | 55 ++
.../trees/plans/visitor/CommandVisitor.java | 5 +
.../doris/nereids/parser/NereidsParserTest.java | 94 ++
.../plans/commands/merge/MergeIntoCommandTest.java | 959 +++++++++++++++++++++
.../data/load_p0/merge_into/test_merge_into.out | 21 +
.../load_p0/merge_into/test_merge_into.groovy | 254 ++++++
17 files changed, 2066 insertions(+), 10 deletions(-)
diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
index 521ef91040b..ca33b65e552 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
@@ -340,6 +340,7 @@ LOW_PRIORITY: 'LOW_PRIORITY';
MANUAL: 'MANUAL';
MAP: 'MAP';
MATCH: 'MATCH';
+MATCHED: 'MATCHED';
MATCH_ALL: 'MATCH_ALL';
MATCH_ANY: 'MATCH_ANY';
MATCH_NAME: 'MATCH_NAME';
diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index 38fe0e9247b..2113b806c04 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -145,6 +145,10 @@ supportedDmlStatement
partitionSpec? tableAlias
(USING relations)?
whereClause? #delete
+ | explain? cte? MERGE INTO targetTable=multipartIdentifier
+ (AS? identifier)? USING srcRelation=relationPrimary
+ ON expression
+ (mergeMatchedClause | mergeNotMatchedClause)+
#mergeInto
| LOAD LABEL lableName=multipartIdentifier
LEFT_PAREN dataDescs+=dataDesc (COMMA dataDescs+=dataDesc)* RIGHT_PAREN
(withRemoteStorageSystem)?
@@ -164,6 +168,16 @@ supportedDmlStatement
| TRUNCATE TABLE multipartIdentifier specifiedPartition? FORCE?
#truncateTable
;
+mergeMatchedClause
+ : WHEN MATCHED (AND casePredicate=expression)? THEN
+ (UPDATE SET updateAssignmentSeq | DELETE)
+ ;
+
+mergeNotMatchedClause
+ : WHEN NOT MATCHED (AND casePredicate=expression)? THEN
+ INSERT cols=identifierList? VALUES rowConstructor
+ ;
+
supportedCreateStatement
: CREATE (EXTERNAL | TEMPORARY)? TABLE (IF NOT EXISTS)?
name=multipartIdentifier
((ctasCols=identifierList)? | (LEFT_PAREN columnDefs (COMMA
indexDefs)? COMMA? RIGHT_PAREN))
@@ -2057,6 +2071,7 @@ nonReserved
| LOGICAL
| MANUAL
| MAP
+ | MATCHED
| MATCH_ALL
| MATCH_ANY
| MATCH_PHRASE
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/StmtType.java
b/fe/fe-core/src/main/java/org/apache/doris/analysis/StmtType.java
index b929f198c77..af9537cf8c6 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/StmtType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/StmtType.java
@@ -38,6 +38,7 @@ public enum StmtType {
INSTALL,
KILL,
LOAD,
+ MERGE_INTO,
OTHER,
OPTIMIZE,
PAUSE,
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
index ac2d2fd53cd..6bd014aec59 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Column.java
@@ -1288,6 +1288,10 @@ public class Column implements GsonPostProcessable {
return colName;
}
+ public boolean isGeneratedColumn() {
+ return generatedColumnInfo != null;
+ }
+
public GeneratedColumnInfo getGeneratedColumnInfo() {
return generatedColumnInfo;
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
index bda8f0f8122..f3fb4914ff5 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/OlapTable.java
@@ -1691,6 +1691,10 @@ public class OlapTable extends Table implements
MTMVRelatedTableIf, GsonPostProc
return getBaseSchema().stream().anyMatch(column ->
!column.isVisible());
}
+ public boolean hasGeneratedColumn() {
+ return getBaseSchema().stream().anyMatch(Column::isGeneratedColumn);
+ }
+
public Type getSequenceType() {
if (getSequenceCol() == null) {
return null;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index c43718126c6..2c7ac74c0ba 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -256,6 +256,9 @@ import
org.apache.doris.nereids.DorisParser.LockTablesContext;
import org.apache.doris.nereids.DorisParser.LogicalBinaryContext;
import org.apache.doris.nereids.DorisParser.LogicalNotContext;
import org.apache.doris.nereids.DorisParser.MapLiteralContext;
+import org.apache.doris.nereids.DorisParser.MergeIntoContext;
+import org.apache.doris.nereids.DorisParser.MergeMatchedClauseContext;
+import org.apache.doris.nereids.DorisParser.MergeNotMatchedClauseContext;
import org.apache.doris.nereids.DorisParser.ModifyColumnClauseContext;
import org.apache.doris.nereids.DorisParser.ModifyColumnCommentClauseContext;
import org.apache.doris.nereids.DorisParser.ModifyDistributionClauseContext;
@@ -1000,6 +1003,9 @@ import
org.apache.doris.nereids.trees.plans.commands.load.PauseRoutineLoadComman
import
org.apache.doris.nereids.trees.plans.commands.load.ResumeRoutineLoadCommand;
import
org.apache.doris.nereids.trees.plans.commands.load.ShowCreateRoutineLoadCommand;
import
org.apache.doris.nereids.trees.plans.commands.load.StopRoutineLoadCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeMatchedClause;
+import
org.apache.doris.nereids.trees.plans.commands.merge.MergeNotMatchedClause;
import
org.apache.doris.nereids.trees.plans.commands.refresh.RefreshCatalogCommand;
import
org.apache.doris.nereids.trees.plans.commands.refresh.RefreshDatabaseCommand;
import
org.apache.doris.nereids.trees.plans.commands.refresh.RefreshDictionaryCommand;
@@ -1372,6 +1378,49 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
return withExplain(command, ctx.explain());
}
+ @Override
+ public Object visitMergeInto(MergeIntoContext ctx) {
+ return ParserUtils.withOrigin(ctx, () -> {
+ List<String> targetNameParts =
visitMultipartIdentifier(ctx.targetTable);
+ Optional<String> targetAlias = Optional.ofNullable(
+ ctx.identifier() != null ? ctx.identifier().getText() :
null);
+ LogicalPlan source = plan(ctx.relationPrimary());
+ Expression onClause = typedVisit(ctx.expression());
+ List<MergeMatchedClause> matchedClauses =
visit(ctx.mergeMatchedClause(), MergeMatchedClause.class);
+ List<MergeNotMatchedClause> notMatchedClauses =
visit(ctx.mergeNotMatchedClause(),
+ MergeNotMatchedClause.class);
+ Optional<LogicalPlan> cte = Optional.empty();
+ if (ctx.cte() != null) {
+ cte = Optional.ofNullable(withCte(source, ctx.cte()));
+ }
+ return withExplain(new MergeIntoCommand(targetNameParts,
targetAlias, cte,
+ source, onClause, matchedClauses, notMatchedClauses),
ctx.explain());
+ });
+ }
+
+ @Override
+ public MergeMatchedClause
visitMergeMatchedClause(MergeMatchedClauseContext ctx) {
+ return ParserUtils.withOrigin(ctx, () -> {
+ Optional<Expression> casePredicate = Optional.ofNullable(
+ ctx.casePredicate != null ? typedVisit(ctx.casePredicate)
: null);
+ boolean isDelete = ctx.DELETE() != null;
+ List<EqualTo> updateAssignments = isDelete ? ImmutableList.of() :
+ visitUpdateAssignmentSeq(ctx.updateAssignmentSeq());
+ return new MergeMatchedClause(casePredicate, updateAssignments,
isDelete);
+ });
+ }
+
+ @Override
+ public MergeNotMatchedClause
visitMergeNotMatchedClause(MergeNotMatchedClauseContext ctx) {
+ return ParserUtils.withOrigin(ctx, () -> {
+ Optional<Expression> casePredicate = Optional.ofNullable(
+ ctx.casePredicate != null ? typedVisit(ctx.casePredicate)
: null);
+ List<String> cols = ctx.cols != null ?
visitIdentifierList(ctx.cols) : ImmutableList.of();
+ List<NamedExpression> row =
visitRowConstructor(ctx.rowConstructor());
+ return new MergeNotMatchedClause(casePredicate, cols, row);
+ });
+ }
+
/**
* return a pair, first will be true if partitions is temp partition,
select is a list to present partition list.
*/
@@ -2445,7 +2494,7 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
}
@Override
- public LogicalPlan visitInlineTable(InlineTableContext ctx) {
+ public UnboundInlineTable visitInlineTable(InlineTableContext ctx) {
List<RowConstructorContext> rowConstructorContexts =
ctx.rowConstructor();
ImmutableList.Builder<List<NamedExpression>> rows
=
ImmutableList.builderWithExpectedSize(rowConstructorContexts.size());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
index 582b048ceb3..bb75f846d51 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java
@@ -354,7 +354,7 @@ public class BindSink implements AnalysisRuleFactory {
List<Column> shadowColumns = Lists.newArrayList();
// generate slots not mentioned in sql, mv slots and shaded slots.
for (Column column : boundSink.getTargetTable().getFullSchema()) {
- if (column.getGeneratedColumnInfo() != null) {
+ if (column.isGeneratedColumn()) {
generatedColumns.add(column);
continue;
} else if (column.isMaterializedViewColumn()) {
@@ -814,7 +814,7 @@ public class BindSink implements AnalysisRuleFactory {
++extraColumnsNum;
processedColsName.add(col.getName());
}
- } else if (col.getGeneratedColumnInfo() != null) {
+ } else if (col.isGeneratedColumn()) {
++extraColumnsNum;
processedColsName.add(col.getName());
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
index f50bd047f3a..f2e88c1df7d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
@@ -147,6 +147,7 @@ public enum PlanType {
DROP_DICTIONARY_COMMAND,
CREATE_SQL_BLOCK_RULE_COMMAND,
DELETE_COMMAND,
+ MERGE_INTO_COMMAND,
EXPLAIN_COMMAND,
EXPLAIN_DICTIONARY_COMMAND,
EXPORT_COMMAND,
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
index c439af0625b..133122ad91c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
@@ -111,10 +111,10 @@ public class UpdateCommand extends Command implements
ForwardWithSync, Explainab
Map<String, Expression> colNameToExpression =
Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
Map<String, Expression> partialUpdateColNameToExpression =
Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
for (EqualTo equalTo : assignments) {
- List<String> nameParts = ((UnboundSlot)
equalTo.left()).getNameParts();
- checkAssignmentColumn(ctx, nameParts);
- colNameToExpression.put(nameParts.get(nameParts.size() - 1),
equalTo.right());
-
partialUpdateColNameToExpression.put(nameParts.get(nameParts.size() - 1),
equalTo.right());
+ List<String> colNameParts = ((UnboundSlot)
equalTo.left()).getNameParts();
+ checkAssignmentColumn(ctx, colNameParts, this.nameParts,
this.tableAlias);
+ colNameToExpression.put(colNameParts.get(colNameParts.size() - 1),
equalTo.right());
+
partialUpdateColNameToExpression.put(colNameParts.get(colNameParts.size() - 1),
equalTo.right());
}
// check if any key in update clause
if (targetTable.getFullSchema().stream().filter(Column::isKey)
@@ -198,7 +198,15 @@ public class UpdateCommand extends Command implements
ForwardWithSync, Explainab
DMLCommandType.UPDATE, logicalQuery);
}
- private void checkAssignmentColumn(ConnectContext ctx, List<String>
columnNameParts) {
+ /**
+ * check assignment column valid or not.
+ * @param ctx connect context
+ * @param columnNameParts qualified column name
+ * @param tableNameParts qualified target table name
+ * @param tableAlias target table alias
+ */
+ public static void checkAssignmentColumn(ConnectContext ctx, List<String>
columnNameParts,
+ List<String> tableNameParts, String tableAlias) {
if (columnNameParts.size() <= 1) {
return;
}
@@ -212,10 +220,10 @@ public class UpdateCommand extends Command implements
ForwardWithSync, Explainab
} else {
throw new AnalysisException("column in assignment list is invalid,
" + String.join(".", columnNameParts));
}
- if (dbName != null && this.tableAlias != null) {
+ if (dbName != null && tableAlias != null) {
throw new AnalysisException("column in assignment list is invalid,
" + String.join(".", columnNameParts));
}
- List<String> tableQualifier = RelationUtil.getQualifierName(ctx,
nameParts);
+ List<String> tableQualifier = RelationUtil.getQualifierName(ctx,
tableNameParts);
if (!ExpressionAnalyzer.sameTableName(tableAlias == null ?
tableQualifier.get(2) : tableAlias, tableName)
|| (dbName != null
&&
!ExpressionAnalyzer.compareDbNameIgnoreClusterName(tableQualifier.get(1),
dbName))) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
new file mode 100644
index 00000000000..b9bcbbc390c
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
@@ -0,0 +1,530 @@
+// 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.trees.plans.commands.merge;
+
+import org.apache.doris.analysis.ColumnDef.DefaultValue;
+import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.analyzer.UnboundAlias;
+import org.apache.doris.nereids.analyzer.UnboundRelation;
+import org.apache.doris.nereids.analyzer.UnboundSlot;
+import org.apache.doris.nereids.analyzer.UnboundStar;
+import org.apache.doris.nereids.analyzer.UnboundTableSinkCreator;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.parser.LogicalPlanBuilderAssistant;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.rules.exploration.join.JoinReorderContext;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Not;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.If;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Now;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
+import org.apache.doris.nereids.trees.plans.Explainable;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.commands.Command;
+import org.apache.doris.nereids.trees.plans.commands.ForwardWithSync;
+import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.DMLCommandType;
+import
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.util.RelationUtil;
+import org.apache.doris.nereids.util.Utils;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.thrift.TPartialUpdateNewRowPolicy;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * merge into table
+ */
+public class MergeIntoCommand extends Command implements ForwardWithSync,
Explainable {
+ private static final String BRANCH_LABEL =
"__DORIS_MERGE_INTO_BRANCH_LABEL__";
+
+ private final List<String> targetNameParts;
+ private final Optional<String> targetAlias;
+ private final List<String> targetNameInPlan;
+ private final Optional<LogicalPlan> cte;
+ private final LogicalPlan source;
+ private final Expression onClause;
+ private final List<MergeMatchedClause> matchedClauses;
+ private final List<MergeNotMatchedClause> notMatchedClauses;
+
+ /**
+ * constructor.
+ */
+ public MergeIntoCommand(List<String> targetNameParts, Optional<String>
targetAlias,
+ Optional<LogicalPlan> cte, LogicalPlan source,
+ Expression onClause, List<MergeMatchedClause> matchedClauses,
+ List<MergeNotMatchedClause> notMatchedClauses) {
+ super(PlanType.MERGE_INTO_COMMAND);
+ this.targetNameParts = Utils.fastToImmutableList(
+ Objects.requireNonNull(targetNameParts, "targetNameParts
should not be null"));
+ this.targetAlias = Objects.requireNonNull(targetAlias, "targetAlias
should not be null");
+ if (targetAlias.isPresent()) {
+ this.targetNameInPlan = ImmutableList.of(targetAlias.get());
+ } else {
+ this.targetNameInPlan = ImmutableList.copyOf(targetNameParts);
+ }
+ this.cte = Objects.requireNonNull(cte, "cte should not be null");
+ this.source = Objects.requireNonNull(source, "source should not be
null");
+ this.onClause = Objects.requireNonNull(onClause, "onClause should not
be null");
+ this.matchedClauses = Utils.fastToImmutableList(
+ Objects.requireNonNull(matchedClauses, "matchedClauses should
not be null"));
+ this.notMatchedClauses = Utils.fastToImmutableList(
+ Objects.requireNonNull(notMatchedClauses, "notMatchedClauses
should not be null"));
+ }
+
+ @Override
+ public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ new InsertIntoTableCommand(completeQueryPlan(ctx), Optional.empty(),
Optional.empty(),
+ Optional.empty(), true, Optional.empty()).run(ctx, executor);
+ }
+
+ @Override
+ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+ return visitor.visitMergeIntoCommand(this, context);
+ }
+
+ @Override
+ public Plan getExplainPlan(ConnectContext ctx) {
+ return completeQueryPlan(ctx);
+ }
+
+ private OlapTable getTargetTable(ConnectContext ctx) {
+ List<String> qualifiedTableName = RelationUtil.getQualifierName(ctx,
targetNameParts);
+ TableIf table = RelationUtil.getTable(qualifiedTableName,
ctx.getEnv(), Optional.empty());
+ if (!(table instanceof OlapTable) || !((OlapTable)
table).getEnableUniqueKeyMergeOnWrite()) {
+ throw new AnalysisException("merge into command only support MOW
unique key olapTable");
+ }
+ return ((OlapTable) table);
+ }
+
+ @Override
+ public StmtType stmtType() {
+ return StmtType.MERGE_INTO;
+ }
+
+ /**
+ * generate target right outer join source.
+ */
+ private LogicalPlan generateBasePlan() {
+ LogicalPlan plan = LogicalPlanBuilderAssistant.withCheckPolicy(
+ new UnboundRelation(
+ StatementScopeIdGenerator.newRelationId(),
+ targetNameParts
+ )
+ );
+ if (targetAlias.isPresent()) {
+ plan = new LogicalSubQueryAlias<>(targetAlias.get(), plan);
+ }
+ return new LogicalJoin<>(JoinType.LEFT_OUTER_JOIN,
+ ImmutableList.of(), ImmutableList.of(onClause),
+ source, plan, JoinReorderContext.EMPTY);
+ }
+
+ /**
+ * generate a branch number column to indicate this row matched witch
branch
+ */
+ private NamedExpression generateBranchLabel(NamedExpression deleteSign) {
+ Expression matchedLabel = new NullLiteral(IntegerType.INSTANCE);
+ for (int i = matchedClauses.size() - 1; i >= 0; i--) {
+ MergeMatchedClause clause = matchedClauses.get(i);
+ if (i != matchedClauses.size() - 1 &&
!clause.getCasePredicate().isPresent()) {
+ throw new AnalysisException("Only the last matched clause
could without case predicate.");
+ }
+ Expression currentResult = new IntegerLiteral(i);
+ if (clause.getCasePredicate().isPresent()) {
+ matchedLabel = new If(clause.getCasePredicate().get(),
currentResult, matchedLabel);
+ } else {
+ matchedLabel = currentResult;
+ }
+ }
+ Expression notMatchedLabel = new NullLiteral(IntegerType.INSTANCE);
+ for (int i = notMatchedClauses.size() - 1; i >= 0; i--) {
+ MergeNotMatchedClause clause = notMatchedClauses.get(i);
+ if (i != notMatchedClauses.size() - 1 &&
!clause.getCasePredicate().isPresent()) {
+ throw new AnalysisException("Only the last not matched clause
could without case predicate.");
+ }
+ Expression currentResult = new IntegerLiteral(i +
matchedClauses.size());
+ if (clause.getCasePredicate().isPresent()) {
+ notMatchedLabel = new If(clause.getCasePredicate().get(),
currentResult, notMatchedLabel);
+ } else {
+ notMatchedLabel = currentResult;
+ }
+ }
+ return new UnboundAlias(new If(new Not(new IsNull(deleteSign)),
+ matchedLabel, notMatchedLabel), BRANCH_LABEL);
+ }
+
+ private List<Expression> generateDeleteProjection(List<Column> columns) {
+ ImmutableList.Builder<Expression> builder = ImmutableList.builder();
+ for (Column column : columns) {
+ // delete
+ if (column.isDeleteSignColumn()) {
+ builder.add(new TinyIntLiteral(((byte) 1)));
+ } else if ((!column.isVisible() && !column.isSequenceColumn()) ||
column.isGeneratedColumn()) {
+ // skip this column
+ continue;
+ } else {
+ List<String> nameParts = Lists.newArrayList(targetNameInPlan);
+ nameParts.add(column.getName());
+ builder.add(new UnboundSlot(nameParts));
+ }
+ }
+ return builder.build();
+ }
+
+ private List<Expression> generateUpdateProjection(MergeMatchedClause
clause,
+ List<Column> columns, OlapTable targetTable, ConnectContext ctx) {
+ ImmutableList.Builder<Expression> builder = ImmutableList.builder();
+ Map<String, Expression> colNameToExpression =
Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+ // update
+ for (EqualTo equalTo : clause.getAssignments()) {
+ List<String> nameParts = ((UnboundSlot)
equalTo.left()).getNameParts();
+ UpdateCommand.checkAssignmentColumn(ctx, nameParts,
targetNameParts, targetAlias.orElse(null));
+ if (colNameToExpression.put(nameParts.get(nameParts.size() - 1),
equalTo.right()) != null) {
+ throw new AnalysisException("Duplicate column name in update:
" + nameParts.get(nameParts.size() - 1));
+ }
+ }
+ for (Column column : columns) {
+ DataType dataType = DataType.fromCatalogType(column.getType());
+ if (colNameToExpression.containsKey(column.getName())) {
+ if (column.isKey()) {
+ throw new AnalysisException("Only value columns of unique
table could be updated");
+ }
+ if (column.isGeneratedColumn()) {
+ throw new AnalysisException("The value specified for
generated column '"
+ + column.getName() + "' in table '" +
targetTable.getName() + "' is not allowed.");
+ }
+ builder.add(new
Cast(colNameToExpression.get(column.getName()), dataType));
+ colNameToExpression.remove(column.getName());
+ } else if (column.isGeneratedColumn() || (!column.isVisible()
+ && !column.isDeleteSignColumn() &&
!column.isSequenceColumn())) {
+ // skip these columns
+ continue;
+ } else if (column.hasOnUpdateDefaultValue()) {
+ builder.add(new Cast(new NereidsParser().parseExpression(
+ column.getOnUpdateDefaultValueSql()), dataType));
+ } else {
+ List<String> nameParts = Lists.newArrayList(targetNameInPlan);
+ nameParts.add(column.getName());
+ builder.add(new Cast(new UnboundSlot(nameParts), dataType));
+ }
+ }
+ if (!colNameToExpression.isEmpty()) {
+ throw new AnalysisException("unknown column in assignment list: "
+ + String.join(", ", colNameToExpression.keySet()));
+ }
+ return builder.build();
+ }
+
+ private List<Expression>
generateInsertWithoutColListProjection(MergeNotMatchedClause clause,
+ List<Column> columns, OlapTable targetTable, boolean
hasSequenceCol, int seqColumnIndex,
+ Optional<Column> seqMappingColInTable, Optional<Type> seqColType) {
+ ImmutableList.Builder<Expression> builder = ImmutableList.builder();
+ if (hasSequenceCol && seqColumnIndex < 0) {
+ if ((!seqMappingColInTable.isPresent() ||
seqMappingColInTable.get().getDefaultValue() == null
+ || !seqMappingColInTable.get().getDefaultValue()
+ .equalsIgnoreCase(DefaultValue.CURRENT_TIMESTAMP))) {
+ throw new AnalysisException("Table " + targetTable.getName()
+ + " has sequence column, need to specify the sequence
column");
+ }
+ }
+ Expression sqlColExpr = new Now();
+ for (int i = 0; i < clause.getRow().size(); i++) {
+ DataType columnType =
DataType.fromCatalogType(columns.get(i).getType());
+ NamedExpression rowItem = clause.getRow().get(i);
+ Expression value;
+ if (rowItem instanceof Alias || rowItem instanceof UnboundAlias) {
+ value = rowItem.child(0);
+ } else {
+ value = rowItem;
+ }
+ if (columns.get(i).isGeneratedColumn()) {
+ if (!(value instanceof DefaultValueSlot)) {
+ throw new AnalysisException("The value specified for
generated column '"
+ + columns.get(i).getName()
+ + "' in table '" + targetTable.getName() + "' is
not allowed.");
+ }
+ continue;
+ }
+ value = new Cast(value, columnType);
+ if (i == seqColumnIndex) {
+ sqlColExpr = value;
+ }
+ builder.add(value);
+ }
+ // delete sign
+ builder.add(new TinyIntLiteral(((byte) 0)));
+ // sequence column
+ if (hasSequenceCol) {
+ builder.add(new Cast(sqlColExpr,
seqColType.map(DataType::fromCatalogType).get()));
+ }
+ return builder.build();
+ }
+
+ private List<Expression>
generateInsertWithColListProjection(MergeNotMatchedClause clause,
+ List<Column> columns, OlapTable targetTable, boolean
hasSequenceCol,
+ String seqColumnName, Optional<Column> seqMappingColInTable,
Optional<Type> seqColType) {
+ ImmutableList.Builder<Expression> builder = ImmutableList.builder();
+ Map<String, Expression> colNameToExpression =
Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0; i < clause.getColNames().size(); i++) {
+ String targetColumnName = clause.getColNames().get(i);
+ NamedExpression rowItem = clause.getRow().get(i);
+ if (rowItem instanceof Alias || rowItem instanceof UnboundAlias) {
+ colNameToExpression.put(targetColumnName, rowItem.child(0));
+ } else {
+ colNameToExpression.put(targetColumnName, rowItem);
+ }
+ }
+ if (colNameToExpression.size() != clause.getColNames().size()) {
+ throw new AnalysisException("insert has duplicate column names");
+ }
+ if (hasSequenceCol) {
+ if (seqColumnName == null || seqColumnName.isEmpty()) {
+ seqColumnName = Column.SEQUENCE_COL;
+ }
+ if (!colNameToExpression.containsKey(seqColumnName)
+ && (!seqMappingColInTable.isPresent() ||
seqMappingColInTable.get().getDefaultValue() == null
+ || !seqMappingColInTable.get().getDefaultValue()
+ .equalsIgnoreCase(DefaultValue.CURRENT_TIMESTAMP))) {
+ throw new AnalysisException("Table " + targetTable.getName()
+ + " has sequence column, need to specify the sequence
column");
+ }
+ }
+ for (Column column : columns) {
+ DataType type = DataType.fromCatalogType(column.getType());
+ if (column.isGeneratedColumn()) {
+ if (colNameToExpression.containsKey(column.getName())) {
+ if (!(colNameToExpression.get(column.getName()) instanceof
DefaultValueSlot)) {
+ throw new AnalysisException("The value specified for
generated column '"
+ + column.getName() + "' in table '" +
targetTable.getName() + "' is not allowed.");
+ }
+ colNameToExpression.remove(column.getName());
+ }
+ continue;
+ } else if (!column.isVisible()) {
+ // skip these columns
+ continue;
+ } else if (colNameToExpression.containsKey(column.getName())) {
+ builder.add(new
Cast(colNameToExpression.get(column.getName()), type));
+ if (!column.getName().equalsIgnoreCase(seqColumnName)) {
+ colNameToExpression.remove(column.getName());
+ }
+ } else {
+ if (column.getDefaultValueSql() == null) {
+ if (!column.isAllowNull() && !column.isAutoInc()) {
+ throw new AnalysisException("Column has no default
value,"
+ + " column=" + column.getName());
+ }
+ builder.add(new NullLiteral(type));
+ } else {
+ Expression defaultExpr;
+ try {
+ // it comes from the original planner, if default
value expression is
+ // null, we use the literal string of the default
value, or it may be
+ // default value function, like CURRENT_TIMESTAMP.
+ Expression unboundDefaultValue = new
NereidsParser().parseExpression(
+ column.getDefaultValueSql());
+ if (unboundDefaultValue instanceof UnboundAlias) {
+ unboundDefaultValue = ((UnboundAlias)
unboundDefaultValue).child();
+ }
+ defaultExpr = new Cast(unboundDefaultValue, type);
+ } catch (Exception e) {
+ throw new AnalysisException(e.getMessage(),
e.getCause());
+ }
+ builder.add(defaultExpr);
+ }
+ }
+ }
+ builder.add(colNameToExpression.getOrDefault(Column.DELETE_SIGN, new
TinyIntLiteral(((byte) 0))));
+ colNameToExpression.remove(Column.DELETE_SIGN);
+ if (hasSequenceCol) {
+ Expression forSeqCol;
+ if (colNameToExpression.containsKey(Column.SEQUENCE_COL)) {
+ forSeqCol = colNameToExpression.get(Column.SEQUENCE_COL);
+ colNameToExpression.remove(Column.SEQUENCE_COL);
+ colNameToExpression.remove(seqColumnName);
+ } else if (colNameToExpression.containsKey(seqColumnName)) {
+ forSeqCol = colNameToExpression.get(seqColumnName);
+ colNameToExpression.remove(seqColumnName);
+ } else {
+ forSeqCol = new Now();
+ }
+ builder.add(new Cast(forSeqCol,
seqColType.map(DataType::fromCatalogType).get()));
+ }
+ if (!colNameToExpression.isEmpty()) {
+ throw new AnalysisException("unknown column in target table: "
+ + String.join(", ", colNameToExpression.keySet()));
+ }
+ return builder.build();
+ }
+
+ private List<NamedExpression> generateFinalProjections(List<String>
colNames,
+ List<List<Expression>> finalProjections) {
+ for (List<Expression> projection : finalProjections) {
+ if (projection.size() != finalProjections.get(0).size()) {
+ throw new AnalysisException("Column count doesn't match each
other");
+ }
+ }
+ ImmutableList.Builder<NamedExpression> outputProjectionsBuilder =
ImmutableList.builder();
+ for (int i = 0; i < finalProjections.get(0).size(); i++) {
+ Expression project = new NullLiteral();
+ for (int j = 0; j < finalProjections.size(); j++) {
+ project = new If(new EqualTo(new UnboundSlot(BRANCH_LABEL),
new IntegerLiteral(j)),
+ finalProjections.get(j).get(i), project);
+ }
+ outputProjectionsBuilder.add(new UnboundAlias(project,
colNames.get(i)));
+ }
+ return outputProjectionsBuilder.build();
+ }
+
+ /**
+ * complete merge into plan.
+ */
+ private LogicalPlan completeQueryPlan(ConnectContext ctx) {
+ // check insert include all keys
+ OlapTable targetTable = getTargetTable(ctx);
+ List<Column> columns = targetTable.getBaseSchema(true);
+ // compute sequence column info
+ boolean hasSequenceCol = targetTable.hasSequenceCol();
+ String seqColName = null;
+ int seqColumnIndex = -1;
+ Optional<Column> seqMappingColInTable = Optional.empty();
+ if (hasSequenceCol) {
+ seqColName = targetTable.getSequenceMapCol();
+ String finalSeqColName = seqColName;
+ if (seqColName != null) {
+ for (int i = 0; i < columns.size(); i++) {
+ Column column = columns.get(i);
+ if (column.getName().equalsIgnoreCase(seqColName)) {
+ seqColumnIndex = i;
+ break;
+ }
+ }
+ seqMappingColInTable = columns.stream()
+ .filter(col ->
col.getName().equalsIgnoreCase(finalSeqColName))
+ .findFirst();
+ }
+
+ }
+ if (seqColumnIndex != -1 && !seqMappingColInTable.isPresent()) {
+ throw new AnalysisException("sequence column is not contained in"
+ + " target table " + targetTable.getName());
+ }
+
+ // generate base plan
+ LogicalPlan plan = generateBasePlan();
+ // generate a project to add delete sign, seq column, label and mark
+ ImmutableList.Builder<NamedExpression> outputProjections =
ImmutableList.builder();
+ outputProjections.add(new UnboundStar(ImmutableList.of()));
+ List<String> targetDeleteSignNameParts =
Lists.newArrayList(targetNameInPlan);
+ targetDeleteSignNameParts.add(Column.DELETE_SIGN);
+ NamedExpression deleteSign = new
UnboundSlot(targetDeleteSignNameParts);
+ outputProjections.add(deleteSign);
+ outputProjections.add(generateBranchLabel(deleteSign));
+ if (hasSequenceCol) {
+ List<String> targetSeqColNameParts =
Lists.newArrayList(targetNameInPlan);
+ targetSeqColNameParts.add(Column.SEQUENCE_COL);
+ NamedExpression seqCol = new UnboundSlot(targetSeqColNameParts);
+ outputProjections.add(seqCol);
+ }
+ plan = new LogicalProject<>(outputProjections.build(), plan);
+ // remove all lines that do not be used for update, delete and insert
+ plan = new LogicalFilter<>(ImmutableSet.of(new Not(new IsNull(new
UnboundSlot(BRANCH_LABEL)))), plan);
+ // compute final project by branch number and add delete sign
+ List<List<Expression>> finalProjections = Lists.newArrayList();
+ // matched
+ for (MergeMatchedClause clause : matchedClauses) {
+ if (clause.isDelete()) {
+ finalProjections.add(generateDeleteProjection(columns));
+ } else {
+ finalProjections.add(generateUpdateProjection(clause, columns,
targetTable, ctx));
+ }
+ }
+ // not matched
+ long columnCount = columns.stream().filter(Column::isVisible).count();
+ for (MergeNotMatchedClause clause : notMatchedClauses) {
+ if (clause.getColNames().isEmpty()) {
+ if (columnCount != clause.getRow().size()) {
+ throw new AnalysisException("Column count doesn't match
value count");
+ }
+
finalProjections.add(generateInsertWithoutColListProjection(clause, columns,
targetTable,
+ hasSequenceCol, seqColumnIndex, seqMappingColInTable,
+ Optional.ofNullable(targetTable.getSequenceType())));
+ } else {
+ if (clause.getColNames().size() != clause.getRow().size()) {
+ throw new AnalysisException("Column count doesn't match
value count");
+ }
+
finalProjections.add(generateInsertWithColListProjection(clause, columns,
targetTable,
+ hasSequenceCol, seqColName, seqMappingColInTable,
+ Optional.ofNullable(targetTable.getSequenceType())));
+ }
+ }
+ List<String> colNames = columns.stream()
+ .filter(c -> (c.isVisible() && !c.isGeneratedColumn())
+ || c.isDeleteSignColumn() || c.isSequenceColumn())
+ .map(Column::getName)
+ .collect(ImmutableList.toImmutableList());
+ plan = new LogicalProject<>(generateFinalProjections(colNames,
finalProjections), plan);
+
+ // TODO 6, 7, 8, 9 strict mode
+ // 6. add a set of new columns used for group by: if(mark = 1, target
keys + mark, insert keys + mark)
+ // 7. add window node, partition by group by key, order by 1, row
number, count(update) as uc, max(delete) as dc
+ // 8. get row_number = 1
+ // 9. assert_true(uc <= 1 and (uc = 0 || dc = 0) (optional)
+
+ if (cte.isPresent()) {
+ plan = (LogicalPlan) cte.get().withChildren(plan);
+ }
+ plan = UnboundTableSinkCreator.createUnboundTableSink(targetNameParts,
colNames, ImmutableList.of(),
+ false, ImmutableList.of(), false,
TPartialUpdateNewRowPolicy.APPEND,
+ DMLCommandType.INSERT, plan);
+ return plan;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeMatchedClause.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeMatchedClause.java
new file mode 100644
index 00000000000..e7636dd0c46
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeMatchedClause.java
@@ -0,0 +1,55 @@
+// 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.trees.plans.commands.merge;
+
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.util.Utils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * matched clause for merge into.
+ */
+public class MergeMatchedClause {
+
+ private final Optional<Expression> casePredicate;
+ private final List<EqualTo> assignments;
+ private final boolean isDelete;
+
+ public MergeMatchedClause(Optional<Expression> casePredicate,
List<EqualTo> assignments, boolean isDelete) {
+ this.casePredicate = Objects.requireNonNull(casePredicate,
"casePredicate should not be null");
+ this.assignments = Utils.fastToImmutableList(
+ Objects.requireNonNull(assignments, "assignments should not be
null"));
+ this.isDelete = isDelete;
+ }
+
+ public Optional<Expression> getCasePredicate() {
+ return casePredicate;
+ }
+
+ public List<EqualTo> getAssignments() {
+ return assignments;
+ }
+
+ public boolean isDelete() {
+ return isDelete;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeNotMatchedClause.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeNotMatchedClause.java
new file mode 100644
index 00000000000..4a091b1875d
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeNotMatchedClause.java
@@ -0,0 +1,55 @@
+// 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.trees.plans.commands.merge;
+
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.util.Utils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * not matched clause for merge into.
+ */
+public class MergeNotMatchedClause {
+
+ private final Optional<Expression> casePredicate;
+ private final List<String> colNames;
+ private final List<NamedExpression> row;
+
+ public MergeNotMatchedClause(Optional<Expression> casePredicate,
+ List<String> colNames, List<NamedExpression> row) {
+ this.casePredicate = Objects.requireNonNull(casePredicate,
"casePredicate should not be null");
+ this.colNames =
Utils.fastToImmutableList(Objects.requireNonNull(colNames, "colNames should not
be null"));
+ this.row = Utils.fastToImmutableList(Objects.requireNonNull(row, "row
should not be null"));
+ }
+
+ public Optional<Expression> getCasePredicate() {
+ return casePredicate;
+ }
+
+ public List<String> getColNames() {
+ return colNames;
+ }
+
+ public List<NamedExpression> getRow() {
+ return row;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
index b663110a5ca..374f5fbda7f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
@@ -297,6 +297,7 @@ import
org.apache.doris.nereids.trees.plans.commands.load.PauseRoutineLoadComman
import
org.apache.doris.nereids.trees.plans.commands.load.ResumeRoutineLoadCommand;
import
org.apache.doris.nereids.trees.plans.commands.load.ShowCreateRoutineLoadCommand;
import
org.apache.doris.nereids.trees.plans.commands.load.StopRoutineLoadCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
import
org.apache.doris.nereids.trees.plans.commands.refresh.RefreshCatalogCommand;
import
org.apache.doris.nereids.trees.plans.commands.refresh.RefreshDatabaseCommand;
import
org.apache.doris.nereids.trees.plans.commands.refresh.RefreshDictionaryCommand;
@@ -354,6 +355,10 @@ public interface CommandVisitor<R, C> {
return visitCommand(deleteFromUsingCommand, context);
}
+ default R visitMergeIntoCommand(MergeIntoCommand mergeIntoCommand, C
context) {
+ return visitCommand(mergeIntoCommand, context);
+ }
+
default R visitLoadCommand(LoadCommand loadCommand, C context) {
return visitCommand(loadCommand, context);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
index 0de0b043f36..a6137e6c59d 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
@@ -46,6 +46,7 @@ import
org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
import
org.apache.doris.nereids.trees.plans.commands.ExplainCommand.ExplainLevel;
import org.apache.doris.nereids.trees.plans.commands.OptimizeTableCommand;
import org.apache.doris.nereids.trees.plans.commands.ReplayCommand;
+import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalCTE;
import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
@@ -1301,4 +1302,97 @@ public class NereidsParserTest extends ParserTestBase {
.assertThrowsExactly(SyntaxParseException.class)
.assertMessageContains("mismatched input ')' expecting");
}
+
+ @Test
+ public void testMergeInto() throws Exception {
+ NereidsParser parser = new NereidsParser();
+ String sql;
+ LogicalPlan logicalPlan;
+
+ // base case
+ sql = "MERGE INTO target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT VALUES (c1, c2, c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + target alias
+ sql = "MERGE INTO target target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT VALUES (c1, c2, c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + target alias with as
+ sql = "MERGE INTO target AS target USING source ON target.c1 =
source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT VALUES (c1, c2, c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + insert column list
+ sql = "MERGE INTO target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (c1, c2,
c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + without not matched
+ sql = "MERGE INTO target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE ";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + without delete matched
+ sql = "MERGE INTO target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (c1, c2,
c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + without update matched
+ sql = "MERGE INTO target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (c1, c2,
c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case + insert with case predicate
+ sql = "MERGE INTO target USING source ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED AND source.c1 < 10 THEN INSERT (c1, c2,
c3) VALUES (c1, c2, c3)";
+ logicalPlan = parser.parseSingle(sql);
+ Assertions.assertInstanceOf(MergeIntoCommand.class, logicalPlan);
+
+ // base case without and matched or not matched
+ String invalidSql1 = "MERGE INTO target USING source ON target.c1 =
source.c1 ";
+ Assertions.assertThrows(ParseException.class, () ->
parser.parseSingle(invalidSql1));
+
+ // base case without using
+ String invalidSql2 = "MERGE INTO target ON target.c1 = source.c1 "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT VALUES (c1, c2, c3)";
+ Assertions.assertThrows(ParseException.class, () ->
parser.parseSingle(invalidSql2));
+
+ // base case without on clause
+ String invalidSql3 = "MERGE INTO target USING source "
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT VALUES (c1, c2, c3)";
+ Assertions.assertThrows(ParseException.class, () ->
parser.parseSingle(invalidSql3));
+
+ // base case without target table
+ String invalidSql4 = "MERGE INTO USING source ON target.c1 = source.c1
"
+ + "WHEN MATCHED AND target.c2 > 5 THEN UPDATE SET c2 = c2 * 5 "
+ + "WHEN MATCHED THEN DELETE "
+ + "WHEN NOT MATCHED THEN INSERT VALUES (c1, c2, c3)";
+ Assertions.assertThrows(ParseException.class, () ->
parser.parseSingle(invalidSql4));
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
new file mode 100644
index 00000000000..ca04126c1c1
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommandTest.java
@@ -0,0 +1,959 @@
+// 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.trees.plans.commands.merge;
+
+import org.apache.doris.analysis.DefaultValueExprDef;
+import org.apache.doris.analysis.IntLiteral;
+import org.apache.doris.catalog.AggregateType;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.GeneratedColumnInfo;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.nereids.analyzer.UnboundAlias;
+import org.apache.doris.nereids.analyzer.UnboundFunction;
+import org.apache.doris.nereids.analyzer.UnboundRelation;
+import org.apache.doris.nereids.analyzer.UnboundSlot;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Not;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.If;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCheckPolicy;
+import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
+import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.DateV2Type;
+import org.apache.doris.qe.ConnectContext;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+
+public class MergeIntoCommandTest {
+
+ @Test
+ public void testGenerateBasePlanWithAlias() throws Exception {
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, new NullLiteral(),
+ ImmutableList.of(), ImmutableList.of()
+ );
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateBasePlan = clazz.getDeclaredMethod("generateBasePlan");
+ generateBasePlan.setAccessible(true);
+ LogicalPlan result = (LogicalPlan) generateBasePlan.invoke(command);
+ Assertions.assertInstanceOf(LogicalJoin.class, result);
+ LogicalJoin<?, ?> logicalJoin = (LogicalJoin<?, ?>) result;
+ Assertions.assertEquals(1, logicalJoin.getOtherJoinConjuncts().size());
+ Expression onClause = logicalJoin.getOtherJoinConjuncts().get(0);
+ Assertions.assertEquals(new NullLiteral(), onClause);
+ Assertions.assertEquals(JoinType.LEFT_OUTER_JOIN,
logicalJoin.getJoinType());
+ Assertions.assertEquals(source, logicalJoin.left());
+ Assertions.assertInstanceOf(LogicalSubQueryAlias.class,
logicalJoin.right());
+ LogicalSubQueryAlias<?> alias = (LogicalSubQueryAlias<?>)
logicalJoin.right();
+ Assertions.assertEquals("alias", alias.getAlias());
+ }
+
+ @Test
+ public void testGenerateBasePlanWithoutAlias() throws Exception {
+ List<String> nameParts = ImmutableList.of("ctl", "db", "tbl");
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ MergeIntoCommand command = new MergeIntoCommand(
+ nameParts, Optional.empty(), Optional.empty(),
+ source, new NullLiteral(),
+ ImmutableList.of(), ImmutableList.of()
+ );
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateBasePlan = clazz.getDeclaredMethod("generateBasePlan");
+ generateBasePlan.setAccessible(true);
+ LogicalPlan result = (LogicalPlan) generateBasePlan.invoke(command);
+ Assertions.assertInstanceOf(LogicalJoin.class, result);
+ LogicalJoin<?, ?> logicalJoin = (LogicalJoin<?, ?>) result;
+ Assertions.assertEquals(1, logicalJoin.getOtherJoinConjuncts().size());
+ Expression onClause = logicalJoin.getOtherJoinConjuncts().get(0);
+ Assertions.assertEquals(new NullLiteral(), onClause);
+ Assertions.assertEquals(JoinType.LEFT_OUTER_JOIN,
logicalJoin.getJoinType());
+ Assertions.assertEquals(source, logicalJoin.left());
+ Assertions.assertInstanceOf(LogicalCheckPolicy.class,
logicalJoin.right());
+ Assertions.assertInstanceOf(UnboundRelation.class,
logicalJoin.right().child(0));
+ UnboundRelation unboundRelation = (UnboundRelation)
logicalJoin.right().child(0);
+ Assertions.assertEquals(nameParts, unboundRelation.getNameParts());
+ }
+
+ @Test
+ public void testGenerateBranchLabel() throws Exception {
+ List<MergeMatchedClause> matchedClauses = ImmutableList.of(
+ new MergeMatchedClause(Optional.of(new IntegerLiteral(1)),
ImmutableList.of(), true),
+ new MergeMatchedClause(Optional.of(new IntegerLiteral(2)),
ImmutableList.of(), true),
+ new MergeMatchedClause(Optional.empty(), ImmutableList.of(),
true)
+ );
+ List<MergeNotMatchedClause> notMatchedClauses = ImmutableList.of(
+ new MergeNotMatchedClause(Optional.of(new IntegerLiteral(3)),
ImmutableList.of(), ImmutableList.of()),
+ new MergeNotMatchedClause(Optional.of(new IntegerLiteral(4)),
ImmutableList.of(), ImmutableList.of()),
+ new MergeNotMatchedClause(Optional.empty(),
ImmutableList.of(), ImmutableList.of())
+ );
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, matchedClauses, notMatchedClauses);
+ UnboundSlot unboundSlot = new UnboundSlot("alias",
"__DORIS_DELETE_SIGN__");
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateBranchLabel =
clazz.getDeclaredMethod("generateBranchLabel", NamedExpression.class);
+ generateBranchLabel.setAccessible(true);
+ NamedExpression result = (NamedExpression)
generateBranchLabel.invoke(command, unboundSlot);
+ Expression matchedLabel = new If(new IntegerLiteral(1), new
IntegerLiteral(0),
+ new If(new IntegerLiteral(2), new IntegerLiteral(1), new
IntegerLiteral(2)));
+ Expression notMatchedLabel = new If(new IntegerLiteral(3), new
IntegerLiteral(3),
+ new If(new IntegerLiteral(4), new IntegerLiteral(4), new
IntegerLiteral(5)));
+ NamedExpression expected = new UnboundAlias(new If(new Not(new
IsNull(unboundSlot)),
+ matchedLabel, notMatchedLabel),
"__DORIS_MERGE_INTO_BRANCH_LABEL__");
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void testGenerateDeleteProjection() throws Exception {
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateDeleteProjection =
clazz.getDeclaredMethod("generateDeleteProjection", List.class);
+ generateDeleteProjection.setAccessible(true);
+ List<Expression> result = (List<Expression>)
generateDeleteProjection.invoke(command, columns);
+ Assertions.assertEquals(4, result.size());
+ Assertions.assertEquals(ImmutableList.of(
+ new UnboundSlot(ImmutableList.of("alias", "c1")),
+ new TinyIntLiteral(((byte) 1)),
+ new UnboundSlot(ImmutableList.of("alias",
Column.SEQUENCE_COL)),
+ new UnboundSlot(ImmutableList.of("alias", "c3"))), result);
+ }
+
+ @Test
+ public void testGenerateUpdateProjection() throws Exception {
+ MergeMatchedClause mergeMatchedClause = new
MergeMatchedClause(Optional.empty(), ImmutableList.of(
+ new EqualTo(new UnboundSlot(Column.SEQUENCE_COL), new
IntegerLiteral(1)),
+ new EqualTo(new UnboundSlot("c4"), new IntegerLiteral(2)),
+ new EqualTo(new UnboundSlot("c5"), new IntegerLiteral(3))
+ ), false);
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column("c4", PrimitiveType.BIGINT),
+ new Column("c5", PrimitiveType.BIGINT),
+ new Column("c6", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", true, new
DefaultValueExprDef("CURRENT_TIMESTAMP"), 1,
+ null, new HashSet<>())
+ );
+
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateUpdateProjection =
clazz.getDeclaredMethod("generateUpdateProjection",
+ MergeMatchedClause.class, List.class, OlapTable.class,
ConnectContext.class);
+ generateUpdateProjection.setAccessible(true);
+
+ try (MockedStatic<UpdateCommand> mockedUpdate =
Mockito.mockStatic(UpdateCommand.class)) {
+ mockedUpdate.when(() ->
UpdateCommand.checkAssignmentColumn(Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any())).thenAnswer(x -> null);
+ List<Expression> result = (List<Expression>)
generateUpdateProjection.invoke(command,
+ mergeMatchedClause, columns, olapTable, null);
+ List<Expression> expected = ImmutableList.of(
+ new Cast(new UnboundSlot(ImmutableList.of("alias", "c1")),
BigIntType.INSTANCE),
+ new Cast(new UnboundSlot(ImmutableList.of("alias",
Column.DELETE_SIGN)), BigIntType.INSTANCE),
+ new Cast(new IntegerLiteral(1), BigIntType.INSTANCE),
+ new Cast(new UnboundSlot(ImmutableList.of("alias", "c3")),
BigIntType.INSTANCE),
+ new Cast(new IntegerLiteral(2), BigIntType.INSTANCE),
+ new Cast(new IntegerLiteral(3), BigIntType.INSTANCE),
+ new Cast(new UnboundFunction("CURRENT_TIMESTAMP",
ImmutableList.of()), BigIntType.INSTANCE)
+ );
+ Assertions.assertEquals(expected, result);
+ }
+ }
+
+ @Test
+ public void testGenerateUpdateProjectionWithDuplicateColumn() throws
Exception {
+ MergeMatchedClause mergeMatchedClause = new
MergeMatchedClause(Optional.empty(), ImmutableList.of(
+ new EqualTo(new UnboundSlot(Column.SEQUENCE_COL), new
IntegerLiteral(1)),
+ new EqualTo(new UnboundSlot("c4"), new IntegerLiteral(2)),
+ new EqualTo(new UnboundSlot("c4"), new IntegerLiteral(3))
+ ), false);
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column("c4", PrimitiveType.BIGINT),
+ new Column("c5", PrimitiveType.BIGINT),
+ new Column("c6", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", true, new
DefaultValueExprDef("CURRENT_TIMESTAMP"), 1,
+ null, new HashSet<>())
+ );
+
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateUpdateProjection =
clazz.getDeclaredMethod("generateUpdateProjection",
+ MergeMatchedClause.class, List.class, OlapTable.class,
ConnectContext.class);
+ generateUpdateProjection.setAccessible(true);
+
+ try (MockedStatic<UpdateCommand> mockedUpdate =
Mockito.mockStatic(UpdateCommand.class)) {
+ mockedUpdate.when(() ->
UpdateCommand.checkAssignmentColumn(Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any())).thenAnswer(x -> null);
+ try {
+ generateUpdateProjection.invoke(command, mergeMatchedClause,
columns, olapTable, null);
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class,
e.getCause());
+ }
+ }
+ }
+
+ @Test
+ public void testGenerateUpdateProjectionWithKey() throws Exception {
+ MergeMatchedClause mergeMatchedClause = new
MergeMatchedClause(Optional.empty(), ImmutableList.of(
+ new EqualTo(new UnboundSlot(Column.SEQUENCE_COL), new
IntegerLiteral(1)),
+ new EqualTo(new UnboundSlot("c1"), new IntegerLiteral(2)),
+ new EqualTo(new UnboundSlot("c5"), new IntegerLiteral(3))
+ ), false);
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column("c4", PrimitiveType.BIGINT),
+ new Column("c5", PrimitiveType.BIGINT),
+ new Column("c6", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", true, new
DefaultValueExprDef("CURRENT_TIMESTAMP"), 1,
+ null, new HashSet<>())
+ );
+
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateUpdateProjection =
clazz.getDeclaredMethod("generateUpdateProjection",
+ MergeMatchedClause.class, List.class, OlapTable.class,
ConnectContext.class);
+ generateUpdateProjection.setAccessible(true);
+
+ try (MockedStatic<UpdateCommand> mockedUpdate =
Mockito.mockStatic(UpdateCommand.class)) {
+ mockedUpdate.when(() ->
UpdateCommand.checkAssignmentColumn(Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any())).thenAnswer(x -> null);
+ try {
+ generateUpdateProjection.invoke(command, mergeMatchedClause,
columns, olapTable, null);
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class,
e.getCause());
+ }
+ }
+ }
+
+ @Test
+ public void testGenerateUpdateProjectionWithGeneratedColumn() throws
Exception {
+ MergeMatchedClause mergeMatchedClause = new
MergeMatchedClause(Optional.empty(), ImmutableList.of(
+ new EqualTo(new UnboundSlot(Column.SEQUENCE_COL), new
IntegerLiteral(1)),
+ new EqualTo(new UnboundSlot("c2"), new IntegerLiteral(2)),
+ new EqualTo(new UnboundSlot("c5"), new IntegerLiteral(3))
+ ), false);
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column("c4", PrimitiveType.BIGINT),
+ new Column("c5", PrimitiveType.BIGINT),
+ new Column("c6", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", true, new
DefaultValueExprDef("CURRENT_TIMESTAMP"), 1,
+ null, new HashSet<>())
+ );
+
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateUpdateProjection =
clazz.getDeclaredMethod("generateUpdateProjection",
+ MergeMatchedClause.class, List.class, OlapTable.class,
ConnectContext.class);
+ generateUpdateProjection.setAccessible(true);
+
+ try (MockedStatic<UpdateCommand> mockedUpdate =
Mockito.mockStatic(UpdateCommand.class)) {
+ mockedUpdate.when(() ->
UpdateCommand.checkAssignmentColumn(Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any())).thenAnswer(x -> null);
+ try {
+ generateUpdateProjection.invoke(command, mergeMatchedClause,
columns, olapTable, null);
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class,
e.getCause());
+ }
+ }
+ }
+
+ @Test
+ public void testGenerateUpdateProjectionWithNonExistsColumn() throws
Exception {
+ MergeMatchedClause mergeMatchedClause = new
MergeMatchedClause(Optional.empty(), ImmutableList.of(
+ new EqualTo(new UnboundSlot(Column.SEQUENCE_COL), new
IntegerLiteral(1)),
+ new EqualTo(new UnboundSlot("c1"), new IntegerLiteral(2)),
+ new EqualTo(new UnboundSlot("c10"), new IntegerLiteral(3))
+ ), false);
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column("c4", PrimitiveType.BIGINT),
+ new Column("c5", PrimitiveType.BIGINT),
+ new Column("c6", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", true, new
DefaultValueExprDef("CURRENT_TIMESTAMP"), 1,
+ null, new HashSet<>())
+ );
+
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateUpdateProjection =
clazz.getDeclaredMethod("generateUpdateProjection",
+ MergeMatchedClause.class, List.class, OlapTable.class,
ConnectContext.class);
+ generateUpdateProjection.setAccessible(true);
+
+ try (MockedStatic<UpdateCommand> mockedUpdate =
Mockito.mockStatic(UpdateCommand.class)) {
+ mockedUpdate.when(() ->
UpdateCommand.checkAssignmentColumn(Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any())).thenAnswer(x -> null);
+ try {
+ generateUpdateProjection.invoke(command, mergeMatchedClause,
columns, olapTable, null);
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class,
e.getCause());
+ }
+ }
+ }
+
+ @Test
+ public void testGenerateInsertWithoutColListProjectionWithoutSeqCol()
throws Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of(),
+ ImmutableList.of(new UnboundSlot("c1"), new UnboundAlias(new
DefaultValueSlot()), new UnboundSlot("c3")));
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithoutColListProjection =
clazz.getDeclaredMethod("generateInsertWithoutColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, int.class, Optional.class, Optional.class);
+ generateInsertWithoutColListProjection.setAccessible(true);
+ List<Expression> result = (List<Expression>)
generateInsertWithoutColListProjection.invoke(command, clause, columns,
olapTable, false, -1, Optional.empty(), Optional.empty());
+ List<Expression> expected = ImmutableList.of(
+ new Cast(new UnboundSlot("c1"), BigIntType.INSTANCE),
+ new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
+ new TinyIntLiteral(((byte) 0))
+ );
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void
testGenerateInsertWithoutColListProjectionWithSeqColumnWithSeqIndex() throws
Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of(),
+ ImmutableList.of(new UnboundSlot("c1"), new UnboundAlias(new
DefaultValueSlot()), new UnboundSlot("c3")));
+
+ Column seqCol = new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, -1, "default", "",
+ true, null, -1, "", true, new
DefaultValueExprDef("CURRENT_TIMESTAMP"), 1,
+ null, new HashSet<>());
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithoutColListProjection =
clazz.getDeclaredMethod("generateInsertWithoutColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, int.class, Optional.class, Optional.class);
+ generateInsertWithoutColListProjection.setAccessible(true);
+ List<Expression> result = (List<Expression>)
generateInsertWithoutColListProjection.invoke(command, clause, columns,
olapTable, true, 2, Optional.of(seqCol),
Optional.of(ScalarType.createType(PrimitiveType.BIGINT)));
+ List<Expression> expected = ImmutableList.of(
+ new Cast(new UnboundSlot("c1"), BigIntType.INSTANCE),
+ new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
+ new TinyIntLiteral(((byte) 0)),
+ new Cast(new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
BigIntType.INSTANCE)
+ );
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void
testGenerateInsertWithoutColListProjectionWithSeqColumnWithSeqIndexWithoutDefaultValue()
throws Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of(),
+ ImmutableList.of(new UnboundSlot("c1"), new UnboundAlias(new
DefaultValueSlot()), new UnboundSlot("c3")));
+
+ Column seqCol = new Column("c3", PrimitiveType.BIGINT);
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithoutColListProjection =
clazz.getDeclaredMethod("generateInsertWithoutColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, int.class, Optional.class, Optional.class);
+ generateInsertWithoutColListProjection.setAccessible(true);
+ List<Expression> result = (List<Expression>)
generateInsertWithoutColListProjection.invoke(command, clause, columns,
olapTable, true, 2, Optional.of(seqCol),
Optional.of(ScalarType.createType(PrimitiveType.BIGINT)));
+ List<Expression> expected = ImmutableList.of(
+ new Cast(new UnboundSlot("c1"), BigIntType.INSTANCE),
+ new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
+ new TinyIntLiteral(((byte) 0)),
+ new Cast(new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
BigIntType.INSTANCE)
+ );
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void
testGenerateInsertWithoutColListProjectionWithSeqColumnWithoutSeqIndex() throws
Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1),
+ new Column(Column.VERSION_COL,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of(),
+ ImmutableList.of(new UnboundSlot("c1"), new UnboundAlias(new
DefaultValueSlot()), new UnboundSlot("c3")));
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithoutColListProjection =
clazz.getDeclaredMethod("generateInsertWithoutColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, int.class, Optional.class, Optional.class);
+ generateInsertWithoutColListProjection.setAccessible(true);
+ try {
+ generateInsertWithoutColListProjection.invoke(command, clause,
columns, olapTable, true, -1, Optional.empty(), Optional.empty());
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+
+ @Test
+ public void
testGenerateInsertWithoutColListProjectionWithGeneratedColumnWithoutDefaultValue()
throws Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of(),
+ ImmutableList.of(new UnboundSlot("c1"), new UnboundSlot("c2"),
new UnboundSlot("c3")));
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithoutColListProjection =
clazz.getDeclaredMethod("generateInsertWithoutColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, int.class, Optional.class, Optional.class);
+ generateInsertWithoutColListProjection.setAccessible(true);
+ try {
+ generateInsertWithoutColListProjection.invoke(command, clause,
columns, olapTable, false, -1, Optional.empty(), Optional.empty());
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+
+ @Test
+ public void testGenerateInsertWithColListProjectionWithoutSeqCol() throws
Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of("c1", "c2", "c3"),
+ ImmutableList.of(new UnboundSlot("c1"), new UnboundAlias(new
DefaultValueSlot()), new UnboundSlot("c3")));
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ // generated column
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, -1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column("c3", PrimitiveType.BIGINT),
+ // auto inc
+ new Column("c4", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, 1, null, "",
+ true, null, -1, null, false, null, 1,
+ null, new HashSet<>()),
+ // null default
+ new Column("c5", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, true, null, ""),
+ // default expr
+ new Column("c6", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, "1", ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithColListProjection =
clazz.getDeclaredMethod("generateInsertWithColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, String.class, Optional.class, Optional.class);
+ generateInsertWithColListProjection.setAccessible(true);
+ List<Expression> result = (List<Expression>)
generateInsertWithColListProjection.invoke(command, clause, columns, olapTable,
false, "", Optional.empty(), Optional.empty());
+ List<Expression> expected = ImmutableList.of(
+ new Cast(new UnboundSlot("c1"), BigIntType.INSTANCE),
+ new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
+ new NullLiteral(BigIntType.INSTANCE),
+ new NullLiteral(BigIntType.INSTANCE),
+ new Cast(new TinyIntLiteral((byte) 1), BigIntType.INSTANCE),
+ new TinyIntLiteral(((byte) 0))
+ );
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void
testGenerateInsertWithColListProjectionWithSeqColWithSeqIndex() throws
Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of("c1", "c3"),
+ ImmutableList.of(new UnboundSlot("c1"), new
UnboundSlot("c3")));
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+ Column seqCol = new Column(Column.SEQUENCE_COL,
ScalarType.createType(PrimitiveType.DATEV2), false,
+ AggregateType.NONE, false, -1, null, "",
+ true, null, -1, "", false, null, 1,
+ null, new HashSet<>());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithColListProjection =
clazz.getDeclaredMethod("generateInsertWithColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, String.class, Optional.class, Optional.class);
+ generateInsertWithColListProjection.setAccessible(true);
+ List<Expression> result = (List<Expression>)
generateInsertWithColListProjection.invoke(command, clause, columns, olapTable,
true, "c3", Optional.of(seqCol),
Optional.of(ScalarType.createType(PrimitiveType.DATEV2)));
+ List<Expression> expected = ImmutableList.of(
+ new Cast(new UnboundSlot("c1"), BigIntType.INSTANCE),
+ new Cast(new UnboundSlot("c3"), BigIntType.INSTANCE),
+ new TinyIntLiteral(((byte) 0)),
+ new Cast(new UnboundSlot("c3"), DateV2Type.INSTANCE)
+ );
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void
testGenerateInsertWithColListProjectionWithSeqColWithoutSeqIndexWithoutDefaultValue()
throws Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of("c1", "c3"),
+ ImmutableList.of(new UnboundSlot("c1"), new
UnboundSlot("c3")));
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithColListProjection =
clazz.getDeclaredMethod("generateInsertWithColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, String.class, Optional.class, Optional.class);
+ generateInsertWithColListProjection.setAccessible(true);
+ try {
+ generateInsertWithColListProjection.invoke(command, clause,
columns, olapTable, true, "", Optional.empty(), Optional.empty());
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+
+ @Test
+ public void testGenerateInsertWithColListProjectionWithDuplicateColLabel()
throws Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of("c1", "c1"),
+ ImmutableList.of(new UnboundSlot("c1"), new
UnboundSlot("c3")));
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithColListProjection =
clazz.getDeclaredMethod("generateInsertWithColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, String.class, Optional.class, Optional.class);
+ generateInsertWithColListProjection.setAccessible(true);
+ try {
+ generateInsertWithColListProjection.invoke(command, clause,
columns, olapTable, false, "", Optional.empty(), Optional.empty());
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+
+ @Test
+ public void testGenerateInsertWithColListProjectionWithGeneratedCol()
throws Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of("c1", "c2"),
+ ImmutableList.of(new UnboundSlot("c1"), new
UnboundSlot("c2")));
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c2", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, -1, "default", "",
+ true, null, -1, "", false, null, 1,
+ new GeneratedColumnInfo("cc", new IntLiteral(1)), new
HashSet<>()),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithColListProjection =
clazz.getDeclaredMethod("generateInsertWithColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, String.class, Optional.class, Optional.class);
+ generateInsertWithColListProjection.setAccessible(true);
+ try {
+ generateInsertWithColListProjection.invoke(command, clause,
columns, olapTable, false, "", Optional.empty(), Optional.empty());
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+
+ @Test
+ public void
testGenerateInsertWithColListProjectionWithNotNullWithoutDefaultValue() throws
Exception {
+ OlapTable olapTable = Mockito.mock(OlapTable.class);
+ Mockito.when(olapTable.getName()).thenReturn("olap_table");
+
+ MergeNotMatchedClause clause = new
MergeNotMatchedClause(Optional.empty(), ImmutableList.of("c1", "c3"),
+ ImmutableList.of(new UnboundSlot("c1"), new
UnboundSlot("c3")));
+
+ List<Column> columns = ImmutableList.of(
+ new Column("c1", ScalarType.createType(PrimitiveType.BIGINT),
true, AggregateType.NONE, "", ""),
+ new Column("c3", PrimitiveType.BIGINT),
+ new Column("c5", ScalarType.createType(PrimitiveType.BIGINT),
false,
+ AggregateType.NONE, false, null, ""),
+ new Column(Column.DELETE_SIGN,
ScalarType.createType(PrimitiveType.BIGINT), false,
+ AggregateType.NONE, false, "", false, -1)
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateInsertWithColListProjection =
clazz.getDeclaredMethod("generateInsertWithColListProjection",
+ MergeNotMatchedClause.class, List.class, OlapTable.class,
boolean.class, String.class, Optional.class, Optional.class);
+ generateInsertWithColListProjection.setAccessible(true);
+ try {
+ generateInsertWithColListProjection.invoke(command, clause,
columns, olapTable, false, "", Optional.empty(), Optional.empty());
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+
+ @Test
+ public void testGenerateFinalProjections() throws Exception {
+ List<String> colNames = ImmutableList.of("c1", "c2");
+ List<List<Expression>> finalProjections = ImmutableList.of(
+ ImmutableList.of(new IntegerLiteral(11), new
IntegerLiteral(12)),
+ ImmutableList.of(new IntegerLiteral(21), new
IntegerLiteral(22)),
+ ImmutableList.of(new IntegerLiteral(31), new
IntegerLiteral(32)),
+ ImmutableList.of(new IntegerLiteral(41), new
IntegerLiteral(42))
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateFinalProjections =
clazz.getDeclaredMethod("generateFinalProjections", List.class, List.class);
+ generateFinalProjections.setAccessible(true);
+ List<NamedExpression> result = (List<NamedExpression>)
generateFinalProjections.invoke(command, colNames, finalProjections);
+ List<NamedExpression> expected = ImmutableList.of(
+ new UnboundAlias(new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(3)), new
IntegerLiteral(41),
+ new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(2)), new
IntegerLiteral(31),
+ new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(1)), new
IntegerLiteral(21),
+ new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(0)), new
IntegerLiteral(11), new NullLiteral())))), "c1"),
+ new UnboundAlias(new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(3)), new
IntegerLiteral(42),
+ new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(2)), new
IntegerLiteral(32),
+ new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(1)), new
IntegerLiteral(22),
+ new If(new EqualTo(new
UnboundSlot("__DORIS_MERGE_INTO_BRANCH_LABEL__"), new IntegerLiteral(0)), new
IntegerLiteral(12), new NullLiteral())))), "c2")
+ );
+
+ Assertions.assertEquals(expected, result);
+ }
+
+ @Test
+ public void testGenerateFinalProjectionsWithDiffSize() throws Exception {
+ List<String> colNames = ImmutableList.of("c1", "c2", "c3");
+ List<List<Expression>> finalProjections = ImmutableList.of(
+ ImmutableList.of(new IntegerLiteral(11), new
IntegerLiteral(12)),
+ ImmutableList.of(new IntegerLiteral(21), new
IntegerLiteral(22)),
+ ImmutableList.of(new IntegerLiteral(31), new
IntegerLiteral(32)),
+ ImmutableList.of(new IntegerLiteral(41), new
IntegerLiteral(42))
+ );
+
+ LogicalPlan source = new LogicalEmptyRelation(new RelationId(1),
ImmutableList.of());
+ Expression onClause = new NullLiteral();
+ MergeIntoCommand command = new MergeIntoCommand(
+ ImmutableList.of("ctl", "db", "tbl"), Optional.of("alias"),
Optional.empty(),
+ source, onClause, ImmutableList.of(), ImmutableList.of());
+
+ Class<?> clazz =
Class.forName("org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand");
+ Method generateFinalProjections =
clazz.getDeclaredMethod("generateFinalProjections", List.class, List.class);
+ generateFinalProjections.setAccessible(true);
+ try {
+ generateFinalProjections.invoke(command, colNames,
finalProjections);
+ } catch (InvocationTargetException e) {
+ Assertions.assertInstanceOf(AnalysisException.class, e.getCause());
+ }
+ }
+}
diff --git a/regression-test/data/load_p0/merge_into/test_merge_into.out
b/regression-test/data/load_p0/merge_into/test_merge_into.out
new file mode 100644
index 00000000000..fac504ffd17
--- /dev/null
+++ b/regression-test/data/load_p0/merge_into/test_merge_into.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !base_1 --
+\N \N
+1 10
+3 33
+
+-- !seq_col_1 --
+\N \N
+1 10
+3 33
+
+-- !seq_map_1 --
+\N \N 2025-02-02
+1 10 2020-02-02
+3 33 2025-02-02
+
+-- !gen_col_1 --
+\N \N \N
+1 10 11
+3 33 36
+
diff --git a/regression-test/suites/load_p0/merge_into/test_merge_into.groovy
b/regression-test/suites/load_p0/merge_into/test_merge_into.groovy
new file mode 100644
index 00000000000..0422be5954d
--- /dev/null
+++ b/regression-test/suites/load_p0/merge_into/test_merge_into.groovy
@@ -0,0 +1,254 @@
+// 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.
+
+suite("test_merge_into") {
+ multi_sql """
+ DROP TABLE IF EXISTS merge_into_source_table FORCE;
+ DROP TABLE IF EXISTS merge_into_target_base_table FORCE;
+ DROP TABLE IF EXISTS merge_into_target_seq_col_table FORCE;
+ DROP TABLE IF EXISTS merge_into_target_seq_map_table FORCE;
+ DROP TABLE IF EXISTS merge_into_target_gen_col_table FORCE;
+
+ CREATE TABLE `merge_into_source_table` (
+ `c1` int NULL,
+ `c2` varchar(255) NULL
+ ) ENGINE=OLAP
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1"
+ );
+
+ CREATE TABLE `merge_into_target_base_table` (
+ `c1` int NULL,
+ `c2` varchar(255) NULL
+ ) ENGINE=OLAP
+ UNIQUE KEY(`c1`)
+ DISTRIBUTED BY HASH(`c1`)
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1"
+ );
+
+ CREATE TABLE `merge_into_target_seq_col_table` (
+ `c1` int NULL,
+ `c2` varchar(255) NULL
+ ) ENGINE=OLAP
+ UNIQUE KEY(`c1`)
+ DISTRIBUTED BY HASH(`c1`) BUCKETS 10
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1",
+ "function_column.sequence_type" = "date"
+ );
+
+ CREATE TABLE `merge_into_target_seq_map_table` (
+ `c1` int NULL,
+ `c2` varchar(255) NULL,
+ `c3` date NULL
+ ) ENGINE=OLAP
+ UNIQUE KEY(`c1`)
+ DISTRIBUTED BY HASH(`c1`) BUCKETS 10
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1",
+ "function_column.sequence_col" = "c3"
+ );
+
+ CREATE TABLE `merge_into_target_gen_col_table` (
+ `c1` int NULL,
+ `c2` int NULL,
+ `c3` bigint AS (c1 + c2) NULL
+ ) ENGINE=OLAP
+ UNIQUE KEY(`c1`)
+ DISTRIBUTED BY HASH(`c1`) BUCKETS 10
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1"
+ );
+
+ INSERT INTO merge_into_source_table VALUES (1, 12), (2, 22), (3, 33),
(null, null);
+ INSERT INTO merge_into_target_base_table VALUES (1, 1), (2, 10);
+ INSERT INTO merge_into_target_seq_col_table (c1, c2,
__DORIS_SEQUENCE_COL__) VALUES (1, 1, '2020-02-02'), (2, 10, '2020-02-02');
+ INSERT INTO merge_into_target_seq_map_table VALUES (1, 1, '2020-02-02'),
(2, 10, '2020-02-02');
+ INSERT INTO merge_into_target_gen_col_table (c1, c2) VALUES (1, 1), (2,
10);
+
+ SYNC;
+ """
+
+ // base merge, test null safe equals here
+ sql """
+ WITH tmp AS (SELECT * FROM merge_into_source_table)
+ MERGE INTO merge_into_target_base_table t1
+ USING tmp t2
+ ON t1.c1 <=> t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2)
+ """
+
+ sql """
+ MERGE INTO merge_into_target_seq_col_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT (c1, c2, __DORIS_SEQUENCE_COL__)
VALUES(t2.c1, t2.c2, '2025-02-02')
+ """
+
+ sql """
+ MERGE INTO merge_into_target_seq_map_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2, '2025-02-02')
+ """
+
+ sql """
+ MERGE INTO merge_into_target_gen_col_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT (c1, c2) VALUES(t2.c1, t2.c2)
+ """
+
+ sql """
+ SYNC
+ """
+
+ order_qt_base_1 """
+ SELECT * FROM merge_into_target_base_table;
+ """
+ order_qt_seq_col_1 """
+ SELECT * FROM merge_into_target_seq_col_table;
+ """
+ order_qt_seq_map_1 """
+ SELECT * FROM merge_into_target_seq_map_table;
+ """
+ order_qt_gen_col_1 """
+ SELECT * FROM merge_into_target_gen_col_table;
+ """
+
+ // target has seq col but insert without seq col
+ test {
+ sql """
+ MERGE INTO merge_into_target_seq_col_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT (c1, c2) VALUES(t2.c1, t2.c2)
+ """
+
+ exception """has sequence column, need to specify the sequence
column"""
+ }
+
+ // target has generated col, update try to update it
+ test {
+ sql """
+ MERGE INTO merge_into_target_gen_col_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10, c3 = 10
+ WHEN NOT MATCHED THEN INSERT (c1, c2) VALUES(t2.c1, t2.c2)
+ """
+
+ exception """The value specified for generated column 'c3'"""
+ }
+
+ // target has generated col, insert try to insert it explicitly
+ test {
+ sql """
+ MERGE INTO merge_into_target_gen_col_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES(t2.c1, t2.c2,
t2.c2)
+ """
+
+ exception """The value specified for generated column 'c3'"""
+ }
+
+ // update key column
+ test {
+ sql """
+ MERGE INTO merge_into_target_base_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c1 = 10
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2)
+ """
+
+ exception """Only value columns of unique table could be updated"""
+ }
+
+ // update not exist column
+ test {
+ sql """
+ MERGE INTO merge_into_target_base_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c4 = 10
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2)
+ """
+
+ exception """unknown column in assignment list: c4"""
+ }
+
+ // insert not exist column
+ test {
+ sql """
+ MERGE INTO merge_into_target_base_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES(t2.c1, t2.c2,
t2.c2)
+ """
+
+ exception """unknown column in target table: c3"""
+ }
+
+ // matched clause without predicate in the middle
+ test {
+ sql """
+ MERGE INTO merge_into_target_base_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2)
+ """
+
+ exception """Only the last matched clause could without case
predicate"""
+ }
+
+ // not matched clause without predicate in the middle
+ test {
+ sql """
+ MERGE INTO merge_into_target_base_table t1
+ USING merge_into_source_table t2
+ ON t1.c1 = t2.c1
+ WHEN MATCHED AND t1.c2 = 10 THEN DELETE
+ WHEN MATCHED THEN UPDATE SET c2 = 10
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2)
+ WHEN NOT MATCHED THEN INSERT VALUES(t2.c1, t2.c2)
+ """
+
+ exception """Only the last not matched clause could without case
predicate"""
+ }
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]