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)");
+  }
+}


Reply via email to