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


##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -980,17 +1369,25 @@ private void 
swapPrimaryKeyForeignTables(PrimaryKeyConstraint pk,
 
     // ==================== EditLog integration ====================
 
-    private void logAddConstraint(TableNameInfo tableNameInfo,
+    private EditLog.EditLogItem submitAddConstraint(TableNameInfo 
tableNameInfo,
             Constraint constraint) {
         AlterConstraintLog log = new AlterConstraintLog(
                 constraint, tableNameInfo);
-        Env.getCurrentEnv().getEditLog().logAddConstraint(log);
+        return Env.getCurrentEnv().getEditLog()
+                .submitEdit(OperationType.OP_ADD_CONSTRAINT, log);

Review Comment:
   [P1] Snapshot constraint logs before releasing the locks
   
   `AlterConstraintLog` keeps this same mutable `Constraint` object, and 
`submitEdit` only queues the `Writable`; Gson serialization happens later in 
`JournalBatch.addJournal` on the flusher. After a PK ADD releases its 
table/manager locks but before that queued payload is serialized, an FK ADD can 
mutate the PK via `addForeignTable`. The earlier PK journal record then 
includes a reverse reference created by the later operation. With non-batch 
flushing, a crash after the PK write but before the FK write replays a ghost 
reference to an FK that never became durable, so subsequent PK/table drops can 
be rejected incorrectly. FIFO ordering does not snapshot mutable payloads. 
Please serialize/deep-copy the log while the metadata locks are held, enqueue 
that immutable snapshot, and add a paused-flusher crash-prefix replay test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -893,6 +1172,116 @@ private void validateTableAndColumns(TableNameInfo 
tableNameInfo,
                         fk.getReferencedColumnNames(),
                         toKey(refTableInfo));
             }
+        } else if (constraint instanceof DistributionMappingConstraint) {
+            validateDistributionMappingConstraint(
+                    tableNameInfo, table, (DistributionMappingConstraint) 
constraint);
+        }
+        return table;
+    }
+
+    private void validateResolvedConstraint(TableNameInfo tableNameInfo, 
TableIf table,
+            TableIf referencedTable, Constraint constraint) {
+        if (constraint instanceof PrimaryKeyConstraint) {
+            validateColumnsExist(table,
+                    ((PrimaryKeyConstraint) constraint).getPrimaryKeyNames(),
+                    toKey(tableNameInfo));
+        } else if (constraint instanceof UniqueConstraint) {
+            validateColumnsExist(table,
+                    ((UniqueConstraint) constraint).getUniqueColumnNames(),
+                    toKey(tableNameInfo));
+        } else if (constraint instanceof ForeignKeyConstraint) {
+            if (referencedTable == null) {
+                throw new AnalysisException("Referenced table changed while 
adding constraint on "
+                        + tableNameInfo);
+            }
+            ForeignKeyConstraint foreignKey = (ForeignKeyConstraint) 
constraint;
+            validateColumnsExist(table, foreignKey.getForeignKeyNames(), 
toKey(tableNameInfo));
+            validateColumnsExist(referencedTable, 
foreignKey.getReferencedColumnNames(),
+                    toKey(foreignKey.getReferencedTableName()));
+        } else if (constraint instanceof DistributionMappingConstraint) {
+            validateDistributionMappingConstraint(
+                    tableNameInfo, table, (DistributionMappingConstraint) 
constraint);
+        }
+    }
+
+    private TableIf resolveTableIfPresent(TableNameInfo tableNameInfo) {
+        try {
+            return resolveTableForValidation(tableNameInfo);
+        } catch (AnalysisException e) {
+            LOG.debug("Table {} is unavailable while synchronizing table-local 
constraints",
+                    tableNameInfo, e);
+            return null;
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void putTableLocalConstraint(TableIf table, String constraintName, 
Constraint constraint) {
+        if (table instanceof Table) {
+            ((Table) 
table).getTableAttributes().getConstraintsMap().put(constraintName, constraint);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void removeTableLocalConstraint(TableIf table, String 
constraintName) {
+        if (table instanceof Table) {
+            ((Table) 
table).getTableAttributes().getConstraintsMap().remove(constraintName);
+        }
+    }
+
+    private void validateDistributionMappingConstraint(TableNameInfo 
tableNameInfo, TableIf table,
+            DistributionMappingConstraint constraint) {
+        if (!(table instanceof OlapTable)) {
+            throw new AnalysisException("Distribution mapping constraint only 
supports OLAP tables");
+        }
+        validateColumnsExist(table, constraint.getDeterminantColumnNames(), 
toKey(tableNameInfo));
+        validateColumnsExist(table, constraint.getDistributionColumnNames(), 
toKey(tableNameInfo));
+        TreeSet<String> determinantColumns = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        determinantColumns.addAll(constraint.getDeterminantColumnNames());
+        if (determinantColumns.size() != 
constraint.getDeterminantColumnNames().size()) {
+            throw new AnalysisException("Determinant columns in distribution 
mapping constraint must be unique");
+        }
+        TreeSet<String> distributionColumns = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        distributionColumns.addAll(constraint.getDistributionColumnNames());
+        if (distributionColumns.size() != 
constraint.getDistributionColumnNames().size()) {
+            throw new AnalysisException("Distribution columns in distribution 
mapping constraint must be unique");
+        }
+
+        OlapTable olapTable = (OlapTable) table;
+        if (!(olapTable.getDefaultDistributionInfo() instanceof 
HashDistributionInfo)) {
+            throw new AnalysisException("Distribution mapping constraint 
requires hash distribution");
+        }
+        List<String> tableDistributionColumns = ((HashDistributionInfo) 
olapTable.getDefaultDistributionInfo())
+                .getDistributionColumns().stream()
+                .map(column -> column.getName().toLowerCase(Locale.ROOT))
+                .collect(Collectors.toList());
+        List<String> constraintDistributionColumns = 
constraint.getDistributionColumnNames().stream()
+                .map(column -> column.toLowerCase(Locale.ROOT))
+                .collect(Collectors.toList());
+        int previousIndex = -1;
+        for (String column : constraintDistributionColumns) {
+            int index = tableDistributionColumns.indexOf(column);
+            if (index <= previousIndex) {
+                throw new AnalysisException("Distribution columns in 
distribution mapping constraint"
+                        + " must be an ordered subset of table distribution 
columns");
+            }
+            previousIndex = index;
+        }
+    }
+
+    private void validateFrontendVersionsForDistributionMappingConstraint() {
+        String currentVersion = Version.DORIS_BUILD_VERSION + "-" + 
Version.DORIS_BUILD_SHORT_HASH;
+        List<String> incompatibleFrontends = new ArrayList<>();
+        for (Frontend frontend : Env.getCurrentEnv().getFrontends(null)) {

Review Comment:
   [P2] Fence frontend admission with the subtype check
   
   This snapshots the frontend map under the ConstraintManager lock, but `ADD 
FRONTEND` uses the unrelated Env lock. An older follower can be inserted just 
after this loop and its `OP_ADD_FRONTEND` can be queued before or after 
`OP_ADD_CONSTRAINT`; in either order it eventually loads a 
`DistributionMappingConstraint` that its Gson factory does not know and fails 
image/edit-log deserialization. Later old-FE admission is unguarded for the 
same reason, and the candidate cannot report `Frontend.version` until after it 
has become ready, which is already after image loading/replay. The PR-body 
warning documents this failure but does not make the two metadata operations 
safe. Please use one capability/admission fence (or a backward-readable 
encoding) and add a latch-based concurrent ADD plus later-admission replay test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -361,40 +617,53 @@ private void dropConstraintsByPrefix(String prefix) {
      */
     public void renameTable(TableNameInfo oldTableInfo,
             TableNameInfo newTableInfo) {
-        String oldKey = toKey(oldTableInfo);
-        String newKey = toKey(newTableInfo);
         writeLock();
         try {
-            // Move this table's own constraints
-            Map<String, Constraint> tableConstraints
-                    = constraintsMap.remove(oldKey);
-            if (tableConstraints != null) {
-                constraintsMap.put(newKey, tableConstraints);
+            renameTableWithoutLock(oldTableInfo, newTableInfo);
+        } finally {
+            writeUnlock();
+        }
+    }
+
+    /** Move every qualified table key when a database is renamed. */
+    public void renameDatabase(String catalogName, String oldDbName, String 
newDbName) {
+        String oldPrefix = catalogName + "." + oldDbName + ".";
+        writeLock();
+        try {
+            List<TableNameInfo> oldTableInfos = 
constraintsMap.keySet().stream()
+                    .filter(key -> key.startsWith(oldPrefix))
+                    .map(TableNameInfo::new)
+                    .collect(Collectors.toList());
+            for (TableNameInfo oldTableInfo : oldTableInfos) {

Review Comment:
   [P2] Rewrite database constraints in one pass
   
   This invokes `renameTableWithoutLock` once for every constrained table in 
the database, but each invocation rescans every table's constraints and every 
FK/PK reference. Renaming a database with M constrained tables in a catalog 
containing N constraints is therefore O(M*N) (quadratic when most constraints 
are in that database), while the internal-catalog, database, and 
ConstraintManager write locks are all held and before the rename journal 
completes. Large constraint catalogs can stall metadata operations and replay. 
Please move all affected keys in one pass and rewrite references with a single 
traversal (and avoid per-table INFO logging), with a many-table scale test.



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