This is an automated email from the ASF dual-hosted git repository.
924060929 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 794d514479e [feature](hive) Support static partition overwrite for
hive tables (#65991)
794d514479e is described below
commit 794d514479e038c883c1b2aa42069d17f98968a9
Author: Lijia Liu <[email protected]>
AuthorDate: Wed Jul 29 18:23:01 2026 +0800
[feature](hive) Support static partition overwrite for hive tables (#65991)
Add Hive static partition regression and FE plan tests, and fix
explicit-column INSERT cases to use the supported
PARTITION-before-column-list syntax.
---
.../nereids/analyzer/UnboundHiveTableSink.java | 49 ++-
.../nereids/analyzer/UnboundTableSinkCreator.java | 9 +-
.../doris/nereids/rules/analysis/BindSink.java | 92 ++++-
.../insert/InsertOverwriteTableCommand.java | 3 +-
.../trees/plans/commands/insert/InsertUtils.java | 17 +-
.../datasource/hive/HiveDDLAndDMLPlanTest.java | 104 ++++++
.../write/test_hive_write_static_partition.groovy | 383 +++++++++++++++++++++
7 files changed, 639 insertions(+), 18 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundHiveTableSink.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundHiveTableSink.java
index 4ffbc0230a0..134f2fc2e54 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundHiveTableSink.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundHiveTableSink.java
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.analyzer;
import org.apache.doris.nereids.memo.GroupExpression;
import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.DMLCommandType;
@@ -26,8 +27,10 @@ import
org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
/**
@@ -35,14 +38,30 @@ import java.util.Optional;
*/
public class UnboundHiveTableSink<CHILD_TYPE extends Plan> extends
UnboundBaseExternalTableSink<CHILD_TYPE> {
+ // Static partition key-value pairs for INSERT [OVERWRITE] ... PARTITION
+ // (col='val', ...)
+ private final Map<String, Expression> staticPartitionKeyValues;
+
public UnboundHiveTableSink(List<String> nameParts, List<String> colNames,
List<String> hints,
List<String> partitions, CHILD_TYPE child) {
this(nameParts, colNames, hints, partitions, DMLCommandType.NONE,
- Optional.empty(), Optional.empty(), child);
+ Optional.empty(), Optional.empty(), child, null);
+ }
+
+ public UnboundHiveTableSink(List<String> nameParts,
+ List<String> colNames,
+ List<String> hints,
+ List<String> partitions,
+ DMLCommandType dmlCommandType,
+ Optional<GroupExpression> groupExpression,
+ Optional<LogicalProperties> logicalProperties,
+ CHILD_TYPE child) {
+ this(nameParts, colNames, hints, partitions, dmlCommandType,
+ groupExpression, logicalProperties, child, null);
}
/**
- * constructor
+ * constructor with static partition
*/
public UnboundHiveTableSink(List<String> nameParts,
List<String> colNames,
@@ -51,14 +70,21 @@ public class UnboundHiveTableSink<CHILD_TYPE extends Plan>
extends UnboundBaseEx
DMLCommandType dmlCommandType,
Optional<GroupExpression> groupExpression,
Optional<LogicalProperties> logicalProperties,
- CHILD_TYPE child) {
+ CHILD_TYPE child,
+ Map<String, Expression>
staticPartitionKeyValues) {
super(nameParts, PlanType.LOGICAL_UNBOUND_HIVE_TABLE_SINK,
ImmutableList.of(), groupExpression,
logicalProperties, colNames, dmlCommandType, child, hints,
partitions);
+ this.staticPartitionKeyValues = staticPartitionKeyValues != null
+ ? ImmutableMap.copyOf(staticPartitionKeyValues)
+ : null;
}
- @Override
- public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
- return visitor.visitUnboundHiveTableSink(this, context);
+ public Map<String, Expression> getStaticPartitionKeyValues() {
+ return staticPartitionKeyValues;
+ }
+
+ public boolean hasStaticPartition() {
+ return staticPartitionKeyValues != null &&
!staticPartitionKeyValues.isEmpty();
}
@Override
@@ -66,19 +92,24 @@ public class UnboundHiveTableSink<CHILD_TYPE extends Plan>
extends UnboundBaseEx
Preconditions.checkArgument(children.size() == 1,
"UnboundHiveTableSink only accepts one child");
return new UnboundHiveTableSink<>(nameParts, colNames, hints,
partitions,
- dmlCommandType, groupExpression, Optional.empty(),
children.get(0));
+ dmlCommandType, groupExpression, Optional.empty(),
children.get(0), staticPartitionKeyValues);
+ }
+
+ @Override
+ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+ return visitor.visitUnboundHiveTableSink(this, context);
}
@Override
public Plan withGroupExpression(Optional<GroupExpression> groupExpression)
{
return new UnboundHiveTableSink<>(nameParts, colNames, hints,
partitions,
- dmlCommandType, groupExpression,
Optional.of(getLogicalProperties()), child());
+ dmlCommandType, groupExpression,
Optional.of(getLogicalProperties()), child(), staticPartitionKeyValues);
}
@Override
public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression>
groupExpression,
Optional<LogicalProperties>
logicalProperties, List<Plan> children) {
return new UnboundHiveTableSink<>(nameParts, colNames, hints,
partitions,
- dmlCommandType, groupExpression, logicalProperties,
children.get(0));
+ dmlCommandType, groupExpression, logicalProperties,
children.get(0), staticPartitionKeyValues);
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
index ff0cfc71264..ecadbf367d4 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java
@@ -83,7 +83,7 @@ public class UnboundTableSinkCreator {
}
/**
- * create unbound sink for DML plan with static partition support for
Iceberg.
+ * create unbound sink for DML plan with static partition support for
Iceberg / Hive / MaxCompute.
*/
public static LogicalSink<? extends Plan>
createUnboundTableSink(List<String> nameParts,
List<String> colNames, List<String> hints, boolean
temporaryPartition, List<String> partitions,
@@ -98,7 +98,7 @@ public class UnboundTableSinkCreator {
Optional.empty(), plan);
} else if (curCatalog instanceof HMSExternalCatalog) {
return new UnboundHiveTableSink<>(nameParts, colNames, hints,
partitions,
- dmlCommandType, Optional.empty(), Optional.empty(), plan);
+ dmlCommandType, Optional.empty(), Optional.empty(), plan,
staticPartitionKeyValues);
} else if (curCatalog instanceof IcebergExternalCatalog) {
return new UnboundIcebergTableSink<>(nameParts, colNames, hints,
partitions,
dmlCommandType, Optional.empty(), Optional.empty(), plan,
staticPartitionKeyValues, false);
@@ -114,8 +114,7 @@ public class UnboundTableSinkCreator {
/**
* create unbound sink for DML plan with auto detect overwrite partition
enable
- * and static partition support for Iceberg.
- * TODO: staticPartitionKeyValues is only used for Iceberg, support other
catalog types in future.
+ * and static partition support for Iceberg / Hive / MaxCompute.
*/
public static LogicalSink<? extends Plan>
createUnboundTableSinkMaybeOverwrite(List<String> nameParts,
List<String> colNames, List<String> hints, boolean
temporaryPartition, List<String> partitions,
@@ -139,7 +138,7 @@ public class UnboundTableSinkCreator {
Optional.empty(), plan);
} else if (curCatalog instanceof HMSExternalCatalog &&
!isAutoDetectPartition) {
return new UnboundHiveTableSink<>(nameParts, colNames, hints,
partitions,
- dmlCommandType, Optional.empty(), Optional.empty(), plan);
+ dmlCommandType, Optional.empty(), Optional.empty(), plan,
staticPartitionKeyValues);
} else if (curCatalog instanceof IcebergExternalCatalog &&
!isAutoDetectPartition) {
return new UnboundIcebergTableSink<>(nameParts, colNames, hints,
partitions,
dmlCommandType, Optional.empty(), Optional.empty(), plan,
staticPartitionKeyValues, false);
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 8c580171b21..9a34aa5403f 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
@@ -123,6 +123,7 @@ import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -690,11 +691,44 @@ public class BindSink implements AnalysisRuleFactory {
+ "(input format: " + inputFormat + "). LZO tables are
read-only in Doris.");
}
+ // Get static partition columns if present:
+ // INSERT [OVERWRITE] TABLE t PARTITION(col='val', ...) SELECT ...
+ Map<String, Expression> staticPartitions =
sink.getStaticPartitionKeyValues();
+ Set<String> staticPartitionColNames = staticPartitions != null
+ ? staticPartitions.keySet()
+ : Sets.newHashSet();
+ // Hive column/partition names are case-insensitive (stored lowercase
in HMS), so
+ // e.g. PARTITION(dt='21', DT='22') refers to the same column twice.
Detect such
+ // case-insensitive duplicates here and fail fast, instead of silently
letting a
+ // later value overwrite an earlier one in the case-insensitive
columnToOutput map.
+ // Use Locale.ROOT to keep the folding locale-independent (matching
how the Hive
+ // schema is loaded), otherwise e.g. the Turkish locale would fold 'I'
to 'ı' and
+ // fail to match a metadata column named 'id'.
+ Set<String> lowerStaticPartitionColNames = Sets.newHashSet();
+ for (String name : staticPartitionColNames) {
+ if
(!lowerStaticPartitionColNames.add(name.toLowerCase(Locale.ROOT))) {
+ throw new AnalysisException("Duplicate partition column: " +
name);
+ }
+ }
+
+ // Validate static partition columns against the hive table's
partition columns
+ if (sink.hasStaticPartition()) {
+ validateHiveStaticPartition(staticPartitions, table);
+ }
+
+ // Build bindColumns: exclude static partition columns from the
columns that
+ // need to come from SELECT, because their values come from the
PARTITION clause.
List<Column> bindColumns;
if (sink.getColNames().isEmpty()) {
- bindColumns =
table.getBaseSchema(true).stream().collect(ImmutableList.toImmutableList());
+ bindColumns = table.getBaseSchema(true).stream()
+ .filter(col ->
!lowerStaticPartitionColNames.contains(col.getName().toLowerCase(Locale.ROOT)))
+ .collect(ImmutableList.toImmutableList());
} else {
bindColumns = sink.getColNames().stream().map(cn -> {
+ if
(lowerStaticPartitionColNames.contains(cn.toLowerCase(Locale.ROOT))) {
+ throw new AnalysisException(String.format(
+ "column %s is a static partition column, should
not be in the insert column list", cn));
+ }
Column column = table.getColumn(cn);
if (column == null) {
throw new AnalysisException(String.format("column %s is
not found in table %s",
@@ -715,15 +749,71 @@ public class BindSink implements AnalysisRuleFactory {
Optional.empty(),
child);
// we need to insert all the columns of the target table
+ // (except the static partition columns which are supplied by the
PARTITION clause)
if (boundSink.getCols().size() != child.getOutput().size()) {
throw new AnalysisException("insert into cols should be
corresponding to the query output");
}
Map<String, NamedExpression> columnToOutput = getColumnToOutput(ctx,
table, false, false,
boundSink, child);
+
+ // For static partition columns, add constant expressions from the
PARTITION clause.
+ // This ensures every written row carries the static partition value,
so all rows
+ // land in the specified partition (and INSERT OVERWRITE only
overwrites that partition).
+ if (!staticPartitionColNames.isEmpty()) {
+ for (Map.Entry<String, Expression> entry :
staticPartitions.entrySet()) {
+ Expression valueExpr = entry.getValue();
+ Column column = table.getColumn(entry.getKey());
+ if (column != null) {
+ Expression castExpr = TypeCoercionUtils.castIfNotSameType(
+ valueExpr,
DataType.fromCatalogType(column.getType()));
+ // Use the canonical (lowercase) column name from the
table schema as both the
+ // map key and the alias name, so it aligns with
getOutputProjectByCoercion which
+ // looks up columnToOutput by table.getFullSchema() column
names.
+ columnToOutput.put(column.getName(), new Alias(castExpr,
column.getName()));
+ }
+ }
+ }
+
LogicalProject<?> fullOutputProject =
getOutputProjectByCoercion(table.getFullSchema(), child, columnToOutput);
return boundSink.withChildAndUpdateOutput(fullOutputProject);
}
+ /**
+ * Validate static partition specification for Hive table.
+ * The specified columns must be partition columns of the target hive
table,
+ * and each partition value must be a literal.
+ */
+ private void validateHiveStaticPartition(Map<String, Expression>
staticPartitions, HMSExternalTable table) {
+ if (staticPartitions == null || staticPartitions.isEmpty()) {
+ return;
+ }
+ Set<String> partitionColNames = table.getPartitionColumnNames();
+ if (partitionColNames.isEmpty()) {
+ throw new AnalysisException(
+ String.format("Table %s is not a partitioned table, cannot
use static partition syntax",
+ table.getName()));
+ }
+ // build a case-insensitive view of partition column names
+ Set<String> lowerPartitionColNames = Sets.newHashSet();
+ for (String name : partitionColNames) {
+ lowerPartitionColNames.add(name.toLowerCase());
+ }
+ for (Map.Entry<String, Expression> entry :
staticPartitions.entrySet()) {
+ String partitionColName = entry.getKey();
+ Expression partitionValue = entry.getValue();
+ if
(!lowerPartitionColNames.contains(partitionColName.toLowerCase())) {
+ throw new AnalysisException(
+ String.format("Unknown partition column '%s' in table
'%s'. Available partition columns: %s",
+ partitionColName, table.getName(),
partitionColNames));
+ }
+ if (!(partitionValue instanceof Literal)) {
+ throw new AnalysisException(
+ String.format("Partition value for column '%s' must be
a literal, but got: %s",
+ partitionColName, partitionValue));
+ }
+ }
+ }
+
private Plan
bindIcebergTableSink(MatchingContext<UnboundIcebergTableSink<Plan>> ctx) {
UnboundIcebergTableSink<?> sink = ctx.root;
Pair<IcebergExternalDatabase, IcebergExternalTable> pair =
bind(ctx.cascadesContext, sink);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertOverwriteTableCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertOverwriteTableCommand.java
index 7d5d0e49e77..9817db7796c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertOverwriteTableCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertOverwriteTableCommand.java
@@ -375,7 +375,8 @@ public class InsertOverwriteTableCommand extends Command
implements NeedAuditEnc
false,
TPartialUpdateNewRowPolicy.APPEND,
sink.getDMLCommandType(),
- (LogicalPlan) (sink.child(0)));
+ (LogicalPlan) (sink.child(0)),
+ sink.getStaticPartitionKeyValues());
insertCtx = new HiveInsertCommandContext();
((HiveInsertCommandContext) insertCtx).setOverwrite(true);
} else if (logicalQuery instanceof UnboundIcebergTableSink) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java
index fa5e34046d1..b555c8790db 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java
@@ -99,6 +99,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -379,12 +380,24 @@ public class InsertUtils {
staticPartitions = ((UnboundIcebergTableSink<?>)
unboundLogicalSink).getStaticPartitionKeyValues();
} else if (unboundLogicalSink instanceof UnboundMaxComputeTableSink) {
staticPartitions = ((UnboundMaxComputeTableSink<?>)
unboundLogicalSink).getStaticPartitionKeyValues();
+ } else if (unboundLogicalSink instanceof UnboundHiveTableSink) {
+ staticPartitions = ((UnboundHiveTableSink<?>)
unboundLogicalSink).getStaticPartitionKeyValues();
}
if (staticPartitions != null && !staticPartitions.isEmpty()
&& CollectionUtils.isEmpty(unboundLogicalSink.getColNames())) {
- Set<String> staticPartitionColNames = staticPartitions.keySet();
+ // Static partition columns get their values from the PARTITION
clause instead of the
+ // inline VALUES list, so they must be excluded from the implicit
target schema before
+ // the column-count check below. Otherwise e.g. Hive table (v, dt)
with
+ // PARTITION(dt='x') VALUES (1) would be checked against 2 columns
and fail with
+ // "Column count doesn't match value count".
+ // Match case-insensitively via Locale.ROOT because partition
column names are
+ // case-insensitive (Hive stores them lowercase in HMS), avoiding
locale-dependent
+ // folding (e.g. the Turkish 'I').
+ Set<String> staticPartitionColNames =
staticPartitions.keySet().stream()
+ .map(name -> name.toLowerCase(Locale.ROOT))
+ .collect(Collectors.toSet());
columns = columns.stream()
- .filter(column ->
!staticPartitionColNames.contains(column.getName()))
+ .filter(column ->
!staticPartitionColNames.contains(column.getName().toLowerCase(Locale.ROOT)))
.collect(ImmutableList.toImmutableList());
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/hive/HiveDDLAndDMLPlanTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/hive/HiveDDLAndDMLPlanTest.java
index 5ff126ac30e..a7235b54420 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/hive/HiveDDLAndDMLPlanTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/hive/HiveDDLAndDMLPlanTest.java
@@ -573,6 +573,109 @@ public class HiveDDLAndDMLPlanTest extends
TestWithFeService {
checkPartTableSinkPlan(schema, partTargetTable,
physicalOverwriteSink2);
}
+ @Test
+ public void testInsertStaticPartitionPlanSql() throws Exception {
+ switchHive();
+ useDatabase(mockedDbName);
+ String insertTable = "static_partition_insert_table";
+ createTargetTable(insertTable);
+
+ List<Column> schema = new ArrayList<Column>() {
+ {
+ add(new Column("col1", PrimitiveType.INT));
+ add(new Column("pt1", PrimitiveType.VARCHAR, true));
+ add(new Column("pt2", PrimitiveType.STRING, true));
+ add(new Column("pt3", PrimitiveType.DATE, true));
+ }
+ };
+ Set<String> parts = new HashSet<String>() {
+ {
+ add("pt1");
+ add("pt2");
+ add("pt3");
+ }
+ };
+ mockTargetTable(schema, parts);
+ String partTargetTable = "part_" + insertTable;
+
+ String insertSql = "INSERT INTO " + partTargetTable
+ + " PARTITION(PT1='v1', pt2='v2', PT3='2020-03-13') SELECT 1";
+ PhysicalPlan physicalSink = getPhysicalPlan(insertSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), false);
+ checkPartTableSinkPlan(schema, partTargetTable, physicalSink);
+
+ String insertOverwriteSql = "INSERT OVERWRITE TABLE " + partTargetTable
+ + " PARTITION(PT1='v1', PT2='v2', PT3='2020-03-13') SELECT 1";
+ PhysicalPlan physicalOverwriteSink =
getPhysicalPlan(insertOverwriteSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), true);
+ checkPartTableSinkPlan(schema, partTargetTable, physicalOverwriteSink);
+
+ // Inline VALUES form without an explicit column list: the static
partition columns must be
+ // excluded from the implicit target schema, otherwise the single
VALUES row (col1 only)
+ // would be checked against all 4 columns and fail with "Column count
doesn't match".
+ String insertValuesSql = "INSERT INTO " + partTargetTable
+ + " PARTITION(PT1='v1', pt2='v2', PT3='2020-03-13') VALUES
(1)";
+ PhysicalPlan physicalValuesSink = getPhysicalPlan(insertValuesSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), false);
+ checkPartTableSinkPlan(schema, partTargetTable, physicalValuesSink);
+
+ String insertOverwriteValuesSql = "INSERT OVERWRITE TABLE " +
partTargetTable
+ + " PARTITION(PT1='v1', PT2='v2', PT3='2020-03-13') VALUES
(1)";
+ PhysicalPlan physicalOverwriteValuesSink =
getPhysicalPlan(insertOverwriteValuesSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), true);
+ checkPartTableSinkPlan(schema, partTargetTable,
physicalOverwriteValuesSink);
+ }
+
+ @Test
+ public void testInsertStaticPartitionErrorSql() throws Exception {
+ switchHive();
+ useDatabase(mockedDbName);
+ String insertTable = "static_partition_error_table";
+ createTargetTable(insertTable);
+
+ List<Column> schema = new ArrayList<Column>() {
+ {
+ add(new Column("col1", PrimitiveType.INT));
+ add(new Column("pt1", PrimitiveType.VARCHAR, true));
+ add(new Column("pt2", PrimitiveType.STRING, true));
+ add(new Column("pt3", PrimitiveType.DATE, true));
+ }
+ };
+ Set<String> parts = new HashSet<String>() {
+ {
+ add("pt1");
+ add("pt2");
+ add("pt3");
+ }
+ };
+ mockTargetTable(schema, parts);
+ String partTargetTable = "part_" + insertTable;
+
+ String nonLiteralPartitionSql = "INSERT INTO " + partTargetTable
+ + " PARTITION(pt1=concat('v', '1'), pt2='v2',
pt3='2020-03-13') SELECT 1";
+
ExceptionChecker.expectThrowsWithMsg(org.apache.doris.nereids.exceptions.AnalysisException.class,
+ "must be a literal",
+ () -> getPhysicalPlan(nonLiteralPartitionSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), false));
+
+ String duplicateStaticPartitionSql = "INSERT INTO " + partTargetTable
+ + " PARTITION(PT1='v1', pt2='v2', pt3='2020-03-13') (col1,
pt1) SELECT 1, 'dup'";
+
ExceptionChecker.expectThrowsWithMsg(org.apache.doris.nereids.exceptions.AnalysisException.class,
+ "is a static partition column",
+ () -> getPhysicalPlan(duplicateStaticPartitionSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), false));
+
+ // The same partition column specified twice with different casing is
a duplicate.
+ // Hive column names are case-insensitive, so pt1 and PT1 refer to the
same column and
+ // must be rejected instead of silently letting one value overwrite
the other.
+ String caseInsensitiveDupPartitionSql = "INSERT INTO " +
partTargetTable
+ + " PARTITION(pt1='v1', PT1='v2', pt2='v3', pt3='2020-03-13')
SELECT 1";
+
ExceptionChecker.expectThrowsWithMsg(org.apache.doris.nereids.exceptions.AnalysisException.class,
+ "Duplicate partition column",
+ () -> getPhysicalPlan(caseInsensitiveDupPartitionSql,
+ new PhysicalProperties(new
DistributionSpecHiveTableSinkHashPartitioned()), false));
+ }
+
private static void checkUnpartTableSinkPlan(List<Column> schema, String
unPartTargetTable, PhysicalPlan physicalSink) {
Assertions.assertSame(physicalSink.getType(),
PlanType.PHYSICAL_DISTRIBUTE);
// check exchange
@@ -742,3 +845,4 @@ public class HiveDDLAndDMLPlanTest extends
TestWithFeService {
checkedHiveCols.addAll(checkArrayCols);
}
}
+
diff --git
a/regression-test/suites/external_table_p0/hive/write/test_hive_write_static_partition.groovy
b/regression-test/suites/external_table_p0/hive/write/test_hive_write_static_partition.groovy
new file mode 100644
index 00000000000..9160a23c307
--- /dev/null
+++
b/regression-test/suites/external_table_p0/hive/write/test_hive_write_static_partition.groovy
@@ -0,0 +1,383 @@
+// 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_hive_write_static_partition",
"p0,external,hive,external_docker,external_docker_hive") {
+
+ // Static partition insert:
+ // INSERT [OVERWRITE] TABLE t PARTITION(col='val', ...) SELECT ...
+ // The partition column value comes from the PARTITION clause instead of
the query output.
+ def testStaticPartitionWrite = { String catalog_name ->
+ String tableName = "hive_static_par_tbl"
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ hive_docker """
+ CREATE TABLE ${tableName} (
+ tag_value string,
+ user_id string,
+ ts int
+ )
+ PARTITIONED BY (ts_date string)
+ STORED AS parquet;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ // 1. INSERT INTO ... PARTITION(ts_date='2026-07-24') without listing
the partition column in SELECT.
+ // This used to fail with "insert into cols should be corresponding
to the query output".
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-24')
+ SELECT 'tagA', 'u1', 100;
+ """
+ // 2. Append another row into the SAME partition.
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-24')
+ SELECT 'tagB', 'u2', 200;
+ """
+ // 3. Insert into a DIFFERENT partition.
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-25')
+ SELECT 'tagC', 'u3', 300;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ // Verify: 2 rows in partition 2026-07-24 and 1 row in 2026-07-25,
partition column filled correctly.
+ def all = sql """ select tag_value, user_id, ts, ts_date from
${tableName} order by ts_date, ts; """
+ assertEquals(3, all.size())
+ assertEquals("tagA", all[0][0]); assertEquals("2026-07-24", all[0][3])
+ assertEquals("tagB", all[1][0]); assertEquals("2026-07-24", all[1][3])
+ assertEquals("tagC", all[2][0]); assertEquals("2026-07-25", all[2][3])
+
+ // Partition pruning should work on the statically written partition
value.
+ def p24 = sql """ select tag_value from ${tableName} where
ts_date='2026-07-24' order by ts; """
+ assertEquals(2, p24.size())
+
+ // 4. INSERT OVERWRITE a single partition: it should only replace
2026-07-24, leaving 2026-07-25 intact.
+ sql """
+ INSERT OVERWRITE TABLE ${tableName} PARTITION(ts_date='2026-07-24')
+ SELECT 'tagX', 'u9', 999;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ def afterOverwrite = sql """ select tag_value, user_id, ts, ts_date
from ${tableName} order by ts_date, ts; """
+ assertEquals(2, afterOverwrite.size())
+ // partition 2026-07-24 replaced by a single row
+ assertEquals("tagX", afterOverwrite[0][0]); assertEquals(999,
afterOverwrite[0][2])
+ assertEquals("2026-07-24", afterOverwrite[0][3])
+ // partition 2026-07-25 untouched
+ assertEquals("tagC", afterOverwrite[1][0]); assertEquals("2026-07-25",
afterOverwrite[1][3])
+
+ // 5. Static partition combined with an explicit (non-partition)
column list.
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-26')
(tag_value, user_id, ts)
+ SELECT 'tagD', 'u4', 400;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+ def p26 = sql """ select tag_value, ts_date from ${tableName} where
ts_date='2026-07-26'; """
+ assertEquals(1, p26.size())
+ assertEquals("tagD", p26[0][0])
+
+ // 6. Partition column name matching should be case-insensitive.
+ sql """
+ INSERT INTO ${tableName} PARTITION(TS_DATE='2026-07-27')
+ SELECT 'tagE', 'u5', 500;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+ def p27 = sql """ select tag_value, ts_date from ${tableName} where
ts_date='2026-07-27'; """
+ assertEquals(1, p27.size())
+ assertEquals("tagE", p27[0][0])
+ assertEquals("2026-07-27", p27[0][1])
+
+ // 7. Inline VALUES form (no explicit column list): partition column
value comes from the
+ // PARTITION clause, so VALUES only supplies the non-partition
columns.
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-28')
+ VALUES ('tagF', 'u6', 600);
+ """
+ sql """ refresh catalog ${catalog_name}; """
+ def p28 = sql """ select tag_value, user_id, ts, ts_date from
${tableName} where ts_date='2026-07-28'; """
+ assertEquals(1, p28.size())
+ assertEquals("tagF", p28[0][0]); assertEquals("u6", p28[0][1])
+ assertEquals(600, p28[0][2]); assertEquals("2026-07-28", p28[0][3])
+
+ // 8. INSERT OVERWRITE with inline VALUES should only replace the
targeted partition.
+ sql """
+ INSERT OVERWRITE TABLE ${tableName} PARTITION(ts_date='2026-07-28')
+ VALUES ('tagG', 'u7', 700);
+ """
+ sql """ refresh catalog ${catalog_name}; """
+ def p28ow = sql """ select tag_value, ts, ts_date from ${tableName}
where ts_date='2026-07-28'; """
+ assertEquals(1, p28ow.size())
+ assertEquals("tagG", p28ow[0][0]); assertEquals(700, p28ow[0][1])
+ assertEquals("2026-07-28", p28ow[0][2])
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ }
+
+ def testStaticPartitionMultiColumnWrite = { String catalog_name ->
+ String tableName = "hive_static_multi_par_tbl"
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ hive_docker """
+ CREATE TABLE ${tableName} (
+ tag_value string,
+ user_id string,
+ ts int
+ )
+ PARTITIONED BY (dt string, region string)
+ STORED AS parquet;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ sql """
+ INSERT INTO ${tableName} PARTITION(dt='2026-07-24', region='bj')
+ SELECT 'tagM1', 'u10', 10;
+ """
+ sql """
+ INSERT INTO ${tableName} PARTITION(dt='2026-07-24', region='sh')
+ SELECT 'tagM2', 'u11', 20;
+ """
+ sql """
+ INSERT INTO ${tableName} PARTITION(dt='2026-07-25', region='bj')
+ SELECT 'tagM3', 'u12', 30;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ def beforeOverwrite = sql """
+ select tag_value, dt, region from ${tableName}
+ order by dt, region, tag_value;
+ """
+ assertEquals(3, beforeOverwrite.size())
+ assertEquals("tagM1", beforeOverwrite[0][0]);
assertEquals("2026-07-24", beforeOverwrite[0][1]); assertEquals("bj",
beforeOverwrite[0][2])
+ assertEquals("tagM2", beforeOverwrite[1][0]);
assertEquals("2026-07-24", beforeOverwrite[1][1]); assertEquals("sh",
beforeOverwrite[1][2])
+ assertEquals("tagM3", beforeOverwrite[2][0]);
assertEquals("2026-07-25", beforeOverwrite[2][1]); assertEquals("bj",
beforeOverwrite[2][2])
+
+ sql """
+ INSERT OVERWRITE TABLE ${tableName} PARTITION(dt='2026-07-24',
region='bj')
+ SELECT 'tagMX', 'u19', 999;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ def afterOverwrite = sql """
+ select tag_value, dt, region from ${tableName}
+ order by dt, region, tag_value;
+ """
+ assertEquals(3, afterOverwrite.size())
+ assertEquals("tagMX", afterOverwrite[0][0]);
assertEquals("2026-07-24", afterOverwrite[0][1]); assertEquals("bj",
afterOverwrite[0][2])
+ assertEquals("tagM2", afterOverwrite[1][0]);
assertEquals("2026-07-24", afterOverwrite[1][1]); assertEquals("sh",
afterOverwrite[1][2])
+ assertEquals("tagM3", afterOverwrite[2][0]);
assertEquals("2026-07-25", afterOverwrite[2][1]); assertEquals("bj",
afterOverwrite[2][2])
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ }
+
+ // Hybrid static/dynamic partition write: the PARTITION clause fixes only
a prefix of the
+ // partition columns (dt), while the remaining partition column (region)
stays dynamic and is
+ // sourced from the query output. A single statement can therefore emit
multiple dynamic
+ // partition values (region) under the same static prefix (dt). This
exercises a distinct sink
+ // routing path (synthesized dt slot + child-sourced region slot).
+ def testHybridStaticDynamicPartitionWrite = { String catalog_name ->
+ String tableName = "hive_hybrid_par_tbl"
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ hive_docker """
+ CREATE TABLE ${tableName} (
+ tag_value string,
+ user_id string,
+ ts int
+ )
+ PARTITIONED BY (dt string, region string)
+ STORED AS parquet;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ // 1. One INSERT with static dt='2026-07-24' emitting TWO dynamic
regions (bj, sh).
+ // The SELECT only supplies non-static columns: tag_value, user_id,
ts, region.
+ sql """
+ INSERT INTO ${tableName} PARTITION(dt='2026-07-24')
+ SELECT 'h_bj', 'u1', 10, 'bj'
+ UNION ALL
+ SELECT 'h_sh', 'u2', 20, 'sh';
+ """
+ // 2. A sibling static prefix dt='2026-07-25', also emitting two
dynamic regions.
+ sql """
+ INSERT INTO ${tableName} PARTITION(dt='2026-07-25')
+ SELECT 's_bj', 'u3', 30, 'bj'
+ UNION ALL
+ SELECT 's_gz', 'u4', 40, 'gz';
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ def all = sql """
+ select tag_value, user_id, ts, dt, region from ${tableName}
+ order by dt, region;
+ """
+ assertEquals(4, all.size())
+ assertEquals("h_bj", all[0][0]); assertEquals(10, all[0][2]);
assertEquals("2026-07-24", all[0][3]); assertEquals("bj", all[0][4])
+ assertEquals("h_sh", all[1][0]); assertEquals(20, all[1][2]);
assertEquals("2026-07-24", all[1][3]); assertEquals("sh", all[1][4])
+ assertEquals("s_bj", all[2][0]); assertEquals(30, all[2][2]);
assertEquals("2026-07-25", all[2][3]); assertEquals("bj", all[2][4])
+ assertEquals("s_gz", all[3][0]); assertEquals(40, all[3][2]);
assertEquals("2026-07-25", all[3][3]); assertEquals("gz", all[3][4])
+
+ // Partition pruning should work on both the static and the dynamic
partition columns.
+ def bjRows = sql """ select tag_value from ${tableName} where
region='bj' order by dt; """
+ assertEquals(2, bjRows.size())
+ assertEquals("h_bj", bjRows[0][0]); assertEquals("s_bj", bjRows[1][0])
+
+ // 3. Hybrid INSERT OVERWRITE under static dt='2026-07-24' re-emitting
the same two regions.
+ // It must only replace the dt='2026-07-24' partitions and leave
the dt='2026-07-25'
+ // sibling prefix completely untouched.
+ sql """
+ INSERT OVERWRITE TABLE ${tableName} PARTITION(dt='2026-07-24')
+ SELECT 'o_bj', 'u5', 50, 'bj'
+ UNION ALL
+ SELECT 'o_sh', 'u6', 60, 'sh';
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ def afterOverwrite = sql """
+ select tag_value, user_id, ts, dt, region from ${tableName}
+ order by dt, region;
+ """
+ assertEquals(4, afterOverwrite.size())
+ // dt='2026-07-24' partitions replaced with new values.
+ assertEquals("o_bj", afterOverwrite[0][0]); assertEquals(50,
afterOverwrite[0][2]); assertEquals("2026-07-24", afterOverwrite[0][3]);
assertEquals("bj", afterOverwrite[0][4])
+ assertEquals("o_sh", afterOverwrite[1][0]); assertEquals(60,
afterOverwrite[1][2]); assertEquals("2026-07-24", afterOverwrite[1][3]);
assertEquals("sh", afterOverwrite[1][4])
+ // dt='2026-07-25' sibling prefix untouched.
+ assertEquals("s_bj", afterOverwrite[2][0]); assertEquals(30,
afterOverwrite[2][2]); assertEquals("2026-07-25", afterOverwrite[2][3]);
assertEquals("bj", afterOverwrite[2][4])
+ assertEquals("s_gz", afterOverwrite[3][0]); assertEquals(40,
afterOverwrite[3][2]); assertEquals("2026-07-25", afterOverwrite[3][3]);
assertEquals("gz", afterOverwrite[3][4])
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ }
+
+ // Error cases for static partition validation.
+ def testStaticPartitionErrors = { String catalog_name ->
+ String tableName = "hive_static_par_err_tbl"
+ String nonParTableName = "hive_static_par_nonpar_tbl"
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ hive_docker """
+ CREATE TABLE ${tableName} (
+ tag_value string,
+ user_id string,
+ ts int
+ )
+ PARTITIONED BY (ts_date string)
+ STORED AS parquet;
+ """
+ hive_docker """ DROP TABLE IF EXISTS ${nonParTableName}; """
+ hive_docker """
+ CREATE TABLE ${nonParTableName} (
+ tag_value string,
+ user_id string
+ )
+ STORED AS parquet;
+ """
+ sql """ refresh catalog ${catalog_name}; """
+
+ // 5.1 partition column not exists
+ test {
+ sql """
+ INSERT INTO ${tableName} PARTITION(not_exist_col='2026-07-24')
+ SELECT 'tagA', 'u1', 100;
+ """
+ exception "Unknown partition column"
+ }
+
+ // 5.2 static partition column also appears in the insert column list
+ test {
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-24')
(tag_value, user_id, ts, ts_date)
+ SELECT 'tagA', 'u1', 100, '2026-07-24';
+ """
+ exception "is a static partition column"
+ }
+
+ // 5.3 use static partition syntax on a non-partitioned table
+ test {
+ sql """
+ INSERT INTO ${nonParTableName} PARTITION(ts_date='2026-07-24')
+ SELECT 'tagA', 'u1';
+ """
+ exception "is not a partitioned table"
+ }
+
+ // 5.4 partition value must be a literal instead of an expression
+ test {
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date=concat('2026-07',
'-24'))
+ SELECT 'tagA', 'u1', 100;
+ """
+ exception "must be a literal"
+ }
+
+ // 5.5 case-insensitive partition column name should also be
recognized in insert column list validation
+ test {
+ sql """
+ INSERT INTO ${tableName} PARTITION(TS_DATE='2026-07-24')
(tag_value, user_id, ts, ts_date)
+ SELECT 'tagA', 'u1', 100, '2026-07-24';
+ """
+ exception "is a static partition column"
+ }
+
+ // 5.6 the same partition column specified twice with different casing
is a duplicate.
+ // Hive column names are case-insensitive, so ts_date and TS_DATE
are the same column.
+ // This must fail fast instead of silently letting one value
overwrite the other.
+ test {
+ sql """
+ INSERT INTO ${tableName} PARTITION(ts_date='2026-07-24',
TS_DATE='2026-07-25')
+ SELECT 'tagA', 'u1', 100;
+ """
+ exception "Duplicate partition column"
+ }
+
+ hive_docker """ DROP TABLE IF EXISTS ${tableName}; """
+ hive_docker """ DROP TABLE IF EXISTS ${nonParTableName}; """
+ }
+
+ String enabled = context.config.otherConfigs.get("enableHiveTest")
+ if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+ logger.info("disable Hive test.")
+ return;
+ }
+
+ for (String hivePrefix : ["hive2", "hive3"]) {
+ setHivePrefix(hivePrefix)
+ try {
+ String hms_port = context.config.otherConfigs.get(hivePrefix +
"HmsPort")
+ String hdfs_port = context.config.otherConfigs.get(hivePrefix +
"HdfsPort")
+ String catalog_name = "test_${hivePrefix}_write_static_partition"
+ String externalEnvIp =
context.config.otherConfigs.get("externalEnvIp")
+
+ sql """drop catalog if exists ${catalog_name}"""
+ sql """create catalog if not exists ${catalog_name} properties (
+ 'type'='hms',
+ 'hive.metastore.uris' =
'thrift://${externalEnvIp}:${hms_port}',
+ 'fs.defaultFS' = 'hdfs://${externalEnvIp}:${hdfs_port}'
+ );"""
+ sql """use `${catalog_name}`.`write_test`"""
+ logger.info("hive sql: use `write_test`")
+ hive_docker """use `write_test`"""
+
+ sql """set enable_fallback_to_original_planner=false;"""
+
+ testStaticPartitionWrite(catalog_name)
+ testStaticPartitionMultiColumnWrite(catalog_name)
+ testHybridStaticDynamicPartitionWrite(catalog_name)
+ testStaticPartitionErrors(catalog_name)
+
+ sql """drop catalog if exists ${catalog_name}"""
+ } finally {
+ }
+ }
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]