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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectRelation.java:
##########
@@ -319,4 +322,28 @@ private void collectFromTableStream(BaseTableStream 
tableStream, CascadesContext
         List<String> tableQualifier = tableStream.getBaseTableFullQualifiers();
         statementContext.getAndCacheTable(tableQualifier, tableFrom, 
unboundRelation);
     }
+
+    private boolean isUnderInitialFilter(Plan plan, UnboundRelation relation) {
+        if (plan instanceof LogicalFilter && containsRelation(plan.child(0), 
relation)) {

Review Comment:
   [P1] Do not treat filter ancestry as proof this scan is filtered
   
   This returns true for every relation anywhere below a `LogicalFilter`, 
without proving that the predicate will reach that scan. For `Filter(i.k = 1) 
-> Join(internal i, Hive h)`, `h` is therefore not marked as an unfiltered 
latest relation, so the pre-lock warmup skips it; rewrite leaves `h` DEFERRED 
and `QueryPartitionCollector` resolves its full HMS view after 
`StatementContext.lock()`, holding `i`'s read lock across the external RPC and 
O(all partitions) build. This is distinct from the existing eager-warmup 
thread: an unrelated branch's predicate defeats the attempted fix for the 
cold-unfiltered collector path. Because collection is still unbound and 
barriers can also prevent pushdown, conservatively warm unless the parsed shape 
proves a connector-prunable filter will reach this scan, and add a 
filtered-join regression test.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTable.java:
##########
@@ -714,9 +690,69 @@ static boolean schemaCacheDisabled(Connector connector) {
 
     @Override
     public Map<String, PartitionItem> 
getNameToPartitionItems(Optional<MvccSnapshot> snapshot) {
+        if (supportsConnectorPartitionPruning()) {
+            PluginDrivenMvccSnapshot pin = getOrMaterialize(snapshot);
+            if (!pin.getNameToPartitionItem().isEmpty()) {
+                return pin.getNameToPartitionItem();
+            }
+            // The latest Hive query pin intentionally carries no partition 
map so selective scans can send a
+            // predicate to HMS first. Consumers that explicitly ask for a 
partition map (MTMV alignment,
+            // no-filter scan finalization, and a connector-declined pruning 
fallback) require the real full
+            // view instead of treating that query-only pin as an empty table.
+            return super.getNameToPartitionItems(snapshot);
+        }
         return getOrMaterialize(snapshot).getNameToPartitionItem();
     }
 
+    @Override
+    public Optional<Map<String, PartitionItem>> 
getNameToPartitionItemsForScan(Optional<MvccSnapshot> snapshot) {
+        if (supportsConnectorPartitionPruning()) {
+            PluginDrivenMvccSnapshot pin = getOrMaterialize(snapshot);
+            if (!pin.getNameToPartitionItem().isEmpty()) {
+                // This statement's pin already carries a materialized view: 
reuse it instead of paying another
+                // connector round-trip that could observe a different remote 
generation.
+                return Optional.of(pin.getNameToPartitionItem());
+            }
+        }
+        return super.getNameToPartitionItemsForScan(snapshot);
+    }
+
+    /**
+     * Threads this statement's MVCC pin onto the handle the partition view is 
enumerated from, so a
+     * time-travel / {@code @options} query never prunes against the latest 
generation: the data scan reads the
+     * pinned snapshot, and a latest view can be missing (or contain) 
partitions it will never read.
+     */
+    @Override
+    protected ConnectorTableHandle pinPartitionViewHandle(ConnectorTableHandle 
handle,
+            ConnectorMetadata metadata, ConnectorSession session, 
Optional<MvccSnapshot> snapshot) {
+        if (snapshot.isPresent() && snapshot.get() instanceof 
PluginDrivenMvccSnapshot) {
+            return metadata.applySnapshot(session, handle,
+                    ((PluginDrivenMvccSnapshot) 
snapshot.get()).getConnectorSnapshot());
+        }
+        return handle;
+    }
+
+    /**
+     * Materializes the complete partition view for an MTMV refresh before it 
acquires base-table locks.
+     * The lightweight Hive query pin keeps the connector snapshot/freshness 
kind but deliberately omits the
+     * partition map; MTMV alignment needs that map and must not load it while 
holding internal table locks.
+     */
+    public MvccSnapshot materializePartitionViewForMtmv(MvccSnapshot snapshot) 
{
+        if (!supportsConnectorPartitionPruning() || !(snapshot instanceof 
PluginDrivenMvccSnapshot)) {
+            return snapshot;
+        }
+        PluginDrivenMvccSnapshot pin = (PluginDrivenMvccSnapshot) snapshot;
+        if (!pin.getNameToPartitionItem().isEmpty()) {

Review Comment:
   [P1] Preserve the materialized-empty snapshot state
   
   A partitioned Hive table with zero partitions produces an authoritative 
empty map here, but the returned snapshot is still indistinguishable from the 
lightweight deferred pin. During `syncPartitionsIfNeeded`, `alignMvPartition` 
runs under `MetaLockUtils.readLockTables` and reaches `getAndCopyPartitionItems 
-> getNameToPartitionItems`; line 695 sees the empty map and performs a second 
live HMS enumeration, so it both restores external I/O under the locks and can 
align against a newer generation than the pre-lock snapshot. This is a distinct 
residual after the existing MTMV fix: the warmup did run successfully, but its 
empty result lost the materialized state. Carry an explicit 
deferred/materialized flag (with materialized-empty valid) and test that an 
empty MTMV view is listed exactly once before locking.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -1358,6 +1378,116 @@ public Collection<ExternalTablePreloadInfo> 
getExternalTablePreloadInfos() {
         return 
Collections.unmodifiableCollection(externalTablePreloadInfos.values());
     }
 
+    /**
+     * 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 the MV partition 
collector can need, BEFORE the
+     * internal table read locks are taken. This step is unconditional: 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, so 
the collector would have to
+     * enumerate the table's whole partition view - an unbounded connector 
round-trip plus an O(all partitions)
+     * build - with the statement's internal tables locked. Materializing the 
same view here moves that work
+     * outside the lock window; the collector then reuses it.</p>
+     *
+     * <p>WHY it is skipped when no internal read lock is taken or no MV 
rewrite is enabled at all: with no
+     * locked internal table the enumeration blocks nothing, and the 
materialized view has exactly one consumer
+     * (the MV partition collector), which no planner hook runs when BOTH MV 
rewrite switches are off. The gate
+     * is deliberately the OR of the two switches - the query hook is 
registered from
+     * {@code enable_materialized_view_rewrite} and the DML hook from {@code 
enable_dml_materialized_view_rewrite}
+     * - because gating on only one of them leaves the collector enumerating 
under the lock in the other
+     * configuration.</p>
+     */
+    public void preloadDeferredScanPartitionViewsBeforeLock() {
+        ConnectContext connectContext = getConnectContext();
+        if (connectContext == null || connectContext.getSessionVariable() == 
null) {
+            return;
+        }
+        if 
(!connectContext.getSessionVariable().isEnableMaterializedViewRewrite()

Review Comment:
   [P1] Keep the pre-lock warmup for physical scan finalization
   
   This early return assumes the MV partition collector is the only consumer, 
but every no-filter connector-pruning scan is still initialized DEFERRED. In a 
mixed internal/Hive query with both rewrite switches off, `NereidsPlanner` 
acquires the internal read locks and its lock callback translates/finalizes the 
plan; `PluginDrivenScanNode.doFinalize` then calls 
`materializeDeferredSelectedPartitions -> resolveScanPartitionView -> 
getNameToPartitionItemsForScan`, enumerating the full HMS view before 
`releasePlannerResources` unlocks. This is distinct from the existing 
collector-under-lock thread because that hook is disabled here and physical 
finalization is the consumer. Gate warmup on the union of actual MV-collector 
and physical-finalization consumers (without penalizing plan-only EXPLAIN), and 
extend the disabled-MV test through scan finalization.



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