[fix](fe) Skip table snapshot resolution for external views (#69836)

### What problem does this PR solve?

External views currently resolve a table handle when loading the
statement snapshot, before their SQL is expanded. Skip that table-only
path for known views; their base tables retain normal snapshot pinning.

Reject unsupported view scan parameters such as `@branch` and `@tag`
instead of silently ignoring the requested selector.

### Release note

Avoid table metadata requests for external views during snapshot
binding, and reject unsupported view scan parameters.

### Check List (For Author)

For this update: Checkstyle passed for both changed Java files.
Selector-rejection unit tests were added but not run locally.

- Test
    - [x] Regression test
    - [x] Unit Test
    - [x] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
        - [ ] Previous test can cover this change.
        - [ ] No code files have been changed.
        - [ ] Other reason

- Behavior changed:
    - [ ] No.
    - [x] Yes.

- Does this need documentation?
    - [x] No.
    - [ ] Yes.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
This commit is contained in:
daidai authored and GitHub committed 2026-11-03 10:26:41 +08:00
1 parent bdb90856e6
commit 3763f09897
2 files changed
+80

No files matched your search

@@ -505,6 +505,17 @@ public class PluginDrivenMvccExternalTable extends PluginDrivenExternalTable
Optional<TableSnapshot> tableSnapshot,
Optional<TableScanParams> scanParams,
Optional<MvccSnapshot> 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()
@@ -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<MvccSnapshot> 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<String, String> incrementalParams = new HashMap<>();
incrementalParams.put("startSnapshotId", "1");
incrementalParams.put("endSnapshotId", "5");
List<TableScanParams> 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<UnsupportedOperationException> 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();