This is an automated email from the ASF dual-hosted git repository.

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 49bfbdecffb [fix](fe) Avoid table handle lookups for external views 
(#69946)
49bfbdecffb is described below

commit 49bfbdecffb5b37aeadb0982700533e8daf7768d
Author: Gabriel <[email protected]>
AuthorDate: Mon Nov 2 14:47:49 2026 +0800

    [fix](fe) Avoid table handle lookups for external views (#69946)
    
    ### What problem does this PR solve?
    
    Related PR: #69836
    
    Selecting an external Iceberg REST view can fail during query binding
    with `BadRequestException: ... is not an Iceberg compatible table`, even
    when the catalog supports reading the view definition.
    
    `BindRelation` loads statement snapshots before expanding the view SQL.
    The snapshot loader calls
    `PluginDrivenExternalTable.resolveConnectorTableHandle`, which
    unconditionally asks the connector for a table handle using the view
    name. A REST catalog can reject that table request with HTTP 400 instead
    of reporting a missing table. The Iceberg connector only converts
    `NoSuchTableException` to an empty handle, so the error escapes before
    the existing view-existence fallback can run.
    
    Resolve the object's view classification before requesting a table
    handle. Known views return no table handle and retain the live
    view-existence check, so a view deleted outside Doris still invalidates
    its cached lookup. Snapshot loading can then return an empty view pin
    and continue to view expansion; the referenced base tables keep their
    normal snapshot handling. Ordinary table handles and table-load errors
    retain their existing behavior.
    
    This addresses the shared handle-resolution boundary, including callers
    outside the snapshot loader. #69836 proposes skipping the
    snapshot-loader entry instead.
    
    ### Release note
    
    Fix external view queries failing when an Iceberg REST catalog rejects
    table metadata requests for view names.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test
        - [ ] No need to test or manual test.
    - Behavior changed:
        - [ ] No.
    - [x] Yes. External views no longer issue a table-handle request during
    resolution.
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    Validation:
    - `run-fe-ut.sh --run` with `PluginDrivenExternalViewHandleTest`,
    `PluginDrivenExternalTableTest`, `PluginDrivenMvccExternalTableTest`,
    and `PluginDrivenMissingTableTest`: **164 tests passed**, with no
    failures, errors, or skips.
    - Before the fix, both view cases failed with the reproduced
    table-endpoint error; the ordinary table cases passed.
    - `mvn checkstyle:check -pl fe-core`: **0 violations**.
    - `git diff --check` passed.
    
    No live Databricks integration test was run.
---
 .../plugin/PluginDrivenExternalTable.java          |   5 +-
 .../plugin/PluginDrivenExternalViewHandleTest.java | 111 +++++++++++++++++++++
 2 files changed, 115 insertions(+), 1 deletion(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalTable.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalTable.java
index 93d78e42e67..6e3115668d0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalTable.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalTable.java
@@ -140,7 +140,10 @@ public class PluginDrivenExternalTable extends 
ExternalTable {
             }
         });
         String dbName = db != null ? db.getRemoteName() : "";
-        Optional<ConnectorTableHandle> handle = 
metadata.getTableHandle(session, dbName, getRemoteName());
+        // Some catalogs reject table requests for views with non-404 errors. 
Classify views before
+        // probing the table endpoint so snapshot loading can continue to view 
expansion.
+        Optional<ConnectorTableHandle> handle = supportsView() && isView()
+                ? Optional.empty() : metadata.getTableHandle(session, dbName, 
getRemoteName());
         // A view has no table handle. Check live view existence before 
treating an empty handle as a
         // dropped table; the cached isView flag can outlive a remote view 
deletion.
         if (!handle.isPresent() && !metadata.viewExists(session, dbName, 
getRemoteName())) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/plugin/PluginDrivenExternalViewHandleTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/plugin/PluginDrivenExternalViewHandleTest.java
new file mode 100644
index 00000000000..9723ac0ac52
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/plugin/PluginDrivenExternalViewHandleTest.java
@@ -0,0 +1,111 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.datasource.plugin;
+
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.connector.spi.Connector;
+import org.apache.doris.connector.spi.ConnectorCapability;
+import org.apache.doris.connector.spi.ConnectorMetadata;
+import org.apache.doris.connector.spi.ConnectorSession;
+import org.apache.doris.connector.spi.DorisConnectorException;
+import org.apache.doris.connector.spi.handle.ConnectorTableHandle;
+import org.apache.doris.datasource.ExternalDatabase;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+import java.util.Set;
+
+public class PluginDrivenExternalViewHandleTest {
+    @Test
+    public void viewDoesNotProbeTableEndpoint() {
+        Fixture f = new Fixture(true);
+        Mockito.when(f.metadata.viewExists(f.session, "remote_db", 
"remote_object")).thenReturn(true);
+        Mockito.when(f.metadata.getTableHandle(f.session, "remote_db", 
"remote_object"))
+                .thenThrow(new DorisConnectorException("Object is not an 
Iceberg compatible table"));
+
+        Assertions.assertTrue(f.table.resolveConnectorTableHandle(f.session, 
f.metadata).isEmpty());
+
+        Mockito.verify(f.metadata, 
Mockito.never()).getTableHandle(Mockito.any(), Mockito.any(), Mockito.any());
+        Mockito.verifyNoInteractions(f.invalidate);
+    }
+
+    @Test
+    public void deletedViewStillInvalidatesLookup() {
+        Fixture f = new Fixture(true);
+        // The locally cached view classification can survive an out-of-band 
view deletion.
+        Mockito.when(f.metadata.viewExists(f.session, "remote_db", 
"remote_object")).thenReturn(false);
+        Mockito.when(f.metadata.getTableHandle(f.session, "remote_db", 
"remote_object"))
+                .thenThrow(new DorisConnectorException("Object is not an 
Iceberg compatible table"));
+
+        Assertions.assertTrue(f.table.resolveConnectorTableHandle(f.session, 
f.metadata).isEmpty());
+
+        Mockito.verify(f.invalidate).run();
+        Mockito.verify(f.metadata, 
Mockito.never()).getTableHandle(Mockito.any(), Mockito.any(), Mockito.any());
+    }
+
+    @Test
+    public void ordinaryTableStillResolvesHandle() {
+        Fixture f = new Fixture(false);
+        ConnectorTableHandle handle = Mockito.mock(ConnectorTableHandle.class);
+        Mockito.when(f.metadata.getTableHandle(f.session, "remote_db", 
"remote_object"))
+                .thenReturn(Optional.of(handle));
+
+        Assertions.assertSame(handle, 
f.table.resolveConnectorTableHandle(f.session, f.metadata).orElseThrow());
+
+        Mockito.verify(f.metadata, Mockito.never()).viewExists(Mockito.any(), 
Mockito.any(), Mockito.any());
+        Mockito.verifyNoInteractions(f.invalidate);
+    }
+
+    @Test
+    public void ordinaryTableLoadFailureStillPropagates() {
+        Fixture f = new Fixture(false);
+        DorisConnectorException failure = new DorisConnectorException("Table 
metadata request failed");
+        Mockito.when(f.metadata.getTableHandle(f.session, "remote_db", 
"remote_object")).thenThrow(failure);
+
+        Assertions.assertSame(failure, 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> f.table.resolveConnectorTableHandle(f.session, 
f.metadata)));
+
+        Mockito.verifyNoInteractions(f.invalidate);
+    }
+
+    private static class Fixture {
+        final ConnectorMetadata metadata = 
Mockito.mock(ConnectorMetadata.class);
+        final ConnectorSession session = Mockito.mock(ConnectorSession.class);
+        final Runnable invalidate = Mockito.mock(Runnable.class);
+        final PluginDrivenExternalTable table =
+                Mockito.mock(PluginDrivenExternalTable.class, 
Mockito.CALLS_REAL_METHODS);
+
+        Fixture(boolean view) {
+            Connector connector = Mockito.mock(Connector.class);
+            
Mockito.when(connector.getCapabilities()).thenReturn(Set.of(ConnectorCapability.SUPPORTS_VIEW));
+            PluginDrivenExternalCatalog catalog = 
Mockito.mock(PluginDrivenExternalCatalog.class);
+            Mockito.when(catalog.getConnector()).thenReturn(connector);
+            ExternalDatabase<?> database = 
Mockito.mock(ExternalDatabase.class);
+            Mockito.when(database.getRemoteName()).thenReturn("remote_db");
+            Mockito.when(database.observeTableLookup(Mockito.eq(table), 
Mockito.any()))
+                    .thenReturn(Optional.of(invalidate));
+            Deencapsulation.setField(table, "catalog", catalog);
+            Deencapsulation.setField(table, "db", database);
+            Deencapsulation.setField(table, "remoteName", "remote_object");
+            Mockito.doReturn(view).when(table).isView();
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to