This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new fae4af2e16f branch-4.2: [fix](fe) Lock table metadata while 
constructing UPDATE plans (#69974)
fae4af2e16f is described below

commit fae4af2e16f48786bd5bfca2c060cefc7e25e94e
Author: lihangyu <[email protected]>
AuthorDate: Tue Nov 3 10:09:48 2026 +0800

    branch-4.2: [fix](fe) Lock table metadata while constructing UPDATE plans 
(#69974)
    
    cherry-pick #69880
    
    Conflicts in UpdateCommand.java / UpdateCommandTest.java resolved: kept
    branch-4.2 UPDATE planning APIs and wrapped metadata reads + plan
    construction in readLock/readUnlock; tests use ImmutableList.of instead
    of List.of (Java 8 target).
---
 .../trees/plans/commands/UpdateCommand.java        | 159 +++++++++++----------
 .../nereids/trees/plans/UpdateCommandTest.java     | 116 +++++++++++++++
 2 files changed, 199 insertions(+), 76 deletions(-)

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 06237ec19e7..adee7e5bb3d 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
@@ -137,94 +137,101 @@ public class UpdateCommand extends Command implements 
ForwardWithSync, Explainab
     public LogicalPlan completeQueryPlan(ConnectContext ctx, LogicalPlan 
logicalQuery) {
         checkTable(ctx);
 
-        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> 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)
-                .anyMatch(column -> 
partialUpdateColNameToExpression.containsKey(column.getName()))) {
-            throw new AnalysisException("Only value columns of unique table 
could be updated");
-        }
-        List<NamedExpression> selectItems = Lists.newArrayList();
-        String tableName = tableAlias != null ? tableAlias : 
Util.getTempTableDisplayName(targetTable.getName());
-        Expression setExpr = null;
-        for (Column column : targetTable.getFullSchema()) {
-            // if it sets sequence column in stream load phase, the sequence 
map column is null, we query it.
-            if (!column.isVisible() && !column.isSequenceColumn()) {
-                continue;
+        // Light schema changes rebuild the column metadata in place under the 
table write lock.
+        // Keep all schema reads consistent until the UPDATE logical plan has 
been constructed.
+        targetTable.readLock();
+        try {
+            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> 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());
             }
-            if (colNameToExpression.containsKey(column.getName())) {
-                Expression expr = colNameToExpression.get(column.getName());
-                // when updating the sequence map column, the real sequence 
column need to set with the same value.
-                boolean isSequenceMapColumn = targetTable.hasSequenceCol()
-                        && targetTable.getSequenceMapCol() != null
-                        && 
column.getName().equalsIgnoreCase(targetTable.getSequenceMapCol());
-                if (setExpr == null && isSequenceMapColumn) {
-                    setExpr = expr;
+            // check if any key in update clause
+            if (targetTable.getFullSchema().stream().filter(Column::isKey)
+                    .anyMatch(column -> 
partialUpdateColNameToExpression.containsKey(column.getName()))) {
+                throw new AnalysisException("Only value columns of unique 
table could be updated");
+            }
+            List<NamedExpression> selectItems = Lists.newArrayList();
+            String tableName = tableAlias != null ? tableAlias : 
Util.getTempTableDisplayName(targetTable.getName());
+            Expression setExpr = null;
+            for (Column column : targetTable.getFullSchema()) {
+                // if it sets sequence column in stream load phase, the 
sequence map column is null, we query it.
+                if (!column.isVisible() && !column.isSequenceColumn()) {
+                    continue;
                 }
-                selectItems.add(expr instanceof UnboundSlot
-                        ? ((NamedExpression) expr)
-                        : new UnboundAlias(expr));
-                colNameToExpression.remove(column.getName());
-            } else {
-                if (column.isSequenceColumn() && setExpr != null) {
-                    selectItems.add(new UnboundAlias(setExpr, 
column.getName()));
-                } else if (column.hasOnUpdateDefaultValue()) {
-                    Expression defualtValueExpression =
-                            new 
NereidsParser().parseExpression(column.getOnUpdateDefaultValueSql());
-                    selectItems.add(new UnboundAlias(defualtValueExpression, 
column.getName()));
+                if (colNameToExpression.containsKey(column.getName())) {
+                    Expression expr = 
colNameToExpression.get(column.getName());
+                    // when updating the sequence map column, the real 
sequence column need to set with the same value.
+                    boolean isSequenceMapColumn = targetTable.hasSequenceCol()
+                            && targetTable.getSequenceMapCol() != null
+                            && 
column.getName().equalsIgnoreCase(targetTable.getSequenceMapCol());
+                    if (setExpr == null && isSequenceMapColumn) {
+                        setExpr = expr;
+                    }
+                    selectItems.add(expr instanceof UnboundSlot
+                            ? ((NamedExpression) expr)
+                            : new UnboundAlias(expr));
+                    colNameToExpression.remove(column.getName());
                 } else {
-                    selectItems.add(new UnboundSlot(tableName, 
column.getName()));
+                    if (column.isSequenceColumn() && setExpr != null) {
+                        selectItems.add(new UnboundAlias(setExpr, 
column.getName()));
+                    } else if (column.hasOnUpdateDefaultValue()) {
+                        Expression defualtValueExpression =
+                                new 
NereidsParser().parseExpression(column.getOnUpdateDefaultValueSql());
+                        selectItems.add(new 
UnboundAlias(defualtValueExpression, column.getName()));
+                    } else {
+                        selectItems.add(new UnboundSlot(tableName, 
column.getName()));
+                    }
                 }
             }
-        }
-        if (!colNameToExpression.isEmpty()) {
-            throw new AnalysisException("unknown column in assignment list: "
-                    + String.join(", ", colNameToExpression.keySet()));
-        }
+            if (!colNameToExpression.isEmpty()) {
+                throw new AnalysisException("unknown column in assignment 
list: "
+                        + String.join(", ", colNameToExpression.keySet()));
+            }
 
-        boolean isPartialUpdate = targetTable.getEnableUniqueKeyMergeOnWrite()
-                && selectItems.size() < targetTable.getColumns().size()
-                && targetTable.getSequenceCol() == null
-                && partialUpdateColNameToExpression.size() <= 
targetTable.getFullSchema().size() * 3 / 10
-                && !targetTable.isUniqKeyMergeOnWriteWithClusterKeys();
+            boolean isPartialUpdate = 
targetTable.getEnableUniqueKeyMergeOnWrite()
+                    && selectItems.size() < targetTable.getColumns().size()
+                    && targetTable.getSequenceCol() == null
+                    && partialUpdateColNameToExpression.size() <= 
targetTable.getFullSchema().size() * 3 / 10
+                    && !targetTable.isUniqKeyMergeOnWriteWithClusterKeys();
 
-        List<String> partialUpdateColNames = new ArrayList<>();
-        List<NamedExpression> partialUpdateSelectItems = new ArrayList<>();
-        if (isPartialUpdate) {
-            for (Column column : targetTable.getFullSchema()) {
-                Expression expr = new UnboundSlot(tableName, column.getName());
-                boolean existInExpr = false;
-                for (String colName : 
partialUpdateColNameToExpression.keySet()) {
-                    if (colName.equalsIgnoreCase(column.getName())) {
-                        expr = 
partialUpdateColNameToExpression.get(column.getName());
-                        existInExpr = true;
-                        break;
+            List<String> partialUpdateColNames = new ArrayList<>();
+            List<NamedExpression> partialUpdateSelectItems = new ArrayList<>();
+            if (isPartialUpdate) {
+                for (Column column : targetTable.getFullSchema()) {
+                    Expression expr = new UnboundSlot(tableName, 
column.getName());
+                    boolean existInExpr = false;
+                    for (String colName : 
partialUpdateColNameToExpression.keySet()) {
+                        if (colName.equalsIgnoreCase(column.getName())) {
+                            expr = 
partialUpdateColNameToExpression.get(column.getName());
+                            existInExpr = true;
+                            break;
+                        }
+                    }
+                    if (column.isKey() || existInExpr) {
+                        partialUpdateSelectItems.add(expr instanceof 
UnboundSlot
+                                ? ((NamedExpression) expr)
+                                : new UnboundAlias(expr));
+                        partialUpdateColNames.add(column.getName());
                     }
-                }
-                if (column.isKey() || existInExpr) {
-                    partialUpdateSelectItems.add(expr instanceof UnboundSlot
-                            ? ((NamedExpression) expr)
-                            : new UnboundAlias(expr));
-                    partialUpdateColNames.add(column.getName());
                 }
             }
-        }
 
-        logicalQuery = new LogicalProject<>(isPartialUpdate ? 
partialUpdateSelectItems : selectItems, logicalQuery);
-        if (cte.isPresent()) {
-            logicalQuery = ((LogicalPlan) 
cte.get().withChildren(logicalQuery));
+            logicalQuery = new LogicalProject<>(isPartialUpdate ? 
partialUpdateSelectItems : selectItems, logicalQuery);
+            if (cte.isPresent()) {
+                logicalQuery = ((LogicalPlan) 
cte.get().withChildren(logicalQuery));
+            }
+            // make UnboundTableSink
+            return UnboundTableSinkCreator.createUnboundTableSink(nameParts,
+                    isPartialUpdate ? partialUpdateColNames : 
ImmutableList.of(), ImmutableList.of(),
+                    false, ImmutableList.of(), isPartialUpdate, 
TPartialUpdateNewRowPolicy.APPEND,
+                    DMLCommandType.UPDATE, logicalQuery);
+        } finally {
+            targetTable.readUnlock();
         }
-        // make UnboundTableSink
-        return UnboundTableSinkCreator.createUnboundTableSink(nameParts,
-                isPartialUpdate ? partialUpdateColNames : ImmutableList.of(), 
ImmutableList.of(),
-                false, ImmutableList.of(), isPartialUpdate, 
TPartialUpdateNewRowPolicy.APPEND,
-                DMLCommandType.UPDATE, logicalQuery);
     }
 
     /**
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/UpdateCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/UpdateCommandTest.java
index 4151115a2c8..2a38b488bf7 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/UpdateCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/UpdateCommandTest.java
@@ -17,16 +17,34 @@
 
 package org.apache.doris.nereids.trees.plans;
 
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.nereids.analyzer.UnboundTableSink;
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.nereids.util.PlanPatternMatchSupported;
+import org.apache.doris.nereids.util.RelationUtil;
 import org.apache.doris.utframe.TestWithFeService;
 
+import com.google.common.collect.ForwardingMap;
+import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
 public class UpdateCommandTest extends TestWithFeService implements 
PlanPatternMatchSupported {
     @Override
     public void runBeforeAll() throws Exception {
@@ -89,6 +107,104 @@ public class UpdateCommandTest extends TestWithFeService 
implements PlanPatternM
                 );
     }
 
+    @Test
+    public void testUpdatePlanWithConcurrentSchemaRebuild() throws Exception {
+        OlapTable table = (OlapTable) RelationUtil.getTable(
+                RelationUtil.getQualifierName(connectContext, 
ImmutableList.of("t1")),
+                connectContext.getEnv(), Optional.empty());
+        Assertions.assertTrue(table.getEnableUniqueKeyMergeOnWrite());
+        Map<String, Column> columns = Deencapsulation.getField(table, 
"nameToColumn");
+        ExecutorService schemaChange = Executors.newSingleThreadExecutor();
+        AtomicBoolean schemaRebuiltDuringCopy = new AtomicBoolean();
+        AtomicBoolean copyStarted = new AtomicBoolean();
+        // Keep the real TreeMap iterator, but try a schema rebuild after its 
first entry is read.
+        // Without the UPDATE read lock, clear/put in rebuildFullSchema 
invalidates this iterator.
+        Map<String, Column> instrumentedColumns = new ForwardingMap<String, 
Column>() {
+            @Override
+            protected Map<String, Column> delegate() {
+                return columns;
+            }
+
+            @Override
+            public Collection<Column> values() {
+                Collection<Column> values = columns.values();
+                return new AbstractCollection<Column>() {
+                    @Override
+                    public int size() {
+                        return values.size();
+                    }
+
+                    @Override
+                    public Iterator<Column> iterator() {
+                        Iterator<Column> iterator = values.iterator();
+                        return new Iterator<Column>() {
+                            @Override
+                            public boolean hasNext() {
+                                return iterator.hasNext();
+                            }
+
+                            @Override
+                            public Column next() {
+                                Column column = iterator.next();
+                                if (copyStarted.compareAndSet(false, true)) {
+                                    
schemaRebuiltDuringCopy.set(Assertions.assertDoesNotThrow(() ->
+                                            schemaChange.submit(() -> 
tryRebuildSchema(table))
+                                                    .get(10, 
TimeUnit.SECONDS)));
+                                }
+                                return column;
+                            }
+                        };
+                    }
+                };
+            }
+        };
+        Deencapsulation.setField(table, "nameToColumn", instrumentedColumns);
+        try {
+            UpdateCommand command = (UpdateCommand) new NereidsParser()
+                    .parseSingle("update t1 set v1 = v1 + 1 where k1 = 1");
+            LogicalPlan plan = command.completeQueryPlan(connectContext, 
command.getLogicalQuery());
+            Assertions.assertTrue(((UnboundTableSink<?>) 
plan).isPartialUpdate());
+            Assertions.assertTrue(copyStarted.get(), "The test must reach the 
column-map copy");
+            Assertions.assertFalse(schemaRebuiltDuringCopy.get(), "Schema 
changes must wait for UPDATE planning");
+            Assertions.assertTrue(schemaChange.submit(() -> 
tryRebuildSchema(table)).get(10, TimeUnit.SECONDS),
+                    "UPDATE planning must release its read lock before 
returning");
+            PlanChecker.from(connectContext, plan).analyze(plan).rewrite();
+        } finally {
+            schemaChange.shutdownNow();
+            Deencapsulation.setField(table, "nameToColumn", columns);
+        }
+    }
+
+    @Test
+    public void testUpdatePlanReleasesReadLockOnAnalysisFailure() throws 
Exception {
+        OlapTable table = (OlapTable) RelationUtil.getTable(
+                RelationUtil.getQualifierName(connectContext, 
ImmutableList.of("t1")),
+                connectContext.getEnv(), Optional.empty());
+        ExecutorService schemaChange = Executors.newSingleThreadExecutor();
+        try {
+            UpdateCommand command = (UpdateCommand) new NereidsParser()
+                    .parseSingle("update t1 set k1 = 2 where k1 = 1");
+            Assertions.assertThrows(AnalysisException.class,
+                    () -> command.completeQueryPlan(connectContext, 
command.getLogicalQuery()));
+            Assertions.assertTrue(schemaChange.submit(() -> 
tryRebuildSchema(table)).get(10, TimeUnit.SECONDS),
+                    "Failed UPDATE planning must release its read lock");
+        } finally {
+            schemaChange.shutdownNow();
+        }
+    }
+
+    private static boolean tryRebuildSchema(OlapTable table) {
+        if (!table.tryWriteLock(0, TimeUnit.SECONDS)) {
+            return false;
+        }
+        try {
+            table.rebuildFullSchema();
+            return true;
+        } finally {
+            table.writeUnlock();
+        }
+    }
+
     @Test
     public void testFromClauseUpdate() {
         String sql = "update t1 a set v1 = t2.v1 + 2, v2 = a.v1 * 2 "


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to