You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by yi...@apache.org on 2026/10/23 06:13:52 UTC
(doris) branch branch-4.2 updated: [fix](fe) Allow table access through Iceberg JDBC V0 catalogs (branch-4.2) (#69485)
This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 8e5b079051b [fix](fe) Allow table access through Iceberg JDBC V0 catalogs (branch-4.2) (#69485)
8e5b079051b is described below
commit 8e5b079051bea5b4bff40857eb65b96c96995c3f
Author: Gabriel <li...@selectdb.com>
AuthorDate: Fri Oct 23 14:13:41 2026 +0800
[fix](fe) Allow table access through Iceberg JDBC V0 catalogs (branch-4.2) (#69485)
### What problem does this PR solve?
Issue Number: N/A
Related PR: #69452
Problem Summary:
Backport #69452 to branch-4.2. Iceberg's JDBC catalog implements
`ViewCatalog` even when its V0 metadata schema cannot store views. Treat
unsupported JDBC view probes as no views so ordinary table listing and
reads work without requiring a V1 migration. Preserve V1 view support,
REST view routing, explicit view-operation errors, and real
SQL/authorization failures.
Preserve the requested DROP kind and lifetime before invoking the legacy
metadata API, which accepts none of these flags and can purge an
ordinary table. Reject `DROP VIEW` and `DROP MATERIALIZED VIEW` on a
table, including `IF EXISTS`. An external permanent table cannot satisfy
`DROP TEMPORARY TABLE`: return without mutation for `IF EXISTS`,
otherwise report an unknown table. Reject `DROP TABLE` on a view while
preserving matching table/view drops and missing-object `IF EXISTS`
behavior.
Add eight drop-dispatch tests using the actual JDBC V0 view guards and a
recording writable catalog. Assert that rejected/no-op statements
preserve table state and rows without purge or delete journaling. Extend
the PostgreSQL V0 regression with all six wrong-kind SQL forms and check
table listing and data after every statement.
### Release note
Fix table access through Iceberg JDBC V0 catalogs. Prevent wrong-kind
external DROP statements from deleting existing table data.
### Check List (For Author)
- Test
- [x] Unit Test: 29 targeted tests passed, zero failures/errors/skips.
Includes eight new drop-kind tests, six JDBC view-probe tests, and
existing metadata-operation tests. Before the fix, five drop-kind tests
reproduced the unsafe mutation behavior on each branch.
- [x] FE Checkstyle: zero violations.
- [ ] Regression test: SELECT-only V0 scenarios and six writable V0
wrong-kind DROP checks added; release-branch end-to-end SQL execution
not performed locally. Regression Groovy syntax checked.
- Behavior changed:
- [x] Yes. V0 view probes no longer block table access; mismatched DROP
statements cannot reach destructive table operations.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../apache/doris/datasource/ExternalCatalog.java | 20 ++
.../datasource/iceberg/IcebergMetadataOps.java | 25 ++-
.../datasource/ExternalCatalogDropKindTest.java | 221 +++++++++++++++++++++
.../iceberg/IcebergJdbcViewProbeTest.java | 144 ++++++++++++++
.../iceberg/test_iceberg_jdbc_catalog.groovy | 101 ++++++++++
5 files changed, 507 insertions(+), 4 deletions(-)
diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
index 0501f8c603d..3c83b18f181 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
@@ -29,6 +29,8 @@ import org.apache.doris.catalog.TableIf;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.Pair;
import org.apache.doris.common.ThreadPoolManager;
@@ -1303,6 +1305,24 @@ public abstract class ExternalCatalog
}
throw new DdlException("Failed to get table: '" + tableName + "' in database: " + dbName);
}
+ // The metadata API receives no kind/lifetime flags and may purge an ordinary table.
+ // External catalogs have no temporary tables or MTMVs; reject mismatches before dispatch.
+ if (mustTemporary) {
+ if (ifExists) {
+ return;
+ }
+ ErrorReport.reportDdlException(ErrorCode.ERR_UNKNOWN_TABLE, tableName, dbName);
+ }
+ if (isMtmv) {
+ ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_OBJECT, dbName, tableName, "MTMV",
+ "External catalogs do not support materialized views");
+ }
+ boolean actualIsView = dorisTable.isView();
+ if (isView != actualIsView) {
+ // IF EXISTS must not relax the type check for an existing object.
+ ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_OBJECT, dbName, tableName, isView ? "VIEW" : "TABLE",
+ "Use DROP " + (actualIsView ? "VIEW" : "TABLE") + " instead");
+ }
try {
metadataOps.dropTable(dorisTable, ifExists);
DropInfo info = new DropInfo(getName(), db.getFullName(), dorisTable.getName(), db.getFullName());
diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
index 536ff56c891..723cdab4441 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
@@ -77,6 +77,7 @@ import org.apache.iceberg.exceptions.NoSuchNamespaceException;
import org.apache.iceberg.expressions.Expressions;
import org.apache.iceberg.expressions.Literal;
import org.apache.iceberg.expressions.Term;
+import org.apache.iceberg.jdbc.JdbcCatalog;
import org.apache.iceberg.rest.RESTSessionCatalog;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.TypeUtil;
@@ -100,6 +101,7 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -2061,6 +2063,19 @@ public class IcebergMetadataOps implements ExternalMetadataOps {
return userSessionCatalog != null && userSessionCatalog.isNestedNamespaceEnabled();
}
+ private <T> T probeViews(ViewCatalog viewCatalog, Supplier<T> probe, T noViews) {
+ try {
+ return probe.get();
+ } catch (UnsupportedOperationException e) {
+ // JdbcCatalog implements ViewCatalog even for V0, which cannot store views. The SDK
+ // rejects these probes before SQL; do not require a V1 migration merely to read tables.
+ if (viewCatalog instanceof JdbcCatalog) {
+ return noViews;
+ }
+ throw e;
+ }
+ }
+
private boolean isViewCatalogEnabled() {
return viewCatalogEnabled;
}
@@ -2141,8 +2156,8 @@ public class IcebergMetadataOps implements ExternalMetadataOps {
}
private boolean viewExistsInternal(SessionContext ctx, String remoteDbName, String remoteViewName) {
- return viewCatalog(ctx).map(activeCatalog ->
- activeCatalog.viewExists(getTableIdentifier(remoteDbName, remoteViewName))).orElse(false);
+ return viewCatalog(ctx).map(activeCatalog -> probeViews(activeCatalog,
+ () -> activeCatalog.viewExists(getTableIdentifier(remoteDbName, remoteViewName)), false)).orElse(false);
}
private View loadViewInternal(String dbName, String tblName) {
@@ -2159,8 +2174,10 @@ public class IcebergMetadataOps implements ExternalMetadataOps {
}
private List<String> listViewNamesInternal(SessionContext ctx, String dbName) {
- return viewCatalog(ctx).map(activeCatalog -> activeCatalog.listViews(getNamespace(dbName))
- .stream().map(TableIdentifier::name).collect(Collectors.toList())).orElseGet(Collections::emptyList);
+ return viewCatalog(ctx).map(activeCatalog -> probeViews(activeCatalog,
+ () -> activeCatalog.listViews(getNamespace(dbName)).stream()
+ .map(TableIdentifier::name).collect(Collectors.toList()), Collections.<String>emptyList()))
+ .orElseGet(Collections::emptyList);
}
private void performDropView(SessionContext ctx, String remoteDbName, String remoteViewName) throws DdlException {
diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDropKindTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDropKindTest.java
new file mode 100644
index 00000000000..0bacc0fd0c3
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDropKindTest.java
@@ -0,0 +1,221 @@
+// 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;
+
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.security.authentication.ExecutionAuthenticator;
+import org.apache.doris.datasource.iceberg.IcebergMetadataOps;
+import org.apache.doris.persist.EditLog;
+
+import mockit.Mock;
+import mockit.MockUp;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.jdbc.JdbcCatalog;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class ExternalCatalogDropKindTest {
+ @Test
+ public void dropViewCannotDeleteV0Table() {
+ for (boolean ifExists : new boolean[] {false, true}) {
+ DropCatalog catalog = new DropCatalog(false);
+ Assertions.assertThrows(DdlException.class,
+ () -> catalog.dropTable("db", "t", true, false, ifExists, false, false));
+ catalog.assertTableUnchanged();
+ }
+ }
+
+ @Test
+ public void dropMaterializedViewCannotDeleteV0Table() {
+ for (boolean ifExists : new boolean[] {false, true}) {
+ DropCatalog catalog = new DropCatalog(false);
+ Assertions.assertThrows(DdlException.class,
+ () -> catalog.dropTable("db", "t", false, true, ifExists, false, false));
+ catalog.assertTableUnchanged();
+ }
+ }
+
+ @Test
+ public void dropTemporaryTableCannotDeleteV0Table() {
+ DropCatalog catalog = new DropCatalog(false);
+ Assertions.assertThrows(DdlException.class,
+ () -> catalog.dropTable("db", "t", false, false, false, true, false));
+ catalog.assertTableUnchanged();
+ }
+
+ @Test
+ public void dropTemporaryTableIfExistsDoesNotDeleteV0Table() throws Exception {
+ DropCatalog catalog = new DropCatalog(false);
+ catalog.dropTable("db", "t", false, false, true, true, false);
+ catalog.assertTableUnchanged();
+ }
+
+ @Test
+ public void dropTableCannotDeleteView() {
+ for (boolean ifExists : new boolean[] {false, true}) {
+ DropCatalog catalog = new DropCatalog(true);
+ Assertions.assertThrows(DdlException.class,
+ () -> catalog.dropTable("db", "t", false, false, ifExists, false, false));
+ Assertions.assertEquals(0, catalog.sdk.viewDrops);
+ Assertions.assertTrue(catalog.sdk.present);
+ Mockito.verifyNoInteractions(catalog.editLog);
+ }
+ }
+
+ @Test
+ public void matchingV0TableDropStillWorks() throws Exception {
+ DropCatalog catalog = new DropCatalog(false);
+ catalog.dropTable("db", "t", false, false, false, false, false);
+ Assertions.assertFalse(catalog.sdk.present);
+ Assertions.assertEquals(1, catalog.sdk.tableDrops);
+ Assertions.assertTrue(catalog.sdk.purge);
+ Mockito.verify(catalog.editLog).logDropTable(Mockito.any());
+ }
+
+ @Test
+ public void matchingViewDropStillWorks() throws Exception {
+ DropCatalog catalog = new DropCatalog(true);
+ catalog.dropTable("db", "t", true, false, false, false, false);
+ Assertions.assertFalse(catalog.sdk.present);
+ Assertions.assertEquals(1, catalog.sdk.viewDrops);
+ Assertions.assertEquals(0, catalog.sdk.tableDrops);
+ Mockito.verify(catalog.editLog).logDropTable(Mockito.any());
+ }
+
+ @Test
+ public void missingObjectIfExistsStillDoesNotMutate() throws Exception {
+ for (boolean view : new boolean[] {false, true}) {
+ DropCatalog catalog = new DropCatalog(false);
+ Mockito.when(catalog.database.getTableNullable("t")).thenReturn(null);
+ catalog.dropTable("db", "t", view, false, true, false, false);
+ catalog.assertTableUnchanged();
+ }
+ }
+
+ private static class DropCatalog extends ExternalCatalog {
+ private final RecordingJdbcCatalog sdk;
+ private final ExternalDatabase<ExternalTable> database;
+ private final EditLog editLog = Mockito.mock(EditLog.class);
+
+ private DropCatalog(boolean view) {
+ super(1L, "drop_kind_test", InitCatalogLog.Type.TEST, "");
+ initialized = true;
+ catalogProperty = new CatalogProperty(null, Collections.emptyMap());
+ executionAuthenticator = new ExecutionAuthenticator() {
+ };
+ sdk = new RecordingJdbcCatalog(view);
+ metadataOps = new IcebergMetadataOps(this, sdk) {
+ @Override
+ public void afterDropTable(String dbName, String tableName) {
+ // Cache retirement is outside this test's drop-kind and mutation boundary.
+ }
+ };
+ database = Mockito.mock(ExternalDatabase.class);
+ ExternalTable table = Mockito.mock(ExternalTable.class);
+ Mockito.when(database.getTableNullable("t")).thenReturn(table);
+ Mockito.when(database.getFullName()).thenReturn("db");
+ Mockito.when(table.getName()).thenReturn("t");
+ Mockito.when(table.getRemoteName()).thenReturn("t");
+ Mockito.when(table.getDbName()).thenReturn("db");
+ Mockito.when(table.getRemoteDbName()).thenReturn("db");
+ Mockito.when(table.isView()).thenReturn(view);
+ Env env = Mockito.mock(Env.class);
+ Mockito.when(env.getEditLog()).thenReturn(editLog);
+ new MockUp<Env>() {
+ @Mock
+ Env getCurrentEnv() {
+ return env;
+ }
+ };
+ }
+
+ private void assertTableUnchanged() {
+ Assertions.assertTrue(sdk.present);
+ Assertions.assertEquals(Arrays.asList(1, 2), sdk.rows);
+ Assertions.assertEquals(0, sdk.tableDrops);
+ Assertions.assertEquals(0, sdk.viewDrops);
+ Mockito.verifyNoInteractions(editLog);
+ }
+
+ @Override
+ protected void initLocalObjectsImpl() {
+ }
+
+ @Override
+ public ExternalDatabase<ExternalTable> getDbNullable(String dbName) {
+ return database;
+ }
+
+ @Override
+ protected List<String> listTableNamesFromRemote(SessionContext ctx, String dbName) {
+ return Collections.singletonList("t");
+ }
+
+ @Override
+ public boolean tableExist(SessionContext ctx, String dbName, String tblName) {
+ return sdk.present;
+ }
+ }
+
+ // Retain the real SDK's V0 view guard while recording the destructive table operation it used to block.
+ private static class RecordingJdbcCatalog extends JdbcCatalog {
+ private final boolean view;
+ private final List<Integer> rows = new ArrayList<>(Arrays.asList(1, 2));
+ private boolean present = true;
+ private boolean purge;
+ private int tableDrops;
+ private int viewDrops;
+
+ private RecordingJdbcCatalog(boolean view) {
+ this.view = view;
+ }
+
+ @Override
+ public boolean viewExists(TableIdentifier identifier) {
+ return view ? present : super.viewExists(identifier);
+ }
+
+ @Override
+ public boolean tableExists(TableIdentifier identifier) {
+ return present;
+ }
+
+ @Override
+ public boolean dropTable(TableIdentifier identifier, boolean purge) {
+ this.purge = purge;
+ tableDrops++;
+ present = false;
+ rows.clear();
+ return true;
+ }
+
+ @Override
+ public boolean dropView(TableIdentifier identifier) {
+ viewDrops++;
+ present = false;
+ return true;
+ }
+ }
+}
diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergJdbcViewProbeTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergJdbcViewProbeTest.java
new file mode 100644
index 00000000000..4796697531a
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergJdbcViewProbeTest.java
@@ -0,0 +1,144 @@
+// 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.iceberg;
+
+import org.apache.doris.common.security.authentication.ExecutionAuthenticator;
+import org.apache.doris.datasource.ExternalCatalog;
+
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.SupportsNamespaces;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.catalog.ViewCatalog;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.jdbc.JdbcCatalog;
+import org.apache.iceberg.jdbc.UncheckedSQLException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class IcebergJdbcViewProbeTest {
+ private static final Namespace NAMESPACE = Namespace.of("db");
+ private static final TableIdentifier TABLE = TableIdentifier.of(NAMESPACE, "table");
+ private static final TableIdentifier VIEW = TableIdentifier.of(NAMESPACE, "view");
+
+ private IcebergMetadataOps ops(Catalog catalog) {
+ ExternalCatalog dorisCatalog = Mockito.mock(ExternalCatalog.class);
+ Mockito.when(dorisCatalog.getProperties()).thenReturn(Collections.emptyMap());
+ Mockito.when(dorisCatalog.getExecutionAuthenticator()).thenReturn(new ExecutionAuthenticator() {
+ });
+ return new IcebergMetadataOps(dorisCatalog, catalog);
+ }
+
+ @Test
+ public void v0ListsTablesWithoutViewSupport() {
+ Assertions.assertEquals(Collections.singletonList("table"), ops(new V0JdbcCatalog()).listTableNames("db"));
+ }
+
+ @Test
+ public void v0HasNoViews() {
+ IcebergMetadataOps ops = ops(new V0JdbcCatalog());
+ Assertions.assertEquals(Collections.emptyList(), ops.listViewNames("db"));
+ Assertions.assertFalse(ops.viewExists("db", "table"));
+ Assertions.assertFalse(ops.viewExists("db", "missing"));
+ }
+
+ @Test
+ public void explicitV0ViewLoadStillFails() {
+ IcebergMetadataOps ops = ops(new V0JdbcCatalog());
+ RuntimeException failure = Assertions.assertThrows(RuntimeException.class, () -> ops.loadView("db", "view"));
+ Assertions.assertInstanceOf(UnsupportedOperationException.class, failure.getCause());
+ }
+
+ @Test
+ public void jdbcViewSupportIsNotDisabledByCatalogType() {
+ IcebergMetadataOps ops = ops(new V0JdbcCatalog() {
+ @Override
+ public List<TableIdentifier> listTables(Namespace namespace) {
+ return Arrays.asList(TABLE, VIEW);
+ }
+
+ @Override
+ public List<TableIdentifier> listViews(Namespace namespace) {
+ return Collections.singletonList(VIEW);
+ }
+
+ @Override
+ public boolean viewExists(TableIdentifier identifier) {
+ return VIEW.equals(identifier);
+ }
+ });
+ Assertions.assertEquals(Collections.singletonList("table"), ops.listTableNames("db"));
+ Assertions.assertEquals(Collections.singletonList("view"), ops.listViewNames("db"));
+ Assertions.assertTrue(ops.viewExists("db", "view"));
+ Assertions.assertFalse(ops.viewExists("db", "table"));
+ }
+
+ @Test
+ public void jdbcProbeFailuresAreNotHidden() {
+ for (RuntimeException failure : Arrays.asList(
+ new ForbiddenException("Access denied"), new UncheckedSQLException("Connection failed"))) {
+ IcebergMetadataOps ops = ops(new V0JdbcCatalog() {
+ @Override
+ public List<TableIdentifier> listViews(Namespace namespace) {
+ throw failure;
+ }
+
+ @Override
+ public boolean viewExists(TableIdentifier identifier) {
+ throw failure;
+ }
+ });
+ Assertions.assertSame(failure, Assertions.assertThrows(failure.getClass(),
+ () -> ops.listTableNames("db")));
+ Assertions.assertSame(failure, Assertions.assertThrows(failure.getClass(),
+ () -> ops.listViewNames("db")));
+ Assertions.assertSame(failure, Assertions.assertThrows(RuntimeException.class,
+ () -> ops.viewExists("db", "view")).getCause());
+ }
+ }
+
+ @Test
+ public void unsupportedViewsInOtherCatalogsAreNotHidden() {
+ UnsupportedOperationException failure = new UnsupportedOperationException("Unsupported view operation");
+ Catalog catalog = Mockito.mock(Catalog.class,
+ Mockito.withSettings().extraInterfaces(SupportsNamespaces.class, ViewCatalog.class));
+ Mockito.when(catalog.listTables(NAMESPACE)).thenReturn(Collections.singletonList(TABLE));
+ Mockito.when(((ViewCatalog) catalog).listViews(NAMESPACE)).thenThrow(failure);
+ Mockito.when(((ViewCatalog) catalog).viewExists(VIEW)).thenThrow(failure);
+ IcebergMetadataOps ops = ops(catalog);
+ Assertions.assertSame(failure, Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> ops.listTableNames("db")));
+ Assertions.assertSame(failure, Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> ops.listViewNames("db")));
+ Assertions.assertSame(failure, Assertions.assertThrows(RuntimeException.class,
+ () -> ops.viewExists("db", "view")).getCause());
+ }
+
+ // Keep the SDK's actual V0 listViews/viewExists guards; only table listing avoids a live JDBC connection.
+ private static class V0JdbcCatalog extends JdbcCatalog {
+ @Override
+ public List<TableIdentifier> listTables(Namespace namespace) {
+ return Collections.singletonList(TABLE);
+ }
+ }
+}
diff --git a/regression-test/suites/external_table_p0/iceberg/test_iceberg_jdbc_catalog.groovy b/regression-test/suites/external_table_p0/iceberg/test_iceberg_jdbc_catalog.groovy
index 2ed8af6c586..368fccc008b 100644
--- a/regression-test/suites/external_table_p0/iceberg/test_iceberg_jdbc_catalog.groovy
+++ b/regression-test/suites/external_table_p0/iceberg/test_iceberg_jdbc_catalog.groovy
@@ -119,7 +119,106 @@ suite("test_iceberg_jdbc_catalog", "p0,external,iceberg,external_docker,external
}
}
+ String v0_writer = "test_iceberg_jdbc_v0_writer"
+ String v0_reader = "test_iceberg_jdbc_v0_reader"
+ String v0_database = "iceberg_jdbc_v0_test"
+ String v0_uri = "jdbc:postgresql://${externalEnvIp}:${jdbc_port}/${v0_database}"
+ def driverLoader = new URLClassLoader([new File(local_driver_path).toURI().toURL()] as URL[],
+ this.class.classLoader)
+ def pgDriver = driverLoader.loadClass("org.postgresql.Driver").getDeclaredConstructor().newInstance()
+ def pgProperties = new Properties()
+ pgProperties.setProperty("user", "postgres")
+ pgProperties.setProperty("password", "123456")
+ def pgConnection = pgDriver.connect("jdbc:postgresql://${externalEnvIp}:${jdbc_port}/${pg_db}", pgProperties)
try {
+ def statement = pgConnection.createStatement()
+ try {
+ // JDBC schema detection scans all PostgreSQL schemas, so isolate V0 from the V1 database.
+ statement.execute("DROP DATABASE IF EXISTS ${v0_database} WITH (FORCE)")
+ statement.execute("CREATE DATABASE ${v0_database}")
+ statement.execute("DROP ROLE IF EXISTS iceberg_jdbc_v0_reader")
+ statement.execute("CREATE ROLE iceberg_jdbc_v0_reader LOGIN PASSWORD '123456'")
+ } finally {
+ statement.close()
+ }
+ pgConnection.close()
+ pgConnection = pgDriver.connect(v0_uri, pgProperties)
+ statement = pgConnection.createStatement()
+ try {
+ // Use the PyIceberg-compatible V0 layout, which has no iceberg_type column.
+ statement.execute("""CREATE TABLE iceberg_tables (
+ catalog_name VARCHAR(255) NOT NULL, table_namespace VARCHAR(255) NOT NULL,
+ table_name VARCHAR(255) NOT NULL, metadata_location VARCHAR(1000),
+ previous_metadata_location VARCHAR(1000),
+ PRIMARY KEY (catalog_name, table_namespace, table_name))""")
+ statement.execute("""CREATE TABLE iceberg_namespace_properties (
+ catalog_name VARCHAR(255) NOT NULL, namespace VARCHAR(255) NOT NULL,
+ property_key VARCHAR(255), property_value VARCHAR(1000),
+ PRIMARY KEY (catalog_name, namespace, property_key))""")
+ statement.execute("GRANT CONNECT ON DATABASE ${v0_database} TO iceberg_jdbc_v0_reader")
+ statement.execute("GRANT USAGE ON SCHEMA public TO iceberg_jdbc_v0_reader")
+ statement.execute("GRANT SELECT ON ALL TABLES IN SCHEMA public TO iceberg_jdbc_v0_reader")
+ } finally {
+ statement.close()
+ }
+ } finally {
+ pgConnection.close()
+ driverLoader.close()
+ }
+
+ def createV0Catalog = { String name, String uri, String user ->
+ sql """DROP CATALOG IF EXISTS ${name}"""
+ sql """CREATE CATALOG ${name} PROPERTIES (
+ 'type' = 'iceberg', 'iceberg.catalog.type' = 'jdbc',
+ 'uri' = '${uri}', 'warehouse' = 's3://warehouse/jdbc_v0_wh/',
+ 'iceberg.jdbc.driver_url' = '${driver_name}',
+ 'iceberg.jdbc.driver_class' = 'org.postgresql.Driver',
+ 'iceberg.jdbc.user' = '${user}', 'iceberg.jdbc.password' = '123456',
+ 'iceberg.jdbc.catalog_name' = 'jdbc_v0_catalog',
+ 'iceberg.jdbc.init-catalog-tables' = 'false', 'iceberg.jdbc.schema-version' = 'V0',
+ 's3.endpoint' = 'http://${externalEnvIp}:${minio_port}',
+ 's3.access_key' = 'admin', 's3.secret_key' = 'password',
+ 's3.region' = 'us-east-1', 'use_path_style' = 'true')"""
+ }
+
+ try {
+ createV0Catalog(v0_writer, v0_uri, "postgres")
+ sql """CREATE DATABASE IF NOT EXISTS ${v0_writer}.v0_db"""
+ sql """CREATE TABLE ${v0_writer}.v0_db.v0_table (id INT) PROPERTIES ('format-version' = '2')"""
+ sql """INSERT INTO ${v0_writer}.v0_db.v0_table VALUES (1), (2)"""
+ // Wrong-kind DROP statements must never purge a permanent V0 table, even with IF EXISTS.
+ for (def dropKind : [[statement: "DROP VIEW", error: "is not VIEW"],
+ [statement: "DROP MATERIALIZED VIEW", error: "is not MTMV"],
+ [statement: "DROP TEMPORARY TABLE", error: "Unknown table"]]) {
+ for (boolean ifExists : [false, true]) {
+ String dropSql = "${dropKind.statement} ${ifExists ? 'IF EXISTS ' : ''}${v0_writer}.v0_db.v0_table"
+ if (dropKind.statement == "DROP TEMPORARY TABLE" && ifExists) {
+ sql dropSql
+ } else {
+ test {
+ sql dropSql
+ exception dropKind.error
+ }
+ }
+ assertEquals([["v0_table"]], sql("SHOW TABLES FROM ${v0_writer}.v0_db"))
+ assertEquals([[1], [2]], sql("SELECT * FROM ${v0_writer}.v0_db.v0_table ORDER BY id"))
+ }
+ }
+ // PostgreSQL enforces read-only transactions, so accidental catalog migration or writes fail.
+ createV0Catalog(v0_reader, v0_uri + "?options=-c%20default_transaction_read_only%3Don",
+ "iceberg_jdbc_v0_reader")
+ assertTrue(sql("SHOW DATABASES FROM ${v0_reader}").collect { it[0] }.contains("v0_db"))
+ assertEquals([["v0_table"]], sql("SHOW TABLES FROM ${v0_reader}.v0_db"))
+ assertEquals("id", sql("DESC ${v0_reader}.v0_db.v0_table")[0][0])
+ assertEquals([[1], [2]], sql("SELECT * FROM ${v0_reader}.v0_db.v0_table ORDER BY id"))
+ def v0_snapshot = sql("SELECT snapshot_id FROM ${v0_reader}.v0_db.v0_table\$snapshots")[0][0]
+ assertEquals([[1], [2]], sql("""SELECT * FROM ${v0_reader}.v0_db.v0_table
+ FOR VERSION AS OF ${v0_snapshot} ORDER BY id"""))
+ sql """INSERT INTO ${v0_writer}.v0_db.v0_table VALUES (3)"""
+ sql """REFRESH CATALOG ${v0_reader}"""
+ assertEquals([[3L]], sql("SELECT COUNT(*) FROM ${v0_reader}.v0_db.v0_table"))
+ assertEquals([[2L]], sql("""SELECT COUNT(*) FROM ${v0_reader}.v0_db.v0_table
+ FOR VERSION AS OF ${v0_snapshot}"""))
// Clean up existing catalog
sql """DROP CATALOG IF EXISTS ${catalog_name}"""
@@ -363,6 +462,8 @@ suite("test_iceberg_jdbc_catalog", "p0,external,iceberg,external_docker,external
try {
sql """SWITCH internal"""
sql """DROP CATALOG IF EXISTS ${catalog_name}"""
+ sql """DROP CATALOG IF EXISTS ${v0_reader}"""
+ sql """DROP CATALOG IF EXISTS ${v0_writer}"""
} catch (Exception e) {
logger.warn("Failed to cleanup catalog: ${e.message}")
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org