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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1006,6 +1129,267 @@ private PhysicalPlan chooseBestPlanWithRequiredGroups(
         return physicalPlan;
     }
 
+    /**
+     * Compute the set of USE_MV hint qualifiers that successfully rewrote.
+     * Marks USE_MV hint parameters that did NOT rewrite-success as UnUsed.
+     */
+    private static Set<List<String>> 
computeRequiredUseMvIdentifiers(StatementContext statementContext) {
+        Optional<UseMvHint> useMvHint = 
statementContext.getUseMvHint("USE_MV");
+        if (!useMvHint.isPresent()) {
+            return ImmutableSet.of();
+        }
+        Set<List<String>> useMvQualifiers = 
useMvHint.get().getUseMvTableColumnMap().keySet();
+        Set<List<String>> rewrittenSuccessSet = 
statementContext.getMaterializationRewrittenSuccessSet();
+        Set<List<String>> required = new HashSet<>(useMvQualifiers);
+        required.retainAll(rewrittenSuccessSet);
+        // Mark USE_MV qualifiers that did not rewrite-success as UnUsed
+        Set<List<String>> notRewritten = new HashSet<>(useMvQualifiers);
+        notRewritten.removeAll(rewrittenSuccessSet);
+        for (List<String> qualifier : notRewritten) {
+            // Update hint map to mark as not-chosen (false) with failure 
reason
+            useMvHint.get().getUseMvTableColumnMap().put(qualifier, false);
+        }
+        if (!notRewritten.isEmpty()) {
+            // If some USE_MV hints failed to rewrite, set status to UNUSED 
with reason
+            useMvHint.get().setStatus(Hint.HintStatus.UNUSED);
+            useMvHint.get().setErrorMessage("MV(s) not rewrite-success after 
CBO: " + notRewritten);
+        }
+        return required;
+    }
+
+    /**
+     * Extract the MV qualifier(s) from a scan plan if it matches a required 
USE_MV identifier.
+     * Returns the set of MV qualifiers this plan represents.
+     */
+    private static Set<List<String>> getScanMvIdentifiers(Plan plan,
+            Set<List<String>> requiredUseMvIdentifiers) {
+        Set<List<String>> result = new HashSet<>();
+        if (plan instanceof PhysicalOlapScan) {
+            PhysicalOlapScan scan = (PhysicalOlapScan) plan;
+            OlapTable table = scan.getTable();
+            if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+                List<String> qualifier = new ArrayList<>();
+                qualifier.add(table.getDatabase().getCatalog().getName());
+                qualifier.add(table.getDatabase().getFullName());
+                qualifier.add(table.getName());
+                
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+                if (requiredUseMvIdentifiers.contains(qualifier)) {
+                    result.add(qualifier);
+                }
+            }
+        }
+        if (plan instanceof PhysicalCatalogRelation) {
+            PhysicalCatalogRelation rel = (PhysicalCatalogRelation) plan;
+            if (rel.getTable() instanceof MTMV) {
+                List<String> qualifier = rel.getTable().getFullQualifiers();
+                if (requiredUseMvIdentifiers.contains(qualifier)) {
+                    result.add(qualifier);
+                }
+            }
+            // Also check for sync MV via OlapScan (PhysicalOlapScan is a 
PhysicalCatalogRelation)
+            if (rel instanceof PhysicalOlapScan) {
+                PhysicalOlapScan scan = (PhysicalOlapScan) rel;
+                OlapTable table = scan.getTable();
+                if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+                    List<String> qualifier = new ArrayList<>();
+                    qualifier.add(table.getDatabase().getCatalog().getName());
+                    qualifier.add(table.getDatabase().getFullName());
+                    qualifier.add(table.getName());
+                    
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+                    if (requiredUseMvIdentifiers.contains(qualifier)) {
+                        result.add(qualifier);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Memoized bottom-up DFS: compute all MV identifiers reachable from this 
group's subtree
+     * through physical GroupExpressions (including enforcers).
+     */
+    private static void computeReachableMvIdentifiers(Group group,
+            Map<Group, Set<List<String>>> cache, Set<List<String>> 
requiredUseMvIdentifiers) {
+        if (cache.containsKey(group)) {
+            return;
+        }
+        Set<List<String>> reachable = new HashSet<>();
+        // Put in cache BEFORE recursing to break cycles
+        cache.put(group, reachable);
+
+        for (GroupExpression ge : group.getPhysicalExpressions()) {
+            Set<List<String>> geReachable = getScanMvIdentifiers(ge.getPlan(), 
requiredUseMvIdentifiers);
+            for (Group child : ge.children()) {
+                computeReachableMvIdentifiers(child, cache, 
requiredUseMvIdentifiers);
+                geReachable.addAll(cache.get(child));
+            }
+            reachable.addAll(geReachable);
+        }
+        for (GroupExpression enforcer : group.getEnforcers().values()) {
+            Set<List<String>> enforcerReachable = new HashSet<>();
+            for (Group child : enforcer.children()) {
+                computeReachableMvIdentifiers(child, cache, 
requiredUseMvIdentifiers);
+                enforcerReachable.addAll(cache.get(child));
+            }
+            reachable.addAll(enforcerReachable);
+        }
+    }
+
+    /**
+     * Choose the cheapest physical plan that contains all required MV 
identifiers.
+     * At each group, picks the cheapest physical GroupExpression whose own + 
children's
+     * reachable sets collectively cover all remaining required MV 
identifiers, then
+     * distributes the remaining requirements among children and recurses.
+     */
+    private static PhysicalPlan chooseBestPlanWithRequiredMaterializations(
+            Group group, PhysicalProperties properties,
+            Set<List<String>> remaining, Map<Group, Set<List<String>>> 
reachableCache,
+            CascadesContext cascadesContext) {
+        remaining = new HashSet<>(remaining);
+
+        // Remove MVs that this group's own scan expressions cover
+        Set<List<String>> ownMvs = new HashSet<>();
+        for (GroupExpression ge : group.getPhysicalExpressions()) {
+            ownMvs.addAll(getScanMvIdentifiers(ge.getPlan(), remaining));
+        }
+        remaining.removeAll(ownMvs);
+
+        if (remaining.isEmpty()) {
+            return chooseBestPlan(group, properties, cascadesContext);
+        }
+
+        GroupExpression chosenGE = null;
+        double chosenCost = Double.MAX_VALUE;
+
+        // Check all physical GEs in this group
+        for (GroupExpression ge : group.getPhysicalExpressions()) {
+            if (!ge.getLowestCostTable().containsKey(properties)) {
+                continue;
+            }
+            double geCost = ge.getCostByProperties(properties);
+            if (geCost >= chosenCost) {
+                continue;
+            }
+            Set<List<String>> childReachable = new HashSet<>();
+            for (Group child : ge.children()) {
+                Set<List<String>> cr = reachableCache.get(child);
+                if (cr != null) {
+                    childReachable.addAll(cr);
+                }
+            }
+            if (childReachable.containsAll(remaining)) {
+                chosenGE = ge;
+                chosenCost = geCost;
+            }
+        }
+
+        // Also check enforcers
+        for (GroupExpression enforcer : group.getEnforcers().values()) {
+            if (!enforcer.getLowestCostTable().containsKey(properties)) {
+                continue;
+            }
+            double eCost = enforcer.getCostByProperties(properties);
+            if (eCost >= chosenCost) {
+                continue;
+            }
+            Set<List<String>> childReachable = new HashSet<>();
+            for (Group child : enforcer.children()) {
+                Set<List<String>> cr = reachableCache.get(child);
+                if (cr != null) {
+                    childReachable.addAll(cr);
+                }
+            }
+            if (childReachable.containsAll(remaining)) {
+                chosenGE = enforcer;
+                chosenCost = eCost;
+            }
+        }
+
+        if (chosenGE == null) {
+            // The required materialization can not be produced with the 
required properties, for example
+            // the hinted materialization is not costed for the required 
distribution, so fall back to the
+            // cost based plan of this subtree instead of failing the query.
+            markUseMvNotChosen(cascadesContext.getStatementContext(), 
remaining);
+            return chooseBestPlan(group, properties, cascadesContext);
+        }
+
+        // Handle enforcer marking (same as chooseBestPlan)
+        if ((chosenGE.getPlan() instanceof PhysicalDistribute
+                && group.getEnforcerSpecs().containsKey(
+                        ((PhysicalDistribute<?>) 
chosenGE.getPlan()).getDistributionSpec()))
+                || group.getEnforcers().containsKey(chosenGE)) {
+            group.addChosenEnforcerId(chosenGE.getId().asInt());
+            group.addChosenEnforcerProperties(properties);
+        } else {
+            group.setChosenProperties(properties);
+            group.setChosenGroupExpressionId(chosenGE.getId().asInt());
+        }
+
+        // Distribute remaining among children and recurse
+        List<PhysicalProperties> inputProps = 
chosenGE.getInputPropertiesList(properties);
+        List<Plan> planChildren = Lists.newArrayList();
+        for (int i = 0; i < chosenGE.arity(); i++) {
+            Group child = chosenGE.child(i);
+            Set<List<String>> childReachable = 
reachableCache.getOrDefault(child, new HashSet<>());
+            Set<List<String>> childRemaining = new HashSet<>(remaining);
+            childRemaining.retainAll(childReachable);
+            planChildren.add(chooseBestPlanWithRequiredMaterializations(
+                    child, inputProps.get(i), childRemaining, reachableCache, 
cascadesContext));
+        }
+
+        Plan plan = chosenGE.getPlan().withChildren(planChildren);
+        if (!(plan instanceof PhysicalPlan)) {
+            throw new AnalysisException("Result plan must be PhysicalPlan");
+        }
+        plan = plan.withGroupExpression(Optional.of(chosenGE));
+        PhysicalPlan physicalPlan = ((PhysicalPlan) 
plan).withPhysicalPropertiesAndStats(
+                properties, group.getStatistics());

Review Comment:
   [P1] Revalidate distribution contracts after forcing an MV child
   
   The recursive child substitution can invalidate a join that was costed as 
colocate. A concrete shape is colocated, unpartitioned HASH(k) tables A/B, with 
A's duplicate-key prefix (dt,k), and a refreshed, non-colocated async m defined 
as `SELECT k,dt,v FROM A WHERE dt>=0`, HASH(k), with a different bucket count. 
For `SELECT /*+ USE_MV(m) */ A.k,A.v,B.k FROM A JOIN B ON A.k=B.k WHERE 
A.dt=1`, use NOT_IN_RBO and a finite `filter_cost_factor` larger than A's 
estimated scan cost plus 1. A's dt prefix matches, whereas m's leading k does 
not, so Filter(A) stays cheaper despite m's zero scan cost. The filtered m can 
rewrite Filter(dt=1), while predicate compensation rejects rewriting the 
underlying unrestricted Scan(A).
   
   The costed tree is `HashJoin(Filter(dt=1, Scan(A)), Scan(B))` with no 
exchange. This selector replaces its left child with `Project(m.k AS A.k#1, 
...) -> Filter(m.dt=1) -> Scan(m)`. Its NATURAL hash property compares equal to 
the requested A property because `DistributionSpecHash.equals` ignores 
table/index/partition identity; this copy then assigns A's metadata to that MV 
project. `JoinUtils.shouldColocateJoin` and translation consequently still see 
colocated A/B and omit the exchange needed for m/B. Metadata-only recomputation 
also compares equal and does not repair it. Different bucket layouts can miss 
matching rows or fail scheduling. Re-derive actual child outputs and re-run 
property regulation/enforcement when forcing alternatives, rather than 
retaining the original parent's distribution contract.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/exploration/mv/InitMaterializationContextHook.java:
##########
@@ -131,15 +136,53 @@ protected void 
doInitMaterializationContext(CascadesContext cascadesContext) {
     private List<MaterializationContext> 
getMvIdWithUseMvHint(List<MaterializationContext> mtmvCtxs,
                                                                 UseMvHint 
useMvHint) {
         List<MaterializationContext> hintMTMVs = new ArrayList<>();
+        Set<List<String>> matchedQualifiers = new HashSet<>();
         for (MaterializationContext mtmvCtx : mtmvCtxs) {
             List<String> mvQualifier = 
mtmvCtx.generateMaterializationIdentifier();
-            if (useMvHint.getUseMvTableColumnMap().containsKey(mvQualifier)) {
+            Optional<List<String>> matchedParameter = 
useMvHint.findUseMvParameter(mvQualifier);
+            if (matchedParameter.isPresent()) {
                 hintMTMVs.add(mtmvCtx);
+                matchedQualifiers.add(matchedParameter.get());
+                if (!matchedParameter.get().equals(mvQualifier)) {
+                    // The sync MV is referenced by its bare name, e.g. 
USE_MV(mv1) for table.mv1.
+                    // Replace the parameter with the full identifier, so that 
the later
+                    // rewrite-success check and the hint status are computed 
on the same qualifier.
+                    
useMvHint.getUseMvTableColumnMap().remove(matchedParameter.get());

Review Comment:
   [P2] Preserve exact async references during sync-name normalization
   
   A sync index and an async MV can have the same name because sync names are 
table-local. Suppose t(k,v) has sync `m AS SELECT k AS mk FROM t`, while 
refreshed async m contains `SELECT k,v FROM t`. For `SELECT /*+ 
USE_MV(internal.db.m) */ k,v FROM t` (or the bare-name form), the input is 
`Project(k,v) -> Scan(t)` and only async m can answer it.
   
   `doInitMaterializationContext` filters sync contexts first. 
`findUseMvParameter` now matches the three-segment async identifier to sync 
t.m, and this removal replaces it with [catalog,db,t,m] before query 
compatibility is checked. The later async-context filter therefore excludes the 
requested async m, while sync m cannot supply v, so the valid rewrite is lost. 
This also breaks the previously unambiguous fully qualified async form. 
Preserve original aliases or resolve exact async matches before consuming a 
three-segment parameter as a sync bare name.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/NereidsPlanner.java:
##########
@@ -1006,6 +1129,267 @@ private PhysicalPlan chooseBestPlanWithRequiredGroups(
         return physicalPlan;
     }
 
+    /**
+     * Compute the set of USE_MV hint qualifiers that successfully rewrote.
+     * Marks USE_MV hint parameters that did NOT rewrite-success as UnUsed.
+     */
+    private static Set<List<String>> 
computeRequiredUseMvIdentifiers(StatementContext statementContext) {
+        Optional<UseMvHint> useMvHint = 
statementContext.getUseMvHint("USE_MV");
+        if (!useMvHint.isPresent()) {
+            return ImmutableSet.of();
+        }
+        Set<List<String>> useMvQualifiers = 
useMvHint.get().getUseMvTableColumnMap().keySet();
+        Set<List<String>> rewrittenSuccessSet = 
statementContext.getMaterializationRewrittenSuccessSet();
+        Set<List<String>> required = new HashSet<>(useMvQualifiers);
+        required.retainAll(rewrittenSuccessSet);
+        // Mark USE_MV qualifiers that did not rewrite-success as UnUsed
+        Set<List<String>> notRewritten = new HashSet<>(useMvQualifiers);
+        notRewritten.removeAll(rewrittenSuccessSet);
+        for (List<String> qualifier : notRewritten) {
+            // Update hint map to mark as not-chosen (false) with failure 
reason
+            useMvHint.get().getUseMvTableColumnMap().put(qualifier, false);
+        }
+        if (!notRewritten.isEmpty()) {
+            // If some USE_MV hints failed to rewrite, set status to UNUSED 
with reason
+            useMvHint.get().setStatus(Hint.HintStatus.UNUSED);
+            useMvHint.get().setErrorMessage("MV(s) not rewrite-success after 
CBO: " + notRewritten);
+        }
+        return required;
+    }
+
+    /**
+     * Extract the MV qualifier(s) from a scan plan if it matches a required 
USE_MV identifier.
+     * Returns the set of MV qualifiers this plan represents.
+     */
+    private static Set<List<String>> getScanMvIdentifiers(Plan plan,
+            Set<List<String>> requiredUseMvIdentifiers) {
+        Set<List<String>> result = new HashSet<>();
+        if (plan instanceof PhysicalOlapScan) {
+            PhysicalOlapScan scan = (PhysicalOlapScan) plan;
+            OlapTable table = scan.getTable();
+            if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+                List<String> qualifier = new ArrayList<>();
+                qualifier.add(table.getDatabase().getCatalog().getName());
+                qualifier.add(table.getDatabase().getFullName());
+                qualifier.add(table.getName());
+                
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+                if (requiredUseMvIdentifiers.contains(qualifier)) {
+                    result.add(qualifier);
+                }
+            }
+        }
+        if (plan instanceof PhysicalCatalogRelation) {
+            PhysicalCatalogRelation rel = (PhysicalCatalogRelation) plan;
+            if (rel.getTable() instanceof MTMV) {
+                List<String> qualifier = rel.getTable().getFullQualifiers();
+                if (requiredUseMvIdentifiers.contains(qualifier)) {
+                    result.add(qualifier);
+                }
+            }
+            // Also check for sync MV via OlapScan (PhysicalOlapScan is a 
PhysicalCatalogRelation)
+            if (rel instanceof PhysicalOlapScan) {
+                PhysicalOlapScan scan = (PhysicalOlapScan) rel;
+                OlapTable table = scan.getTable();
+                if (table.getBaseIndexId() != scan.getSelectedIndexId()) {
+                    List<String> qualifier = new ArrayList<>();
+                    qualifier.add(table.getDatabase().getCatalog().getName());
+                    qualifier.add(table.getDatabase().getFullName());
+                    qualifier.add(table.getName());
+                    
qualifier.add(table.getIndexNameById(scan.getSelectedIndexId()));
+                    if (requiredUseMvIdentifiers.contains(qualifier)) {
+                        result.add(qualifier);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Memoized bottom-up DFS: compute all MV identifiers reachable from this 
group's subtree
+     * through physical GroupExpressions (including enforcers).
+     */
+    private static void computeReachableMvIdentifiers(Group group,
+            Map<Group, Set<List<String>>> cache, Set<List<String>> 
requiredUseMvIdentifiers) {
+        if (cache.containsKey(group)) {
+            return;
+        }
+        Set<List<String>> reachable = new HashSet<>();
+        // Put in cache BEFORE recursing to break cycles
+        cache.put(group, reachable);
+
+        for (GroupExpression ge : group.getPhysicalExpressions()) {
+            Set<List<String>> geReachable = getScanMvIdentifiers(ge.getPlan(), 
requiredUseMvIdentifiers);
+            for (Group child : ge.children()) {
+                computeReachableMvIdentifiers(child, cache, 
requiredUseMvIdentifiers);
+                geReachable.addAll(cache.get(child));
+            }
+            reachable.addAll(geReachable);
+        }
+        for (GroupExpression enforcer : group.getEnforcers().values()) {
+            Set<List<String>> enforcerReachable = new HashSet<>();
+            for (Group child : enforcer.children()) {
+                computeReachableMvIdentifiers(child, cache, 
requiredUseMvIdentifiers);
+                enforcerReachable.addAll(cache.get(child));
+            }
+            reachable.addAll(enforcerReachable);
+        }
+    }
+
+    /**
+     * Choose the cheapest physical plan that contains all required MV 
identifiers.
+     * At each group, picks the cheapest physical GroupExpression whose own + 
children's
+     * reachable sets collectively cover all remaining required MV 
identifiers, then
+     * distributes the remaining requirements among children and recurses.
+     */
+    private static PhysicalPlan chooseBestPlanWithRequiredMaterializations(
+            Group group, PhysicalProperties properties,
+            Set<List<String>> remaining, Map<Group, Set<List<String>>> 
reachableCache,
+            CascadesContext cascadesContext) {
+        remaining = new HashSet<>(remaining);
+
+        // Remove MVs that this group's own scan expressions cover
+        Set<List<String>> ownMvs = new HashSet<>();
+        for (GroupExpression ge : group.getPhysicalExpressions()) {
+            ownMvs.addAll(getScanMvIdentifiers(ge.getPlan(), remaining));
+        }
+        remaining.removeAll(ownMvs);
+
+        if (remaining.isEmpty()) {
+            return chooseBestPlan(group, properties, cascadesContext);
+        }
+
+        GroupExpression chosenGE = null;
+        double chosenCost = Double.MAX_VALUE;
+
+        // Check all physical GEs in this group
+        for (GroupExpression ge : group.getPhysicalExpressions()) {
+            if (!ge.getLowestCostTable().containsKey(properties)) {
+                continue;
+            }
+            double geCost = ge.getCostByProperties(properties);
+            if (geCost >= chosenCost) {
+                continue;
+            }
+            Set<List<String>> childReachable = new HashSet<>();
+            for (Group child : ge.children()) {
+                Set<List<String>> cr = reachableCache.get(child);
+                if (cr != null) {
+                    childReachable.addAll(cr);
+                }
+            }
+            if (childReachable.containsAll(remaining)) {
+                chosenGE = ge;
+                chosenCost = geCost;
+            }
+        }
+
+        // Also check enforcers
+        for (GroupExpression enforcer : group.getEnforcers().values()) {
+            if (!enforcer.getLowestCostTable().containsKey(properties)) {
+                continue;
+            }
+            double eCost = enforcer.getCostByProperties(properties);
+            if (eCost >= chosenCost) {
+                continue;
+            }
+            Set<List<String>> childReachable = new HashSet<>();
+            for (Group child : enforcer.children()) {
+                Set<List<String>> cr = reachableCache.get(child);
+                if (cr != null) {
+                    childReachable.addAll(cr);
+                }
+            }
+            if (childReachable.containsAll(remaining)) {
+                chosenGE = enforcer;
+                chosenCost = eCost;
+            }
+        }
+
+        if (chosenGE == null) {
+            // The required materialization can not be produced with the 
required properties, for example
+            // the hinted materialization is not costed for the required 
distribution, so fall back to the
+            // cost based plan of this subtree instead of failing the query.
+            markUseMvNotChosen(cascadesContext.getStatementContext(), 
remaining);
+            return chooseBestPlan(group, properties, cascadesContext);
+        }
+
+        // Handle enforcer marking (same as chooseBestPlan)
+        if ((chosenGE.getPlan() instanceof PhysicalDistribute
+                && group.getEnforcerSpecs().containsKey(
+                        ((PhysicalDistribute<?>) 
chosenGE.getPlan()).getDistributionSpec()))
+                || group.getEnforcers().containsKey(chosenGE)) {
+            group.addChosenEnforcerId(chosenGE.getId().asInt());
+            group.addChosenEnforcerProperties(properties);
+        } else {
+            group.setChosenProperties(properties);
+            group.setChosenGroupExpressionId(chosenGE.getId().asInt());
+        }
+
+        // Distribute remaining among children and recurse
+        List<PhysicalProperties> inputProps = 
chosenGE.getInputPropertiesList(properties);
+        List<Plan> planChildren = Lists.newArrayList();
+        for (int i = 0; i < chosenGE.arity(); i++) {
+            Group child = chosenGE.child(i);
+            Set<List<String>> childReachable = 
reachableCache.getOrDefault(child, new HashSet<>());
+            Set<List<String>> childRemaining = new HashSet<>(remaining);
+            childRemaining.retainAll(childReachable);
+            planChildren.add(chooseBestPlanWithRequiredMaterializations(
+                    child, inputProps.get(i), childRemaining, reachableCache, 
cascadesContext));
+        }
+
+        Plan plan = chosenGE.getPlan().withChildren(planChildren);
+        if (!(plan instanceof PhysicalPlan)) {
+            throw new AnalysisException("Result plan must be PhysicalPlan");
+        }
+        plan = plan.withGroupExpression(Optional.of(chosenGE));
+        PhysicalPlan physicalPlan = ((PhysicalPlan) 
plan).withPhysicalPropertiesAndStats(
+                properties, group.getStatistics());
+        return physicalPlan;
+    }
+
+    /**
+     * After final physical plan is chosen, validate USE_MV hint status
+     * by checking which required MVs are actually in the chosen plan.
+     */
+    public static void finalizeUseMvHintStatus(StatementContext 
statementContext,
+            List<MaterializationContext> materializationContexts, PhysicalPlan 
physicalPlan) {
+        Optional<UseMvHint> useMvHint = 
statementContext.getUseMvHint("USE_MV");
+        if (!useMvHint.isPresent()) {
+            return;
+        }
+        // Do not override SYNTAX_ERROR status set during init phase
+        if (useMvHint.get().isSyntaxError()) {
+            return;
+        }
+        // Collect actually chosen MV qualifiers from the physical plan
+        Set<List<String>> chosenQualifiers = 
MaterializationContext.getChosenMvsQualifiers(

Review Comment:
   [P2] Count selected sync indexes independently of rewrite success
   
   For a projection sync index `m AS SELECT k AS mk FROM t`, `EXPLAIN SELECT 
/*+ USE_MV(t.m) */ mk FROM t INDEX m` with NOT_IN_RBO produces `Project(mk) -> 
PhysicalOlapScan(t, selectedIndexId=m)`. `BindRelation` binds that index 
directly. The new pre-record hook puts its identifier in the statement 
rewrite-success set, but the context's `success` remains false: the MV rule's 
precheck rejects rewriting a scan already on a non-base index.
   
   `getChosenMvsQualifiers` filters contexts through 
`MaterializationContext::isSuccess`, so this call returns no chosen m even 
though the final scan uses it; the finalizer then reports UnUsed and marks its 
map entry false. The deleted scan-cost branch reported Used for the same 
supported direct-index query. Final actual-selection status should derive 
identifiers from selected scans, or include applicable sync contexts without 
requiring a separate successful CBO rewrite.



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