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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java:
##########
@@ -160,30 +218,218 @@ public static Set<Expression> 
constructPredicates(Set<PartitionItem> partitions,
      */
     @VisibleForTesting
     public static Set<Expression> constructPredicates(Set<PartitionItem> 
partitions, Slot colSlot) {
+        return constructPredicates(partitions, colSlot, Optional.empty());
+    }
+
+    private static Set<Expression> constructPredicates(Set<PartitionItem> 
partitions, Slot colSlot,
+            Optional<Type> columnType) {
         Set<Expression> predicates = new HashSet<>();
         if (partitions.isEmpty()) {
             return Sets.newHashSet(BooleanLiteral.TRUE);
         }
         if (partitions.iterator().next() instanceof ListPartitionItem) {
             for (PartitionItem item : partitions) {
-                predicates.add(convertListPartitionToIn(item, colSlot));
+                predicates.add(convertListPartitionToIn(item, colSlot, 
columnType));
             }
         } else {
             for (PartitionItem item : partitions) {
-                predicates.add(convertRangePartitionToCompare(item, colSlot));
+                predicates.add(convertRangePartitionToCompare(item, colSlot, 
columnType));
+            }
+        }
+        return predicates;
+    }
+
+    /**
+     * The predicate a base table is read through when the refresh is to read 
exactly these partitions of it.
+     *
+     * <p>A partition of a list partitioned table holds one key per partition 
column, and the column the MV
+     * partition is named by is only one of them. A predicate on that column 
alone also reaches the
+     * partitions whose other keys differ -- a table partitioned by (d, 
region) has one partition of
+     * (d0, 'US') and one of (d0, 'EU'), and `d = d0` reaches both, while only 
the second is a partition
+     * this refresh is to read; a later drop of the first would then leave its 
rows in the MV partition
+     * while the snapshot, which names only the second, still calls it 
synchronized. So a list partition is
+     * pinned to its whole key. A range partition is pinned to its bounds, 
which is the same thing: a base
+     * table partitioned by range has a single partition column, see
+     * {@code RangePartitionItem#toPartitionKeyDesc(int)}.
+     *
+     * <p>The partitions are never empty: a table the caller scopes with no 
partition is read as nothing
+     * before this is reached, see {@code constructTableWithPredicates}.
+     */
+    private static Set<Expression> 
constructPredicatesOfBasePartitions(Set<PartitionItem> partitions,
+            OlapTable baseTable, String colName) {
+        return constructPredicatesOfBasePartitions(partitions, baseTable, 
colName, UnboundSlot::new);
+    }
+
+    /**
+     * The same, with the partition columns read through the slots the caller 
names them by. A caller that
+     * writes these predicates into a plan that is already bound and is never 
bound again -- the union
+     * compensation -- has to pass that plan's own slots: an unbound slot 
there fails the rewrite instead of
+     * narrowing the read.
+     */
+    private static Set<Expression> 
constructPredicatesOfBasePartitions(Set<PartitionItem> partitions,
+            OlapTable baseTable, String colName, Function<String, Slot> 
slotOfColumn) {
+        List<Column> partitionColumns = baseTable.getPartitionColumns();
+        List<Type> partitionColumnTypes = Lists.transform(partitionColumns, 
Column::getType);
+        if (!(partitions.iterator().next() instanceof ListPartitionItem)) {
+            Set<Expression> predicates = new HashSet<>();
+            for (PartitionItem item : partitions) {
+                predicates.add(convertRangePartitionToCompare(item, 
slotOfColumn.apply(colName),
+                        Optional.of(partitionColumnTypes.get(0))));
             }
+            return predicates;
+        }
+        List<Slot> partitionSlots = Lists.newArrayList();
+        for (Column partitionColumn : partitionColumns) {
+            partitionSlots.add(slotOfColumn.apply(partitionColumn.getName()));
+        }
+        Set<Expression> predicates = new HashSet<>();
+        for (PartitionItem item : partitions) {
+            predicates.add(convertListPartitionToKey(item, partitionSlots, 
partitionColumnTypes));
         }
         return predicates;
     }
 
-    private static Expression convertPartitionKeyToLiteral(PartitionKey key) {
-        return Literal.fromLegacyLiteral(key.getKeys().get(0),
-                Type.fromPrimitiveType(key.getTypes().get(0)));
+    /**
+     * The predicate for a table whose mapped partitions include a list 
partition's default one.
+     *
+     * <p>That partition takes the rows no other partition of the table 
claims, and an MV partition takes the
+     * rows whose own key falls in it wherever the base table placed them -- 
so those rows are the ones the MV
+     * partition's own key range names, and no predicate on the partition 
columns picks them out on its own: a
+     * test on those columns reaches the rows an explicit partition holds as 
readily as it reaches theirs.
+     * The read is therefore written the other way round, as the record's own 
partitions plus what the key
+     * range holds beyond them:
+     *
+     * <ul>
+     *   <li>the partitions this MV partition is recorded with, pinned by 
their whole key, and</li>
+     *   <li>the rows of the MV partitions' key range that no explicit 
partition the mapping does not name
+     *       holds, which is the default partition's rows in that range and 
nothing else.</li>
+     * </ul>
+     *
+     * <p>An explicit partition the window left out shares the MV partition's 
key range with a retained one --
+     * the shape this scope is about, a list partitioned table's partitions 
holding several keys of the MV's
+     * partition column -- and reading it back would put its rows in the MV 
partition while no snapshot names
+     * it, leaving them there through a later drop of it. Subtracting the 
partitions the mapping does not name
+     * is what keeps the read to the record; the partitions it does name are 
added back by the first part
+     * whether or not their keys fall in the range, the way every other scoped 
read pins them.
+     */
+    private static Set<Expression> 
constructPredicatesOfADefaultPartitionTable(Set<PartitionItem> mvItems,
+            Set<PartitionItem> mappedItems, Set<String> readable, OlapTable 
baseTable, String colName)
+            throws AnalysisException {
+        List<Column> partitionColumns = baseTable.getPartitionColumns();
+        List<Type> partitionColumnTypes = Lists.transform(partitionColumns, 
Column::getType);
+        List<Slot> partitionSlots = Lists.newArrayList();
+        for (Column partitionColumn : partitionColumns) {
+            partitionSlots.add(new UnboundSlot(partitionColumn.getName()));
+        }
+        Set<PartitionItem> notMapped = Sets.newHashSet();
+        for (String partitionName : baseTable.getPartitionNames()) {
+            if (readable.contains(partitionName)) {
+                continue;
+            }
+            PartitionItem item = 
baseTable.getPartitionItemOrAnalysisException(partitionName);
+            if (!item.isDefaultPartition()) {
+                notMapped.add(item);
+            }
+        }
+        Expression inRange = ExpressionUtils.or(constructPredicates(mvItems, 
colName,
+                Optional.of(partitionColumnType(baseTable, colName))));
+        if (!notMapped.isEmpty()) {
+            inRange = ExpressionUtils.and(inRange, new Not(ExpressionUtils.or(

Review Comment:
   [P1] Keep default rows that predate a new explicit key. A LIST(d,k) table 
may already hold (2020,1) in p_default when ADD PARTITION p_expired VALUES IN 
((2020,1)) runs; ADD creates an empty partition and does not migrate that row. 
With p_kept containing (2020,2) and a two-year sync window excluding p_expired, 
this NOT(<=>) removes the old row from the mapped default scan even though 
p_default is recorded in the MV snapshot. A COMPLETE refresh then silently 
omits committed base data from the MV and considers it synchronized. Scope the 
exclusion by physical partition or account for rows already in default; cover 
insert-before-ADD.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java:
##########
@@ -1619,6 +1621,58 @@ public Map<String, Map<MTMVRelatedTableIf, Set<String>>> 
calculatePartitionMappi
         return res;
     }
 
+    /**
+     * The list partition each base table of this MV has that takes the rows 
no other partition of it claims,
+     * by table, or none for a table that has no such partition.
+     *
+     * <p>Read once per mapping rather than per MV partition: the mapping 
describes every MV partition and the
+     * answer is the table's, not the partition's. The partition metadata is 
read without a lock, like the
+     * rest of the mapping this is part of.
+     */
+    private Map<MTMVRelatedTableIf, String> defaultListPartitionsOf() throws 
AnalysisException {
+        Map<MTMVRelatedTableIf, String> res = Maps.newHashMap();
+        for (MTMVRelatedTableIf pctTable : mvPartitionInfo.getPctTables()) {
+            if (!(pctTable instanceof OlapTable)) {
+                continue;
+            }
+            OlapTable olapTable = (OlapTable) pctTable;
+            if (!(olapTable.getPartitionInfo() instanceof ListPartitionInfo)) {
+                continue;
+            }
+            for (String partitionName : olapTable.getPartitionNames()) {

Review Comment:
   [P2] Protect the default LIST metadata walk used by EXPLAIN. EXPLAIN REFRESH 
COMPLETE calls MTMVRefreshContext.buildContext before ExplainCommand invokes 
planner.plan and takes table read locks. getAndCopyPartitionItems() releases 
its own lock before this new getPartitionNames()/item walk, while ADD/DROP 
modifies OlapTable's TreeMap under the write lock. DDL can cause a 
ConcurrentModificationException, a missing-name AnalysisException, or a mapping 
from two metadata states. Derive the default name from the locked 
partition-item copy or hold a read lock across this walk.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java:
##########
@@ -160,30 +218,218 @@ public static Set<Expression> 
constructPredicates(Set<PartitionItem> partitions,
      */
     @VisibleForTesting
     public static Set<Expression> constructPredicates(Set<PartitionItem> 
partitions, Slot colSlot) {
+        return constructPredicates(partitions, colSlot, Optional.empty());
+    }
+
+    private static Set<Expression> constructPredicates(Set<PartitionItem> 
partitions, Slot colSlot,
+            Optional<Type> columnType) {
         Set<Expression> predicates = new HashSet<>();
         if (partitions.isEmpty()) {
             return Sets.newHashSet(BooleanLiteral.TRUE);
         }
         if (partitions.iterator().next() instanceof ListPartitionItem) {
             for (PartitionItem item : partitions) {
-                predicates.add(convertListPartitionToIn(item, colSlot));
+                predicates.add(convertListPartitionToIn(item, colSlot, 
columnType));
             }
         } else {
             for (PartitionItem item : partitions) {
-                predicates.add(convertRangePartitionToCompare(item, colSlot));
+                predicates.add(convertRangePartitionToCompare(item, colSlot, 
columnType));
+            }
+        }
+        return predicates;
+    }
+
+    /**
+     * The predicate a base table is read through when the refresh is to read 
exactly these partitions of it.
+     *
+     * <p>A partition of a list partitioned table holds one key per partition 
column, and the column the MV
+     * partition is named by is only one of them. A predicate on that column 
alone also reaches the
+     * partitions whose other keys differ -- a table partitioned by (d, 
region) has one partition of
+     * (d0, 'US') and one of (d0, 'EU'), and `d = d0` reaches both, while only 
the second is a partition
+     * this refresh is to read; a later drop of the first would then leave its 
rows in the MV partition
+     * while the snapshot, which names only the second, still calls it 
synchronized. So a list partition is
+     * pinned to its whole key. A range partition is pinned to its bounds, 
which is the same thing: a base
+     * table partitioned by range has a single partition column, see
+     * {@code RangePartitionItem#toPartitionKeyDesc(int)}.
+     *
+     * <p>The partitions are never empty: a table the caller scopes with no 
partition is read as nothing
+     * before this is reached, see {@code constructTableWithPredicates}.
+     */
+    private static Set<Expression> 
constructPredicatesOfBasePartitions(Set<PartitionItem> partitions,
+            OlapTable baseTable, String colName) {
+        return constructPredicatesOfBasePartitions(partitions, baseTable, 
colName, UnboundSlot::new);
+    }
+
+    /**
+     * The same, with the partition columns read through the slots the caller 
names them by. A caller that
+     * writes these predicates into a plan that is already bound and is never 
bound again -- the union
+     * compensation -- has to pass that plan's own slots: an unbound slot 
there fails the rewrite instead of
+     * narrowing the read.
+     */
+    private static Set<Expression> 
constructPredicatesOfBasePartitions(Set<PartitionItem> partitions,
+            OlapTable baseTable, String colName, Function<String, Slot> 
slotOfColumn) {
+        List<Column> partitionColumns = baseTable.getPartitionColumns();
+        List<Type> partitionColumnTypes = Lists.transform(partitionColumns, 
Column::getType);
+        if (!(partitions.iterator().next() instanceof ListPartitionItem)) {
+            Set<Expression> predicates = new HashSet<>();
+            for (PartitionItem item : partitions) {
+                predicates.add(convertRangePartitionToCompare(item, 
slotOfColumn.apply(colName),
+                        Optional.of(partitionColumnTypes.get(0))));
             }
+            return predicates;
+        }
+        List<Slot> partitionSlots = Lists.newArrayList();
+        for (Column partitionColumn : partitionColumns) {
+            partitionSlots.add(slotOfColumn.apply(partitionColumn.getName()));
+        }
+        Set<Expression> predicates = new HashSet<>();
+        for (PartitionItem item : partitions) {
+            predicates.add(convertListPartitionToKey(item, partitionSlots, 
partitionColumnTypes));
         }
         return predicates;
     }
 
-    private static Expression convertPartitionKeyToLiteral(PartitionKey key) {
-        return Literal.fromLegacyLiteral(key.getKeys().get(0),
-                Type.fromPrimitiveType(key.getTypes().get(0)));
+    /**
+     * The predicate for a table whose mapped partitions include a list 
partition's default one.
+     *
+     * <p>That partition takes the rows no other partition of the table 
claims, and an MV partition takes the
+     * rows whose own key falls in it wherever the base table placed them -- 
so those rows are the ones the MV
+     * partition's own key range names, and no predicate on the partition 
columns picks them out on its own: a
+     * test on those columns reaches the rows an explicit partition holds as 
readily as it reaches theirs.
+     * The read is therefore written the other way round, as the record's own 
partitions plus what the key
+     * range holds beyond them:
+     *
+     * <ul>
+     *   <li>the partitions this MV partition is recorded with, pinned by 
their whole key, and</li>
+     *   <li>the rows of the MV partitions' key range that no explicit 
partition the mapping does not name
+     *       holds, which is the default partition's rows in that range and 
nothing else.</li>
+     * </ul>
+     *
+     * <p>An explicit partition the window left out shares the MV partition's 
key range with a retained one --
+     * the shape this scope is about, a list partitioned table's partitions 
holding several keys of the MV's
+     * partition column -- and reading it back would put its rows in the MV 
partition while no snapshot names
+     * it, leaving them there through a later drop of it. Subtracting the 
partitions the mapping does not name
+     * is what keeps the read to the record; the partitions it does name are 
added back by the first part
+     * whether or not their keys fall in the range, the way every other scoped 
read pins them.
+     */
+    private static Set<Expression> 
constructPredicatesOfADefaultPartitionTable(Set<PartitionItem> mvItems,
+            Set<PartitionItem> mappedItems, Set<String> readable, OlapTable 
baseTable, String colName)
+            throws AnalysisException {
+        List<Column> partitionColumns = baseTable.getPartitionColumns();
+        List<Type> partitionColumnTypes = Lists.transform(partitionColumns, 
Column::getType);
+        List<Slot> partitionSlots = Lists.newArrayList();
+        for (Column partitionColumn : partitionColumns) {
+            partitionSlots.add(new UnboundSlot(partitionColumn.getName()));
+        }
+        Set<PartitionItem> notMapped = Sets.newHashSet();
+        for (String partitionName : baseTable.getPartitionNames()) {

Review Comment:
   [P2] Use the mapping's locked partition metadata to build exclusions. 
MTMVTask.buildRefreshContext releases base-table read locks before this 
getPartitionNames()/item lookup; concurrent ADD/DROP can abort refresh with 
ConcurrentModificationException or AnalysisException, outside the task's 
RPC/cloud retry. An ADD for a key in this MV range after the exclusion list is 
built but before planner locking can also let the scan read new-partition rows 
absent from the recorded mapping and snapshot. The earlier mapped-item lookup 
in from() has the same post-lock race for any scoped OLAP table. Carry one 
stable partition-item snapshot into both predicate paths.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java:
##########
@@ -160,30 +218,218 @@ public static Set<Expression> 
constructPredicates(Set<PartitionItem> partitions,
      */
     @VisibleForTesting
     public static Set<Expression> constructPredicates(Set<PartitionItem> 
partitions, Slot colSlot) {
+        return constructPredicates(partitions, colSlot, Optional.empty());
+    }
+
+    private static Set<Expression> constructPredicates(Set<PartitionItem> 
partitions, Slot colSlot,
+            Optional<Type> columnType) {
         Set<Expression> predicates = new HashSet<>();
         if (partitions.isEmpty()) {
             return Sets.newHashSet(BooleanLiteral.TRUE);
         }
         if (partitions.iterator().next() instanceof ListPartitionItem) {
             for (PartitionItem item : partitions) {
-                predicates.add(convertListPartitionToIn(item, colSlot));
+                predicates.add(convertListPartitionToIn(item, colSlot, 
columnType));
             }
         } else {
             for (PartitionItem item : partitions) {
-                predicates.add(convertRangePartitionToCompare(item, colSlot));
+                predicates.add(convertRangePartitionToCompare(item, colSlot, 
columnType));
+            }
+        }
+        return predicates;
+    }
+
+    /**
+     * The predicate a base table is read through when the refresh is to read 
exactly these partitions of it.
+     *
+     * <p>A partition of a list partitioned table holds one key per partition 
column, and the column the MV
+     * partition is named by is only one of them. A predicate on that column 
alone also reaches the
+     * partitions whose other keys differ -- a table partitioned by (d, 
region) has one partition of
+     * (d0, 'US') and one of (d0, 'EU'), and `d = d0` reaches both, while only 
the second is a partition
+     * this refresh is to read; a later drop of the first would then leave its 
rows in the MV partition
+     * while the snapshot, which names only the second, still calls it 
synchronized. So a list partition is
+     * pinned to its whole key. A range partition is pinned to its bounds, 
which is the same thing: a base
+     * table partitioned by range has a single partition column, see
+     * {@code RangePartitionItem#toPartitionKeyDesc(int)}.
+     *
+     * <p>The partitions are never empty: a table the caller scopes with no 
partition is read as nothing
+     * before this is reached, see {@code constructTableWithPredicates}.
+     */
+    private static Set<Expression> 
constructPredicatesOfBasePartitions(Set<PartitionItem> partitions,
+            OlapTable baseTable, String colName) {
+        return constructPredicatesOfBasePartitions(partitions, baseTable, 
colName, UnboundSlot::new);
+    }
+
+    /**
+     * The same, with the partition columns read through the slots the caller 
names them by. A caller that
+     * writes these predicates into a plan that is already bound and is never 
bound again -- the union
+     * compensation -- has to pass that plan's own slots: an unbound slot 
there fails the rewrite instead of
+     * narrowing the read.
+     */
+    private static Set<Expression> 
constructPredicatesOfBasePartitions(Set<PartitionItem> partitions,
+            OlapTable baseTable, String colName, Function<String, Slot> 
slotOfColumn) {
+        List<Column> partitionColumns = baseTable.getPartitionColumns();
+        List<Type> partitionColumnTypes = Lists.transform(partitionColumns, 
Column::getType);
+        if (!(partitions.iterator().next() instanceof ListPartitionItem)) {
+            Set<Expression> predicates = new HashSet<>();
+            for (PartitionItem item : partitions) {
+                predicates.add(convertRangePartitionToCompare(item, 
slotOfColumn.apply(colName),
+                        Optional.of(partitionColumnTypes.get(0))));
             }
+            return predicates;
+        }
+        List<Slot> partitionSlots = Lists.newArrayList();
+        for (Column partitionColumn : partitionColumns) {
+            partitionSlots.add(slotOfColumn.apply(partitionColumn.getName()));
+        }
+        Set<Expression> predicates = new HashSet<>();
+        for (PartitionItem item : partitions) {
+            predicates.add(convertListPartitionToKey(item, partitionSlots, 
partitionColumnTypes));
         }
         return predicates;
     }
 
-    private static Expression convertPartitionKeyToLiteral(PartitionKey key) {
-        return Literal.fromLegacyLiteral(key.getKeys().get(0),
-                Type.fromPrimitiveType(key.getTypes().get(0)));
+    /**
+     * The predicate for a table whose mapped partitions include a list 
partition's default one.
+     *
+     * <p>That partition takes the rows no other partition of the table 
claims, and an MV partition takes the
+     * rows whose own key falls in it wherever the base table placed them -- 
so those rows are the ones the MV
+     * partition's own key range names, and no predicate on the partition 
columns picks them out on its own: a
+     * test on those columns reaches the rows an explicit partition holds as 
readily as it reaches theirs.
+     * The read is therefore written the other way round, as the record's own 
partitions plus what the key
+     * range holds beyond them:
+     *
+     * <ul>
+     *   <li>the partitions this MV partition is recorded with, pinned by 
their whole key, and</li>
+     *   <li>the rows of the MV partitions' key range that no explicit 
partition the mapping does not name
+     *       holds, which is the default partition's rows in that range and 
nothing else.</li>
+     * </ul>
+     *
+     * <p>An explicit partition the window left out shares the MV partition's 
key range with a retained one --
+     * the shape this scope is about, a list partitioned table's partitions 
holding several keys of the MV's
+     * partition column -- and reading it back would put its rows in the MV 
partition while no snapshot names
+     * it, leaving them there through a later drop of it. Subtracting the 
partitions the mapping does not name
+     * is what keeps the read to the record; the partitions it does name are 
added back by the first part
+     * whether or not their keys fall in the range, the way every other scoped 
read pins them.
+     */
+    private static Set<Expression> 
constructPredicatesOfADefaultPartitionTable(Set<PartitionItem> mvItems,
+            Set<PartitionItem> mappedItems, Set<String> readable, OlapTable 
baseTable, String colName)
+            throws AnalysisException {
+        List<Column> partitionColumns = baseTable.getPartitionColumns();
+        List<Type> partitionColumnTypes = Lists.transform(partitionColumns, 
Column::getType);
+        List<Slot> partitionSlots = Lists.newArrayList();
+        for (Column partitionColumn : partitionColumns) {
+            partitionSlots.add(new UnboundSlot(partitionColumn.getName()));
+        }
+        Set<PartitionItem> notMapped = Sets.newHashSet();

Review Comment:
   [P2] Limit default-partition exclusions to keys that can overlap this MV 
batch. With N distinct explicit LIST keys plus p_default, a one-partition batch 
maps one explicit key, but this loop turns the other N-1 keys into a 
NOT(OR(...)) predicate for every batch. COMPLETE refresh therefore builds and 
plans about N(N-1) tuple terms, even when those other keys cannot match this 
batch's MV range. Prune exclusions by projected MV key or use physical 
partition scope so a many-partition MV does not incur quadratic FE planning 
work.



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