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 8a4868a9da0 [fix](fe) Normalize connector table errors (#66628)
8a4868a9da0 is described below
commit 8a4868a9da0e2392acdd00bbfd7f37fc20a5e325
Author: Gabriel <[email protected]>
AuthorDate: Thu Aug 13 14:06:16 2026 +0800
[fix](fe) Normalize connector table errors (#66628)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Several negative connector queries expose generic
plugin or filesystem implementation details instead of stable
connector-facing errors. Equivalent failures can also produce different
messages during metadata binding, system-table resolution, and scan
planning. Use the connector display name for system-table constraint
errors, report a normal table-not-found error when a remotely resolved
handle disappears, and normalize Iceberg metadata-file misses across all
table-loading entry points by inspecting the complete exception cause
chain.
### Release note
Paimon and Iceberg negative queries now return stable connector-facing
errors for unsupported system-table scans, missing redirected tables,
and missing Iceberg metadata files.
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- [ ] 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. Connector errors no longer expose generic plugin or
filesystem wrapper details in these paths.
- 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
---
.../iceberg/IcebergConnectorMetadata.java | 28 ++++--
.../connector/iceberg/IcebergExceptionUtils.java | 61 +++++++++++++
.../connector/iceberg/IcebergScanPlanProvider.java | 96 +++++++++++++-------
.../iceberg/IcebergConnectorMetadataMvccTest.java | 32 +++++++
.../iceberg/IcebergConnectorMetadataTest.java | 50 +++++++++++
.../iceberg/IcebergScanPlanProviderTest.java | 100 +++++++++++++++++++++
.../iceberg/RecordingIcebergCatalogOps.java | 10 +++
.../datasource/scan/PluginDrivenScanNode.java | 29 ++++--
.../scan/PluginDrivenScanNodeSysHandleTest.java | 53 ++++++++++-
.../PluginDrivenScanNodeSysTableGuardTest.java | 81 +++++++++++++++++
10 files changed, 490 insertions(+), 50 deletions(-)
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
index 0e6425a8f07..2546fe4690e 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
@@ -412,7 +412,15 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
try {
exists = context.executeAuthenticated(() ->
catalogOps.tableExists(dbName, tableName));
} catch (Exception e) {
- throw new RuntimeException("Failed to check table exist, error
message is:" + e.getMessage(), e);
+ // Preserve Optional.empty even when an auth/catalog layer wraps
NoSuchTableException.
+ if (ExceptionUtils.getThrowableList(e).stream()
+ .anyMatch(NoSuchTableException.class::isInstance)) {
+ return Optional.empty();
+ }
+ // Existence checks in several catalogs load metadata internally,
so normalize them at the shared
+ // handle boundary just like explicit table loads.
+ throw IcebergExceptionUtils.wrapTableLoadFailure(new
IcebergTableHandle(dbName, tableName), e,
+ "Failed to check table exist, error message is:");
}
if (!exists) {
return Optional.empty();
@@ -646,7 +654,8 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
try {
return context.executeAuthenticated(() ->
resolveTableForRead(session, handle));
} catch (Exception e) {
- throw new RuntimeException("Failed to load table, error message
is:" + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(
+ handle, e, "Failed to load table, error message is:");
}
}
@@ -688,7 +697,8 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
base,
MetadataTableType.from(handle.getSysTableName()));
});
} catch (Exception e) {
- throw new RuntimeException("Failed to load table, error message
is:" + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(
+ handle, e, "Failed to load table, error message is:");
}
}
@@ -1919,8 +1929,8 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
() -> buildMvccPartitionViewUncached(session,
iceHandle)));
});
} catch (Exception e) {
- throw new RuntimeException("Failed to build iceberg MVCC partition
view, error message is:"
- + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(iceHandle, e,
+ "Failed to build iceberg MVCC partition view, error
message is:");
}
}
@@ -1967,8 +1977,8 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
TableIdentifier.of(iceHandle.getDbName(),
iceHandle.getTableName()), partitionCache);
});
} catch (Exception e) {
- throw new RuntimeException("Failed to list iceberg partition
names, error message is:"
- + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(iceHandle, e,
+ "Failed to list iceberg partition names, error message
is:");
}
}
@@ -2004,8 +2014,8 @@ public class IcebergConnectorMetadata implements
ConnectorMetadata {
return listPartitionsViewCache.get(key, () ->
listPartitionsUncached(session, iceHandle));
});
} catch (Exception e) {
- throw new RuntimeException("Failed to list iceberg partitions,
error message is:"
- + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(iceHandle, e,
+ "Failed to list iceberg partitions, error message is:");
}
}
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergExceptionUtils.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergExceptionUtils.java
new file mode 100644
index 00000000000..db84ade0d7f
--- /dev/null
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergExceptionUtils.java
@@ -0,0 +1,61 @@
+// 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.connector.iceberg;
+
+import org.apache.doris.connector.spi.DorisConnectorException;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.iceberg.exceptions.NotFoundException;
+
+import java.io.FileNotFoundException;
+
+final class IcebergExceptionUtils {
+
+ private IcebergExceptionUtils() {
+ }
+
+ static RuntimeException wrapTableLoadFailure(
+ IcebergTableHandle handle, Exception failure, String
fallbackPrefix) {
+ if (isMetadataNotFound(failure)) {
+ return metadataNotFound(handle, failure);
+ }
+ return new RuntimeException(fallbackPrefix + failure.getMessage(),
failure);
+ }
+
+ static RuntimeException wrapMetadataReadFailure(
+ IcebergTableHandle handle, RuntimeException failure) {
+ if (isMetadataNotFound(failure)) {
+ return metadataNotFound(handle, failure);
+ }
+ return failure;
+ }
+
+ private static boolean isMetadataNotFound(Throwable failure) {
+ // Iceberg and FileIO implementations wrap missing metadata
differently. Inspect every cause so eager
+ // planners and background lazy split iteration preserve one stable
table-scoped error contract.
+ return ExceptionUtils.getThrowableList(failure).stream()
+ .anyMatch(cause -> cause instanceof NotFoundException
+ || cause instanceof FileNotFoundException);
+ }
+
+ private static DorisConnectorException metadataNotFound(
+ IcebergTableHandle handle, Throwable failure) {
+ return new DorisConnectorException("Metadata not found in metadata
location for table "
+ + handle.getDbName() + "." + handle.getTableName(), failure);
+ }
+}
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
index 4f0f8417c66..02c5ac5bcf8 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
@@ -418,8 +418,18 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
*/
@Override
public List<ConnectorScanRange> planScan(ConnectorSession session,
ConnectorScanRequest request) {
- return planScanInternal(session, request.getTableHandle(),
request.getColumns(),
- request.getFilter(), request.isCountPushdown());
+ IcebergTableHandle handle = (IcebergTableHandle)
request.getTableHandle();
+ try {
+ return planScanInternal(session, handle, request.getColumns(),
+ request.getFilter(), request.isCountPushdown());
+ } catch (RuntimeException e) {
+ // Normal data scans and native position_deletes run on File
Scanner V2. Keep the serialized JNI
+ // system-table route untouched because its deferred reads belong
to the V1 scanner contract.
+ if (!handle.isSystemTable() || isPositionDeletesSysTable(handle)) {
+ throw IcebergExceptionUtils.wrapMetadataReadFailure(handle, e);
+ }
+ throw e;
+ }
}
/**
@@ -465,8 +475,10 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
fileCount += (added == null ? 0 : added) + (existing == null ?
0 : existing);
}
} catch (IOException e) {
- throw new RuntimeException("Failed to count iceberg manifest files
for batch decision, error message is:"
- + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(iceHandle, e,
+ "Failed to count iceberg manifest files for batch
decision, error message is:");
+ } catch (RuntimeException e) {
+ throw IcebergExceptionUtils.wrapMetadataReadFailure(iceHandle, e);
}
return fileCount >= threshold ? fileCount : -1;
}
@@ -501,9 +513,15 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
long fileSplitSize = sessionLong(session, FILE_SPLIT_SIZE, 0L);
long sliceSize = fileSplitSize > 0 ? fileSplitSize
: sessionLong(session, MAX_FILE_SPLIT_SIZE,
DEFAULT_MAX_FILE_SPLIT_SIZE);
- CloseableIterable<FileScanTask> tasks = streamingFileScanTasks(scan,
session, table, filter, sliceSize);
- return new IcebergStreamingSplitSource(tasks, table, formatVersion,
partitioned,
- orderedPartitionKeys, zone, uriNormalizer, sliceSize,
iceHandle.getRewriteFileScope());
+ try {
+ CloseableIterable<FileScanTask> tasks = streamingFileScanTasks(
+ scan, session, table, filter, sliceSize);
+ return new IcebergStreamingSplitSource(tasks, table,
formatVersion, partitioned,
+ orderedPartitionKeys, zone, uriNormalizer, sliceSize,
+ iceHandle.getRewriteFileScope(), iceHandle);
+ } catch (RuntimeException e) {
+ throw IcebergExceptionUtils.wrapMetadataReadFailure(iceHandle, e);
+ }
}
private static ConnectorSplitSource emptySplitSource() {
@@ -564,6 +582,7 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
private final UnaryOperator<String> uriNormalizer;
private final long sliceSize;
private final Set<String> rewriteScope;
+ private final IcebergTableHandle handle;
// Lazily opened on first hasNext() so the ctor never throws —
iceberg's ParallelIterable submits
// manifest readers in tasks.iterator(), which can fail; opening it
eagerly here would throw out of
// streamSplits() BEFORE the source is returned, leaking the
planFiles() iterable (the engine pump's
@@ -578,7 +597,8 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
IcebergStreamingSplitSource(CloseableIterable<FileScanTask> tasks,
Table table, int formatVersion,
boolean partitioned, List<String> orderedPartitionKeys, ZoneId
zone,
- UnaryOperator<String> uriNormalizer, long sliceSize,
Set<String> rewriteScope) {
+ UnaryOperator<String> uriNormalizer, long sliceSize,
Set<String> rewriteScope,
+ IcebergTableHandle handle) {
this.tasks = tasks;
this.table = table;
this.formatVersion = formatVersion;
@@ -588,25 +608,31 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
this.uriNormalizer = uriNormalizer;
this.sliceSize = sliceSize;
this.rewriteScope = rewriteScope;
+ this.handle = handle;
}
@Override
public boolean hasNext() {
- if (buffered != null) {
- return true;
- }
- if (iterator == null) {
- iterator = tasks.iterator();
- }
- while (iterator.hasNext()) {
- IcebergScanRange range = buildRangeForTask(iterator.next(),
table, formatVersion, partitioned,
- orderedPartitionKeys, zone, uriNormalizer, sliceSize,
rewriteScope, null, scratch);
- if (range != null) {
- buffered = range;
+ try {
+ if (buffered != null) {
return true;
}
+ if (iterator == null) {
+ iterator = tasks.iterator();
+ }
+ while (iterator.hasNext()) {
+ IcebergScanRange range =
buildRangeForTask(iterator.next(), table, formatVersion, partitioned,
+ orderedPartitionKeys, zone, uriNormalizer,
sliceSize, rewriteScope, null, scratch);
+ if (range != null) {
+ buffered = range;
+ return true;
+ }
+ }
+ return false;
+ } catch (RuntimeException e) {
+ // Lazy manifest opening happens on the split-pump thread,
after streamSplits has returned.
+ throw IcebergExceptionUtils.wrapMetadataReadFailure(handle, e);
}
- return false;
}
@Override
@@ -1606,7 +1632,11 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
if (!systemTable) {
scanSchema = pinnedSchema(table, iceHandle);
exactScan = buildScan(table, iceHandle, filter, session);
- applicableEqualityDeleteFieldIds =
cachedApplicableEqualityDeleteFieldIds(table, exactScan);
+ try {
+ applicableEqualityDeleteFieldIds =
cachedApplicableEqualityDeleteFieldIds(table, exactScan);
+ } catch (RuntimeException e) {
+ throw IcebergExceptionUtils.wrapMetadataReadFailure(iceHandle,
e);
+ }
hasApplicableEqualityDeletes =
!applicableEqualityDeleteFieldIds.isEmpty();
Optional<Map<Integer, List<String>>> nameMapping =
IcebergSchemaUtils.extractNameMapping(table);
if (requiresCurrentScanSemantics(
@@ -1625,11 +1655,17 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
// PERF-03: the non-system format resolution falls back to an
unfiltered whole-table planFiles() when the
// table sets neither write-format nor write.format.default; memoize
that inference per (table, snapshot)
// across queries via formatCache (pure metadata, no credential gate).
Null cache (offline) resolves live.
- props.put(ScanNodePropertyKeys.FILE_FORMAT_TYPE,
- systemTable ? "jni"
- : IcebergWriterHelper.getFileFormat(table,
+ String fileFormatType = "jni";
+ if (!systemTable) {
+ try {
+ fileFormatType = IcebergWriterHelper.getFileFormat(table,
TableIdentifier.of(iceHandle.getDbName(),
iceHandle.getTableName()), formatCache)
- .name().toLowerCase(Locale.ROOT));
+ .name().toLowerCase(Locale.ROOT);
+ } catch (RuntimeException e) {
+ throw IcebergExceptionUtils.wrapMetadataReadFailure(iceHandle,
e);
+ }
+ }
+ props.put(ScanNodePropertyKeys.FILE_FORMAT_TYPE, fileFormatType);
// [D-065] System (metadata) tables ($snapshots/$files/...) read via
the JNI serialized-split path
// (planSystemTableScan): the metadata-table schema travels INSIDE the
serialized FileScanTask, so BE
// needs neither the base-table path_partition_keys (a metadata table
is not base-spec partitioned ->
@@ -2801,13 +2837,13 @@ public class IcebergScanPlanProvider implements
ConnectorScanPlanProvider {
// re-validates the credential even on a scope hit).
IcebergCatalogOps ops = catalogOpsResolver.apply(session);
Table raw = IcebergStatementScope.sharedTable(session,
handle.getDbName(), handle.getTableName(), () -> {
- if (context == null) {
- return loadRawTable(ops, handle);
- }
try {
- return context.executeAuthenticated(() -> loadRawTable(ops,
handle));
+ return context == null
+ ? loadRawTable(ops, handle)
+ : context.executeAuthenticated(() -> loadRawTable(ops,
handle));
} catch (Exception e) {
- throw new RuntimeException("Failed to load table for scan,
error message is:" + e.getMessage(), e);
+ throw IcebergExceptionUtils.wrapTableLoadFailure(
+ handle, e, "Failed to load table for scan, error
message is:");
}
});
return wrapTableForScan(raw);
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataMvccTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataMvccTest.java
index be8514b11fa..6aec247222b 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataMvccTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataMvccTest.java
@@ -34,6 +34,7 @@ import org.apache.iceberg.Schema;
import org.apache.iceberg.Table;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.NotFoundException;
import org.apache.iceberg.inmemory.InMemoryCatalog;
import org.apache.iceberg.types.Types;
import org.junit.jupiter.api.Assertions;
@@ -536,4 +537,35 @@ public class IcebergConnectorMetadataMvccTest {
Assertions.assertEquals(1, ctx.authCount);
Assertions.assertFalse(ops.log.contains("loadTable:db1.t1"),
"loadTable must sit inside executeAuthenticated");
}
+
+ @Test
+ public void listPartitionNamesNormalizesDeepMetadataNotFoundFailure() {
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.loadTableFailure = new RuntimeException("catalog wrapper",
+ new RuntimeException("storage wrapper", new
NotFoundException("metadata is missing")));
+ IcebergConnectorMetadata md = new IcebergConnectorMetadata(
+ ops, IcebergCatalogProperties.of(Collections.emptyMap()), new
RecordingConnectorContext());
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.listPartitionNames(null, handle()));
+ Assertions.assertEquals("Metadata not found in metadata location for
table db1.t1", ex.getMessage());
+ }
+
+ @Test
+ public void parallelPartitionReadersNormalizeDeepMetadataNotFoundFailure()
{
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.loadTableFailure = new RuntimeException("catalog wrapper",
+ new RuntimeException("storage wrapper", new
NotFoundException("metadata is missing")));
+ IcebergConnectorMetadata md = new IcebergConnectorMetadata(
+ ops, IcebergCatalogProperties.of(Collections.emptyMap()), new
RecordingConnectorContext());
+
+ DorisConnectorException listFailure =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.listPartitions(null, handle(), Optional.empty()));
+ Assertions.assertEquals("Metadata not found in metadata location for
table db1.t1",
+ listFailure.getMessage());
+ DorisConnectorException mvccFailure =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> md.getMvccPartitionView(null, handle()));
+ Assertions.assertEquals("Metadata not found in metadata location for
table db1.t1",
+ mvccFailure.getMessage());
+ }
}
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
index 61f275f3593..02932e2d174 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
@@ -35,6 +35,8 @@ import org.apache.iceberg.SortOrder;
import org.apache.iceberg.TableProperties;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.NoSuchTableException;
+import org.apache.iceberg.exceptions.NotFoundException;
import org.apache.iceberg.inmemory.InMemoryCatalog;
import org.apache.iceberg.types.Types;
import org.apache.iceberg.view.ImmutableSQLViewRepresentation;
@@ -43,6 +45,7 @@ import org.apache.iceberg.view.ViewVersion;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -89,6 +92,32 @@ public class IcebergConnectorMetadataTest {
Types.NestedField.optional(2, "name", Types.StringType.get()));
}
+ @Test
+ public void missingMetadataFileHasStableTableError() {
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.loadTableFailure = new RuntimeException(
+ "Failed to open input stream", new
FileNotFoundException("missing metadata file"));
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> metadataWith(ops).getTableSchema(null, new
IcebergTableHandle("default", "missing_table")));
+ Assertions.assertTrue(ex.getMessage().contains(
+ "Metadata not found in metadata location for table
default.missing_table"), ex.getMessage());
+ }
+
+ @Test
+ public void missingMetadataFileForSystemTableHasStableTableError() {
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.loadTableFailure = new RuntimeException(
+ "Failed to open input stream", new
FileNotFoundException("missing metadata file"));
+ IcebergTableHandle handle = IcebergTableHandle.forSystemTable(
+ "default", "missing_table", "snapshots", -1L, null, -1L);
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> metadataWith(ops).getTableSchema(null, handle));
+ Assertions.assertTrue(ex.getMessage().contains(
+ "Metadata not found in metadata location for table
default.missing_table"), ex.getMessage());
+ }
+
/**
* A view version whose summary records {@code engine-name} (when
non-null) and whose current version
* carries a SQL representation for {@code reprDialect} (when non-null) —
driving the sql/dialect extraction
@@ -579,6 +608,27 @@ public class IcebergConnectorMetadataTest {
Assertions.assertFalse(handleOpt.isPresent());
}
+ @Test
+ public void getTableHandleNormalizesDeepMetadataNotFoundFailure() {
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.tableExistsFailure = new RuntimeException("catalog wrapper",
+ new RuntimeException("storage wrapper", new
NotFoundException("metadata is missing")));
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> metadataWith(ops).getTableHandle(null, "db1",
"missing_table"));
+ Assertions.assertEquals(
+ "Metadata not found in metadata location for table
db1.missing_table", ex.getMessage());
+ }
+
+ @Test
+ public void getTableHandlePreservesMissingTableAsEmpty() {
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.tableExistsFailure = new RuntimeException("catalog wrapper",
+ new NoSuchTableException("table disappeared"));
+
+ Assertions.assertTrue(metadataWith(ops).getTableHandle(null, "db1",
"missing_table").isEmpty());
+ }
+
// ---------------------------------------------------------------------
// getTableSchema — load via seam, parse columns + table props
// ---------------------------------------------------------------------
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
index 9d0041bc602..911b6073a9b 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
@@ -20,6 +20,7 @@ package org.apache.doris.connector.iceberg;
import org.apache.doris.connector.spi.ConnectorSession;
import org.apache.doris.connector.spi.ConnectorStatementScope;
import org.apache.doris.connector.spi.ConnectorType;
+import org.apache.doris.connector.spi.DorisConnectorException;
import org.apache.doris.connector.spi.handle.ConnectorColumnHandle;
import org.apache.doris.connector.spi.pushdown.ConnectorColumnRef;
import org.apache.doris.connector.spi.pushdown.ConnectorComparison;
@@ -73,6 +74,7 @@ import org.apache.iceberg.util.SerializationUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.ZoneId;
@@ -209,6 +211,87 @@ public class IcebergScanPlanProviderTest {
Assertions.assertEquals("t1", ops.lastLoadTable);
}
+ @Test
+ public void planScanMissingMetadataFileHasStableTableError() {
+ RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+ ops.loadTableFailure = new RuntimeException(
+ "Failed to open input stream", new
FileNotFoundException("missing metadata file"));
+ IcebergScanPlanProvider provider = new IcebergScanPlanProvider(
+ IcebergCatalogProperties.of(Collections.emptyMap()), ops);
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> provider.planScan(null, ConnectorScanRequest.builder(
+ new IcebergTableHandle("default", "missing_table"),
Collections.emptyList()).build()));
+ Assertions.assertTrue(ex.getMessage().contains(
+ "Metadata not found in metadata location for table
default.missing_table"), ex.getMessage());
+ }
+
+ @Test
+ public void planScanMissingManifestListHasStableTableError() {
+ Table table = createTable("missing_manifest_list", SCHEMA,
PartitionSpec.unpartitioned());
+ table.newAppend().appendFile(dataFile(table.spec(),
+ "s3://b/db/missing_manifest_list/f1.parquet", 1024, null,
null)).commit();
+ table.io().deleteFile(table.currentSnapshot().manifestListLocation());
+ IcebergScanPlanProvider provider = providerOver(table);
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> provider.planScan(emptySession(),
ConnectorScanRequest.builder(
+ new IcebergTableHandle("db1",
"missing_manifest_list"), Collections.emptyList()).build()));
+ Assertions.assertTrue(ex.getMessage().contains(
+ "Metadata not found in metadata location for table
db1.missing_manifest_list"), ex.getMessage());
+ }
+
+ @Test
+ public void streamingMissingManifestListHasStableTableError() {
+ Table table = createTable("missing_stream_manifest", SCHEMA,
PartitionSpec.unpartitioned());
+ table.newAppend().appendFile(dataFile(table.spec(),
+ "s3://b/db/missing_stream_manifest/f1.parquet", 1024, null,
null)).commit();
+ table.io().deleteFile(table.currentSnapshot().manifestListLocation());
+ IcebergScanPlanProvider provider = providerOver(table);
+ IcebergTableHandle handle = new IcebergTableHandle("db1",
"missing_stream_manifest");
+
+ DorisConnectorException estimate =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> provider.streamingSplitEstimate(batchSession(1, true),
handle, Optional.empty(), false));
+ Assertions.assertTrue(estimate.getMessage().contains(
+ "Metadata not found in metadata location for table
db1.missing_stream_manifest"),
+ estimate.getMessage());
+
+ DorisConnectorException lazy =
Assertions.assertThrows(DorisConnectorException.class, () -> {
+ try (ConnectorSplitSource source = provider.streamSplits(
+ emptySession(), handle, Collections.emptyList(),
Optional.empty(), -1L)) {
+ source.hasNext();
+ }
+ });
+ Assertions.assertTrue(lazy.getMessage().contains(
+ "Metadata not found in metadata location for table
db1.missing_stream_manifest"),
+ lazy.getMessage());
+ }
+
+ @Test
+ public void scanPropertiesMissingDeleteManifestHasStableTableError() {
+ Schema schema = new Schema(
+ Arrays.asList(
+ Types.NestedField.required(1, "id",
Types.IntegerType.get()),
+ Types.NestedField.optional(2, "name",
Types.StringType.get())),
+ Collections.singleton(1));
+ Table table = createTable("missing_delete_manifest", schema,
PartitionSpec.unpartitioned(),
+ Collections.singletonMap(TableProperties.FORMAT_VERSION, "2"));
+ table.newAppend().appendFile(dataFile(table.spec(),
+ "s3://b/db/missing_delete_manifest/f1.parquet", 1024, null,
null)).commit();
+ table.newRowDelta().addDeletes(equalityDeleteFile(
+ "s3://b/db/missing_delete_manifest/eq.parquet",
FileFormat.PARQUET, 1)).commit();
+ String deleteManifest =
table.currentSnapshot().deleteManifests(table.io()).get(0).path().toString();
+ table.io().deleteFile(deleteManifest);
+ IcebergScanPlanProvider provider = providerOver(table);
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> provider.getScanNodeProperties(emptySession(),
+ new IcebergTableHandle("db1",
"missing_delete_manifest"),
+ Collections.singletonList(new
IcebergColumnHandle("name", 2)), Optional.empty()));
+ Assertions.assertTrue(ex.getMessage().contains(
+ "Metadata not found in metadata location for table
db1.missing_delete_manifest"), ex.getMessage());
+ }
+
@Test
public void planScanResolvesTableInsideAuthContext() {
// The remote loadTable must sit INSIDE context.executeAuthenticated
so the FE-injected Kerberos UGI
@@ -1358,6 +1441,23 @@ public class IcebergScanPlanProviderTest {
.build());
}
+ @Test
+ public void positionDeletesMissingManifestListHasStableTableError() {
+ DeleteFile deleteFile =
FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned())
+ .ofPositionDeletes()
+ .withPath("s3://b/db/t1/pos-delete.parquet")
+ .withFileSizeInBytes(100)
+ .withRecordCount(1)
+ .build();
+ Table table = tableWithPositionDelete(deleteFile);
+ table.io().deleteFile(table.currentSnapshot().manifestListLocation());
+
+ DorisConnectorException ex =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> planPositionDeletes(table, Collections.emptyList()));
+ Assertions.assertTrue(ex.getMessage().contains(
+ "Metadata not found in metadata location for table db1.t1"),
ex.getMessage());
+ }
+
@Test
public void
planScanPositionDeletesEmitsNativeRangeMatchingTheBeRoutingContract() {
// WHY: this is THE contract. BE routes a range into
iceberg_position_delete_sys_table_reader iff
diff --git
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingIcebergCatalogOps.java
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingIcebergCatalogOps.java
index 9f834eab01d..6a9526ea273 100644
---
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingIcebergCatalogOps.java
+++
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingIcebergCatalogOps.java
@@ -60,6 +60,8 @@ final class RecordingIcebergCatalogOps implements
IcebergCatalogOps {
boolean databaseExists;
/** Canned existence answer for {@link #tableExists(String, String)}. */
boolean tableExists;
+ /** Optional exact failure thrown by {@link #tableExists(String, String)}.
*/
+ RuntimeException tableExistsFailure;
/** Canned existence answer for {@link #viewExists(String, String)}. */
boolean viewExists;
/** Canned SDK view returned by {@link #loadView(String, String)}. */
@@ -74,6 +76,8 @@ final class RecordingIcebergCatalogOps implements
IcebergCatalogOps {
Table table;
/** When set, {@link #loadTable(String, String)} throws instead of
returning {@link #table}. */
boolean throwOnLoadTable;
+ /** Optional exact failure thrown by {@link #loadTable(String, String)}. */
+ RuntimeException loadTableFailure;
/** When set, {@link #loadTable(String, String)} throws {@link
NoSuchTableException} (concurrent-drop race). */
boolean throwNoSuchTableOnLoadTable;
/**
@@ -163,6 +167,9 @@ final class RecordingIcebergCatalogOps implements
IcebergCatalogOps {
log.add("tableExists:" + dbName + "." + tableName);
lastExistsDb = dbName;
lastExistsTable = tableName;
+ if (tableExistsFailure != null) {
+ throw tableExistsFailure;
+ }
return tableExists;
}
@@ -201,6 +208,9 @@ final class RecordingIcebergCatalogOps implements
IcebergCatalogOps {
if (throwNoSuchTableOnLoadTable) {
throw new NoSuchTableException("simulated missing table %s.%s",
dbName, tableName);
}
+ if (loadTableFailure != null) {
+ throw loadTableFailure;
+ }
if (throwOnLoadTable) {
throw new RuntimeException("simulated loadTable failure for " +
dbName + "." + tableName);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/PluginDrivenScanNode.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/PluginDrivenScanNode.java
index 7710b46224c..2328e34a00f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/PluginDrivenScanNode.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/scan/PluginDrivenScanNode.java
@@ -65,6 +65,7 @@ import
org.apache.doris.datasource.plugin.PluginDrivenMetadata;
import org.apache.doris.datasource.plugin.PluginDrivenSysExternalTable;
import org.apache.doris.datasource.split.FileSplit;
import org.apache.doris.datasource.split.PluginDrivenSplit;
+import org.apache.doris.nereids.exceptions.AnalysisException;
import
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan.SelectedPartitions;
import org.apache.doris.planner.PlanNodeId;
import org.apache.doris.planner.ScanContext;
@@ -215,15 +216,17 @@ public class PluginDrivenScanNode extends
FileQueryScanNode {
Connector connector = catalog.getConnector();
ConnectorSession session = catalog.buildConnectorSession();
ConnectorMetadata metadata = PluginDrivenMetadata.get(session,
connector);
- String dbName = table.getDb() != null ? table.getDb().getRemoteName()
: "";
// Resolve through the table's sys-aware seam (NOT raw
metadata.getTableHandle): for a normal
// table this is identical to getTableHandle(session, dbName,
remoteName), but for a
// PluginDrivenSysExternalTable the override returns the connector's
SYSTEM handle (carrying
// sysTableName + forceJni), so the scan path threads force-JNI
correctly for binlog/audit_log.
ConnectorTableHandle handle =
table.resolveConnectorTableHandle(session, metadata)
- .orElseThrow(() -> new RuntimeException(
- "Table handle not found for plugin-driven table: " +
dbName + "."
- + table.getRemoteName()));
+ // Use analysis semantics and local names: mapped remote
identifiers are connector internals and
+ // a generic RuntimeException would be reported as
ERR_UNKNOWN_ERROR by EXPLAIN.
+ .orElseThrow(() -> new AnalysisException(
+ "Table '" + catalog.getName() + "."
+ + (table.getDb() == null ? "" :
table.getDb().getFullName()) + "." + table.getName()
+ + "' does not exist"));
return new PluginDrivenScanNode(id, desc, needCheckColumnPriv, sv,
scanContext, connector, session, handle);
}
@@ -1279,6 +1282,7 @@ public class PluginDrivenScanNode extends
FileQueryScanNode {
if (!(getTargetTable() instanceof PluginDrivenSysExternalTable)) {
return;
}
+ String connectorName = connectorDisplayName();
boolean timeTravelSupported = sysTableSupportsTimeTravel();
TableScanParams scanParams = getScanParams();
if (scanParams != null) {
@@ -1289,23 +1293,32 @@ public class PluginDrivenScanNode extends
FileQueryScanNode {
String sysTableName = sysTableName();
if (scanParams.incrementalRead()) {
if (!sysTableSupportsScanParam(p ->
p.supportsSystemTableIncrementalRead(sysTableName))) {
- throw new UserException("Plugin system table '" +
sysTableName
+ throw new UserException(connectorName + " system table '"
+ sysTableName
+ "' does not support INCR scan params.");
}
} else if (scanParams.isOptions()) {
if (!sysTableSupportsScanParam(p ->
p.supportsSystemTableOptions(sysTableName))) {
- throw new UserException("Plugin system table '" +
sysTableName
+ throw new UserException(connectorName + " system table '"
+ sysTableName
+ "' does not support OPTIONS scan params.");
}
} else if (!timeTravelSupported) {
- throw new UserException("Plugin system tables do not support
scan params.");
+ throw new UserException(connectorName + " system tables do not
support scan params.");
}
}
if (getQueryTableSnapshot() != null && !timeTravelSupported) {
- throw new UserException("Plugin system tables do not support time
travel.");
+ throw new UserException(connectorName + " system tables do not
support time travel.");
}
}
+ private String connectorDisplayName() throws UserException {
+ String engine = getTargetTable().getEngine();
+ // The engine is already the connector-owned display name; changing
its case corrupts identities such
+ // as iRODS and makes equivalent connector errors differ by execution
path.
+ return engine == null || engine.isEmpty()
+ ? "Plugin"
+ : engine;
+ }
+
/**
* Whether the connector honors THIS scan's selector on THIS system table
— the exact mirror of
* {@link #checkSysTableScanConstraints}' accept set, so a pin is resolved
for precisely the queries
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysHandleTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysHandleTest.java
index 6004ad6b8b8..a04072fd0bd 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysHandleTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysHandleTest.java
@@ -30,6 +30,7 @@ import org.apache.doris.datasource.SessionContext;
import org.apache.doris.datasource.plugin.PluginDrivenExternalCatalog;
import org.apache.doris.datasource.plugin.PluginDrivenExternalTable;
import org.apache.doris.datasource.plugin.PluginDrivenSysExternalTable;
+import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.planner.PlanNodeId;
import org.apache.doris.planner.ScanContext;
import org.apache.doris.qe.SessionVariable;
@@ -78,7 +79,7 @@ public class PluginDrivenScanNodeSysHandleTest {
ConnectorTableHandle sysHandle =
Mockito.mock(ConnectorTableHandle.class);
TestablePluginCatalog catalog = new TestablePluginCatalog("paimon",
metadata, session);
- ExternalDatabase<PluginDrivenExternalTable> db = mockDb("REMOTE_DB");
+ ExternalDatabase<PluginDrivenExternalTable> db = mockDb("local_db",
"REMOTE_DB");
// Base handle resolved from the SOURCE remote name (not the
"$"-suffixed sys remote name);
// the connector then maps base handle + "binlog" -> the sys handle.
NOTE: there is no stub
@@ -127,7 +128,7 @@ public class PluginDrivenScanNodeSysHandleTest {
ConnectorTableHandle baseHandle =
Mockito.mock(ConnectorTableHandle.class);
TestablePluginCatalog catalog = new TestablePluginCatalog("paimon",
metadata, session);
- ExternalDatabase<PluginDrivenExternalTable> db = mockDb("REMOTE_DB");
+ ExternalDatabase<PluginDrivenExternalTable> db = mockDb("local_db",
"REMOTE_DB");
Mockito.when(metadata.getTableHandle(session, "REMOTE_DB",
"REMOTE_TBL"))
.thenReturn(Optional.of(baseHandle));
@@ -148,6 +149,51 @@ public class PluginDrivenScanNodeSysHandleTest {
.getSysTableHandle(Mockito.any(), Mockito.any(),
Mockito.anyString());
}
+ @Test
+ public void createReportsMissingTableWhenHandleDisappears() {
+ ConnectorMetadata metadata = Mockito.mock(ConnectorMetadata.class);
+ ConnectorSession session = Mockito.mock(ConnectorSession.class);
+
Mockito.when(session.getStatementScope()).thenReturn(ConnectorStatementScope.NONE);
+ TestablePluginCatalog catalog = new TestablePluginCatalog("hms",
metadata, session);
+ ExternalDatabase<PluginDrivenExternalTable> db = mockDb("local_db",
"REMOTE_DB");
+ Mockito.when(metadata.getTableHandle(session, "REMOTE_DB",
"dropped_table"))
+ .thenReturn(Optional.empty());
+ PluginDrivenExternalTable table = bareTable(catalog, db,
"dropped_table");
+
+ AnalysisException ex = Assertions.assertThrows(AnalysisException.class,
+ () -> PluginDrivenScanNode.create(new PlanNodeId(0),
+ new TupleDescriptor(new TupleId(0)), false, new
SessionVariable(),
+ ScanContext.EMPTY, catalog, table));
+ Assertions.assertEquals("Table 'test-catalog.local_db.tbl' does not
exist", ex.getMessage());
+ }
+
+ @Test
+ public void createReportsMappedLocalNameWhenSysHandleDisappears() {
+ ConnectorMetadata metadata = Mockito.mock(ConnectorMetadata.class);
+ ConnectorSession session = Mockito.mock(ConnectorSession.class);
+
Mockito.when(session.getStatementScope()).thenReturn(ConnectorStatementScope.NONE);
+ ConnectorTableHandle baseHandle =
Mockito.mock(ConnectorTableHandle.class);
+ TestablePluginCatalog catalog = new TestablePluginCatalog("hms",
metadata, session);
+ ExternalDatabase<PluginDrivenExternalTable> db = mockDb("local_db",
"REMOTE_DB");
+ Mockito.when(metadata.getTableHandle(session, "REMOTE_DB",
"REMOTE_TBL"))
+ .thenReturn(Optional.of(baseHandle));
+ Mockito.when(metadata.getSysTableHandle(session, baseHandle, "files"))
+ .thenReturn(Optional.empty());
+ PluginDrivenExternalTable base = bareTable(catalog, db, "REMOTE_TBL");
+ PluginDrivenSysExternalTable sysTable = new
PluginDrivenSysExternalTable(base, "files") {
+ @Override
+ protected synchronized void makeSureInitialized() {
+ // no-op: skip Env-backed catalog/db init
+ }
+ };
+
+ AnalysisException ex = Assertions.assertThrows(AnalysisException.class,
+ () -> PluginDrivenScanNode.create(new PlanNodeId(0),
+ new TupleDescriptor(new TupleId(0)), false, new
SessionVariable(),
+ ScanContext.EMPTY, catalog, sysTable));
+ Assertions.assertEquals("Table 'test-catalog.local_db.tbl$files' does
not exist", ex.getMessage());
+ }
+
// ==================== helpers (mirror PluginDrivenSysTableTest)
====================
private static PluginDrivenExternalTable
bareTable(PluginDrivenExternalCatalog catalog,
@@ -161,8 +207,9 @@ public class PluginDrivenScanNodeSysHandleTest {
}
@SuppressWarnings("unchecked")
- private static ExternalDatabase<PluginDrivenExternalTable> mockDb(String
remoteName) {
+ private static ExternalDatabase<PluginDrivenExternalTable> mockDb(String
localName, String remoteName) {
ExternalDatabase<PluginDrivenExternalTable> db =
Mockito.mock(ExternalDatabase.class);
+ Mockito.when(db.getFullName()).thenReturn(localName);
Mockito.when(db.getRemoteName()).thenReturn(remoteName);
return db;
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysTableGuardTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysTableGuardTest.java
index 60247a27d22..0d034d4740a 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysTableGuardTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/scan/PluginDrivenScanNodeSysTableGuardTest.java
@@ -52,6 +52,13 @@ import org.mockito.Mockito;
*/
public class PluginDrivenScanNodeSysTableGuardTest {
+ private static PluginDrivenSysExternalTable sysTableWithEngine(String
engine) {
+ PluginDrivenSysExternalTable table =
Mockito.mock(PluginDrivenSysExternalTable.class);
+ Mockito.doReturn(engine).when(table).getEngine();
+ Mockito.doReturn("files").when(table).getSysTableName();
+ return table;
+ }
+
private static PluginDrivenScanNode guardOnlyNode() throws Exception {
PluginDrivenScanNode node = Mockito.mock(PluginDrivenScanNode.class,
Mockito.CALLS_REAL_METHODS);
// Default: no scan-params, no snapshot, and a connector whose sys
tables do NOT time-travel
@@ -93,6 +100,80 @@ public class PluginDrivenScanNodeSysTableGuardTest {
"time-travel rejection must carry the expected message, got: "
+ ex.getMessage());
}
+ @Test
+ public void sysTableTimeTravelErrorNamesConnector() throws Exception {
+ PluginDrivenScanNode node = guardOnlyNode();
+ PluginDrivenSysExternalTable sysTable =
Mockito.mock(PluginDrivenSysExternalTable.class);
+ Mockito.doReturn("paimon").when(sysTable).getEngine();
+ Mockito.doReturn(sysTable).when(node).getTargetTable();
+
Mockito.doReturn(Mockito.mock(TableSnapshot.class)).when(node).getQueryTableSnapshot();
+
+ UserException ex = Assertions.assertThrows(UserException.class,
+ node::checkSysTableScanConstraints);
+ Assertions.assertTrue(ex.getMessage().contains("paimon system tables
do not support time travel."),
+ ex.getMessage());
+ }
+
+ @Test
+ public void sysTableScanParamsErrorNamesConnector() throws Exception {
+ PluginDrivenScanNode node = guardOnlyNode();
+ PluginDrivenSysExternalTable sysTable =
Mockito.mock(PluginDrivenSysExternalTable.class);
+ Mockito.doReturn("paimon").when(sysTable).getEngine();
+ Mockito.doReturn(sysTable).when(node).getTargetTable();
+
Mockito.doReturn(Mockito.mock(TableScanParams.class)).when(node).getScanParams();
+
+ UserException ex = Assertions.assertThrows(UserException.class,
+ node::checkSysTableScanConstraints);
+ Assertions.assertTrue(ex.getMessage().contains("paimon system tables
do not support scan params."),
+ ex.getMessage());
+ }
+
+ @Test
+ public void mixedCaseConnectorNameIsPreservedForIncrementalReadError()
throws Exception {
+ PluginDrivenScanNode node = guardOnlyNode();
+
Mockito.doReturn(sysTableWithEngine("iRODS")).when(node).getTargetTable();
+ TableScanParams incr = Mockito.mock(TableScanParams.class);
+ Mockito.doReturn(true).when(incr).incrementalRead();
+ Mockito.doReturn(incr).when(node).getScanParams();
+
+ UserException ex = Assertions.assertThrows(UserException.class,
node::checkSysTableScanConstraints);
+ Assertions.assertEquals(
+ "iRODS system table 'files' does not support INCR scan
params.", ex.getDetailMessage());
+ }
+
+ @Test
+ public void mixedCaseConnectorNameIsPreservedForOptionsError() throws
Exception {
+ PluginDrivenScanNode node = guardOnlyNode();
+
Mockito.doReturn(sysTableWithEngine("iRODS")).when(node).getTargetTable();
+ TableScanParams options = Mockito.mock(TableScanParams.class);
+ Mockito.doReturn(true).when(options).isOptions();
+ Mockito.doReturn(options).when(node).getScanParams();
+
+ UserException ex = Assertions.assertThrows(UserException.class,
node::checkSysTableScanConstraints);
+ Assertions.assertEquals(
+ "iRODS system table 'files' does not support OPTIONS scan
params.", ex.getDetailMessage());
+ }
+
+ @Test
+ public void mixedCaseConnectorNameIsPreservedForGenericScanParamsError()
throws Exception {
+ PluginDrivenScanNode node = guardOnlyNode();
+
Mockito.doReturn(sysTableWithEngine("iRODS")).when(node).getTargetTable();
+
Mockito.doReturn(Mockito.mock(TableScanParams.class)).when(node).getScanParams();
+
+ UserException ex = Assertions.assertThrows(UserException.class,
node::checkSysTableScanConstraints);
+ Assertions.assertEquals("iRODS system tables do not support scan
params.", ex.getDetailMessage());
+ }
+
+ @Test
+ public void mixedCaseConnectorNameIsPreservedForTimeTravelError() throws
Exception {
+ PluginDrivenScanNode node = guardOnlyNode();
+
Mockito.doReturn(sysTableWithEngine("iRODS")).when(node).getTargetTable();
+
Mockito.doReturn(Mockito.mock(TableSnapshot.class)).when(node).getQueryTableSnapshot();
+
+ UserException ex = Assertions.assertThrows(UserException.class,
node::checkSysTableScanConstraints);
+ Assertions.assertEquals("iRODS system tables do not support time
travel.", ex.getDetailMessage());
+ }
+
@Test
public void sysTableWithoutScanParamsOrSnapshotDoesNotThrow() throws
Exception {
PluginDrivenScanNode node = guardOnlyNode();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]