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


##########
fe/fe-connector/fe-connector-spi/src/main/java/org/apache/doris/connector/spi/ConnectorSession.java:
##########
@@ -103,6 +103,17 @@ default Map<String, String> getSessionProperties() {
         return java.util.Collections.emptyMap();
     }
 
+    /**
+     * Returns whether equivalent external scan plans may be reused within the 
current statement.
+     *
+     * <p>The property must be explicitly present and {@code true}. Sessions 
created by an older engine or by
+     * offline tooling do not carry this setting and therefore keep the 
optimization disabled.</p>
+     */
+    default boolean isExternalScanTaskReuseEnabled() {

Review Comment:
   [P1] Advance the connector API major for this new shared method. Both the 
exact base and this head still advertise API 8.0, while API admission is 
exact-major and the SPI is parent-first. A provider built from this head is 
therefore accepted by a pre-PR API-8 FE, then its direct call to this method 
resolves against the old `ConnectorSession` and can fail with 
`NoSuchMethodError` on the first scan. Please bump 
`connector.plugin.api.version` (and the pinned assertion) to 9.0 so the 
incompatible pair is rejected at load time.



##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -538,8 +540,79 @@ public void releaseReadTransaction(String queryId) {
      */
     @Override
     public List<ConnectorScanRange> planScan(ConnectorSession session, 
ConnectorScanRequest request) {
-        return planScanInternal(session, request.getTableHandle(), 
request.getColumns(),
-                request.getFilter(), request.getLimit(), 
request.isCountPushdown());
+        PaimonTableHandle paimonHandle = (PaimonTableHandle) 
request.getTableHandle();
+        if (session == null || !session.isExternalScanTaskReuseEnabled()) {
+            return planScanInternal(session, request.getTableHandle(), 
request.getColumns(),
+                    request.getFilter(), request.getLimit(), 
request.isCountPushdown());
+        }
+        if (paimonHandle.isSystemTable()) {
+            // System tables resolve their snapshot on the BE and carry 
deferred side effects
+            // (authorized file enumeration); never reuse their planned ranges.
+            return planScanInternal(session, request.getTableHandle(), 
request.getColumns(),
+                    request.getFilter(), request.getLimit(), 
request.isCountPushdown());
+        }
+        // Resolve the table ONCE at the statement scope so both the 
scan-planning path (here) and
+        // the properties path (getScanNodeProperties) observe the SAME table 
generation. Without
+        // this, a no-cache catalog or a schema change between two aliases can 
give alias A's ranges
+        // to alias B's generation-B serialized table. The resolved table is 
cached at the scope;
+        // its latestSnapshotId fences the reuse key so different generations 
never share ranges.
+        long generation = resolvePaimonGeneration(session, paimonHandle);
+        // Statement-scoped reuse: within one statement the identical scan 
(same table, same
+        // branch/options pin, same generation, same projection, same filter, 
same limit, same COUNT
+        // pushdown) plans once and every duplicated relation shares the 
result. Session variables
+        // are constant within a statement and deliberately absent from the 
key.
+        String memoKey = SCAN_REUSE_NAMESPACE + ":" + session.getCatalogId() + 
":" + session.getQueryId();
+        Map<PaimonScanReuseKey, List<ConnectorScanRange>> scanReuse = 
session.getStatementScope().computeIfAbsent(
+                memoKey, () -> new ConcurrentHashMap<>());
+        PaimonScanReuseKey reuseKey = new PaimonScanReuseKey(paimonHandle, 
request, generation);
+        return scanReuse.computeIfAbsent(reuseKey,
+                key -> Collections.unmodifiableList(planScanInternal(session,
+                        request.getTableHandle(), request.getColumns(), 
request.getFilter(),
+                        request.getLimit(), request.isCountPushdown())));
+    }
+
+    /**
+     * Resolve the Paimon table at the statement scope and return its 
generation token
+     * (latestSnapshotId, or -1 when unresolvable). The resolved table is 
cached at the scope so
+     * {@link #getScanNodeProperties} observes the same generation as the 
scan-planning path.
+     */
+    private long resolvePaimonGeneration(ConnectorSession session, 
PaimonTableHandle paimonHandle) {
+        if (session == null || session.getStatementScope() == null
+                || session.getStatementScope() == 
ConnectorStatementScope.NONE) {
+            return -1L;
+        }
+        String tableKey = SCAN_REUSE_NAMESPACE + ":table:" + 
session.getCatalogId() + ":"
+                + paimonHandle.getDatabaseName() + "." + 
paimonHandle.getTableName();
+        try {
+            Table table = session.getStatementScope().computeIfAbsent(tableKey,
+                    () -> resolveScanTable(paimonHandle));
+            if (catalogOps != null) {
+                return catalogOps.latestSnapshotId(table).orElse(-1L);
+            }
+            return -1L;
+        } catch (Exception e) {
+            return -1L;
+        }
+    }
+
+    /**
+     * Resolve the scan table consistently with {@link 
#resolvePaimonGeneration}: when the statement
+     * scope already holds a cached table (set by the reuse path in planScan), 
return that SAME
+     * instance so getScanNodeProperties observes the same generation. 
Otherwise resolve fresh.
+     */
+    private Table resolveScanTableConsistent(ConnectorSession session, 
PaimonTableHandle paimonHandle) {
+        if (session != null && session.isExternalScanTaskReuseEnabled()
+                && session.getStatementScope() != null
+                && session.getStatementScope() != 
ConnectorStatementScope.NONE) {
+            String tableKey = SCAN_REUSE_NAMESPACE + ":table:" + 
session.getCatalogId() + ":"

Review Comment:
   [P1] Key this scoped `Table` by the full resolution identity. 
`resolveScanTable()` selects a relation-specific table using `sysTableName`, 
branch, and effective scan options, but this cache aliases every handle with 
the same catalog/db/table. In a statement mixing a base table with a system 
table or branch, or two different `@options` pins, the node can plan its 
requested relation's ranges while serializing partition/schema properties from 
the first node's cached table; neither the system-range bypass nor the outer 
branch/options-aware reuse key protects this properties cache. Include all 
three discriminators (or bypass system properties) and add mixed-identity 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