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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -489,6 +521,241 @@ public StatementContext createNextExecuteContext() {
         return next;
     }
 
+    public Map<String, DataMaskSpec> getOrEvaluateDataMasks(DataPolicyContext 
policy,
+            java.util.function.Supplier<Map<String, DataMaskSpec>> evaluation) 
{
+        return dataMaskEvaluations.computeIfAbsent(policy, key -> 
Map.copyOf(evaluation.get()));
+    }
+
+    private static List<String> policySchemaFingerprint(List<Column> schema) {
+        return schema.stream().map(column -> 
column.getName().toLowerCase(Locale.ROOT) + ":"
+                + DataType.fromCatalogType(column.getType()).toSql() + ":" + 
column.isAllowNull())
+                .collect(Collectors.toList());
+    }
+
+    /** Establishes raw-value preservation from a complete, successful mask 
evaluation before locks. */
+    public boolean proveInitialPartitionValues(UnboundRelation relation, 
TableIf table) {
+        if (relation.getTableSnapshot().isPresent() || 
relation.getScanParams() != null
+                || !(table instanceof PluginDrivenExternalTable)
+                || !table.supportsExternalMetadataPreload()
+                || !((PluginDrivenExternalTable) 
table).supportsConnectorPartitionPruning()
+                || ((PluginDrivenExternalTable) table).isView()
+                || table.getDatabase() == null || 
table.getDatabase().getCatalog() == null
+                || !(((PluginDrivenExternalTable) table).getCatalog() 
instanceof PluginDrivenExternalCatalog)
+                || mvRefreshPredicates.isPresent()) {
+            return false;
+        }
+        requirePartitionPolicyPlanning();
+        PluginDrivenExternalTable external = (PluginDrivenExternalTable) table;
+        loadSnapshots(table, Optional.empty(), Optional.empty());
+        Optional<MvccSnapshot> pin = getSnapshot(table, Optional.empty(), 
Optional.empty());
+        List<Column> schema = external.getFullSchema(pin);
+        if (schema == null || schema.isEmpty()) {
+            return false;
+        }
+        Optional<SchemaCacheValue> schemaValue = 
external.getSchemaCacheValue(pin);
+        if (schemaValue.isEmpty() || !(schemaValue.get() instanceof 
PluginDrivenSchemaCacheValue)) {
+            return false;
+        }
+        DataPolicyContext policy = capturePartitionPolicy(external, schema);
+        Map<String, DataMaskSpec> masks = 
connectContext.getEnv().getAccessManager()
+                .evalDataMaskPolicies(policy, this);
+        partitionPolicyObservedTables.add(table.getId());
+        if (!masks.isEmpty()) {
+            return false;
+        }
+        partitionPolicyProofs.putIfAbsent(relation.getRelationId(),
+                new PartitionPolicyProof(external, policy, pin, 
policySchemaFingerprint(schema),
+                        
policySchemaFingerprint(external.getPartitionColumns(pin)),
+                        ((PluginDrivenSchemaCacheValue) 
schemaValue.get()).getPartitionColumnRemoteNames()));
+        return true;
+    }
+
+    private DataPolicyContext capturePartitionPolicy(PluginDrivenExternalTable 
table, List<Column> schema) {
+        Set<String> columns = 
schema.stream().map(Column::getName).collect(Collectors.toCollection(LinkedHashSet::new));
+        return capturePartitionPolicyColumns(table, columns);
+    }
+
+    private DataPolicyContext 
capturePartitionPolicyColumns(PluginDrivenExternalTable table, Set<String> 
columns) {
+        return 
connectContext.getEnv().getAccessManager().captureDataPolicyContext(
+                connectContext.getCurrentUserIdentity(), 
table.getDatabase().getCatalog().getName(),
+                table.getDatabase().getFullName(), table.getName(), columns, 
this);
+    }
+
+    public boolean hasPartitionPolicyObservation(TableIf table) {
+        return partitionPolicyObservedTables.contains(table.getId());
+    }
+
+    /** Refuses a bound relation whose actual policy scope or typed output no 
longer justifies its subset. */
+    public DataPolicyContext validatePartitionPolicy(LogicalPlan relation) {
+        requirePartitionPolicyPlanning();
+        if (!(relation instanceof 
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan)) {
+            return null;
+        }
+        org.apache.doris.nereids.trees.plans.logical.LogicalFileScan scan =
+                (org.apache.doris.nereids.trees.plans.logical.LogicalFileScan) 
relation;
+        PartitionPolicyProof proof = 
partitionPolicyProofs.get(scan.getPreloadRelationId());
+        if (proof == null || !usedPartitionPolicyProofs.contains(proof)) {
+            return null;
+        }
+        Set<String> columns = 
scan.getOutput().stream().map(Slot::getName).collect(Collectors.toSet());
+        List<String> output = scan.getOutput().stream().map(slot -> 
slot.getName().toLowerCase(Locale.ROOT)
+                + ":" + slot.getDataType().toSql() + ":" + 
slot.nullable()).collect(Collectors.toList());
+        try {
+            DataPolicyContext actual = 
capturePartitionPolicyColumns(proof.table, columns);
+            if (proof.table != scan.getTable() || !proofMatches(proof, actual, 
false) || !proof.schema.equals(output)) {
+                throw new AnalysisException("Partition policy proof changed 
before binding " + proof.table.getName());
+            }
+            return actual;
+        } catch (RuntimeException failure) {
+            invalidatePartitionPolicy(false);
+            throw failure;
+        }
+    }
+
+    /** Records the raw row-policy observation actually expanded into this 
relation's plan. */
+    public void recordPartitionRowPolicies(LogicalPlan relation, 
List<RowFilterSpec> policies) {
+        if (relation instanceof 
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan) {
+            PartitionPolicyProof proof = partitionPolicyProofs.get(
+                    
((org.apache.doris.nereids.trees.plans.logical.LogicalFileScan) 
relation).getPreloadRelationId());
+            if (proof != null) {
+                List<RowFilterSpec> previous = 
partitionProofRowPolicies.putIfAbsent(proof, List.copyOf(policies));
+                if (previous != null && !previous.equals(policies)) {
+                    invalidatePartitionPolicy(false);
+                    throw new AnalysisException("Row policy changed between 
aliases of " + proof.table.getName());
+                }
+            }
+        }
+    }
+
+    private boolean proofMatches(PartitionPolicyProof proof, DataPolicyContext 
actual, boolean retry) {
+        DataPolicyContext expected = 
executionPolicyContexts.getOrDefault(proof, proof.policy);
+        return proof.table.getDatabase() != null && 
proof.table.getDatabase().getCatalog() != null
+                && ((PluginDrivenExternalCatalog) 
proof.table.getCatalog()).getConnector() == proof.connector
+                && proof.pin.orElse(null) == getSnapshot(proof.table, 
Optional.empty(), Optional.empty()).orElse(null)
+                && proofSchemaMatches(proof)
+                && (retry ? expected.samePolicyScope(actual) : 
expected.equals(actual));
+    }
+
+    private boolean proofSchemaMatches(PartitionPolicyProof proof) {
+        Optional<SchemaCacheValue> schema = 
proof.table.getSchemaCacheValue(proof.pin);
+        if (schema.isEmpty() || !(schema.get() instanceof 
PluginDrivenSchemaCacheValue)) {
+            return false;
+        }
+        PluginDrivenSchemaCacheValue actual = (PluginDrivenSchemaCacheValue) 
schema.get();
+        return proof.schema.equals(policySchemaFingerprint(actual.getSchema()))
+                && 
proof.partitions.equals(policySchemaFingerprint(actual.getPartitionColumns()))
+                && 
proof.remotePartitions.equals(actual.getPartitionColumnRemoteNames());
+    }
+
+    private void requirePartitionPolicyPlanning() {
+        if (partitionPolicyPlanningInvalidated) {
+            throw new AnalysisException("Partition policy planning state was 
invalidated; replan in a fresh statement");
+        }
+    }
+
+    private void invalidatePartitionPolicy(boolean retry) {
+        partitionPolicyEpoch++;
+        dataMaskEvaluations.clear();
+        partitionPolicyObservedTables.clear();
+        boolean used = !usedPartitionPolicyProofs.isEmpty();
+        for (Map.Entry<RelationId, PartitionPolicyProof> entry : 
partitionPolicyProofs.entrySet()) {
+            ExternalTablePreloadInfo info = 
externalTablePreloadInfos.get(entry.getValue().table.getId());
+            if (info != null) {
+                info.getInitialScanPredicates().remove(entry.getKey());
+                info.markUnfilteredLatestRelation();
+                info.clearPartitionViews();
+            }
+        }
+        partitionPolicyPlanningInvalidated |= used;

Review Comment:
   [P2] Allow actual replans after resetting the policy proof
   
   For an ordinary user with no masks, a mixed query with `Filter(h.p = 'x') -> 
CrossJoin(internal_i, HiveScan(h))` uses a selective preload and puts its proof 
in `usedPartitionPolicyProofs`. If execution then reports a cloud replan error 
such as `E-230`, `StmtExecutor.executeWithReplanRetries` calls 
`resetConnectorStatementScope()`, which reaches this assignment, and creates a 
new `NereidsPlanner` with the same `StatementContext`. The next 
collect/preload/lock therefore always throws `Partition policy planning state 
was invalidated; replan in a fresh statement`, even if the transient error is 
gone. The `InsertIntoTableCommand.initPlan` retry loop has the same failure 
when an INSERT SELECT target changes identity/schema after its first plan. Keep 
the old plan revoked while giving actual replan callers fresh planning/proof 
state, and cover an outer replan retry; the new RPC-retry tests only redispatch 
the existing plan.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -1511,6 +1799,249 @@ public Collection<ExternalTablePreloadInfo> 
getExternalTablePreloadInfos() {
         return 
Collections.unmodifiableCollection(externalTablePreloadInfos.values());
     }
 
+    /** Marks collected view relations whose ids will be replaced when the 
definition is bound again. */
+    public void markReparsedViewRelationsForPreload(Set<RelationId> 
relationIds) {
+        externalTablePreloadInfos.values().forEach(info ->
+                relationIds.forEach(info::markReparsedScanRelation));
+    }
+
+    public boolean shouldSkipUnpreloadedScanPartitionPruning() {
+        return skipUnpreloadedScanPartitionPruning;
+    }
+
+    /**
+     * The preload record of one external table, or empty when the table was 
never registered for preload.
+     * Used by lock-sensitive consumers to reuse metadata the pre-lock preload 
pass already materialized.
+     *
+     * @param tableId the table's id, as used by {@link 
#registerExternalTableForPreload}
+     * @return ExternalTablePreloadInfo
+     */
+    public Optional<ExternalTablePreloadInfo> getExternalTablePreloadInfo(long 
tableId) {
+        return Optional.ofNullable(externalTablePreloadInfos.get(tableId));
+    }
+
+    /**
+     * Materializes every deferred connector partition view that can be 
consumed while the internal table read
+     * locks are held. This step is unconditional for physical finalization: 
it exists so the default
+     * configuration gets the lock scope, not only the opt-in {@code 
enable_preload_external_metadata} pass.
+     *
+     * <p>WHY it is needed at all: {@code QueryPartitionCollector} runs from
+     * {@code InitMaterializationContextHook.afterRewrite}, i.e. while {@link 
#lock()} is held, and an unfiltered
+     * connector-pruning file scan is still {@code DEFERRED} at that point. 
Even with both MV rewrite switches
+     * off, {@code PluginDrivenScanNode.doFinalize} still materializes a 
no-filter deferred scan before the
+     * planner releases the internal read locks. Materializing the same view 
here moves both consumers outside
+     * the lock window.</p>
+     *
+     * <p>Plan-only EXPLAIN still runs logical partition pruning, so 
relation-scoped filters must be resolved
+     * before locking even when both MV rewrite switches are off. Only the 
unfiltered full-view warmup can
+     * be skipped in that configuration. The MV gate is the OR of the query 
and DML rewrite switches because
+     * either hook can consume a deferred view.</p>
+     */
+    public void preloadDeferredScanPartitionViewsBeforeLock(boolean 
willFinalizePhysicalPlan) {
+        requirePartitionPolicyPlanning();
+        ConnectContext connectContext = getConnectContext();
+        if (connectContext == null || connectContext.getSessionVariable() == 
null) {
+            return;
+        }
+        boolean mvCollectorWillRun = 
connectContext.getSessionVariable().isEnableMaterializedViewRewrite()
+                || 
connectContext.getSessionVariable().isEnableDmlMaterializedViewRewrite();
+        boolean needsUnfilteredScanView = willFinalizePhysicalPlan || 
mvCollectorWillRun;
+        skipUnpreloadedScanPartitionPruning = !needsUnfilteredScanView;
+        boolean hasPlanReadLockTable = hasAnyPlanReadLockTable();
+        Set<BaseTableInfo> candidateMtmvPctTables = mvCollectorWillRun
+                ? collectCandidateMtmvPctTables() : Collections.emptySet();
+        for (ExternalTablePreloadInfo preloadInfo : 
externalTablePreloadInfos.values()) {
+            if (hasPlanReadLockTable) {
+                preloadFilteredScanPartitionViews(preloadInfo);
+                if (needsUnfilteredScanView) {
+                    preloadDeferredScanPartitionView(preloadInfo);
+                }
+            }
+            preloadMtmvPartitionView(preloadInfo, candidateMtmvPctTables);
+        }
+    }
+
+    /** Resolve relation-scoped filters before internal locks, never sharing 
one alias's subset with another. */
+    private void preloadFilteredScanPartitionViews(ExternalTablePreloadInfo 
info) {
+        ExternalTable table = info.getTable();
+        if (!info.shouldPreloadLatestSnapshot() || !(table instanceof 
PluginDrivenExternalTable)
+                || !((PluginDrivenExternalTable) 
table).supportsConnectorPartitionPruning()) {
+            return;
+        }
+        loadSnapshots(table, Optional.empty(), Optional.empty());
+        Optional<MvccSnapshot> snapshot = getSnapshot(table);
+        List<Column> columns = table.getPartitionColumns(snapshot);
+        for (Map.Entry<RelationId, Expression> entry : 
info.getInitialScanPredicates().entrySet()) {
+            if (info.hasFilteredScanView(entry.getKey())) {
+                continue;
+            }
+            PartitionPolicyProof proof = 
partitionPolicyProofs.get(entry.getKey());
+            if (proof != null && !proofMatches(proof,
+                    capturePartitionPolicy(proof.table, 
proof.table.getFullSchema(snapshot)), false)) {
+                invalidatePartitionPolicy(false);
+                throw new AnalysisException("Partition policy changed before 
selective preload");
+            }
+            // Only bind declared partition columns here. Full analysis, 
coercion, privileges and residual
+            // evaluation remain in their normal stages; unsupported forms use 
the preloaded full view.
+            Expression bound = entry.getValue().rewriteUp(expression -> {
+                if (!(expression instanceof UnboundSlot)) {
+                    return expression;
+                }
+                List<String> names = ((UnboundSlot) expression).getNameParts();
+                String name = names.get(names.size() - 1);
+                return columns.stream().filter(column -> 
column.getName().equalsIgnoreCase(name))
+                        .findFirst().<Expression>map(column -> new 
SlotReference(column.getName(),
+                                DataType.fromCatalogType(column.getType()), 
column.isAllowNull()))
+                        .orElse(expression);
+            });
+            List<Expression> safeConjuncts = 
ExpressionUtils.extractConjunction(bound).stream()
+                    
.filter(StatementContext::isSafeInitialPartitionPredicate).collect(Collectors.toList());
+            ConnectorExpression predicate = safeConjuncts.isEmpty() ? null
+                    : 
NereidsToConnectorExpressionConverter.convert(ExpressionUtils.and(safeConjuncts));
+            Optional<PluginDrivenExternalTable.ConnectorFilteredPartitionView> 
view = predicate == null
+                    ? Optional.empty()
+                    : ((PluginDrivenExternalTable) 
table).applyPartitionFilterForScan(snapshot, predicate);
+            info.setFilteredScanView(entry.getKey(), view, predicate);
+            if (proof != null && view.isPresent()) {
+                usedPartitionPolicyProofs.add(proof);
+            }
+            if (!view.isPresent() && !info.hasScanPartitionView()) {
+                info.setScanPartitionView(((PluginDrivenExternalTable) 
table).getNameToPartitionItemsForScan(snapshot));
+            }
+        }
+    }
+
+    /** Only prefilter typed equality/IN whose later coercion cannot change 
partition-value semantics. */
+    private static boolean isSafeInitialPartitionPredicate(Expression 
expression) {
+        if (expression instanceof EqualTo) {
+            Expression left = expression.child(0);
+            Expression right = expression.child(1);
+            return left instanceof SlotReference && right instanceof Literal
+                    && compatibleInitialPartitionTypes(left.getDataType(), 
right.getDataType())
+                    || right instanceof SlotReference && left instanceof 
Literal
+                    && compatibleInitialPartitionTypes(right.getDataType(), 
left.getDataType());
+        }
+        if (expression instanceof InPredicate && expression.child(0) 
instanceof SlotReference) {
+            DataType type = expression.child(0).getDataType();
+            return expression.children().subList(1, 
expression.arity()).stream()
+                    .allMatch(child -> child instanceof Literal
+                            && compatibleInitialPartitionTypes(type, 
child.getDataType()));
+        }
+        return false;
+    }
+
+    private static boolean compatibleInitialPartitionTypes(DataType column, 
DataType literal) {
+        // In particular, VARCHAR p = 1 is a numeric comparison, not textual p 
= '1': prefiltering it as
+        // strings could drop p=01. Let full analysis/coercion handle mixed 
families on the warmed full view.
+        return column.equals(literal) || column.isIntegralType() && 
literal.isIntegralType()
+                || column.isStringLikeType() && literal.isStringLikeType();
+    }
+
+    /**
+     * Takes a stable copy of the candidate MVs' PCT table identities without 
doing connector I/O while an MV lock
+     * is held. The full external views are materialized only after every MV 
lock has been released.
+     */
+    private Set<BaseTableInfo> collectCandidateMtmvPctTables() {
+        Set<BaseTableInfo> pctTables = new HashSet<>();
+        for (MTMV mtmv : candidateMTMVs) {
+            mtmv.readMvLock();
+            try {
+                mtmv.getMvPartitionInfo().getPctInfos().forEach(
+                        pctInfo -> pctTables.add(pctInfo.getTableInfo()));
+            } finally {
+                mtmv.readMvUnlock();
+            }
+        }
+        return pctTables;
+    }
+
+    /**
+     * Materializes the complete latest partition view needed by an async-MV 
candidate's partition mapping. This
+     * is deliberately separate from the scan view: a filtered scan must 
retain connector predicate pruning, while
+     * {@code MTMV.calculatePartitionMappings} still needs the full partition 
universe for union compensation.
+     */
+    private void preloadMtmvPartitionView(ExternalTablePreloadInfo preloadInfo,
+            Set<BaseTableInfo> candidateMtmvPctTables) {
+        if (preloadInfo.hasMtmvPartitionView() || 
!preloadInfo.shouldPreloadLatestSnapshot()
+                || candidateMtmvPctTables.isEmpty()) {
+            return;
+        }
+        ExternalTable table = preloadInfo.getTable();
+        if (!(table instanceof PluginDrivenExternalTable)
+                || !((PluginDrivenExternalTable) 
table).supportsConnectorPartitionPruning()
+                || !candidateMtmvPctTables.contains(new BaseTableInfo(table))) 
{
+            return;
+        }
+        if (preloadInfo.hasScanPartitionView()) {
+            
preloadInfo.setMtmvPartitionView(preloadInfo.getScanPartitionView());
+            return;
+        }
+        preloadInfo.setMtmvPartitionView(

Review Comment:
   [P2] Skip full mapping warmup for ineligible external MVs
   
   With the default 
`materialized_view_rewrite_enable_contain_external_table=false`, an existing 
partitioned Hive MV is collected as a candidate, but 
`InitMaterializationContextHook.getAvailableMTMVs` discards it before partition 
validation. A selective `Filter(h.p = 'x') -> HiveScan(h)` nevertheless reaches 
this new mapping-view warmup because `collectCandidateMtmvPctTables` includes 
every candidate. It lists/builds all N partitions after selective preload, even 
though no consumer will inspect this full map; this also happens when the query 
names only the Hive base. Thus merely having an ineligible MV restores the O(N) 
planning cost the HMS filter is meant to avoid and can make the query fail on 
an unnecessary full listing. Apply the same eligibility gates before collecting 
tables to warm, and cover a selective query with a partitioned Hive MV and this 
switch disabled.



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