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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalOlapScanToPhysicalOlapScan.java:
##########
@@ -132,12 +140,63 @@ public static DistributionSpec 
convertDistribution(LogicalOlapScan olapScan) {
                         }
                     }
                 }
-                return new DistributionSpecHash(hashColumns, 
ShuffleType.NATURAL, olapScan.getTable().getId(),
-                        olapScan.getSelectedIndexId(), 
Sets.newLinkedHashSet(olapScan.getSelectedPartitionIds()));
+                return createNaturalHashSpec(olapScan, hashDistributionInfo, 
hashColumns, output);
             }
         } else {
             // RandomDistributionInfo
             return DistributionSpecStorageAny.INSTANCE;
         }
     }
+
+    private static DistributionSpecHash createNaturalHashSpec(LogicalOlapScan 
olapScan,
+            HashDistributionInfo hashDistributionInfo, List<ExprId> 
hashColumns, List<Slot> output) {
+        return new DistributionSpecHash(hashColumns, ShuffleType.NATURAL, 
olapScan.getTable().getId(),
+                olapScan.getSelectedIndexId(), 
Sets.newLinkedHashSet(olapScan.getSelectedPartitionIds()),
+                buildDistributionMappings(olapScan.getTable(), 
hashDistributionInfo, output));
+    }
+
+    private static List<DistributionMapping> 
buildDistributionMappings(OlapTable table,
+            HashDistributionInfo hashDistributionInfo, List<Slot> output) {
+        ConnectContext context = ConnectContext.get();
+        if (context == null || 
!context.getSessionVariable().isEnableColocateMappingConstraint()) {
+            return ImmutableList.of();
+        }
+
+        Map<String, ExprId> columnExprIds = new HashMap<>();
+        for (Slot slot : output) {
+            SlotReference slotReference = (SlotReference) slot;
+            slotReference.getOriginalColumn().ifPresent(
+                    column -> 
columnExprIds.put(column.getName().toLowerCase(), slot.getExprId()));
+        }
+        Map<String, Integer> distributionIndices = new HashMap<>();
+        List<Column> distributionColumns = 
hashDistributionInfo.getDistributionColumns();
+        for (int i = 0; i < distributionColumns.size(); i++) {
+            
distributionIndices.put(distributionColumns.get(i).getName().toLowerCase(), i);
+        }
+
+        TableNameInfo tableNameInfo = 
TableNameInfoUtils.fromTableOrNull(table);
+        ImmutableList.Builder<DistributionMapping> mappings = 
ImmutableList.builder();
+        for (DistributionMappingConstraint constraint : 
Env.getCurrentEnv().getConstraintManager()
+                .getDistributionMappingConstraints(tableNameInfo)) {

Review Comment:
   [P1] Migrate mappings when a database is renamed
   
   These constraints are keyed only by `catalog.db.table`, but the leader and 
replay database-rename paths never migrate `ConstraintManager`. After `db1 -> 
db2`, the real table loses its mapping; if `db1.t` is later recreated, this 
lookup returns the stale assertion for an unrelated table because there is no 
table-id check. That stale proof can suppress a required Join Exchange and lose 
rows. Please migrate the keys atomically on rename/replay (or attach them by 
stable IDs) and cover old-name reuse.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalOlapScanToPhysicalOlapScan.java:
##########
@@ -132,12 +140,63 @@ public static DistributionSpec 
convertDistribution(LogicalOlapScan olapScan) {
                         }
                     }
                 }
-                return new DistributionSpecHash(hashColumns, 
ShuffleType.NATURAL, olapScan.getTable().getId(),
-                        olapScan.getSelectedIndexId(), 
Sets.newLinkedHashSet(olapScan.getSelectedPartitionIds()));
+                return createNaturalHashSpec(olapScan, hashDistributionInfo, 
hashColumns, output);
             }
         } else {
             // RandomDistributionInfo
             return DistributionSpecStorageAny.INSTANCE;
         }
     }
+
+    private static DistributionSpecHash createNaturalHashSpec(LogicalOlapScan 
olapScan,
+            HashDistributionInfo hashDistributionInfo, List<ExprId> 
hashColumns, List<Slot> output) {
+        return new DistributionSpecHash(hashColumns, ShuffleType.NATURAL, 
olapScan.getTable().getId(),
+                olapScan.getSelectedIndexId(), 
Sets.newLinkedHashSet(olapScan.getSelectedPartitionIds()),
+                buildDistributionMappings(olapScan.getTable(), 
hashDistributionInfo, output));
+    }
+
+    private static List<DistributionMapping> 
buildDistributionMappings(OlapTable table,
+            HashDistributionInfo hashDistributionInfo, List<Slot> output) {
+        ConnectContext context = ConnectContext.get();
+        if (context == null || 
!context.getSessionVariable().isEnableColocateMappingConstraint()) {
+            return ImmutableList.of();
+        }
+
+        Map<String, ExprId> columnExprIds = new HashMap<>();
+        for (Slot slot : output) {
+            SlotReference slotReference = (SlotReference) slot;
+            slotReference.getOriginalColumn().ifPresent(
+                    column -> 
columnExprIds.put(column.getName().toLowerCase(), slot.getExprId()));

Review Comment:
   [P1] Resolve mapping determinants through base-column provenance
   
   For a selected synchronous MV, a column displayed as `d` can actually be `x 
AS d`; this map then records the `x` ExprId as the declared determinant `d`. 
The existing selected-index hash-key path and the new property derivation both 
use `tryGetBaseColumnName()` to avoid that substitution. With a `d -> k` 
mapping, a join on `L.x = R.d` can therefore appear to cover bucket key `k` and 
suppress a required Exchange, losing matching rows. Please key selected-index 
determinants by verified base provenance and add forced-rollup 
positive/negative coverage.



##########
fe/fe-core/src/test/java/org/apache/doris/catalog/constraint/ConstraintPersistTest.java:
##########
@@ -207,6 +207,24 @@ void externalTableTest() throws Exception {
         Assertions.assertEquals(1, loadedMgr.getConstraints(extTni).size());
     }
 
+    @Test

Review Comment:
   [P2] Preserve mappings across recoverable drops
   
   This JSON round-trip does not cover the supported recycle-bin lifecycle. 
Non-force table drop removes its `ConstraintManager` entry before recycling, 
while table recover/replay restores only the table; database drop/recover has 
the same loss after `dropDatabaseConstraints`. Thus recovered data silently 
loses its declared mapping (including recovery under a new name). Please 
preserve constraints in recycle metadata or defer irreversible removal until 
erase/force-drop, and add table/database recover plus replay coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java:
##########
@@ -92,6 +93,13 @@ public void run(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
         } else if (constraint.isUnique()) {
             addConstraintAndInvalidate(
                     tableNameInfo, new UniqueConstraint(name, 
ImmutableSet.copyOf(columns)));
+        } else if (constraint.isDistributionMapping()) {
+            Pair<ImmutableList<String>, TableIf> distributionColumnsAndTable =
+                    extractColumnsAndTable(ctx, 
constraint.toDistributionProject());
+            Preconditions.checkState(table.getId() == 
distributionColumnsAndTable.second.getId(),
+                    "determinant and distribution columns must belong to the 
same table");
+            addConstraintAndInvalidate(tableNameInfo, new 
DistributionMappingConstraint(
+                    name, constraint.getMappingId(), columns, 
distributionColumnsAndTable.first));

Review Comment:
   [P1] Invalidate SQL-result caches for mapping DDL
   
   Constraint add/drop and their EditLog replay paths invalidate only MTMV 
rewrite caches. Nereids SQL-cache validation records table identity and visible 
data version, and this metadata-only DDL changes neither; keeping this new 
switch enabled also keeps the same variable key. A result cached under a bad 
`NOT ENFORCED` mapping can therefore survive `DROP CONSTRAINT` and continue 
returning rows from the invalid no-shuffle plan. Please invalidate the affected 
table's SQL-cache entries on add/drop and replay, with an identical-query 
regression.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java:
##########
@@ -92,6 +93,13 @@ public void run(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
         } else if (constraint.isUnique()) {
             addConstraintAndInvalidate(
                     tableNameInfo, new UniqueConstraint(name, 
ImmutableSet.copyOf(columns)));
+        } else if (constraint.isDistributionMapping()) {
+            Pair<ImmutableList<String>, TableIf> distributionColumnsAndTable =
+                    extractColumnsAndTable(ctx, 
constraint.toDistributionProject());
+            Preconditions.checkState(table.getId() == 
distributionColumnsAndTable.second.getId(),
+                    "determinant and distribution columns must belong to the 
same table");
+            addConstraintAndInvalidate(tableNameInfo, new 
DistributionMappingConstraint(

Review Comment:
   [P1] Serialize mapping creation with schema changes
   
   A heavy DROP COLUMN checks constraints once, sets the table to 
`SCHEMA_CHANGE`, and keeps the old schema visible until the job publishes. This 
ADD path releases its planner table lock before manager validation; 
`ConstraintManager` holds only its own lock and checks neither the table lock 
nor `OlapTableState.NORMAL`. It can therefore validate and journal a mapping 
after DROP's check but before the column disappears, leaving stale metadata 
that can reactivate on name reuse. Please add a table-state/lock fence and a 
deterministic DROP-vs-ADD replay test.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalOlapScanToPhysicalOlapScan.java:
##########
@@ -132,12 +140,63 @@ public static DistributionSpec 
convertDistribution(LogicalOlapScan olapScan) {
                         }
                     }
                 }
-                return new DistributionSpecHash(hashColumns, 
ShuffleType.NATURAL, olapScan.getTable().getId(),
-                        olapScan.getSelectedIndexId(), 
Sets.newLinkedHashSet(olapScan.getSelectedPartitionIds()));
+                return createNaturalHashSpec(olapScan, hashDistributionInfo, 
hashColumns, output);
             }
         } else {
             // RandomDistributionInfo
             return DistributionSpecStorageAny.INSTANCE;
         }
     }
+
+    private static DistributionSpecHash createNaturalHashSpec(LogicalOlapScan 
olapScan,
+            HashDistributionInfo hashDistributionInfo, List<ExprId> 
hashColumns, List<Slot> output) {
+        return new DistributionSpecHash(hashColumns, ShuffleType.NATURAL, 
olapScan.getTable().getId(),
+                olapScan.getSelectedIndexId(), 
Sets.newLinkedHashSet(olapScan.getSelectedPartitionIds()),
+                buildDistributionMappings(olapScan.getTable(), 
hashDistributionInfo, output));
+    }
+
+    private static List<DistributionMapping> 
buildDistributionMappings(OlapTable table,
+            HashDistributionInfo hashDistributionInfo, List<Slot> output) {
+        ConnectContext context = ConnectContext.get();
+        if (context == null || 
!context.getSessionVariable().isEnableColocateMappingConstraint()) {
+            return ImmutableList.of();
+        }
+
+        Map<String, ExprId> columnExprIds = new HashMap<>();
+        for (Slot slot : output) {
+            SlotReference slotReference = (SlotReference) slot;
+            slotReference.getOriginalColumn().ifPresent(
+                    column -> 
columnExprIds.put(column.getName().toLowerCase(), slot.getExprId()));
+        }
+        Map<String, Integer> distributionIndices = new HashMap<>();
+        List<Column> distributionColumns = 
hashDistributionInfo.getDistributionColumns();
+        for (int i = 0; i < distributionColumns.size(); i++) {
+            
distributionIndices.put(distributionColumns.get(i).getName().toLowerCase(), i);
+        }
+
+        TableNameInfo tableNameInfo = 
TableNameInfoUtils.fromTableOrNull(table);
+        ImmutableList.Builder<DistributionMapping> mappings = 
ImmutableList.builder();
+        for (DistributionMappingConstraint constraint : 
Env.getCurrentEnv().getConstraintManager()
+                .getDistributionMappingConstraints(tableNameInfo)) {
+            ImmutableList.Builder<ExprId> determinants = 
ImmutableList.builder();
+            boolean allDeterminantsAvailable = true;
+            for (String column : constraint.getDeterminantColumnNames()) {
+                ExprId exprId = columnExprIds.get(column.toLowerCase());
+                if (exprId == null) {
+                    allDeterminantsAvailable = false;
+                    break;
+                }
+                determinants.add(exprId);
+            }
+            if (!allDeterminantsAvailable) {
+                continue;
+            }
+            ImmutableList<Integer> targetIndices = 
constraint.getDistributionColumnNames().stream()

Review Comment:
   [P1] Fence mapped columns against RENAME COLUMN
   
   `Env.renameColumn` (including replay) rewrites the table schema and 
hash-distribution names but does not reject or rewrite these constraints. 
Renaming a mapped target leaves its old name absent from `distributionIndices`, 
so this immutable-list collection receives `null` and ordinary scans fail when 
the feature is enabled. Renaming a determinant also leaves a dormant assertion 
that can reactivate on a replacement column. Please atomically rewrite the 
mapping or reject mapped-column renames on leader and replay, with both target 
and determinant tests.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -539,6 +548,12 @@ public String findConstraintWithColumn(
                             .contains(columnName)) {
                         return entry.getKey();
                     }
+                } else if (c instanceof DistributionMappingConstraint) {
+                    DistributionMappingConstraint mapping = 
(DistributionMappingConstraint) c;
+                    if 
(mapping.getDeterminantColumnNames().contains(columnName)

Review Comment:
   [P1] Compare mapped columns case-insensitively
   
   Constraint analysis stores the catalog column's canonical spelling, while 
DROP COLUMN resolves and removes columns with `equalsIgnoreCase`. These 
`List.contains` checks are case-sensitive, so a stored `D1` does not block 
`DROP COLUMN d1`. Re-adding that name later makes the lowercase scan lookup 
bind the stale assertion to the replacement column, which can suppress a 
required Exchange and lose rows. Please use Doris's case-insensitive identifier 
semantics here and add mixed-case drop/re-add coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java:
##########
@@ -216,6 +219,12 @@ public ImmutableList<UniqueConstraint> 
getUniqueConstraints(
                 UniqueConstraint.class);
     }
 
+    /** Returns all distribution mapping constraints for the given table. */
+    public ImmutableList<DistributionMappingConstraint> 
getDistributionMappingConstraints(
+            TableNameInfo tableNameInfo) {
+        return getConstraintsByType(toKey(tableNameInfo), 
DistributionMappingConstraint.class);

Review Comment:
   [P1] Transition mappings during atomic backup restore
   
   `BackupMeta` copies tables/resources but not centralized constraints, while 
atomic restore directly unregisters the staging/origin tables and registers the 
restored object under the origin name without a `ConstraintManager` transition 
(the replay path is the same). Because this getter is name-only, the restored 
table's new ID inherits the origin table's old mapping. A single-partition 
self-join can then pass the same-table/index/partition shortcut and suppress an 
Exchange even when restored data violates that assertion. Please back up and 
atomically restore/drop mappings with the table object, including alias and 
replay tests.



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