This is an automated email from the ASF dual-hosted git repository.
dataroaring 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 c37acc1c301 [Fix](nereids) Fix session variable not take effect for
partial update in multi-statement batch (#60803)
c37acc1c301 is described below
commit c37acc1c301f2baaeeb03d83713987e3863a9502
Author: bobhan1 <[email protected]>
AuthorDate: Fri Feb 27 10:35:09 2026 +0800
[Fix](nereids) Fix session variable not take effect for partial update in
multi-statement batch (#60803)
### What problem does this PR solve?
Problem Summary:
When multiple SQL statements are sent as a single COM_QUERY (e.g., via
JDBC `allowMultiQueries=true`):
`set enable_unique_key_partial_update=true; insert into t1(k,v1)
values(1,100);`
The multi-statement execution flow is:
**Before fix (bug):**
> 1. **Parse ALL statements together (before any execution)**
> - parse `SET enable_unique_key_partial_update=true` → SetVarStmt
> - parse `INSERT INTO t1(k,v1) VALUES(1,100)` → InsertIntoTableCommand
> - `visitInsertTable()` reads session var → `isPartialUpdate=false` ❌
(stale!)
> 2. **Execute statements one by one**
> - execute SetVarStmt → session var set to `true` ✅
> - execute InsertIntoTableCommand
> - `normalizePlan()` still sees `isPartialUpdate=false` from parse
phase ❌
> - partial update NOT applied, unspecified columns overwritten with
defaults
**After fix:**
> 2. **Execute statements one by one**
> - execute SetVarStmt → session var set to `true` ✅
> - execute InsertIntoTableCommand
> - `normalizePlan()` **re-reads session var** → `isPartialUpdate=true`
✅
> - partial update correctly applied, unspecified columns preserved
All statements are parsed together by `NereidsParser.parseSQL()` before
any are executed. In `LogicalPlanBuilder.visitInsertTable()`,
`isEnableUniqueKeyPartialUpdate` and `getPartialUpdateNewRowPolicy` are
read from the session variable during **parse phase**, but the preceding
SET statement hasn't executed yet, so the INSERT reads stale values.
This causes partial update to silently not take effect, overwriting
unspecified columns with default values instead of preserving them.
### Root Cause
`visitInsertTable()` →
`UnboundTableSinkCreator.createUnboundTableSinkMaybeOverwrite()` bakes
session variable values into `UnboundTableSink` at parse time. All
downstream code (`normalizePlan`, `BindSink`, `OlapInsertExecutor`,
`PhysicalPlanTranslator`) reads from the sink object, not from session
variable. So the stale value propagates through the entire pipeline.
### Fix
Re-read both `enable_unique_key_partial_update` and
`partial_update_new_row_policy` session variables from `ConnectContext`
in `InsertUtils.normalizePlanWithoutLock()`. This is the earliest point
in the **execution phase** (after SET has executed), and it's upstream
of all consumers.
Changes:
1. **UnboundTableSink.java** — remove `final` from
`partialUpdateNewKeyPolicy` field and add
`setPartialUpdateNewKeyPolicy()` setter.
2. **InsertUtils.java** — re-read both session variables from
`ConnectContext` and set them on the `UnboundTableSink` before the
existing partial update validation logic.
3. **test_partial_update_multi_stmt.out** — update expected output to
reflect that multi-statement batch partial update now works correctly
(preserves original values instead of overwriting with defaults).
## Further comment
All downstream reads come from the sink object, not from session
variable directly. Since we fix the sink's values in
`normalizePlanWithoutLock()`, all downstream code gets the correct
values without any additional changes:
| Phase | File | Reads from |
|-------|------|-----------|
| Normalize | `InsertUtils.java` | `UnboundTableSink` (fixed here) |
| Bind | `BindSink.java` | `UnboundTableSink` |
| Physical | `LogicalOlapTableSinkToPhysical.java` |
`LogicalOlapTableSink` |
| Translate | `PhysicalPlanTranslator.java` | `PhysicalOlapTableSink` |
| Execute | `OlapInsertExecutor.java` | `PhysicalOlapTableSink` |
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [x] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [x] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
.../doris/nereids/analyzer/UnboundTableSink.java | 6 +-
.../trees/plans/commands/insert/InsertUtils.java | 13 ++
.../test_partial_update_multi_stmt.out | 25 +++
.../test_partial_update_multi_stmt.groovy | 168 +++++++++++++++++++++
4 files changed, 211 insertions(+), 1 deletion(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSink.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSink.java
index b9290c796c2..ed04ad57372 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSink.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundTableSink.java
@@ -50,7 +50,7 @@ public class UnboundTableSink<CHILD_TYPE extends Plan>
extends UnboundLogicalSin
private final boolean temporaryPartition;
private final List<String> partitions;
private boolean isPartialUpdate;
- private final TPartialUpdateNewRowPolicy partialUpdateNewKeyPolicy;
+ private TPartialUpdateNewRowPolicy partialUpdateNewKeyPolicy;
private final DMLCommandType dmlCommandType;
private final boolean autoDetectPartition;
@@ -126,6 +126,10 @@ public class UnboundTableSink<CHILD_TYPE extends Plan>
extends UnboundLogicalSin
this.isPartialUpdate = isPartialUpdate;
}
+ public void setPartialUpdateNewKeyPolicy(TPartialUpdateNewRowPolicy
policy) {
+ this.partialUpdateNewKeyPolicy = policy;
+ }
+
@Override
public Plan withChildren(List<Plan> children) {
Preconditions.checkArgument(children.size() == 1,
"UnboundOlapTableSink only accepts one child");
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 16da19be4ae..b5561f0d77e 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
@@ -295,6 +295,19 @@ public class InsertUtils {
// For JDBC External Table, we always allow certain columns to be
missing during insertion
// Specific check for non-nullable columns only if insertion is
direct VALUES or SELECT constants
}
+ // Re-read partial update settings from session variable to handle
multi-statement
+ // batches where SET and INSERT are parsed together before execution.
+ // Only apply to original INSERT statements, not DELETE/UPDATE
converted to INSERT.
+ if (unboundLogicalSink instanceof UnboundTableSink
+ && unboundLogicalSink.getDMLCommandType() ==
DMLCommandType.INSERT) {
+ ConnectContext ctx = ConnectContext.get();
+ if (ctx != null) {
+ ((UnboundTableSink<? extends Plan>) unboundLogicalSink)
+
.setPartialUpdate(ctx.getSessionVariable().isEnableUniqueKeyPartialUpdate());
+ ((UnboundTableSink<? extends Plan>) unboundLogicalSink)
+
.setPartialUpdateNewKeyPolicy(ctx.getSessionVariable().getPartialUpdateNewRowPolicy());
+ }
+ }
if (table instanceof OlapTable && ((OlapTable) table).getKeysType() ==
KeysType.UNIQUE_KEYS) {
if (unboundLogicalSink instanceof UnboundTableSink
&& ((UnboundTableSink<? extends Plan>)
unboundLogicalSink).isPartialUpdate()) {
diff --git
a/regression-test/data/unique_with_mow_p0/partial_update/test_partial_update_multi_stmt.out
b/regression-test/data/unique_with_mow_p0/partial_update/test_partial_update_multi_stmt.out
new file mode 100644
index 00000000000..546b0cf2c4a
--- /dev/null
+++
b/regression-test/data/unique_with_mow_p0/partial_update/test_partial_update_multi_stmt.out
@@ -0,0 +1,25 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !base1 --
+1 1 1 1
+2 2 2 2
+
+-- !base2 --
+1 1 1 1
+2 2 2 2
+
+-- !multi_stmt_values --
+1 100 1 1
+2 200 2 2
+
+-- !multi_stmt_select --
+1 501 1 1
+2 502 2 2
+
+-- !separate_stmt_values --
+1 100 1 1
+2 200 2 2
+
+-- !separate_stmt_select --
+1 501 1 1
+2 502 2 2
+
diff --git
a/regression-test/suites/unique_with_mow_p0/partial_update/test_partial_update_multi_stmt.groovy
b/regression-test/suites/unique_with_mow_p0/partial_update/test_partial_update_multi_stmt.groovy
new file mode 100644
index 00000000000..7dbc33b5c43
--- /dev/null
+++
b/regression-test/suites/unique_with_mow_p0/partial_update/test_partial_update_multi_stmt.groovy
@@ -0,0 +1,168 @@
+// 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.
+
+import java.sql.Connection
+import java.sql.DriverManager
+import java.sql.Statement
+
+// Test that `set enable_unique_key_partial_update=true; insert into ...`
works correctly
+// when sent as a single multi-statement query (single COM_QUERY).
+// This is a regression test for the issue where session variables set via SET
in a
+// multi-statement batch might not be visible during the parse phase of
subsequent
+// statements in the same batch, because all statements are parsed together
before
+// any of them are executed.
+suite("test_partial_update_multi_stmt", "p0") {
+
+ String db = context.config.getDbNameByFile(context.file)
+ sql "select 1;" // to create database
+
+ String jdbcUrl = context.config.jdbcUrl
+ String urlWithoutSchema = jdbcUrl.substring(jdbcUrl.indexOf("://") + 3)
+ def sql_ip = urlWithoutSchema.substring(0, urlWithoutSchema.indexOf(":"))
+ def sql_port
+ if (urlWithoutSchema.indexOf("/") >= 0) {
+ sql_port = urlWithoutSchema.substring(urlWithoutSchema.indexOf(":") +
1, urlWithoutSchema.indexOf("/"))
+ } else {
+ sql_port = urlWithoutSchema.substring(urlWithoutSchema.indexOf(":") +
1)
+ }
+
+ def tableName1 = "test_partial_update_multi_stmt1"
+ def tableName2 = "test_partial_update_multi_stmt2"
+ def tableName3 = "test_partial_update_multi_stmt3"
+ def tableName4 = "test_partial_update_multi_stmt4"
+
+ def createTable = { tblName ->
+ sql """ DROP TABLE IF EXISTS ${tblName} """
+ sql """
+ CREATE TABLE ${tblName} (
+ `k` int(11) NOT NULL COMMENT "key",
+ `v1` int(11) NULL DEFAULT "10" COMMENT "value1",
+ `v2` int(11) NULL DEFAULT "20" COMMENT "value2",
+ `v3` int(11) NULL DEFAULT "30" COMMENT "value3"
+ ) UNIQUE KEY(`k`)
+ DISTRIBUTED BY HASH(`k`) BUCKETS 1
+ PROPERTIES(
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ ); """
+ sql "insert into ${tblName} values(1, 1, 1, 1), (2, 2, 2, 2);"
+ sql "sync;"
+ }
+
+ // ============================================================
+ // Setup: create all tables with initial data (1,1,1,1),(2,2,2,2)
+ // ============================================================
+ connect(context.config.jdbcUser, context.config.jdbcPassword,
context.config.jdbcUrl) {
+ sql "use ${db};"
+ sql "sync;"
+ createTable(tableName1)
+ createTable(tableName2)
+ createTable(tableName3)
+ createTable(tableName4)
+ qt_base1 "select * from ${tableName1} order by k;"
+ qt_base2 "select * from ${tableName2} order by k;"
+ }
+
+ // ============================================================
+ // Test 1 & 2: multi-statement with allowMultiQueries=true
+ // SET + INSERT sent as a single COM_QUERY
+ // ============================================================
+ def multiStmtUrl =
"jdbc:mysql://${sql_ip}:${sql_port}/${db}?useLocalSessionState=false&allowMultiQueries=true"
+ logger.info("multi-statement JDBC URL: ${multiStmtUrl}")
+
+ try (Connection conn = DriverManager.getConnection(multiStmtUrl,
+ context.config.jdbcUser, context.config.jdbcPassword)) {
+ Statement stmt = conn.createStatement()
+
+ // Test 1: SET + INSERT VALUES as single COM_QUERY
+ logger.info("Test 1: allowMultiQueries=true, SET + INSERT VALUES")
+ stmt.execute(
+ "set enable_unique_key_partial_update=true;" +
+ "set enable_insert_strict=false;" +
+ "insert into ${tableName1}(k, v1) values(1, 100), (2, 200);"
+ )
+ stmt.execute("set enable_unique_key_partial_update=false;")
+
+ // Test 2: SET + INSERT SELECT as single COM_QUERY
+ logger.info("Test 2: allowMultiQueries=true, SET + INSERT SELECT")
+ stmt.execute(
+ "set enable_unique_key_partial_update=true;" +
+ "set enable_insert_strict=false;" +
+ "insert into ${tableName2}(k, v1) select k, v1 + 500 from
${tableName2};"
+ )
+ stmt.execute("set enable_unique_key_partial_update=false;")
+
+ stmt.close()
+ }
+
+ // ============================================================
+ // Test 3 & 4: without allowMultiQueries (statements sent separately)
+ // SET and INSERT are separate COM_QUERY packets
+ // ============================================================
+ def singleStmtUrl =
"jdbc:mysql://${sql_ip}:${sql_port}/${db}?useLocalSessionState=false"
+ logger.info("single-statement JDBC URL: ${singleStmtUrl}")
+
+ try (Connection conn = DriverManager.getConnection(singleStmtUrl,
+ context.config.jdbcUser, context.config.jdbcPassword)) {
+ Statement stmt = conn.createStatement()
+
+ // Test 3: SET then INSERT VALUES as separate COM_QUERY
+ logger.info("Test 3: separate statements, SET + INSERT VALUES")
+ stmt.execute("set enable_unique_key_partial_update=true;")
+ stmt.execute("set enable_insert_strict=false;")
+ stmt.execute("insert into ${tableName3}(k, v1) values(1, 100), (2,
200);")
+ stmt.execute("set enable_unique_key_partial_update=false;")
+
+ // Test 4: SET then INSERT SELECT as separate COM_QUERY
+ logger.info("Test 4: separate statements, SET + INSERT SELECT")
+ stmt.execute("set enable_unique_key_partial_update=true;")
+ stmt.execute("set enable_insert_strict=false;")
+ stmt.execute("insert into ${tableName4}(k, v1) select k, v1 + 500 from
${tableName4};")
+ stmt.execute("set enable_unique_key_partial_update=false;")
+
+ stmt.close()
+ }
+
+ // ============================================================
+ // Verify all results
+ // ============================================================
+ connect(context.config.jdbcUser, context.config.jdbcPassword,
context.config.jdbcUrl) {
+ sql "use ${db};"
+ sql "sync;"
+
+ // Test 1: allowMultiQueries + INSERT VALUES
+ // Expected (partial update): v2,v3 keep old values (1,1) and (2,2)
+ qt_multi_stmt_values "select * from ${tableName1} order by k;"
+
+ // Test 2: allowMultiQueries + INSERT SELECT
+ // Expected (partial update): v2,v3 keep old values
+ qt_multi_stmt_select "select * from ${tableName2} order by k;"
+
+ // Test 3: separate statements + INSERT VALUES
+ // Expected (partial update): v2,v3 keep old values (1,1) and (2,2)
+ qt_separate_stmt_values "select * from ${tableName3} order by k;"
+
+ // Test 4: separate statements + INSERT SELECT
+ // Expected (partial update): v2,v3 keep old values
+ qt_separate_stmt_select "select * from ${tableName4} order by k;"
+
+ sql """ DROP TABLE IF EXISTS ${tableName1} """
+ sql """ DROP TABLE IF EXISTS ${tableName2} """
+ sql """ DROP TABLE IF EXISTS ${tableName3} """
+ sql """ DROP TABLE IF EXISTS ${tableName4} """
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]