diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTable.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTable.java index f12616cfd76..84fbdc2f58e 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTable.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTable.java @@ -505,6 +505,17 @@ public class PluginDrivenMvccExternalTable extends PluginDrivenExternalTable Optional tableSnapshot, Optional scanParams, Optional latestSnapshotFence) { + // Views have no table snapshot. BindRelation expands their SQL and pins the base tables; + // resolving a table handle here would load the view name through the table API. + if (isView()) { + // View expansion cannot apply relation scan parameters to the base tables. + if (scanParams.isPresent()) { + throw new UnsupportedOperationException( + "view not supported with scan params: @" + scanParams.get().getParamType()); + } + return new PluginDrivenMvccSnapshot(emptySnapshot(), + Collections.emptyMap(), Collections.emptyMap()); + } if (!tableSnapshot.isPresent() && !scanParams.isPresent()) { // B5a implicit query-begin (latest) pin. return latestSnapshotFence.isPresent() diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTableTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTableTest.java index f2f9fc645f2..15a35c77df0 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTableTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/mvcc/PluginDrivenMvccExternalTableTest.java @@ -901,6 +901,75 @@ public class PluginDrivenMvccExternalTableTest { // ==================== loadSnapshot: B5a latest materialize ==================== + @Test + public void testViewSnapshotDoesNotUseTableMetadata() { + Fixture f = Fixture.partitioned(); + PluginDrivenMvccExternalTable view = Mockito.spy(f.table); + Mockito.doReturn(true).when(view).isView(); + // A catalog may support the view endpoint but reject table loads for this name. Query binding + // must not depend on whether the unrelated table request returns 404 or another error. + Mockito.when(f.metadata.getTableHandle(f.session, "REMOTE_DB", "REMOTE_TBL")) + .thenThrow(new IllegalStateException("table endpoint is unavailable for this view")); + PluginDrivenMvccSnapshot fence = new PluginDrivenMvccSnapshot( + ConnectorMvccSnapshot.builder().snapshotId(PINNED_SNAPSHOT_ID).build(), + Collections.emptyMap(), Collections.emptyMap()); + + List snapshots = Arrays.asList( + view.loadSnapshot(Optional.empty(), Optional.empty()), + view.loadSnapshot(Optional.empty(), Optional.empty(), Optional.empty()), + view.loadSnapshot(Optional.empty(), Optional.empty(), Optional.of(fence))); + + for (MvccSnapshot snapshot : snapshots) { + PluginDrivenMvccSnapshot pin = Assertions.assertInstanceOf(PluginDrivenMvccSnapshot.class, snapshot); + Assertions.assertEquals(-1L, pin.getConnectorSnapshot().getSnapshotId()); + Assertions.assertNull(pin.getPinnedSchema()); + Assertions.assertTrue(pin.getNameToPartitionItem().isEmpty()); + Assertions.assertTrue(pin.getNameToLastModifiedMillis().isEmpty()); + Assertions.assertSame(f.latestCacheValue, view.getSchemaCacheValue(Optional.of(pin)).get()); + } + Mockito.verifyNoInteractions(f.metadata); + } + + @Test + public void testViewSnapshotRejectsScanParamsWithoutTableMetadata() { + Fixture f = Fixture.partitioned(); + PluginDrivenMvccExternalTable view = Mockito.spy(f.table); + Mockito.doReturn(true).when(view).isView(); + PluginDrivenMvccSnapshot fence = new PluginDrivenMvccSnapshot( + ConnectorMvccSnapshot.builder().snapshotId(PINNED_SNAPSHOT_ID).build(), + Collections.emptyMap(), Collections.emptyMap()); + Map incrementalParams = new HashMap<>(); + incrementalParams.put("startSnapshotId", "1"); + incrementalParams.put("endSnapshotId", "5"); + List selectors = Arrays.asList( + new TableScanParams(TableScanParams.BRANCH, + Collections.emptyMap(), Collections.singletonList("b1")), + new TableScanParams(TableScanParams.BRANCH, + Collections.singletonMap(TableScanParams.PARAMS_NAME, "b1"), Collections.emptyList()), + new TableScanParams(TableScanParams.TAG, + Collections.emptyMap(), Collections.singletonList("t1")), + new TableScanParams(TableScanParams.TAG, + Collections.singletonMap(TableScanParams.PARAMS_NAME, "t1"), Collections.emptyList()), + new TableScanParams(TableScanParams.INCREMENTAL_READ, incrementalParams, Collections.emptyList()), + new TableScanParams(TableScanParams.OPTIONS, + Collections.singletonMap("scan.snapshot-id", "7"), Collections.emptyList())); + + for (TableScanParams selector : selectors) { + List errors = Arrays.asList( + Assertions.assertThrows(UnsupportedOperationException.class, + () -> view.loadSnapshot(Optional.empty(), Optional.of(selector))), + Assertions.assertThrows(UnsupportedOperationException.class, + () -> view.loadSnapshot(Optional.empty(), Optional.of(selector), Optional.empty())), + Assertions.assertThrows(UnsupportedOperationException.class, + () -> view.loadSnapshot(Optional.empty(), Optional.of(selector), Optional.of(fence)))); + for (UnsupportedOperationException error : errors) { + Assertions.assertEquals("view not supported with scan params: @" + selector.getParamType(), + error.getMessage()); + } + } + Mockito.verifyNoInteractions(f.metadata); + } + @Test public void testLoadSnapshotEmptyMaterializes() { Fixture f = Fixture.partitioned();