github-actions[bot] commented on code in PR #66307:
URL: https://github.com/apache/doris/pull/66307#discussion_r3829672638


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/MetastoreEventSyncDriver.java:
##########
@@ -247,6 +247,18 @@ private void applyDescriptors(PluginDrivenExternalCatalog 
catalog, Connector con
     // legacy event.process() bodies called, now generalized to work on a 
flipped catalog.
     private void applyOne(PluginDrivenExternalCatalog catalog, Connector 
connector,
             MetastoreChangeDescriptor descriptor) throws Exception {
+        if (!affectsConstraintMetadata(descriptor)) {
+            applyOneInternal(catalog, connector, descriptor);
+            return;
+        }
+        try (ExternalCatalog.ConstraintMetadataMutationGuard ignored =
+                catalog.beginConstraintMetadataMutation()) {

Review Comment:
   [P1] Transition constraints with external structural events
   
   This guard orders the event against constraint DDL, but the guarded 
`UNREGISTER_DATABASE`, `RENAME_DATABASE`, `UNREGISTER_TABLE`, and 
`RENAME_TABLE` cases only unregister/register external cache objects; they 
never drop or rename `ConstraintManager` entries. For example, after adding an 
external PK/UK/FK on `ext.db.t`, an `UNREGISTER_TABLE` followed by recreation 
of `t` leaves the old name-keyed constraint attached to an unrelated table, so 
Nereids can consume a false uniqueness/FK proof. A rename similarly loses the 
proof at the new name and lets later reuse of the old name reactivate it. The 
explicit plugin table/database DROP and replay paths have the same missing 
transition. Please update the manager within the structural mutation protocol 
(with FK safeguards) and make event, explicit DDL, and replay behavior 
equivalent, with drop/recreate and rename/old-name-reuse coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/JoinUtils.java:
##########
@@ -307,12 +356,85 @@ public static boolean 
couldColocateJoin(DistributionSpecHash leftHashSpec, Distr
                 equalIndices.add(leftIndex);
             }
         }
-        // on conditions must contain all distributed columns
-        if 
(equalIndices.containsAll(leftHashSpec.getExprIdToEquivalenceSet().values())) {
-            return true;
-        } else {
+        return 
equalIndices.containsAll(leftHashSpec.getExprIdToEquivalenceSet().values());
+    }
+
+    private static boolean 
couldColocateJoinOnDistributionMappings(DistributionSpecHash leftHashSpec,
+            DistributionSpecHash rightHashSpec, List<Expression> conjuncts) {
+        return couldColocateJoinOnDistributionMappings(
+                leftHashSpec.getExprIdToEquivalenceSet(), 
leftHashSpec.getDistributionMappings(),
+                rightHashSpec.getExprIdToEquivalenceSet(), 
rightHashSpec.getDistributionMappings(),
+                leftHashSpec.getOrderedShuffledColumns().size(), conjuncts);
+    }
+
+    private static boolean couldColocateJoinOnDistributionMappings(
+            Map<ExprId, Integer> leftDistributionExprToIndex, 
List<DistributionMapping> leftMappings,
+            Map<ExprId, Integer> rightDistributionExprToIndex, 
List<DistributionMapping> rightMappings,
+            int distributionKeyCount, List<Expression> conjuncts) {
+        if (!areAllSlotEqualPredicates(conjuncts)) {
             return false;
         }
+        List<Pair<ExprId, ExprId>> equalExprIds = Lists.newArrayList();
+        Set<Integer> coveredIndices = new HashSet<>();
+        for (Expression expr : conjuncts) {
+            ExprId first = ((SlotReference) ((EqualPredicate) 
expr).left()).getExprId();
+            ExprId second = ((SlotReference) ((EqualPredicate) 
expr).right()).getExprId();
+            equalExprIds.add(Pair.of(first, second));
+
+            Integer leftIndex = leftDistributionExprToIndex.get(first);
+            Integer rightIndex = rightDistributionExprToIndex.get(second);
+            if (leftIndex == null) {
+                leftIndex = leftDistributionExprToIndex.get(second);
+                rightIndex = rightDistributionExprToIndex.get(first);
+            }
+            if (leftIndex != null && Objects.equals(leftIndex, rightIndex)) {
+                coveredIndices.add(leftIndex);
+            }
+        }
+
+        for (DistributionMapping leftMapping : leftMappings) {

Review Comment:
   [P2] Index mapping candidates before comparing determinants
   
   This compares every left mapping with every right mapping for each 
mapping-based join proof. Constraint DDL permits an unbounded number of 
distinct mappings per table, so two tables with L and R mappings make this path 
O(L*R*D*E), even when all mapping IDs are disjoint; the proof is then invoked 
during child regulation, output derivation, and final colocate detection. A 
table pair with many unrelated mapping IDs can therefore make ordinary 
EXPLAIN/planning quadratic in metadata size. Please index one side by 
`(mappingId, targetDistributionIndices, determinantCount)` (and stop once all 
bucket positions are covered), then compare determinants only among matching 
candidates, with a scale test using many disjoint mapping IDs.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java:
##########
@@ -5996,6 +6002,18 @@ private void renameColumn(Database db, OlapTable table, 
String colName,
         if (partitionInfo.getPartitionColumns().stream().anyMatch(c -> 
c.getName().equalsIgnoreCase(colName))) {
             throw new DdlException("Renaming partition columns has problems, 
forbidden in current Doris version");
         }
+        String mappingConstraint =

Review Comment:
   [P1] Replay legacy constrained-column renames
   
   This ordinary-constraint fallback also runs for `isReplay=true`, but older 
releases allowed RENAME COLUMN on PK/UK/FK columns and emitted 
`OP_RENAME_COLUMN`. During upgrade, replaying such an ADD-constraint then 
RENAME sequence now throws here; `replayRenameColumn` catches the exception and 
continues, leaving the follower with the old column while the original 
leader/image has the renamed schema. Distribution mappings cannot occur in an 
old log, so their replay guard does not require broadening this compatibility 
break to legacy constraint types. Please replay historically valid rename 
records (or apply a backward-compatible constraint transition) and add a 
synthetic legacy-log test.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java:
##########
@@ -67,38 +75,123 @@ public DropConstraintCommand(String name, LogicalPlan 
plan) {
     @Override
     public void run(ConnectContext ctx, StmtExecutor executor) throws 
Exception {
         TableNameInfo tableNameInfo;
-        try {
-            TableIf table = extractTable(ctx, plan);
-            tableNameInfo = TableNameInfoUtils.fromCatalogDb(
-                    table.getDatabase().getCatalog(), table.getDatabase(), 
table);
-        } catch (Exception e) {
-            // Table may no longer exist (e.g., external table deleted by 
another system).
-            // Fall back to extracting the table name from the unresolved plan.
-            LOG.warn("Table resolution failed for dropping constraint {}, "
-                    + "falling back to name-based lookup: {}", name, 
e.getMessage());
-            tableNameInfo = extractTableNameFromPlan(ctx);
+        TableNameInfo unresolvedTableName = plan instanceof UnboundRelation
+                ? extractTableNameFromPlan(ctx) : null;
+        CatalogIf<?> unresolvedCatalog = unresolvedTableName == null ? null
+                : 
Env.getCurrentEnv().getCatalogMgr().getCatalog(unresolvedTableName.getCtl());
+        if (unresolvedCatalog instanceof ExternalCatalog) {
+            // External PK/FK/UK constraints are authoritative in 
ConstraintManager. Avoid connector
+            // schema loading so DROP works with cache disabled or session 
cache bypass.
+            ExternalCatalog externalCatalog = (ExternalCatalog) 
unresolvedCatalog;
+            unresolvedTableName = new TableNameInfo(
+                    externalCatalog.getName(), unresolvedTableName.getDb(), 
unresolvedTableName.getTbl());
+            boolean lowerCaseMetaNames =
+                    
Boolean.parseBoolean(externalCatalog.getLowerCaseMetaNames());
+            tableNameInfo = !lowerCaseMetaNames
+                    && externalCatalog.getLowerCaseTableNames() == 0
+                    && externalCatalog.getLowerCaseDatabaseNames() == 0
+                    ? unresolvedTableName
+                    : Env.getCurrentEnv().getConstraintManager()
+                            .canonicalizeExternalTableName(
+                                    unresolvedTableName,
+                                    name,
+                                    lowerCaseMetaNames
+                                            || 
externalCatalog.getLowerCaseDatabaseNames() != 0,
+                                    lowerCaseMetaNames
+                                            || 
externalCatalog.getLowerCaseTableNames() != 0);
+        } else {
+            try {
+                TableIf table = extractTable(ctx, plan);
+                tableNameInfo = TableNameInfoUtils.fromCatalogDb(
+                        table.getDatabase().getCatalog(), table.getDatabase(), 
table);
+            } catch (Exception e) {
+                // Table may no longer exist (e.g., external table deleted by 
another system).
+                // Fall back to extracting the table name from the unresolved 
plan.
+                LOG.warn("Table resolution failed for dropping constraint {}, "
+                        + "falling back to name-based lookup: {}", name, 
e.getMessage());
+                if (unresolvedTableName == null) {
+                    throw e;
+                }
+                tableNameInfo = unresolvedTableName;
+            }
         }
         // must be checked on both paths above: table resolution failing 
(which includes an
         // authorization failure) falls back to a name-only lookup that binds 
nothing.
         checkAlterPriv(ctx, tableNameInfo);
+        Constraint initialConstraint = getConstraintOrThrow(tableNameInfo);
+        List<TableNameInfo> initialCascadeDropTables = Env.getCurrentEnv()
+                
.getConstraintManager().getCascadeDropTables(initialConstraint);
+        List<TableNameInfo> affectedTableInfos = new ArrayList<>();
+        affectedTableInfos.add(tableNameInfo);
+        affectedTableInfos.addAll(initialCascadeDropTables);
+        ConstraintCommandUtils.ExternalCatalogSnapshots 
externalCatalogSnapshots =
+                
ConstraintCommandUtils.snapshotExternalCatalogs(affectedTableInfos);
+
+        Constraint constraint;
+        List<MTMV> dependentMtmvs;
+        EditLog.EditLogItem logItem;
+        try (ConstraintCommandUtils.LockedDatabases lockedDatabases =
+                ConstraintCommandUtils.lockCurrentDatabases(
+                        affectedTableInfos, externalCatalogSnapshots, 
List.of());
+                ConstraintCommandUtils.LockedTables lockedTables =
+                        ConstraintCommandUtils.lockCurrentTablesIfPresent(
+                                lockedDatabases, affectedTableInfos)) {
+            TableIf currentTable = lockedTables.get(tableNameInfo);
+            constraint = getConstraintOrThrow(tableNameInfo);
+            if (constraint instanceof DistributionMappingConstraint
+                    && !(currentTable instanceof OlapTable)) {
+                throw new AnalysisException(
+                        "Distribution mapping constraint requires an OLAP 
table");
+            }
+            List<TableNameInfo> cascadeDropTables = Env.getCurrentEnv()
+                    .getConstraintManager().getCascadeDropTables(constraint);
+            if (!ConstraintCommandUtils.sameTables(
+                    initialCascadeDropTables, cascadeDropTables)) {
+                throw new AnalysisException(
+                        "Foreign key references changed while dropping 
constraint "
+                                + name + " on " + tableNameInfo + ", retry the 
statement");
+            }
+            for (TableNameInfo fkTableInfo : cascadeDropTables) {
+                checkAlterPriv(ctx, fkTableInfo);
+            }
+            dependentMtmvs = getDependentMtmvs(
+                    tableNameInfo, constraint, cascadeDropTables);
+            logItem = Env.getCurrentEnv().getConstraintManager()
+                    .dropConstraintAndSubmit(tableNameInfo, name, 
cascadeDropTables);
+            if (constraint instanceof DistributionMappingConstraint) {
+                Env.getCurrentEnv().getSqlCacheManager()
+                        .invalidateAboutTableAndFencePublication(currentTable);
+            }
+        }
+        if (logItem != null) {

Review Comment:
   [P1] Fence MTMV plans before awaiting durability
   
   Putting `await()` here releases the table locks after the manager entry is 
removed but delays `invalidateRewriteCachesBestEffort` until the journal flush 
completes, which can block indefinitely. Queries in that interval return the 
existing `MTMVCache` immediately, even though its stored rewritten plan may 
have used the dropped PK/UK/FK for group-by or join elimination; a rewrite can 
therefore rely on a proof that no longer exists. Please publish the dependent 
MTMV generation fence as part of the locked in-memory DROP transition, then 
keep only the durability wait outside the metadata locks, with a 
blocked-journal/concurrent-rewrite test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -117,20 +157,90 @@ public void addConstraint(TableNameInfo tableNameInfo, 
String constraintName,
                         tableNameInfo, (ForeignKeyConstraint) constraint);
             }
             tableConstraints.put(constraintName, constraint);
+            if (constraint instanceof DistributionMappingConstraint) {
+                putTableLocalConstraint(table, constraintName, constraint);
+            }
             if (!replay) {
-                logAddConstraint(tableNameInfo, constraint);
+                logItem = submitAddConstraint(tableNameInfo, constraint);
             }
             LOG.info("Added constraint {} on table {}", constraintName, key);
         } finally {
             writeUnlock();
+            if (acquireFrontendAdmission) {
+                frontendAdmissionLock.unlock();
+            }
+        }
+        return logItem;
+    }
+
+    public void acquireFrontendAdmission() throws DdlException {
+        try {
+            if (!frontendAdmissionLock.tryLock(
+                    Config.catalog_try_lock_timeout_ms, 
TimeUnit.MILLISECONDS)) {
+                throw new DdlException("Failed to acquire frontend admission 
lock. Try again");
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new DdlException("Interrupted while acquiring frontend 
admission lock", e);
+        }
+        boolean admitted = false;
+        try {
+            readLock();
+            try {
+                for (Map<String, Constraint> tableConstraints : 
constraintsMap.values()) {

Review Comment:
   [P1] Include recycled mappings in frontend admission
   
   This scans only the live manager index, but a non-force DROP removes that 
entry while deliberately retaining the mapping in the table's `TableAttributes` 
inside `CatalogRecycleBin`; the new recovery test even asserts the entry is 
absent after DROP and rebuilt on RECOVER. Recycle-bin images serialize that 
table through Gson, so ADD FRONTEND now succeeds in this interval even though 
an older candidate still cannot deserialize the retained 
`DistributionMappingConstraint` subtype when it loads the leader image. DROP 
DATABASE has the same hidden-copy state. Please keep a capability flag/count 
that includes serialized recycle-bin mappings (or scan them under the same 
admission fence) until those copies are erased, and cover DROP-without-FORCE 
followed by old-FE admission and recovery.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java:
##########
@@ -5996,6 +6002,18 @@ private void renameColumn(Database db, OlapTable table, 
String colName,
         if (partitionInfo.getPartitionColumns().stream().anyMatch(c -> 
c.getName().equalsIgnoreCase(colName))) {
             throw new DdlException("Renaming partition columns has problems, 
forbidden in current Doris version");
         }
+        String mappingConstraint =
+                
constraintManager.findDistributionMappingConstraintWithColumn(table, colName);
+        if (mappingConstraint == null) {
+            mappingConstraint = constraintManager.findConstraintWithColumn(

Review Comment:
   [P1] Match ordinary constraint columns case-insensitively
   
   This fallback uses `findConstraintWithColumn`, whose PK/UK/FK branches call 
case-sensitive `Collection.contains`, while the actual schema rename resolves 
`colName` through a case-insensitive column map. Thus a constraint stored on 
`k2` does not block `RENAME COLUMN K2 k3`: the schema removes `k2` but the 
persisted constraint keeps that name, and recreating `k2` can reactivate a 
false uniqueness/FK proof on the replacement column. The mapping-specific 
branch already uses `equalsIgnoreCase`; please apply the same identifier 
semantics to ordinary constraints and add alternate-case PK/UK/FK rename 
coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java:
##########
@@ -935,12 +950,8 @@ public void dropColumn(TableIf dorisTable, String 
columnName) throws UserExcepti
         ConnectorMetadata metadata = PluginDrivenMetadata.get(session, 
connector);
         ConnectorTableHandle handle = resolveAlterHandle(externalTable, 
session, metadata);
         long updateTime = System.currentTimeMillis();
-        try {
-            metadata.dropColumn(session, handle, columnName);
-        } catch (DorisConnectorException e) {
-            throw new DdlException(e.getMessage(), e);
-        }
-        afterExternalDdl(externalTable, updateTime);
+        executeSchemaMutation(externalTable, updateTime,

Review Comment:
   [P1] Preserve constraints across plugin column DDL
   
   `executeSchemaMutation` only orders this remote change against constraint 
ADD/DROP; it never validates an already committed constraint. 
`PLUGIN_EXTERNAL_TABLE` DDL reaches this method directly and bypasses 
`SchemaChangeHandler.processDropColumn`, so `ADD CONSTRAINT pk PRIMARY KEY(c)` 
can finish and a later `DROP COLUMN c` still succeeds remotely and refreshes 
the cache while `pk` continues naming the missing column. RENAME has the same 
stale-name behavior, and recreating `c` can reactivate a false optimizer proof. 
Please check the centralized constraints inside the same mutation protocol 
before the remote DROP/RENAME (or atomically rewrite and journal them), with 
existing-constraint and ADD-before-DDL coverage.



-- 
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]

Reply via email to