This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new a7704ccda4 [Cherry-pick to branch-1.3] [#14714] fix(postgresql): drop
tables by schema-qualified name (#14715) (#14738)
a7704ccda4 is described below
commit a7704ccda42a1a8ee82c161f25a2591292db76e0
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Nov 7 10:15:33 2026 +0800
[Cherry-pick to branch-1.3] [#14714] fix(postgresql): drop tables by
schema-qualified name (#14715) (#14738)
**Cherry-pick Information:**
- Original commit: 9107b3b2b72398bca6dd9c35e3a39a7236515537
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
Co-authored-by: Qi Yu <[email protected]>
---
.../operation/PostgreSqlTableOperations.java | 21 +++++
.../TestPostgreSqlTableDropPrivileges.java | 92 ++++++++++++++++++++++
.../operation/TestPostgreSqlTableOperations.java | 42 ++++++++++
3 files changed, 155 insertions(+)
diff --git
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/operation/PostgreSqlTableOperations.java
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/operation/PostgreSqlTableOperations.java
index ddd92fa738..15911bca34 100644
---
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/operation/PostgreSqlTableOperations.java
+++
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/operation/PostgreSqlTableOperations.java
@@ -377,6 +377,27 @@ public class PostgreSqlTableOperations extends
JdbcTableOperations
return "DROP TABLE " + quotedIdentifier(tableName);
}
+ /**
+ * Drops the table by its schema-qualified name. An unqualified name is
resolved through the
+ * search path, and PostgreSQL leaves out a schema the catalog user has no
USAGE privilege on. The
+ * drop would then report the table as missing instead of refusing it with
42501.
+ *
+ * <p>On {@code branch-1.3}, that missing-table result can cause Gravitino
to remove the
+ * registration of a table that still exists. On {@code main}, the
registration is already
+ * preserved when the connector returns {@code false}; schema qualification
ensures the permission
+ * failure is reported as 403.
+ */
+ @Override
+ protected void dropTable(String schemaName, String tableName) {
+ try (Connection connection = getConnection(schemaName)) {
+ JdbcConnectorUtils.executeUpdate(
+ connection,
+ "DROP TABLE " + quotedIdentifier(schemaName) + "." +
quotedIdentifier(tableName));
+ } catch (final SQLException se) {
+ throw this.exceptionMapper.toGravitinoException(se);
+ }
+ }
+
@Override
protected String generatePurgeTableSql(String tableName) {
throw new UnsupportedOperationException(
diff --git
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableDropPrivileges.java
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableDropPrivileges.java
new file mode 100644
index 0000000000..4a7c2450b0
--- /dev/null
+++
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableDropPrivileges.java
@@ -0,0 +1,92 @@
+/*
+ * 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.gravitino.catalog.postgresql.operation;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+import javax.sql.DataSource;
+import
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlColumnDefaultValueConverter;
+import
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlExceptionConverter;
+import
org.apache.gravitino.catalog.postgresql.converter.PostgreSqlTypeConverter;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** A drop the catalog user may not run must be refused, not reported as a
missing table. */
+public class TestPostgreSqlTableDropPrivileges {
+ private static final String QUALIFIED_DROP = "DROP TABLE
\"restricted\".\"t\"\"x\"";
+
+ private Connection connection;
+ private Statement statement;
+ private PostgreSqlTableOperations operations;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ DataSource source = mock(DataSource.class);
+ connection = mock(Connection.class);
+ statement = mock(Statement.class);
+ when(source.getConnection()).thenReturn(connection);
+ when(connection.createStatement()).thenReturn(statement);
+ operations = new PostgreSqlTableOperations();
+ operations.initialize(
+ source,
+ new PostgreSqlExceptionConverter(),
+ new PostgreSqlTypeConverter(),
+ new PostgreSqlColumnDefaultValueConverter(),
+ Map.of("jdbc-database", "database"));
+ }
+
+ @Test
+ void testDropUsesSchemaQualifiedName() throws Exception {
+ Assertions.assertTrue(operations.drop("restricted", "t\"x"));
+ verify(statement).executeUpdate(QUALIFIED_DROP);
+ }
+
+ @Test
+ void testSchemaWithoutUsageIsForbidden() throws Exception {
+ when(statement.executeUpdate(QUALIFIED_DROP))
+ .thenThrow(new SQLException("permission denied for schema restricted",
"42501"));
+ ForbiddenException error =
+ Assertions.assertThrows(
+ ForbiddenException.class, () -> operations.drop("restricted",
"t\"x"));
+ Assertions.assertTrue(error.getMessage().contains("permission denied"));
+ verify(connection).close();
+ }
+
+ @Test
+ void testMissingTableStillReportsNotDropped() throws Exception {
+ when(statement.executeUpdate(QUALIFIED_DROP))
+ .thenThrow(new SQLException("table \"t\" does not exist", "42P01"));
+ Assertions.assertFalse(operations.drop("restricted", "t\"x"));
+ }
+
+ @Test
+ void testMissingSchemaStillReportsNotDropped() throws Exception {
+ when(statement.executeUpdate(QUALIFIED_DROP))
+ .thenThrow(new SQLException("schema \"restricted\" does not exist",
"3F000"));
+ Assertions.assertFalse(operations.drop("restricted", "t\"x"));
+ }
+}
diff --git
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
index 8c59e49292..d4bac133f6 100644
---
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
+++
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/operation/TestPostgreSqlTableOperations.java
@@ -904,6 +904,48 @@ public class TestPostgreSqlTableOperations extends
TestPostgreSql {
}
}
+ @Test
+ void testDropWithoutSchemaUsageIsForbiddenAndKeepsTable() throws
SQLException {
+ String role = RandomNameUtils.genRandomName("drop_role");
+ String schema = RandomNameUtils.genRandomName("drop_schema");
+ String table = RandomNameUtils.genRandomName("drop_table");
+ try (Connection admin = DATA_SOURCE.getConnection()) {
+ JdbcConnectorUtils.executeUpdate(
+ admin, "CREATE ROLE " + role + " LOGIN PASSWORD
'test-only-password'");
+ JdbcConnectorUtils.executeUpdate(admin, "CREATE SCHEMA " + schema);
+ try {
+ JdbcConnectorUtils.executeUpdate(admin, "REVOKE ALL ON SCHEMA " +
schema + " FROM PUBLIC");
+ JdbcConnectorUtils.executeUpdate(
+ admin, "CREATE TABLE " + schema + "." + table + " (id int)");
+ Map<String, String> properties = new HashMap<>();
+ properties.put("jdbc-driver", CONTAINER.getDriverClassName());
+ properties.put("jdbc-url", CONTAINER.getJdbcUrl());
+ properties.put("jdbc-database", admin.getCatalog());
+ properties.put("jdbc-user", role);
+ properties.put("jdbc-password", "test-only-password");
+ properties.put("jdbc.pool.max-size", "1");
+ properties.put("jdbc.pool.min-size", "1");
+ try (BasicDataSource limitedSource =
+ (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+ PostgreSqlTableOperations limited = new PostgreSqlTableOperations();
+ limited.initialize(
+ limitedSource,
+ JDBC_EXCEPTION_CONVERTER,
+ new PostgreSqlTypeConverter(),
+ new PostgreSqlColumnDefaultValueConverter(),
+ properties);
+ Assertions.assertThrows(ForbiddenException.class, () ->
limited.drop(schema, table));
+ Assertions.assertEquals(table, TABLE_OPERATIONS.load(schema,
table).name());
+ }
+ Assertions.assertTrue(TABLE_OPERATIONS.drop(schema, table));
+ Assertions.assertFalse(TABLE_OPERATIONS.drop(schema, table));
+ } finally {
+ JdbcConnectorUtils.executeUpdate(admin, "DROP SCHEMA IF EXISTS " +
schema + " CASCADE");
+ JdbcConnectorUtils.executeUpdate(admin, "DROP ROLE IF EXISTS " + role);
+ }
+ }
+ }
+
@Test
void testLeastPrivilegeCreateDistinguishesMissingSchemaFromForbidden()
throws SQLException {
String role = RandomNameUtils.genRandomName("limited_role");