This is an automated email from the ASF dual-hosted git repository.
yuqi4733 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 4701d105ea [#7979] Improvement(catalogs): Add reserved words check for
MySQL and Postgres (#8002)
4701d105ea is described below
commit 4701d105ea703ba1db502b0f2b1f0f86456f3868
Author: Reuben George <[email protected]>
AuthorDate: Tue Aug 12 12:18:25 2025 +0530
[#7979] Improvement(catalogs): Add reserved words check for MySQL and
Postgres (#8002)
### What changes were proposed in this pull request?
- Added validation for reserved words in PostgreSQL catalog capability.
- Reserved words (like pg_catalog, information_schema) are now checked
for both schema and table scopes to prevent naming conflicts.
### Why are the changes needed?
Prevents users from creating schemas or tables with names that conflict
with system catalogs or schemas.
Fix: #7979
### Does this PR introduce _any_ user-facing change?
Users will receive an error if they attempt to use reserved names for
schemas or tables in PostgreSQL catalogs.
### How was this patch tested?
UTs and ITs
---
.../catalog/mysql/MysqlCatalogCapability.java | 12 +-
.../catalog/mysql/TestMysqlCatalogCapability.java | 206 +++++++++++++++++
.../integration/test/MysqlCatalogCapabilityIT.java | 201 +++++++++++++++++
.../postgresql/PostgreSqlCatalogCapability.java | 12 +-
.../TestPostgreSqlCatalogCapability.java | 248 +++++++++++++++++++++
.../test/PostgreSqlCatalogCapabilityIT.java | 240 ++++++++++++++++++++
6 files changed, 917 insertions(+), 2 deletions(-)
diff --git
a/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlCatalogCapability.java
b/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlCatalogCapability.java
index 61909fa0d4..968490369b 100644
---
a/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlCatalogCapability.java
+++
b/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlCatalogCapability.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.catalog.mysql;
+import java.util.Set;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
@@ -39,13 +40,22 @@ public class MysqlCatalogCapability implements Capability {
*/
public static final String MYSQL_NAME_PATTERN = "^[\\w\\p{L}-$/=]{1,64}$";
+ /** Reserved schema andtable names in MySQL that cannot be used for
user-defined schemas. */
+ private static final Set<String> MYSQL_RESERVED_SCHEMAS =
+ Set.of("mysql", "information_schema", "performance_schema", "sys");
+
@Override
public CapabilityResult specificationOnName(Scope scope, String name) {
- // TODO: Validate the name against reserved words
if (!name.matches(MYSQL_NAME_PATTERN)) {
return CapabilityResult.unsupported(
String.format("The %s name '%s' is illegal.", scope, name));
}
+
+ if (scope == Scope.SCHEMA &&
MYSQL_RESERVED_SCHEMAS.contains(name.toLowerCase())) {
+ return CapabilityResult.unsupported(
+ String.format("The %s name '%s' is reserved and cannot be used.",
scope, name));
+ }
+
return CapabilityResult.SUPPORTED;
}
}
diff --git
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/TestMysqlCatalogCapability.java
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/TestMysqlCatalogCapability.java
new file mode 100644
index 0000000000..f5ff95906a
--- /dev/null
+++
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/TestMysqlCatalogCapability.java
@@ -0,0 +1,206 @@
+/*
+ * 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.mysql;
+
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMysqlCatalogCapability {
+
+ private final MysqlCatalogCapability capability = new
MysqlCatalogCapability();
+
+ @Test
+ void testValidNames() {
+ // testing valid names for all scopes
+ for (Capability.Scope scope : Capability.Scope.values()) {
+ // normal alphanumeric names
+ CapabilityResult result = capability.specificationOnName(scope,
"test_table");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "TestTable123");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "table_with_underscores");
+ Assertions.assertTrue(result.supported());
+
+ // names with allowed special characters
+ result = capability.specificationOnName(scope, "table-with-hyphens");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "table$with$dollar");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "table/with/slash");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "table=with=equals");
+ Assertions.assertTrue(result.supported());
+
+ // names with unicode letters
+ result = capability.specificationOnName(scope, "测试表");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "tableção");
+ Assertions.assertTrue(result.supported());
+
+ // maximum length(64 chars)
+ String maxLengthName = "a".repeat(64);
+ result = capability.specificationOnName(scope, maxLengthName);
+ Assertions.assertTrue(result.supported());
+ }
+ }
+
+ @Test
+ void testInvalidNames() {
+ // Test invalid names for all scopes
+ for (Capability.Scope scope : Capability.Scope.values()) {
+ // empty name
+ CapabilityResult result = capability.specificationOnName(scope, "");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ // name exceeding maximum length (65 characters)
+ String tooLongName = "a".repeat(65);
+ result = capability.specificationOnName(scope, tooLongName);
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ // names with invalid characters
+ result = capability.specificationOnName(scope, "table with space");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table@with@at");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table#with#hash");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table%with%percent");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table.with.dot");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table(with)parentheses");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table[with]brackets");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table{with}braces");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+ }
+ }
+
+ @Test
+ void testReservedSchemaNames() {
+ // Test reserved schema names are rejected
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, "mysql");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
"information_schema");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
"performance_schema");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA, "sys");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ // case insensitive reserved names
+ result = capability.specificationOnName(Capability.Scope.SCHEMA, "MYSQL");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
"Information_Schema");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+ }
+
+ @Test
+ void testReservedNamesNotAppliedToOtherScopes() {
+ // Reserved names should not apply to column scope
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.COLUMN, "mysql");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.COLUMN,
"information_schema");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.COLUMN, "sys");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.COLUMN,
"performance_schema");
+ Assertions.assertTrue(result.supported());
+
+ // Reserved names should not apply to other scopes like FILESET, TOPIC,
etc.
+ result = capability.specificationOnName(Capability.Scope.FILESET, "mysql");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TOPIC,
"information_schema");
+ Assertions.assertTrue(result.supported());
+ }
+
+ @Test
+ void testBoundaryConditions() {
+ // minimum length (1 character)
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, "a");
+ Assertions.assertTrue(result.supported());
+
+ // exactly 64 characters (maximum allowed)
+ String exactMaxName = "a".repeat(64);
+ result = capability.specificationOnName(Capability.Scope.TABLE,
exactMaxName);
+ Assertions.assertTrue(result.supported());
+
+ // exactly 65 characters (exceeds maximum)
+ String exceedsMaxName = "a".repeat(65);
+ result = capability.specificationOnName(Capability.Scope.TABLE,
exceedsMaxName);
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+ }
+
+ @Test
+ void testMixedValidAndInvalidCharacters() {
+ // names that start valid but contain invalid characters
+ CapabilityResult result =
+ capability.specificationOnName(Capability.Scope.TABLE, "valid_start
invalid_space");
+ Assertions.assertFalse(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"valid_start@invalid");
+ Assertions.assertFalse(result.supported());
+
+ // names with mix of valid special characters
+ result =
+ capability.specificationOnName(Capability.Scope.TABLE,
"test_table-with$mixed/chars=ok");
+ Assertions.assertTrue(result.supported());
+ }
+}
diff --git
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/integration/test/MysqlCatalogCapabilityIT.java
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/integration/test/MysqlCatalogCapabilityIT.java
new file mode 100644
index 0000000000..088287fbd1
--- /dev/null
+++
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/integration/test/MysqlCatalogCapabilityIT.java
@@ -0,0 +1,201 @@
+/*
+ * 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.mysql.integration.test;
+
+import org.apache.gravitino.catalog.mysql.MysqlCatalogCapability;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("gravitino-docker-test")
+public class MysqlCatalogCapabilityIT {
+
+ private static MysqlCatalogCapability capability;
+
+ @BeforeAll
+ public static void setup() {
+ capability = new MysqlCatalogCapability();
+ }
+
+ @Test
+ void testMysqlNameValidationIntegration() {
+ // valid mysql schema names should be accepted
+ String[] validSchemaNames = {
+ "user_schema",
+ "test123",
+ "schema_with_underscores",
+ "schema-with-hyphens",
+ "schema$with$dollar",
+ "schema/with/slash",
+ "schema=with=equals",
+ "测试模式", // Unicode support
+ "a".repeat(64) // Maximum length
+ };
+
+ for (String name : validSchemaNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Schema name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+ }
+
+ // valid mysql table names should be accepted
+ String[] validTableNames = {
+ "user_table",
+ "test123",
+ "table_with_underscores",
+ "table-with-hyphens",
+ "table$with$dollar",
+ "table/with/slash",
+ "table=with=equals",
+ "测试表", // Unicode support
+ "b".repeat(64) // Maximum length
+ };
+
+ for (String name : validTableNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Table name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+ }
+ }
+
+ @Test
+ void testMysqlReservedWordsIntegration() {
+ // Test that MySQL reserved schema names are properly rejected
+ String[] reservedSchemaNames = {"mysql", "information_schema",
"performance_schema", "sys"};
+
+ for (String name : reservedSchemaNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertFalse(
+ result.supported(),
+ "Reserved schema name '" + name + "' should be rejected but was
accepted");
+ Assertions.assertTrue(
+ result.unsupportedMessage().contains("reserved"),
+ "Error message should mention 'reserved' for name: " + name);
+ }
+
+ // case insensitivity for schemas
+ String[] mixedCaseReserved = {"MYSQL", "Information_Schema",
"Performance_Schema", "SYS"};
+
+ for (String name : mixedCaseReserved) {
+ CapabilityResult schemaResult =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertFalse(
+ schemaResult.supported(),
+ "Reserved schema name '" + name + "' (mixed case) should be rejected
but was accepted");
+ }
+ }
+
+ @Test
+ void testMysqlInvalidNamesIntegration() {
+ // Test for checking that invalid MySQL names are properly rejected
+ String[] invalidNames = {
+ "", // empty
+ "name with spaces",
+ "name@with@at",
+ "name#with#hash",
+ "name%with%percent",
+ "name.with.dots",
+ "name(with)parentheses",
+ "name[with]brackets",
+ "name{with}braces",
+ "a".repeat(65) // exceeds maximim length
+ };
+
+ for (String name : invalidNames) {
+ CapabilityResult schemaResult =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertFalse(
+ schemaResult.supported(),
+ "Invalid schema name '" + name + "' should be rejected but was
accepted");
+ Assertions.assertTrue(
+ schemaResult.unsupportedMessage().contains("illegal"),
+ "Error message should mention 'illegal' for schema name: " + name);
+
+ CapabilityResult tableResult =
capability.specificationOnName(Capability.Scope.TABLE, name);
+ Assertions.assertFalse(
+ tableResult.supported(),
+ "Invalid table name '" + name + "' should be rejected but was
accepted");
+ Assertions.assertTrue(
+ tableResult.unsupportedMessage().contains("illegal"),
+ "Error message should mention 'illegal' for table name: " + name);
+ }
+ }
+
+ @Test
+ void testMysqlColumnNameValidation() {
+ String[] validColumnNames = {
+ "user_column",
+ "column123",
+ "column_with_underscores",
+ "mysql", // Should be valid for columns even though it's reserved for
schemas/tables
+ "information_schema", // Should be valid for columns
+ "测试列" // testing unicode support here
+ };
+
+ for (String name : validColumnNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.COLUMN, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Column name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+ }
+
+ // invalid column names should be rejected
+ String[] invalidColumnNames = {
+ "column with spaces",
+ "column@invalid",
+ "column#invalid",
+ "a".repeat(65) // Exceeds maximum length
+ };
+
+ for (String name : invalidColumnNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.COLUMN, name);
+ Assertions.assertFalse(
+ result.supported(),
+ "Invalid column name '" + name + "' should be rejected but was
accepted");
+ }
+ }
+
+ @Test
+ void testMysqlBoundaryConditions() {
+ // boundary conditions for name length
+ String exactMaxName = "a".repeat(64);
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, exactMaxName);
+ Assertions.assertTrue(result.supported(), "64-character name should be
valid");
+
+ String tooLongName = "a".repeat(65);
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
tooLongName);
+ Assertions.assertFalse(result.supported(), "65-character name should be
invalid");
+
+ // minimum length
+ result = capability.specificationOnName(Capability.Scope.SCHEMA, "a");
+ Assertions.assertTrue(result.supported(), "Single character name should be
valid");
+ }
+}
diff --git
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalogCapability.java
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalogCapability.java
index 7d1661ea87..3d671565c2 100644
---
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalogCapability.java
+++
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalogCapability.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.catalog.postgresql;
+import java.util.Set;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
@@ -35,13 +36,22 @@ public class PostgreSqlCatalogCapability implements
Capability {
*/
public static final String POSTGRESQL_NAME_PATTERN =
"^[_a-zA-Z\\p{L}/][\\w\\p{L}-$/=]{0,62}$";
+ /** Reserved schema and table names in PostgreSQL that cannot be used for
user-defined schemas. */
+ private static final Set<String> POSTGRESQL_RESERVED_WORDS =
+ Set.of("pg_catalog", "information_schema");
+
@Override
public CapabilityResult specificationOnName(Scope scope, String name) {
- // TODO: Validate the name against reserved words
if (!name.matches(POSTGRESQL_NAME_PATTERN)) {
return CapabilityResult.unsupported(
String.format("The %s name '%s' is illegal.", scope, name));
}
+
+ if (scope == Scope.SCHEMA &&
POSTGRESQL_RESERVED_WORDS.contains(name.toLowerCase())) {
+ return CapabilityResult.unsupported(
+ String.format("The %s name '%s' is reserved and cannot be used.",
scope, name));
+ }
+
return CapabilityResult.SUPPORTED;
}
}
diff --git
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/TestPostgreSqlCatalogCapability.java
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/TestPostgreSqlCatalogCapability.java
new file mode 100644
index 0000000000..f0acb184da
--- /dev/null
+++
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/TestPostgreSqlCatalogCapability.java
@@ -0,0 +1,248 @@
+/*
+ * 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;
+
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestPostgreSqlCatalogCapability {
+
+ private final PostgreSqlCatalogCapability capability = new
PostgreSqlCatalogCapability();
+
+ @Test
+ void testValidNames() {
+ // testing valid names for all scopes
+ for (Capability.Scope scope : Capability.Scope.values()) {
+ // normal alphanumeric names should work
+ CapabilityResult result = capability.specificationOnName(scope,
"test_table");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "TestTable123");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "table_with_underscores");
+ Assertions.assertTrue(result.supported());
+
+ // names starting with underscore are allowed
+ result = capability.specificationOnName(scope, "_test_table");
+ Assertions.assertTrue(result.supported());
+
+ // names with unicode letters should be supported
+ result = capability.specificationOnName(scope, "测试表");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(scope, "tableção");
+ Assertions.assertTrue(result.supported());
+
+ // maximum length (63 characters for PostgreSQL)
+ String maxLengthName = "a".repeat(63);
+ result = capability.specificationOnName(scope, maxLengthName);
+ Assertions.assertTrue(result.supported());
+ }
+ }
+
+ @Test
+ void testInvalidNames() {
+ // Test invalid names for all scopes
+ for (Capability.Scope scope : Capability.Scope.values()) {
+ // empty name should be rejected
+ CapabilityResult result = capability.specificationOnName(scope, "");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ // name exceeding maximum length (64 characters)
+ String tooLongName = "a".repeat(64);
+ result = capability.specificationOnName(scope, tooLongName);
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ // names starting with digits should be rejected
+ result = capability.specificationOnName(scope, "123table");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ // names with invalid characters should be rejected
+ result = capability.specificationOnName(scope, "table with space");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table@with@at");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table#with#hash");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table%with%percent");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+
+ result = capability.specificationOnName(scope, "table.with.dot");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+ }
+ }
+
+ @Test
+ void testValidSpecialCharacters() {
+ for (Capability.Scope scope : Capability.Scope.values()) {
+ // hyphens should be allowed (as per the regex pattern)
+ CapabilityResult result = capability.specificationOnName(scope,
"table-with-hyphen");
+ Assertions.assertTrue(result.supported());
+
+ // dollar signs should be allowed
+ result = capability.specificationOnName(scope, "table$with$dollar");
+ Assertions.assertTrue(result.supported());
+
+ // slashes should be allowed
+ result = capability.specificationOnName(scope, "table/with/slash");
+ Assertions.assertTrue(result.supported());
+
+ // equals signs should be allowed
+ result = capability.specificationOnName(scope, "table=with=equals");
+ Assertions.assertTrue(result.supported());
+ }
+ }
+
+ @Test
+ void testReservedSchemaNames() {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, "pg_catalog");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
"information_schema");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ // case insensitive reserved names should be rejected
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
"PG_CATALOG");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA,
"Information_Schema");
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("reserved"));
+ }
+
+ @Test
+ void testReservedNamesNotAppliedToOtherScopes() {
+ // Reserved names should not apply to column scope
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.COLUMN, "pg_catalog");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.COLUMN,
"information_schema");
+ Assertions.assertTrue(result.supported());
+
+ // Reserved names should not apply to other scopes like FILESET, TOPIC,
etc.
+ result = capability.specificationOnName(Capability.Scope.FILESET,
"pg_catalog");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TOPIC,
"information_schema");
+ Assertions.assertTrue(result.supported());
+ }
+
+ @Test
+ void testBoundaryConditions() {
+ // minimum length (1 character) should work
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, "a");
+ Assertions.assertTrue(result.supported());
+
+ // exactly 63 characters (maximum allowed for PostgreSQL)
+ String exactMaxName = "a".repeat(63);
+ result = capability.specificationOnName(Capability.Scope.TABLE,
exactMaxName);
+ Assertions.assertTrue(result.supported());
+
+ // exactly 64 characters (exceeds maximum)
+ String exceedsMaxName = "a".repeat(64);
+ result = capability.specificationOnName(Capability.Scope.TABLE,
exceedsMaxName);
+ Assertions.assertFalse(result.supported());
+ Assertions.assertTrue(result.unsupportedMessage().contains("illegal"));
+ }
+
+ @Test
+ void testMixedValidAndInvalidCharacters() {
+ // names that start valid but contain invalid characters should be rejected
+ CapabilityResult result =
+ capability.specificationOnName(Capability.Scope.TABLE, "valid_start
invalid_space");
+ Assertions.assertFalse(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"valid_start@invalid");
+ Assertions.assertFalse(result.supported());
+
+ // names with only valid characters should be accepted
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"valid_table_name_123");
+ Assertions.assertTrue(result.supported());
+ }
+
+ @Test
+ void testUnicodeSupport() {
+ // Test various Unicode letters as starting characters
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, "αβγ_table");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"测试_table");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"أحمد_table");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"ñoño_table");
+ Assertions.assertTrue(result.supported());
+
+ // Test Unicode letters in the end
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"_table_测试");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"table_αβγ");
+ Assertions.assertTrue(result.supported());
+ }
+
+ @Test
+ void testValidStartingCharacterCombinations() {
+ // Test all valid starting character types
+ CapabilityResult result =
+ capability.specificationOnName(Capability.Scope.TABLE,
"_underscore_start");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"a_letter_start");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"Z_letter_start");
+ Assertions.assertTrue(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"/slash_start");
+ Assertions.assertTrue(result.supported());
+
+ // invalid starting characters
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"1_number_start");
+ Assertions.assertFalse(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"-hyphen_start");
+ Assertions.assertFalse(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"$dollar_start");
+ Assertions.assertFalse(result.supported());
+
+ result = capability.specificationOnName(Capability.Scope.TABLE,
"=equals_start");
+ Assertions.assertFalse(result.supported());
+ }
+}
diff --git
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/PostgreSqlCatalogCapabilityIT.java
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/PostgreSqlCatalogCapabilityIT.java
new file mode 100644
index 0000000000..a481bf69cc
--- /dev/null
+++
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/integration/test/PostgreSqlCatalogCapabilityIT.java
@@ -0,0 +1,240 @@
+/*
+ * 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.integration.test;
+
+import org.apache.gravitino.catalog.postgresql.PostgreSqlCatalogCapability;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("gravitino-docker-test")
+public class PostgreSqlCatalogCapabilityIT {
+
+ private static PostgreSqlCatalogCapability capability;
+
+ @BeforeAll
+ public static void setup() {
+ capability = new PostgreSqlCatalogCapability();
+ }
+
+ @Test
+ void testPostgreSqlNameValidationIntegration() {
+ // valid PostgreSQL schema names should be accepted
+ String[] validSchemaNames = {
+ "_user_schema",
+ "test123",
+ "schema_with_underscores",
+ "user_schema",
+ "测试模式", // Unicode support
+ "a".repeat(63) // Maximum length for PostgreSQL
+ };
+
+ for (String name : validSchemaNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Schema name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+ }
+
+ // valid PostgreSQL table names should be accepted
+ String[] validTableNames = {
+ "_user_table",
+ "test123",
+ "table_with_underscores",
+ "user_table",
+ "测试表", // Unicode support
+ "b".repeat(63) // Maximum length for PostgreSQL
+ };
+
+ for (String name : validTableNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Table name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+ }
+ }
+
+ @Test
+ void testPostgreSqlReservedWordsIntegration() {
+ String[] reservedSchemaNames = {"pg_catalog", "information_schema"};
+
+ for (String name : reservedSchemaNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertFalse(
+ result.supported(),
+ "Reserved schema name '" + name + "' should be rejected but was
accepted");
+ Assertions.assertTrue(
+ result.unsupportedMessage().contains("reserved"),
+ "Error message should mention 'reserved' for name: " + name);
+ }
+ }
+
+ @Test
+ void testPostgreSqlInvalidNamesIntegration() {
+ // Test that invalid PostgreSQL names are properly rejected
+ String[] invalidNames = {
+ "", // empty name
+ "a".repeat(64), // exceeds maximum length
+ "123table", // starts with digit
+ "table with space", // contains space
+ "table@with@at", // contains @ symbol
+ "table#with#hash", // contains # symbol
+ "table%with%percent", // contains % symbol
+ "table.with.dot" // contains dot
+ };
+
+ for (String name : invalidNames) {
+ for (Capability.Scope scope :
+ new Capability.Scope[] {Capability.Scope.SCHEMA,
Capability.Scope.TABLE}) {
+ CapabilityResult result = capability.specificationOnName(scope, name);
+ Assertions.assertFalse(
+ result.supported(),
+ "Invalid " + scope + " name '" + name + "' should be rejected but
was accepted");
+ Assertions.assertTrue(
+ result.unsupportedMessage().contains("illegal"),
+ "Error message should mention 'illegal' for name: " + name);
+ }
+ }
+ }
+
+ @Test
+ void testPostgreSqlValidSpecialCharactersIntegration() {
+ String[] validNamesWithSpecialChars = {
+ "table-with-hyphen", // hyphens should be allowed
+ "table$with$dollar", // dollar signs should be allowed
+ "table/with/slash", // slashes should be allowed
+ "table=with=equals" // equals signs should be allowed
+ };
+
+ for (String name : validNamesWithSpecialChars) {
+ CapabilityResult schemaResult =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertTrue(
+ schemaResult.supported(),
+ "Schema name with special characters '"
+ + name
+ + "' should be valid but was rejected: "
+ + schemaResult.unsupportedMessage());
+
+ CapabilityResult tableResult =
capability.specificationOnName(Capability.Scope.TABLE, name);
+ Assertions.assertTrue(
+ tableResult.supported(),
+ "Table name with special characters '"
+ + name
+ + "' should be valid but was rejected: "
+ + tableResult.unsupportedMessage());
+ }
+ }
+
+ @Test
+ void testPostgreSqlCaseInsensitiveReservedWords() {
+ // Test that reserved words are rejected regardless of case
+ String[] caseVariants = {
+ "PG_CATALOG", "Pg_Catalog", "pg_CATALOG", "INFORMATION_SCHEMA",
"Information_Schema"
+ };
+
+ for (String name : caseVariants) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertFalse(
+ result.supported(),
+ "Case variant reserved schema name '" + name + "' should be rejected
but was accepted");
+ Assertions.assertTrue(
+ result.unsupportedMessage().contains("reserved"),
+ "Error message should mention 'reserved' for case variant: " + name);
+ }
+ }
+
+ @Test
+ void testPostgreSqlReservedWordsNotAppliedToColumns() {
+ // Reserved schema/table names should be allowed as column names
+ String[] reservedNames = {"pg_catalog", "information_schema"};
+
+ for (String name : reservedNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.COLUMN, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Reserved name '"
+ + name
+ + "' should be allowed as column name but was rejected: "
+ + result.unsupportedMessage());
+ }
+ }
+
+ @Test
+ void testPostgreSqlUnicodeNamesIntegration() {
+ // Test that various Unicode characters work properly in names
+ String[] unicodeNames = {
+ "测试表_integration", // Chinese characters
+ "αβγ_table", // Greek letters
+ "café_table", // Latin with accents
+ "أحمد_table", // Arabic characters
+ "тест_table" // Cyrillic characters
+ };
+
+ for (String name : unicodeNames) {
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Unicode table name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+
+ result = capability.specificationOnName(Capability.Scope.SCHEMA, name);
+ Assertions.assertTrue(
+ result.supported(),
+ "Unicode schema name '"
+ + name
+ + "' should be valid but was rejected: "
+ + result.unsupportedMessage());
+ }
+ }
+
+ @Test
+ void testPostgreSqlBoundaryConditions() {
+ // Test boundary conditions for name length
+
+ // Single character names should work
+ CapabilityResult result =
capability.specificationOnName(Capability.Scope.TABLE, "a");
+ Assertions.assertTrue(result.supported(), "Single character name should be
valid");
+
+ result = capability.specificationOnName(Capability.Scope.TABLE, "_");
+ Assertions.assertTrue(result.supported(), "Single underscore name should
be valid");
+
+ // Exactly 63 characters (PostgreSQL maximum)
+ String maxLengthName = "a".repeat(63);
+ result = capability.specificationOnName(Capability.Scope.TABLE,
maxLengthName);
+ Assertions.assertTrue(
+ result.supported(), "63-character name should be valid (PostgreSQL
maximum)");
+
+ // Exactly 64 characters (exceeds PostgreSQL maximum)
+ String tooLongName = "a".repeat(64);
+ result = capability.specificationOnName(Capability.Scope.TABLE,
tooLongName);
+ Assertions.assertFalse(
+ result.supported(), "64-character name should be rejected (exceeds
PostgreSQL maximum)");
+ }
+}