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]