This is an automated email from the ASF dual-hosted git repository.
bobhan1 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 c02808aae79 [fix](fe) Handle generated columns in delete partial
update (#64884)
c02808aae79 is described below
commit c02808aae79b29e8c02e48f06baf89b43d4c4152
Author: bobhan1 <[email protected]>
AuthorDate: Tue Jun 30 16:37:50 2026 +0800
[fix](fe) Handle generated columns in delete partial update (#64884)
### What problem does this PR solve?
Issue Number: N/A
Related PR: N/A
Original Problem: DELETE failed during analysis on a unique
merge-on-write table that contains a generated column when light delete
is disabled. The original reported table had a generated VARIANT column:
```sql
new_col variant AS (receive_address_detail) NULL
```
and the DELETE failed with:
```text
errCode = 2, detailMessage = cannot find column from target table
/receive_address_detail
```
A minimized reproduction is:
```sql
create table test_gen_col_mow_delete_partial_update (
a int,
b int,
c int as (b + 1),
d int
)
unique key(a)
distributed by hash(a) buckets 1
properties (
"enable_unique_key_merge_on_write" = "true",
"enable_mow_light_delete" = "false",
"replication_num" = "1"
);
```
After inserting rows, running:
```sql
delete from test_gen_col_mow_delete_partial_update where a = 1;
```
failed with:
```text
java.sql.SQLException: errCode = 2, detailMessage = cannot find column from
target table [b]
```
Problem Summary: DELETE on a unique merge-on-write table with light
delete disabled is rewritten to a partial update load that writes key
columns and the delete sign. BindSink used to auto-add every generated
column and then recompute omitted generated columns, which required
ordinary value columns that were not part of the DELETE output and
caused analysis to fail.
This change treats DELETE partial updates specially: generated columns
that are not emitted by the child plan are skipped, and generated
columns that are emitted by the child plan use that child output
directly. Normal partial update generated-column dependency checks are
unchanged.
The tests cover generated value columns, generated key columns, a
generated value-column table with an omitted NOT NULL value column that
has no default value, and the original VARIANT generated-column shape
based on `receive_address_detail`.
### Release note
Fix DELETE failure on unique merge-on-write tables with generated
columns when light delete is disabled.
### Check List (For Author)
- Test: Regression test / Unit Test
- `./build.sh --fe -j100`
- `./run-fe-ut.sh --run
org.apache.doris.nereids.trees.plans.DeleteFromUsingCommandTest`
- `./run-regression-test.sh --run -d
regression-test/suites/ddl_p0/test_create_table_generated_column -s
test_generated_column_delete -forceGenOut`
- `./run-regression-test.sh --run -d
regression-test/suites/ddl_p0/test_create_table_generated_column -s
test_generated_column_delete`
- `./run-regression-test.sh --run -d
regression-test/suites/ddl_p0/test_create_table_generated_column -s
test_partial_update_generated_column`
- Behavior changed: Yes (DELETE now succeeds for unique merge-on-write
tables with generated columns when light delete is disabled.)
- Does this need documentation: No
---
.../doris/nereids/rules/analysis/BindSink.java | 30 +++++--
.../trees/plans/DeleteFromUsingCommandTest.java | 89 ++++++++++++++++++++
.../test_delete_generated_column.out | 12 +++
.../test_delete_generated_column.groovy | 96 ++++++++++++++++++++++
4 files changed, 219 insertions(+), 8 deletions(-)
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 d7da7498ba7..8c580171b21 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
@@ -183,6 +183,7 @@ public class BindSink implements AnalysisRuleFactory {
DatabaseIf database = pair.first;
OlapTable table = pair.second;
boolean isPartialUpdate = sink.isPartialUpdate() &&
table.getKeysType() == KeysType.UNIQUE_KEYS;
+ boolean isDeletePartialUpdate = isPartialUpdate &&
sink.getDMLCommandType() == DMLCommandType.DELETE;
TPartialUpdateNewRowPolicy partialUpdateNewKeyPolicy =
sink.getPartialUpdateNewRowPolicy();
LogicalPlan child = ((LogicalPlan) sink.child());
@@ -194,7 +195,7 @@ public class BindSink implements AnalysisRuleFactory {
// 1. bind target columns: from sink's column names to target tables'
Columns
Pair<List<Column>, Integer> bindColumnsResult =
bindTargetColumns(table, sink.getColNames(), childHasSeqCol,
needExtraSeqCol,
- sink.getDMLCommandType() ==
DMLCommandType.GROUP_COMMIT);
+ sink.getDMLCommandType() ==
DMLCommandType.GROUP_COMMIT, isDeletePartialUpdate);
List<Column> bindColumns = bindColumnsResult.first;
int extraColumnsNum = bindColumnsResult.second;
@@ -279,7 +280,7 @@ public class BindSink implements AnalysisRuleFactory {
}
Map<String, NamedExpression> columnToOutput = getColumnToOutput(
- ctx, table, isPartialUpdate, boundSink, child);
+ ctx, table, isPartialUpdate, isDeletePartialUpdate, boundSink,
child);
LogicalProject<?> fullOutputProject = getOutputProjectByCoercion(
table.getFullSchema(), child, columnToOutput);
List<Column> columns = new ArrayList<>(table.getFullSchema().size());
@@ -371,7 +372,8 @@ public class BindSink implements AnalysisRuleFactory {
private static Map<String, NamedExpression> getColumnToOutput(
MatchingContext<? extends UnboundLogicalSink<Plan>> ctx,
- TableIf table, boolean isPartialUpdate, LogicalTableSink<?>
boundSink, LogicalPlan child) {
+ TableIf table, boolean isPartialUpdate, boolean
isDeletePartialUpdate,
+ LogicalTableSink<?> boundSink, LogicalPlan child) {
// we need to insert all the columns of the target table
// although some columns are not mentions.
// so we add a projects to supply the default value.
@@ -494,6 +496,18 @@ public class BindSink implements AnalysisRuleFactory {
// if processed in upper for loop, will lead to not found slot error
// It's the same reason for moving the processing of materialized
columns down.
for (Column column : generatedColumns) {
+ if (isDeletePartialUpdate) {
+ NamedExpression childOutput = columnToChildOutput.get(column);
+ if (childOutput == null) {
+ continue;
+ }
+ Alias output = new Alias(TypeCoercionUtils.castIfNotSameType(
+ childOutput,
DataType.fromCatalogType(column.getType())), column.getName());
+ columnToOutput.put(column.getName(), output);
+ columnToReplaced.put(column.getName(), output.toSlot());
+ replaceMap.put(output.toSlot(), output.child());
+ continue;
+ }
Map<String, String> currentSessionVars =
ctx.connectContext.getSessionVariable().getAffectQueryResultInPlanVariables();
try (AutoCloseSessionVariable autoClose = new
AutoCloseSessionVariable(ctx.connectContext,
@@ -704,7 +718,7 @@ public class BindSink implements AnalysisRuleFactory {
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,
+ Map<String, NamedExpression> columnToOutput = getColumnToOutput(ctx,
table, false, false,
boundSink, child);
LogicalProject<?> fullOutputProject =
getOutputProjectByCoercion(table.getFullSchema(), child, columnToOutput);
return boundSink.withChildAndUpdateOutput(fullOutputProject);
@@ -780,7 +794,7 @@ public class BindSink implements AnalysisRuleFactory {
+ "Expected " + boundSink.getCols().size() + " columns but
got " + child.getOutput().size());
}
- Map<String, NamedExpression> columnToOutput = getColumnToOutput(ctx,
table, false,
+ Map<String, NamedExpression> columnToOutput = getColumnToOutput(ctx,
table, false, false,
boundSink, child);
// For static partition columns, add constant expressions from
PARTITION clause
@@ -905,7 +919,7 @@ public class BindSink implements AnalysisRuleFactory {
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,
+ Map<String, NamedExpression> columnToOutput = getColumnToOutput(ctx,
table, false, false,
boundSink, child);
LogicalProject<?> fullOutputProject =
getOutputProjectByCoercion(table.getFullSchema(), child, columnToOutput);
return boundSink.withChildAndUpdateOutput(fullOutputProject);
@@ -1132,7 +1146,7 @@ public class BindSink implements AnalysisRuleFactory {
// bindTargetColumns means bind sink node's target columns' names to
target table's columns
private Pair<List<Column>, Integer> bindTargetColumns(OlapTable table,
List<String> colsName,
- boolean childHasSeqCol, boolean needExtraSeqCol, boolean
isGroupCommit) {
+ boolean childHasSeqCol, boolean needExtraSeqCol, boolean
isGroupCommit, boolean isDeletePartialUpdate) {
// if the table set sequence column in stream load phase, the sequence
map column is null, we query it.
if (colsName.isEmpty()) {
// ATTN: group commit without column list should return all base
index column
@@ -1150,7 +1164,7 @@ public class BindSink implements AnalysisRuleFactory {
++extraColumnsNum;
processedColsName.add(col.getName());
}
- } else if (col.isGeneratedColumn()) {
+ } else if (col.isGeneratedColumn() && !isDeletePartialUpdate) {
++extraColumnsNum;
processedColsName.add(col.getName());
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/DeleteFromUsingCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/DeleteFromUsingCommandTest.java
index bf5f660e837..04531e50999 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/DeleteFromUsingCommandTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/DeleteFromUsingCommandTest.java
@@ -68,6 +68,60 @@ public class DeleteFromUsingCommandTest extends
TestWithFeService implements Pla
+ "properties(\n"
+ " \"replication_num\"=\"1\"\n"
+ ")");
+ createTable("create table gen_value (\n"
+ + " a int,\n"
+ + " b int,\n"
+ + " c int as (b + 1),\n"
+ + " d int\n"
+ + ")\n"
+ + "unique key(a)\n"
+ + "distributed by hash(a) buckets 4\n"
+ + "properties(\n"
+ + " \"replication_num\"=\"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\",\n"
+ + " \"enable_mow_light_delete\" = \"false\"\n"
+ + ")");
+ createTable("create table gen_key (\n"
+ + " a int,\n"
+ + " c int as (b + 1),\n"
+ + " b int,\n"
+ + " d int\n"
+ + ")\n"
+ + "unique key(a, c)\n"
+ + "distributed by hash(a) buckets 4\n"
+ + "properties(\n"
+ + " \"replication_num\"=\"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\",\n"
+ + " \"enable_mow_light_delete\" = \"false\"\n"
+ + ")");
+ createTable("create table gen_value_required (\n"
+ + " a int,\n"
+ + " b int,\n"
+ + " c int as (b + 1),\n"
+ + " d int not null\n"
+ + ")\n"
+ + "unique key(a)\n"
+ + "distributed by hash(a) buckets 4\n"
+ + "properties(\n"
+ + " \"replication_num\"=\"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\",\n"
+ + " \"enable_mow_light_delete\" = \"false\"\n"
+ + ")");
+ createTable("create table gen_variant (\n"
+ + " id int not null,\n"
+ + " create_time datetime not null,\n"
+ + " order_no varchar(128) not null,\n"
+ + " receive_address_detail varchar(1024) not null default
\"{}\",\n"
+ + " d int not null,\n"
+ + " new_col variant as (receive_address_detail) null\n"
+ + ")\n"
+ + "unique key(id, create_time, order_no)\n"
+ + "distributed by hash(order_no) buckets 4\n"
+ + "properties(\n"
+ + " \"replication_num\"=\"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\",\n"
+ + " \"enable_mow_light_delete\" = \"false\"\n"
+ + ")");
}
@Test
@@ -106,4 +160,39 @@ public class DeleteFromUsingCommandTest extends
TestWithFeService implements Pla
)
);
}
+
+ @Test
+ public void testDeletePartialUpdateWithGeneratedValueColumn() {
+ assertDeletePartialUpdateAnalyze("delete from gen_value t using src
where t.a = src.k1");
+ }
+
+ @Test
+ public void testDeletePartialUpdateWithGeneratedKeyColumn() {
+ assertDeletePartialUpdateAnalyze("delete from gen_key t using src
where t.a = src.k1");
+ }
+
+ @Test
+ public void testDeletePartialUpdateWithNotNullValueColumn() {
+ assertDeletePartialUpdateAnalyze("delete from gen_value_required t
using src where t.a = src.k1");
+ }
+
+ @Test
+ public void testDeletePartialUpdateWithVariantGeneratedColumn() {
+ assertDeletePartialUpdateAnalyze("delete from gen_variant t using src
where t.id = src.k1");
+ }
+
+ private void assertDeletePartialUpdateAnalyze(String sql) {
+ LogicalPlan parsed = new NereidsParser().parseSingle(sql);
+ Assertions.assertInstanceOf(DeleteFromUsingCommand.class, parsed);
+ DeleteFromUsingCommand command = ((DeleteFromUsingCommand) parsed);
+ LogicalPlan plan = command.completeQueryPlan(connectContext,
command.getLogicalQuery());
+ PlanChecker.from(connectContext, plan)
+ .analyze(plan)
+ .rewrite()
+ .matches(
+ logicalOlapTableSink(
+ logicalProject()
+ )
+ );
+ }
}
diff --git
a/regression-test/data/ddl_p0/test_create_table_generated_column/test_delete_generated_column.out
b/regression-test/data/ddl_p0/test_create_table_generated_column/test_delete_generated_column.out
index 2441607cf64..efaa9bc707a 100644
---
a/regression-test/data/ddl_p0/test_create_table_generated_column/test_delete_generated_column.out
+++
b/regression-test/data/ddl_p0/test_create_table_generated_column/test_delete_generated_column.out
@@ -27,3 +27,15 @@
1 2 3
10 2 12
+-- !delete_mow_without_light_delete_generated_column --
+2 20 21 200
+
+-- !delete_mow_without_light_delete_generated_key_column --
+2 21 20 200
+
+-- !delete_mow_without_light_delete_generated_column_with_not_null_value --
+2 20 21 200
+
+-- !delete_mow_without_light_delete_variant_generated_column --
+2 2026-06-08T11:00 order-2 {"city":"bj"} 200
+
diff --git
a/regression-test/suites/ddl_p0/test_create_table_generated_column/test_delete_generated_column.groovy
b/regression-test/suites/ddl_p0/test_create_table_generated_column/test_delete_generated_column.groovy
index 8cbe723d929..c38250d6b32 100644
---
a/regression-test/suites/ddl_p0/test_create_table_generated_column/test_delete_generated_column.groovy
+++
b/regression-test/suites/ddl_p0/test_create_table_generated_column/test_delete_generated_column.groovy
@@ -70,5 +70,101 @@ suite("test_generated_column_delete") {
) delete from test_par_gen_col_unique t1 using cte where t1.c=cte.c and
t1.b=cte.b"""
qt_delete_query_cte_select "select * from test_par_gen_col_unique order by
a,b,c"
+ multi_sql """
+ drop table if exists test_gen_col_mow_delete_partial_update;
+ create table test_gen_col_mow_delete_partial_update (
+ a int,
+ b int,
+ c int as (b + 1),
+ d int
+ )
+ unique key(a)
+ distributed by hash(a) buckets 1
+ properties (
+ "enable_unique_key_merge_on_write" = "true",
+ "enable_mow_light_delete" = "false",
+ "replication_num" = "1"
+ );
+ insert into test_gen_col_mow_delete_partial_update(a, b, d) values (1,
10, 100), (2, 20, 200);
+ """
+ sql "delete from test_gen_col_mow_delete_partial_update where a = 1;"
+ qt_delete_mow_without_light_delete_generated_column """
+ select * from test_gen_col_mow_delete_partial_update order by a;
+ """
+
+ multi_sql """
+ drop table if exists test_gen_key_mow_delete_partial_update;
+ create table test_gen_key_mow_delete_partial_update (
+ a int,
+ c int as (b + 1),
+ b int,
+ d int
+ )
+ unique key(a, c)
+ distributed by hash(a) buckets 1
+ properties (
+ "enable_unique_key_merge_on_write" = "true",
+ "enable_mow_light_delete" = "false",
+ "replication_num" = "1"
+ );
+ insert into test_gen_key_mow_delete_partial_update(a, b, d) values (1,
10, 100), (2, 20, 200);
+ """
+ sql "delete from test_gen_key_mow_delete_partial_update where a = 1;"
+ qt_delete_mow_without_light_delete_generated_key_column """
+ select * from test_gen_key_mow_delete_partial_update order by a;
+ """
+
+ multi_sql """
+ drop table if exists
test_gen_col_mow_delete_partial_update_not_null_value;
+ create table test_gen_col_mow_delete_partial_update_not_null_value (
+ a int,
+ b int,
+ c int as (b + 1),
+ d int not null
+ )
+ unique key(a)
+ distributed by hash(a) buckets 1
+ properties (
+ "enable_unique_key_merge_on_write" = "true",
+ "enable_mow_light_delete" = "false",
+ "replication_num" = "1"
+ );
+ insert into test_gen_col_mow_delete_partial_update_not_null_value(a,
b, d)
+ values (1, 10, 100), (2, 20, 200);
+ """
+ sql "delete from test_gen_col_mow_delete_partial_update_not_null_value
where a = 1;"
+ qt_delete_mow_without_light_delete_generated_column_with_not_null_value """
+ select * from test_gen_col_mow_delete_partial_update_not_null_value
order by a;
+ """
+
+ multi_sql """
+ drop table if exists test_gen_variant_mow_delete_partial_update;
+ create table test_gen_variant_mow_delete_partial_update (
+ id int not null,
+ create_time datetime not null,
+ order_no varchar(128) not null,
+ receive_address_detail varchar(1024) not null default "{}",
+ d int not null,
+ new_col variant as (receive_address_detail) null
+ )
+ unique key(id, create_time, order_no)
+ distributed by hash(order_no) buckets 1
+ properties (
+ "enable_unique_key_merge_on_write" = "true",
+ "enable_mow_light_delete" = "false",
+ "replication_num" = "1"
+ );
+ insert into test_gen_variant_mow_delete_partial_update(
+ id, create_time, order_no, receive_address_detail, d
+ ) values
+ (1, '2026-06-08 10:00:00', 'order-1', '{"city":"sh"}', 100),
+ (2, '2026-06-08 11:00:00', 'order-2', '{"city":"bj"}', 200);
+ """
+ sql "delete from test_gen_variant_mow_delete_partial_update where id = 1;"
+ qt_delete_mow_without_light_delete_variant_generated_column """
+ select id, create_time, order_no, receive_address_detail, d
+ from test_gen_variant_mow_delete_partial_update order by id;
+ """
+
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]