github-actions[bot] commented on code in PR #65991:
URL: https://github.com/apache/doris/pull/65991#discussion_r3643543363
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertOverwriteTableCommand.java:
##########
@@ -375,7 +375,8 @@ private void insertIntoPartitions(ConnectContext ctx,
StmtExecutor executor, Lis
false,
TPartialUpdateNewRowPolicy.APPEND,
sink.getDMLCommandType(),
- (LogicalPlan) (sink.child(0)));
+ (LogicalPlan) (sink.child(0)),
+ sink.getStaticPartitionKeyValues());
Review Comment:
[P1] Preserve the static target for a zero-row overwrite
`INSERT OVERWRITE TABLE t PARTITION(dt='2026-07-24') SELECT ... WHERE false`
must replace that named partition with an empty result. Here the map is
preserved only on the copied sink and later becomes per-row constants;
`HiveInsertCommandContext` gets no row-independent target. A planned
`PhysicalEmptyRelation` returns before transaction/finalization, while a
runtime-empty query emits no `THivePartitionUpdate` and `HMSTransaction`
synthesizes empty overwrite work only for unpartitioned tables. Both paths
leave the old partition intact. Please carry the canonical fully-static target
into Hive overwrite finalization and add an empty-result regression that
verifies sibling partitions are unchanged.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java:
##########
@@ -690,11 +690,35 @@ private Plan
bindHiveTableSink(MatchingContext<UnboundHiveTableSink<Plan>> ctx)
+ "(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();
+ Set<String> lowerStaticPartitionColNames = Sets.newHashSet();
+ for (String name : staticPartitionColNames) {
+ lowerStaticPartitionColNames.add(name.toLowerCase());
Review Comment:
[P2] Canonicalize static partition keys once
This lowercased set is not a safe identifier-resolution boundary.
`PARTITION(pt='a', PT='b')` produces two parser-map entries; both validate
here, then the case-insensitive `columnToOutput` map silently lets the latter
value replace the former. The zero-argument `toLowerCase()` calls are also
locale-dependent (for example, Turkish `ID` does not match metadata column
`id`). Resolve each key to the canonical Hive `Column` using locale-independent
matching, reject a second key resolving to the same column, and use that
canonical name for arity checks and aliases.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java:
##########
@@ -715,15 +739,69 @@ private Plan
bindHiveTableSink(MatchingContext<UnboundHiveTableSink<Plan>> ctx)
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()) {
+ String colName = entry.getKey();
+ Expression valueExpr = entry.getValue();
+ Column column = table.getColumn(colName);
+ if (column != null) {
+ Expression castExpr = TypeCoercionUtils.castIfNotSameType(
Review Comment:
[P1] Keep overwrite semantics when the target is classified as new
A static overwrite of the default partition can append instead of replace.
For `PARTITION(p='')`, the BE compares the raw value `""` to cached HMS value
`__HIVE_DEFAULT_PARTITION__`, misses the existing partition, and emits `NEW`;
`HMSTransaction.finishInsertTable` then discovers the partition in HMS but
unconditionally converts that update to `INSERT_EXISTING`, even though this is
an overwrite. A stale partition-cache miss has the same result. Please
normalize values consistently, preserve overwrite intent when a `NEW` target
resolves to an existing HMS partition, and propagate the corrected mode to
cache refresh; add repeated empty-string and cache-miss overwrite coverage.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSinkCreator.java:
##########
@@ -98,7 +98,7 @@ public static LogicalSink<? extends Plan>
createUnboundTableSink(List<String> na
Optional.empty(), plan);
} else if (curCatalog instanceof HMSExternalCatalog) {
return new UnboundHiveTableSink<>(nameParts, colNames, hints,
partitions,
Review Comment:
[P1] Include Hive in static-partition VALUES normalization
Adding the static map to the Hive sink is not enough for an inline `VALUES`
child. `InsertUtils.normalizePlanWithoutLock` removes static columns from the
implicit target schema only for Iceberg and MaxCompute, so for Hive table `(v,
dt)`, `PARTITION(dt='x') VALUES (1)` is checked against two columns and fails
with `Column count doesn't match value count` before `BindSink` runs. This
affects both INSERT and OVERWRITE without an explicit column list. Please
include Hive through a shared static-partition-sink path, using canonical key
matching, and add INSERT/OVERWRITE `VALUES` coverage.
##########
regression-test/suites/external_table_p0/hive/write/test_hive_write_static_partition.groovy:
##########
@@ -0,0 +1,270 @@
+// 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; """
Review Comment:
[P2] Use the regression golden-output contract
These deterministic result checks use `sql` plus `assertEquals`, but the
repository instructions require `qt`/`order_qt` and an auto-generated `.out`;
no result file is included in this PR. The suite also drops its tables after
execution even though test artifacts should be retained for failed-run
debugging (the pre-test drops are sufficient). Please convert the result checks
to uniquely named golden queries, generate/commit the `.out`, and remove the
post-test cleanup.
##########
regression-test/suites/external_table_p0/hive/write/test_hive_write_static_partition.groovy:
##########
@@ -0,0 +1,270 @@
+// 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} (tag_value, user_id, ts)
PARTITION(ts_date='2026-07-26')
Review Comment:
[P1] Put `PARTITION` before the explicit column list
The active Nereids insert grammar is `tableName partitionSpec? ... cols?
query`, so this `INSERT INTO t (cols) PARTITION(...)` form fails at the
`PARTITION` token and never exercises explicit-column Hive binding. The same
reversed order is used by the two negative regression cases and
`HiveDDLAndDMLPlanTest`, so those assertions cannot reach the intended
`BindSink` error either. Please use `INSERT INTO t PARTITION(...) (cols) SELECT
...` in both test files and keep a successful explicit-column regression in
that supported order.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java:
##########
@@ -715,15 +739,69 @@ private Plan
bindHiveTableSink(MatchingContext<UnboundHiveTableSink<Plan>> ctx)
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()) {
+ String colName = entry.getKey();
+ Expression valueExpr = entry.getValue();
+ Column column = table.getColumn(colName);
+ if (column != null) {
+ Expression castExpr = TypeCoercionUtils.castIfNotSameType(
+ valueExpr,
DataType.fromCatalogType(column.getType()));
+ columnToOutput.put(colName, new Alias(castExpr, colName));
+ }
+ }
+ }
+
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)) {
Review Comment:
[P2] Accept signed numeric literals
`PARTITION(p=-1)` is a valid constant specification, but the parser
represents unary minus as `Subtract(0, 1)`, not as a `Literal`. Because this
static map is not expression-analyzed, it reaches this `instanceof Literal`
check unchanged and is rejected, while `+1` passes. Please fold/normalize
signed numeric constants before validation (while still rejecting nonconstant
expressions), and cover negative integral and decimal values for INSERT and
OVERWRITE.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]