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


##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/PrimaryKeyConstraint.java:
##########
@@ -19,24 +19,36 @@
 
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.TableIf;
+import org.apache.doris.info.TableNameInfo;
+import org.apache.doris.persist.gson.GsonPostProcessable;
 
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gson.annotations.SerializedName;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-public class PrimaryKeyConstraint extends Constraint {
+public class PrimaryKeyConstraint extends Constraint implements 
GsonPostProcessable {
     @SerializedName(value = "cols")
     private final Set<String> columns;
 
     // record the foreign table which references the primary key
     @SerializedName(value = "ft")
     private final Set<TableIdentifier> foreignTables = new HashSet<>();
 
+    // qualified name strings kept for backward-compatible deserialization
+    @SerializedName(value = "ftn")
+    private final Set<String> foreignTableNameStrs = new HashSet<>();
+
+    @SerializedName(value = "ftni")
+    private final List<TableNameInfo> foreignTableInfos = new ArrayList<>();

Review Comment:
   **Low: `foreignTables` (old Set<TableIdentifier> at line 43) is never 
updated by new code paths**
   
   `addForeignTable(TableNameInfo)` at line 65 only adds to `foreignTableInfos` 
and `foreignTableNameStrs`, not to `foreignTables`. The deprecated 
`getForeignTables()` still reads from `foreignTables`, which returns 
empty/stale results for constraints created via the new `ConstraintManager`.
   
   Please verify no remaining callers of `getForeignTables()` exist. If they 
do, they will silently get wrong results. Consider making `getForeignTables()` 
resolve from `foreignTableInfos` as a safety net, or remove the deprecated 
method entirely if no callers remain.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -0,0 +1,901 @@
+// 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.
+
+package org.apache.doris.catalog.constraint;
+
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.common.io.Writable;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.info.TableNameInfo;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.persist.AlterConstraintLog;
+import org.apache.doris.persist.gson.GsonPostProcessable;
+import org.apache.doris.persist.gson.GsonUtils;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.annotations.SerializedName;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+/**
+ * Centralized manager for all table constraints (PK, FK, UNIQUE).
+ * Constraints are indexed by fully qualified table name (catalog.db.table).
+ */
+public class ConstraintManager implements Writable, GsonPostProcessable {
+
+    private static final Logger LOG = 
LogManager.getLogger(ConstraintManager.class);
+
+    @SerializedName("cm")
+    private final ConcurrentHashMap<String, Map<String, Constraint>> 
constraintsMap
+            = new ConcurrentHashMap<>();
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+    public ConstraintManager() {
+    }
+
+    private static String toKey(TableNameInfo tni) {
+        return tni.getCtl() + "." + tni.getDb() + "." + tni.getTbl();
+    }
+
+    /** Returns true if no constraints are stored. */
+    public boolean isEmpty() {
+        return constraintsMap.isEmpty();
+    }
+
+    private void readLock() {
+        lock.readLock().lock();
+    }
+
+    private void readUnlock() {
+        lock.readLock().unlock();
+    }
+
+    private void writeLock() {
+        lock.writeLock().lock();
+    }
+
+    private void writeUnlock() {
+        lock.writeLock().unlock();
+    }
+
+    /**
+     * Add a constraint to the specified table.
+     * For FK constraints, validates that the referenced PK exists
+     * and registers bidirectional reference via foreignTableInfos.
+     */
+    public void addConstraint(TableNameInfo tableNameInfo, String 
constraintName,
+            Constraint constraint, boolean replay) {
+        String key = toKey(tableNameInfo);
+        writeLock();
+        try {
+            if (!replay) {
+                validateTableAndColumns(tableNameInfo, constraint);

Review Comment:
   **Medium: Validation under write lock may cause long hold times or deadlock**
   
   `validateTableAndColumns()` resolves tables via 
`Env.getCurrentEnv().getCatalogMgr().getCatalog()` → `getDbNullable()` → 
`getTableNullable()`. For external catalogs, this may trigger 
`makeSureInitialized()` which acquires catalog-level synchronized locks and 
potentially makes RPCs.
   
   Holding the `ConstraintManager` write lock while doing catalog resolution 
risks:
   1. Long lock hold times if external catalog initialization is slow
   2. Deadlock if any catalog initialization callback reads constraints from 
`ConstraintManager`
   
   Since validation is read-only and only runs on the non-replay path, consider 
performing it **before** acquiring the write lock:
   ```java
   public void addConstraint(..., boolean replay) {
       if (!replay) {
           validateTableAndColumns(tableNameInfo, constraint);
       }
       writeLock();
       try {
           // ... mutation only ...
       } finally {
           writeUnlock();
       }
   }
   ```
   Note: `checkConstraintNotExistence` must remain under the lock since it's a 
read-check-write sequence.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -0,0 +1,901 @@
+// 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.
+
+package org.apache.doris.catalog.constraint;
+
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.common.io.Writable;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.info.TableNameInfo;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.persist.AlterConstraintLog;
+import org.apache.doris.persist.gson.GsonPostProcessable;
+import org.apache.doris.persist.gson.GsonUtils;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.annotations.SerializedName;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+/**
+ * Centralized manager for all table constraints (PK, FK, UNIQUE).
+ * Constraints are indexed by fully qualified table name (catalog.db.table).
+ */
+public class ConstraintManager implements Writable, GsonPostProcessable {
+
+    private static final Logger LOG = 
LogManager.getLogger(ConstraintManager.class);
+
+    @SerializedName("cm")
+    private final ConcurrentHashMap<String, Map<String, Constraint>> 
constraintsMap
+            = new ConcurrentHashMap<>();
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+    public ConstraintManager() {
+    }
+
+    private static String toKey(TableNameInfo tni) {
+        return tni.getCtl() + "." + tni.getDb() + "." + tni.getTbl();

Review Comment:
   **High: `TableNameInfo` `equals()`/`hashCode()` contract violation impacts 
constraint identity**
   
   `toKey()` always includes the catalog name (`tni.getCtl() + "." + 
tni.getDb() + "." + tni.getTbl()`), but `TableNameInfo.equals()` uses 
`toString()` which omits the `"internal"` catalog prefix. Meanwhile, 
`TableNameInfo.hashCode()` uses `Objects.hash(tbl, db, ctl)` which **includes** 
the raw `ctl` field.
   
   This means two `TableNameInfo` objects that are `equals()` can have 
different `hashCode()` values:
   ```java
   new TableNameInfo("internal", "test", "t1")  // toString()="test.t1", hash 
includes "internal"
   new TableNameInfo(null, "test", "t1")          // toString()="test.t1", hash 
includes null
   // equals() = true, hashCode() = DIFFERENT  → violates Java contract
   ```
   
   This is pre-existing in `TableNameInfo`, but this PR now critically depends 
on it for:
   - `PrimaryKeyConstraint.removeForeignTable(TableNameInfo)` at line 85 — uses 
`equals()` for matching
   - `ForeignKeyConstraint.isReferringPK()` — uses `equals()` on 
`referencedTableInfo`
   - `swapPrimaryKeyForeignTables()` at line 876 — uses `equals()` via stream 
`anyMatch`
   
   If any code path creates a `TableNameInfo` with `ctl=null` (e.g., from the 
`TableNameInfo(String alias)` constructor with a 2-part name like 
`"db.table"`), lookups in collections that use `hashCode()` will silently fail.
   
   **Recommendation:** Fix `TableNameInfo.hashCode()` to be consistent with 
`equals()` — either `return toString().hashCode()` or make `equals()` compare 
fields directly including `ctl`.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java:
##########
@@ -58,35 +57,39 @@ public DropConstraintCommand(String name, LogicalPlan plan) 
{
         this.plan = plan;
     }
 
-    private static List<TableIf> getConstraintRelatedTables(Constraint 
constraint) {
-        List<TableIf> tables = Lists.newArrayList();
-        if (constraint instanceof PrimaryKeyConstraint) {
-            tables.addAll(((PrimaryKeyConstraint) 
constraint).getForeignTables());
-        } else if (constraint instanceof ForeignKeyConstraint) {
-            tables.add(((ForeignKeyConstraint) 
constraint).getReferencedTable());
-        }
-        return tables;
-    }
-
     @Override
     public void run(ConnectContext ctx, StmtExecutor executor) throws 
Exception {
-        TableIf table = extractTable(ctx, plan);
-        table.readLock();
+        TableNameInfo tableNameInfo;
         try {
-            Constraint constraint = table.getConstraintsMapUnsafe().get(name);
-            if (constraint == null) {
-                throw new AnalysisException(
-                        String.format("Unknown constraint %s on table %s.", 
name, table.getName()));
-            }
-        } finally {
-            table.readUnlock();
+            TableIf table = extractTable(ctx, plan);
+            tableNameInfo = new TableNameInfo(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.
+            tableNameInfo = extractTableNameFromPlan(ctx);
         }
-        table.writeLock();
-        try {
-            table.dropConstraint(name, false);
-        } finally {
-            table.writeUnlock();
+        Env.getCurrentEnv().getConstraintManager().dropConstraint(
+                tableNameInfo, name, false);
+    }
+
+    private TableNameInfo extractTableNameFromPlan(ConnectContext ctx) {
+        if (!(plan instanceof UnboundRelation)) {
+            throw new AnalysisException(
+                    "Cannot resolve table for dropping constraint " + name);
+        }
+        UnboundRelation unbound = (UnboundRelation) plan;
+        List<String> parts = unbound.getNameParts();
+        String ctl = ctx.getCurrentCatalog() != null

Review Comment:
   **Low: Fallback may construct incorrect `TableNameInfo` from connect context 
defaults**
   
   `ctx.getCurrentCatalog()` and `ctx.getDatabase()` reflect the current 
session state, which may differ from the original session that created the 
constraint. During forwarded execution (this command implements 
`ForwardWithSync`), the master's connect context may have different defaults.
   
   Since this fallback only triggers when the table no longer exists (line 
66-69 comment), the practical risk is limited. Consider adding a `LOG.warn` 
when falling back to this path to aid debugging unexpected constraint-not-found 
errors.



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