Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
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;
Expand Down Expand Up @@ -1303,6 +1305,24 @@ public void dropTable(String dbName, String tableName, boolean isView, boolean i
}
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Check the object kind without requiring a readable HMS table

This shared dispatch also handles HMS catalogs, where HMSExternalTable.isView() calls makeSureInitialized() and supportedHiveTable(). For an ordinary nontransactional Hive table stored as SequenceFile (or Avro), that initialization throws NotSupportedException because its input format is not supported for scanning. Consequently, a matching DROP TABLE hms.db.t (including IF EXISTS) now fails here before HiveMetadataOps.dropTableImpl can delete the table; previously that path only required existence and a nontransactional table. Resolve the table/view kind from lightweight remote metadata without validating scan support, and add a dispatch test using a real HMS table with an unsupported input format. The current tests mock isView(), so they miss this regression.

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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
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;
Expand All @@ -100,6 +101,7 @@
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;

Expand Down Expand Up @@ -2061,6 +2063,19 @@ private boolean determineNestedNamespaceEnabled() {
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;
}
Expand Down Expand Up @@ -2141,8 +2156,8 @@ private boolean viewExistsInternal(String remoteDbName, String remoteViewName) {
}

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the requested drop kind before allowing the V0 fallback

With a writable JDBC V0 catalog containing an ordinary permanent table c.db.t, DROP VIEW c.db.t now deletes and purges that table. Env.dropView passes isView=true, but ExternalCatalog.dropTable discards it and invokes IcebergMetadataOps.dropTableImpl. This changed probe converts V0's UnsupportedOperationException to false, selecting performDropTable and catalog.dropTable(id, true) instead of stopping before mutation. DROP TEMPORARY TABLE c.db.t and DROP MATERIALIZED VIEW c.db.t also reach this branch because the same dispatch discards mustTemporary and isMtmv; IF EXISTS does not protect an existing table. Preserve or reject the requested kind/lifetime before invoking the table mutation, and add negative tests on a writable V0 catalog proving these statements leave ordinary table metadata and rows intact.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest update and synchronized to #69486.

ExternalCatalog.dropTable now checks the requested object kind and temporary-table lifetime before calling the legacy metadata API. DROP VIEW/DROP MATERIALIZED VIEW cannot delete an existing table, including with IF EXISTS. DROP TEMPORARY TABLE IF EXISTS returns without mutation, while the non-IF EXISTS form reports an unknown table. Reverse table/view mismatches are rejected; matching table and view drops remain supported.

Added eight dispatch tests using the real SDK V0 view guards and a recording writable JDBC catalog. They check table state/rows, purge calls, and delete journaling; five tests failed before the fix and all eight now pass on both branches. Added the six PostgreSQL SQL forms with table-list/data checks after each statement.

Local validation: 29 targeted unit tests pass on branch-4.2 and 27 on branch-4.1, with zero failures/errors/skips; FE Checkstyle passes on both. Regression Groovy syntax was checked; release-branch end-to-end SQL was not run locally. Both PRs have fresh run buildall and /review triggers.

}

private View loadViewInternal(String dbName, String tblName) {
Expand All @@ -2159,8 +2174,10 @@ private List<String> listViewNamesInternal(String dbName) {
}

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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading
Loading