This is an automated email from the ASF dual-hosted git repository.

yuqi4733 pushed a commit to branch internal-main
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit b73baaee9b7d413dc3f774ec2daaf396c84752d1
Author: geyanggang <[email protected]>
AuthorDate: Fri Feb 6 17:01:49 2026 +0800

    [#128]feat (maxcompute-catalog): Add database operations for MaxCompute 
catalog.
---
 build.gradle.kts                                   |   1 +
 .../maxcompute/MaxComputeCatalogCapability.java    |  80 +++++++-
 .../converter/MaxComputeExceptionConverter.java    | 197 ++++++++++++++++++-
 .../operation/MaxComputeDatabaseOperations.java    | 216 ++++++++++++++++++++-
 .../TestMaxComputeExceptionConverter.java          | 188 ++++++++++++++++++
 .../TestMaxComputeDatabaseOperations.java          | 192 ++++++++++++++++++
 6 files changed, 868 insertions(+), 6 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index 22ca5904fa..9a8d599cd9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1053,6 +1053,7 @@ tasks {
       ":catalogs:catalog-hive:copyLibAndConfig",
       ":catalogs:catalog-jdbc-bigquery:copyLibAndConfig",
       ":catalogs:catalog-jdbc-doris:copyLibAndConfig",
+      ":catalogs:catalog-jdbc-maxcompute:copyLibAndConfig",
       ":catalogs:catalog-jdbc-mysql:copyLibAndConfig",
       ":catalogs:catalog-jdbc-oceanbase:copyLibAndConfig",
       ":catalogs:catalog-jdbc-postgresql:copyLibAndConfig",
diff --git 
a/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/MaxComputeCatalogCapability.java
 
b/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/MaxComputeCatalogCapability.java
index f4f0311dbb..5be86acae7 100644
--- 
a/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/MaxComputeCatalogCapability.java
+++ 
b/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/MaxComputeCatalogCapability.java
@@ -18,7 +18,83 @@
  */
 package org.apache.gravitino.catalog.maxcompute;
 
+import java.util.Set;
 import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
 
-/** Capability for MaxCompute catalog. */
-public class MaxComputeCatalogCapability implements Capability {}
+/**
+ * Capability for MaxCompute catalog.
+ *
+ * <p>MaxCompute has the following limitations:
+ *
+ * <ul>
+ *   <li>Schema/Table names are case-insensitive
+ *   <li>Column default values have limited support (only constant values, no 
functions like
+ *       GETDATE())
+ *   <li>Table names must start with a letter or underscore and can only 
contain letters, digits,
+ *       and underscores
+ *   <li>Maximum table name length is 128 bytes
+ * </ul>
+ */
+public class MaxComputeCatalogCapability implements Capability {
+
+  /**
+   * Regular expression for MaxCompute identifier names.
+   *
+   * <p>Rules:
+   *
+   * <ul>
+   *   <li>Must start with a letter (a-z, A-Z) or underscore (_)
+   *   <li>Can contain letters, digits (0-9), and underscores
+   *   <li>Maximum length is 128 characters
+   * </ul>
+   */
+  private static final String MAXCOMPUTE_NAME_PATTERN = 
"^[a-zA-Z_][a-zA-Z0-9_]{0,127}$";
+
+  /** Reserved schema names in MaxCompute that cannot be used for user-defined 
schemas. */
+  private static final Set<String> MAXCOMPUTE_RESERVED_WORDS = 
Set.of("information_schema");
+
+  @Override
+  public CapabilityResult specificationOnName(Scope scope, String name) {
+    // Check name pattern
+    if (!name.matches(MAXCOMPUTE_NAME_PATTERN)) {
+      return CapabilityResult.unsupported(
+          String.format(
+              "The %s name '%s' is illegal. MaxCompute names must start with a 
letter or underscore, "
+                  + "contain only letters, digits, and underscores, and be at 
most 128 characters long.",
+              scope, name));
+    }
+
+    // Check reserved words for schema
+    if (scope == Scope.SCHEMA && 
MAXCOMPUTE_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;
+  }
+
+  /**
+   * MaxCompute table/schema names are case-insensitive. Names are stored in 
lowercase internally.
+   *
+   * @param scope The scope of the capability.
+   * @return The capability result indicating case-insensitivity.
+   */
+  @Override
+  public CapabilityResult caseSensitiveOnName(Scope scope) {
+    return CapabilityResult.unsupported(
+        String.format("MaxCompute does not support case-sensitive %s names.", 
scope));
+  }
+
+  /**
+   * MaxCompute has limited support for column default values. Only constant 
values are supported,
+   * function-based defaults (like GETDATE()) are not supported.
+   *
+   * @return The capability result for column default value support.
+   */
+  @Override
+  public CapabilityResult columnDefaultValue() {
+    // MaxCompute supports constant default values but not function-based 
defaults
+    return CapabilityResult.SUPPORTED;
+  }
+}
diff --git 
a/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/converter/MaxComputeExceptionConverter.java
 
b/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/converter/MaxComputeExceptionConverter.java
index abc69e8de9..fe7f628d6f 100644
--- 
a/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/converter/MaxComputeExceptionConverter.java
+++ 
b/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/converter/MaxComputeExceptionConverter.java
@@ -18,16 +18,211 @@
  */
 package org.apache.gravitino.catalog.maxcompute.converter;
 
+import com.google.common.annotations.VisibleForTesting;
 import java.sql.SQLException;
+import java.util.Arrays;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
 import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
 
-/** Exception converter for MaxCompute. */
+/**
+ * Exception converter for MaxCompute.
+ *
+ * <p>MaxCompute uses ODPS error codes in the format "ODPS-XXXXXXX". This 
converter handles common
+ * error patterns including authentication failures, permission denied, and 
schema/table not found
+ * errors.
+ *
+ * <p>Common ODPS error code patterns:
+ *
+ * <ul>
+ *   <li>ODPS-0110011: Authorization exception (unauthorized)
+ *   <li>ODPS-0030001: Authorization exception (permission denied)
+ *   <li>ODPS-0420095: Access Denied
+ *   <li>Authentication failures: Various patterns in error messages
+ * </ul>
+ */
 public class MaxComputeExceptionConverter extends JdbcExceptionConverter {
 
+  // ODPS error code patterns
+  @VisibleForTesting static final String ODPS_AUTHORIZATION_EXCEPTION = 
"ODPS-0110011";
+
+  @VisibleForTesting static final String ODPS_PERMISSION_DENIED = 
"ODPS-0030001";
+
+  @VisibleForTesting static final String ODPS_ACCESS_DENIED = "ODPS-0420095";
+
+  // Error message patterns for authentication failures
+  @VisibleForTesting
+  static final String[] AUTHENTICATION_FAILURE_PATTERNS = {
+    "Access denied",
+    "authentication failed",
+    "Invalid AccessKeyId",
+    "invalid access key",
+    "Signature not match",
+    "signature does not match",
+    "InvalidAccessKeyId",
+    "SignatureDoesNotMatch"
+  };
+
+  // Error message patterns for permission denied
+  @VisibleForTesting
+  static final String[] PERMISSION_DENIED_PATTERNS = {
+    "Access Denied",
+    "no privilege",
+    "permission denied",
+    "not authorized",
+    "Authorization exception",
+    "You are unauthorized"
+  };
+
+  // Error message patterns for schema not found
+  @VisibleForTesting
+  static final String[] SCHEMA_NOT_FOUND_PATTERNS = {
+    "project not found",
+    "Project not found",
+    "schema not found",
+    "Schema not found",
+    "Unknown project",
+    "unknown project",
+    "does not exist"
+  };
+
+  // Error message patterns for table not found
+  @VisibleForTesting
+  static final String[] TABLE_NOT_FOUND_PATTERNS = {
+    "table not found", "Table not found", "Unknown table", "unknown table"
+  };
+
+  // Error message patterns for connection failures
+  @VisibleForTesting
+  static final String[] CONNECTION_FAILURE_PATTERNS = {
+    "Connection refused",
+    "Connection timed out",
+    "Unable to connect",
+    "Network is unreachable",
+    "No route to host",
+    "Connection reset",
+    "Socket timeout",
+    "Read timed out"
+  };
+
+  /**
+   * Converts a SQLException to a GravitinoRuntimeException.
+   *
+   * @param se the SQLException to convert
+   * @return the corresponding GravitinoRuntimeException
+   */
   @SuppressWarnings("FormatStringAnnotation")
   @Override
   public GravitinoRuntimeException toGravitinoException(SQLException se) {
+    String message = se.getMessage();
+
+    // Check for authentication failures first (most critical)
+    if (isAuthenticationFailure(message)) {
+      return new UnauthorizedException(se, se.getMessage());
+    }
+
+    // Check for permission denied errors
+    if (isPermissionDenied(message)) {
+      return new UnauthorizedException(se, se.getMessage());
+    }
+
+    // Check for connection failures
+    if (isConnectionFailure(message)) {
+      return new ConnectionFailedException(se, se.getMessage());
+    }
+
+    // Check for schema not found errors
+    if (isSchemaNotFound(message)) {
+      return new NoSuchSchemaException(se, se.getMessage());
+    }
+
+    // Check for table not found errors
+    if (isTableNotFound(message)) {
+      return new NoSuchTableException(se, se.getMessage());
+    }
+
+    // Default: return generic runtime exception
     return new GravitinoRuntimeException(se, se.getMessage());
   }
+
+  /**
+   * Checks if the error message indicates an authentication failure.
+   *
+   * @param message the error message
+   * @return true if the message indicates an authentication failure
+   */
+  @VisibleForTesting
+  static boolean isAuthenticationFailure(String message) {
+    if (StringUtils.isBlank(message)) {
+      return false;
+    }
+    return 
Arrays.stream(AUTHENTICATION_FAILURE_PATTERNS).anyMatch(message::contains);
+  }
+
+  /**
+   * Checks if the error message indicates a permission denied error.
+   *
+   * @param message the error message
+   * @return true if the message indicates a permission denied error
+   */
+  @VisibleForTesting
+  static boolean isPermissionDenied(String message) {
+    if (StringUtils.isBlank(message)) {
+      return false;
+    }
+    // Check for ODPS error codes
+    if (message.contains(ODPS_AUTHORIZATION_EXCEPTION)
+        || message.contains(ODPS_PERMISSION_DENIED)
+        || message.contains(ODPS_ACCESS_DENIED)) {
+      return true;
+    }
+    // Check for error message patterns
+    return 
Arrays.stream(PERMISSION_DENIED_PATTERNS).anyMatch(message::contains);
+  }
+
+  /**
+   * Checks if the error message indicates a schema not found error.
+   *
+   * @param message the error message
+   * @return true if the message indicates a schema not found error
+   */
+  @VisibleForTesting
+  static boolean isSchemaNotFound(String message) {
+    if (StringUtils.isBlank(message)) {
+      return false;
+    }
+    return 
Arrays.stream(SCHEMA_NOT_FOUND_PATTERNS).anyMatch(message::contains);
+  }
+
+  /**
+   * Checks if the error message indicates a table not found error.
+   *
+   * @param message the error message
+   * @return true if the message indicates a table not found error
+   */
+  @VisibleForTesting
+  static boolean isTableNotFound(String message) {
+    if (StringUtils.isBlank(message)) {
+      return false;
+    }
+    return Arrays.stream(TABLE_NOT_FOUND_PATTERNS).anyMatch(message::contains);
+  }
+
+  /**
+   * Checks if the error message indicates a connection failure.
+   *
+   * @param message the error message
+   * @return true if the message indicates a connection failure
+   */
+  @VisibleForTesting
+  static boolean isConnectionFailure(String message) {
+    if (StringUtils.isBlank(message)) {
+      return false;
+    }
+    return 
Arrays.stream(CONNECTION_FAILURE_PATTERNS).anyMatch(message::contains);
+  }
 }
diff --git 
a/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/operation/MaxComputeDatabaseOperations.java
 
b/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/operation/MaxComputeDatabaseOperations.java
index 86312413dd..12d4d54046 100644
--- 
a/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/operation/MaxComputeDatabaseOperations.java
+++ 
b/catalogs/catalog-jdbc-maxcompute/src/main/java/org/apache/gravitino/catalog/maxcompute/operation/MaxComputeDatabaseOperations.java
@@ -18,23 +18,205 @@
  */
 package org.apache.gravitino.catalog.maxcompute.operation;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.catalog.jdbc.JdbcSchema;
 import org.apache.gravitino.catalog.jdbc.operation.JdbcDatabaseOperations;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.meta.AuditInfo;
 
-/** Database operations for MaxCompute. */
+/**
+ * Database operations for MaxCompute.
+ *
+ * <p>MaxCompute has a three-layer structure: Project -> Schema -> Table. The 
Project is specified
+ * in the JDBC URL (jdbc:odps:{endpoint}?project={project_name}), and Schema 
is the namespace within
+ * a Project.
+ *
+ * <p>This implementation maps Gravitino Schema to MaxCompute Schema. Use 
"SHOW SCHEMAS" SQL command
+ * to list schemas within the current project.
+ *
+ * <p>Schema operations require enabling schema mode in MaxCompute. This class 
automatically enables
+ * schema mode by executing "set odps.namespace.schema=true" on each 
connection.
+ */
 public class MaxComputeDatabaseOperations extends JdbcDatabaseOperations {
 
+  private static final String BACK_QUOTE = "`";
+
+  /**
+   * Enables schema mode on the given connection.
+   *
+   * <p>MaxCompute has a three-layer structure: Project -> Schema -> Table. 
Schema mode must be
+   * enabled to use schema-level operations like CREATE SCHEMA, DROP SCHEMA, 
etc.
+   *
+   * @param connection the JDBC connection
+   * @throws SQLException if the setting fails
+   */
+  private static void enableSchemaMode(Connection connection) throws 
SQLException {
+    try (Statement statement = connection.createStatement()) {
+      statement.execute("set odps.namespace.schema=true");
+    }
+  }
+
+  /**
+   * Gets a connection with schema mode enabled.
+   *
+   * <p>MaxCompute requires schema mode to be enabled for schema operations. 
This method overrides
+   * the base implementation to enable schema mode on each connection.
+   *
+   * @return a JDBC connection with schema mode enabled
+   * @throws SQLException if connection fails or schema mode cannot be enabled
+   */
+  @Override
+  protected Connection getConnection() throws SQLException {
+    Connection connection = dataSource.getConnection();
+    try {
+      enableSchemaMode(connection);
+      return connection;
+    } catch (SQLException e) {
+      try {
+        connection.close();
+      } catch (SQLException closeException) {
+        // Log but don't mask the original exception
+        LOG.warn("Failed to close connection after enableSchemaMode failure", 
closeException);
+      }
+      throw e;
+    }
+  }
+
+  /**
+   * Lists all schemas in the current MaxCompute project.
+   *
+   * <p>Uses "SHOW SCHEMAS" SQL command to get the list of schemas within the 
current project. Each
+   * project has a default schema named "default" and can have additional 
custom schemas.
+   *
+   * <p>Note: DatabaseMetaData.getSchemas() returns Projects list, not Schemas 
within a Project, so
+   * we use SQL command instead.
+   *
+   * <p>The SHOW SCHEMAS command may return results in different formats 
depending on the JDBC
+   * driver version. This implementation handles both single-column and 
space-separated formats.
+   *
+   * @return a list of schema names
+   */
+  @Override
+  public List<String> listDatabases() {
+    List<String> schemaNames = new ArrayList<>();
+    try (Connection connection = getConnection();
+        Statement statement = connection.createStatement();
+        ResultSet resultSet = statement.executeQuery("SHOW SCHEMAS")) {
+      while (resultSet.next()) {
+        String result = resultSet.getString(1);
+        if (StringUtils.isNotBlank(result)) {
+          // Handle both formats: one schema per row or space-separated schemas
+          String[] schemas = result.trim().split("\\s+");
+          for (String schemaName : schemas) {
+            if (StringUtils.isNotBlank(schemaName) && 
!isSystemDatabase(schemaName)) {
+              schemaNames.add(schemaName);
+            }
+          }
+        }
+      }
+      return schemaNames;
+    } catch (SQLException se) {
+      throw this.exceptionMapper.toGravitinoException(se);
+    }
+  }
+
+  /**
+   * Loads a schema from MaxCompute.
+   *
+   * @param databaseName the name of the schema to load
+   * @return the JdbcSchema representing the schema
+   * @throws NoSuchSchemaException if the schema does not exist
+   */
+  @Override
+  public JdbcSchema load(String databaseName) throws NoSuchSchemaException {
+    List<String> allDatabases = listDatabases();
+    String dbName =
+        allDatabases.stream()
+            .filter(db -> db.equalsIgnoreCase(databaseName))
+            .findFirst()
+            .orElseThrow(
+                () -> new NoSuchSchemaException("Schema %s could not be 
found", databaseName));
+
+    // MaxCompute does not support schema comments or properties via JDBC
+    return JdbcSchema.builder()
+        .withName(dbName)
+        .withProperties(ImmutableMap.of())
+        .withAuditInfo(AuditInfo.EMPTY)
+        .build();
+  }
+
+  /**
+   * Generates the SQL statement to create a schema in MaxCompute.
+   *
+   * <p>MaxCompute syntax: CREATE SCHEMA `schema_name`
+   *
+   * <p>Note: MaxCompute does not support schema comments or properties in 
CREATE SCHEMA statement.
+   *
+   * @param databaseName the name of the schema to create
+   * @param comment the comment for the schema (not supported, will be ignored)
+   * @param properties the properties for the schema (not supported)
+   * @return the SQL statement to create the schema
+   */
   @Override
   public String generateCreateDatabaseSql(
       String databaseName, String comment, Map<String, String> properties) {
-    throw new UnsupportedOperationException("Not implemented yet");
+    if (MapUtils.isNotEmpty(properties)) {
+      throw new UnsupportedOperationException(
+          "MaxCompute does not support setting schema properties.");
+    }
+    String sql = String.format("CREATE SCHEMA %s", 
quoteDatabaseName(databaseName));
+    LOG.info("Generated create schema sql: {}", sql);
+    return sql;
   }
 
+  /**
+   * Generates the SQL statement to drop a schema in MaxCompute.
+   *
+   * <p>MaxCompute syntax: DROP SCHEMA `schema_name`
+   *
+   * <p>Note: MaxCompute does not support CASCADE option in DROP SCHEMA. The 
schema must be empty
+   * before dropping. When cascade is true, this method checks if the schema 
is empty and throws an
+   * exception if it contains tables.
+   *
+   * @param databaseName the name of the schema to drop
+   * @param cascade whether to check for non-empty schema (MaxCompute does not 
support CASCADE)
+   * @return the SQL statement to drop the schema
+   */
   @Override
   public String generateDropDatabaseSql(String databaseName, boolean cascade) {
-    throw new UnsupportedOperationException("Not implemented yet");
+    if (cascade) {
+      // Check if schema has tables before dropping
+      try (Connection connection = getConnection();
+          Statement statement = connection.createStatement()) {
+        String query = String.format("SHOW TABLES IN %s", 
quoteDatabaseName(databaseName));
+        try (ResultSet resultSet = statement.executeQuery(query)) {
+          if (resultSet.next()) {
+            throw new IllegalStateException(
+                String.format(
+                    "Schema %s is not empty. MaxCompute does not support 
CASCADE drop. "
+                        + "Please drop all tables in the schema first.",
+                    databaseName));
+          }
+        }
+      } catch (SQLException se) {
+        throw this.exceptionMapper.toGravitinoException(se);
+      }
+    }
+    String sql = String.format("DROP SCHEMA %s", 
quoteDatabaseName(databaseName));
+    LOG.info("Generated drop schema sql: {}", sql);
+    return sql;
   }
 
   @Override
@@ -46,4 +228,32 @@ public class MaxComputeDatabaseOperations extends 
JdbcDatabaseOperations {
   protected Set<String> createSysDatabaseNameSet() {
     return ImmutableSet.of("information_schema");
   }
+
+  /**
+   * Check whether it is a system database.
+   *
+   * @param dbName The name of the database.
+   * @return whether it is a system database.
+   */
+  @Override
+  protected boolean isSystemDatabase(String dbName) {
+    if (dbName == null) {
+      return false;
+    }
+    return 
createSysDatabaseNameSet().contains(dbName.toLowerCase(Locale.ROOT));
+  }
+
+  /**
+   * Quotes a database/schema name for use in SQL statements.
+   *
+   * <p>Escapes backticks in the name by doubling them, then wraps the name in 
backticks.
+   *
+   * @param databaseName the database name to quote
+   * @return the quoted database name
+   */
+  private String quoteDatabaseName(String databaseName) {
+    // Escape backticks by doubling them
+    String escaped = databaseName.replace(BACK_QUOTE, BACK_QUOTE + BACK_QUOTE);
+    return BACK_QUOTE + escaped + BACK_QUOTE;
+  }
 }
diff --git 
a/catalogs/catalog-jdbc-maxcompute/src/test/java/org/apache/gravitino/catalog/maxcompute/converter/TestMaxComputeExceptionConverter.java
 
b/catalogs/catalog-jdbc-maxcompute/src/test/java/org/apache/gravitino/catalog/maxcompute/converter/TestMaxComputeExceptionConverter.java
new file mode 100644
index 0000000000..65d293b3b9
--- /dev/null
+++ 
b/catalogs/catalog-jdbc-maxcompute/src/test/java/org/apache/gravitino/catalog/maxcompute/converter/TestMaxComputeExceptionConverter.java
@@ -0,0 +1,188 @@
+/*
+ * 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.maxcompute.converter;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.SQLException;
+import org.apache.gravitino.exceptions.ConnectionFailedException;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for MaxComputeExceptionConverter. */
+class TestMaxComputeExceptionConverter {
+
+  private MaxComputeExceptionConverter converter;
+
+  @BeforeEach
+  void setUp() {
+    converter = new MaxComputeExceptionConverter();
+  }
+
+  @Test
+  void testIsAuthenticationFailure() {
+    assertTrue(MaxComputeExceptionConverter.isAuthenticationFailure("Access 
denied for user"));
+    assertTrue(
+        MaxComputeExceptionConverter.isAuthenticationFailure("authentication 
failed for account"));
+    assertTrue(MaxComputeExceptionConverter.isAuthenticationFailure("Invalid 
AccessKeyId"));
+    assertTrue(MaxComputeExceptionConverter.isAuthenticationFailure("invalid 
access key provided"));
+    assertTrue(MaxComputeExceptionConverter.isAuthenticationFailure("Signature 
not match"));
+    assertTrue(
+        MaxComputeExceptionConverter.isAuthenticationFailure("signature does 
not match expected"));
+    
assertTrue(MaxComputeExceptionConverter.isAuthenticationFailure("InvalidAccessKeyId
 error"));
+    
assertTrue(MaxComputeExceptionConverter.isAuthenticationFailure("SignatureDoesNotMatch"));
+
+    assertFalse(MaxComputeExceptionConverter.isAuthenticationFailure(null));
+    assertFalse(MaxComputeExceptionConverter.isAuthenticationFailure(""));
+    assertFalse(MaxComputeExceptionConverter.isAuthenticationFailure("   "));
+    assertFalse(MaxComputeExceptionConverter.isAuthenticationFailure("Some 
other error"));
+  }
+
+  @Test
+  void testIsPermissionDenied() {
+    assertTrue(MaxComputeExceptionConverter.isPermissionDenied("Access Denied 
- no permission"));
+    assertTrue(MaxComputeExceptionConverter.isPermissionDenied("You have no 
privilege to access"));
+    assertTrue(MaxComputeExceptionConverter.isPermissionDenied("permission 
denied for operation"));
+    assertTrue(MaxComputeExceptionConverter.isPermissionDenied("User is not 
authorized"));
+    assertTrue(MaxComputeExceptionConverter.isPermissionDenied("Authorization 
exception occurred"));
+    assertTrue(MaxComputeExceptionConverter.isPermissionDenied("You are 
unauthorized to access"));
+
+    // Test ODPS error codes
+    assertTrue(
+        MaxComputeExceptionConverter.isPermissionDenied(
+            "ODPS-0110011: Authorization exception - You are unauthorized"));
+    assertTrue(
+        MaxComputeExceptionConverter.isPermissionDenied(
+            "ODPS-0030001: Authorization exception - permission denied"));
+    assertTrue(
+        MaxComputeExceptionConverter.isPermissionDenied(
+            "ODPS-0420095: Access Denied - You have no privilege"));
+
+    assertFalse(MaxComputeExceptionConverter.isPermissionDenied(null));
+    assertFalse(MaxComputeExceptionConverter.isPermissionDenied(""));
+    assertFalse(MaxComputeExceptionConverter.isPermissionDenied("   "));
+    assertFalse(MaxComputeExceptionConverter.isPermissionDenied("Some other 
error"));
+  }
+
+  @Test
+  void testIsSchemaNotFound() {
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("project not 
found: test_project"));
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("Project not 
found in region"));
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("schema not 
found: test_schema"));
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("Schema not found 
error"));
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("Unknown project: 
test"));
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("unknown project 
specified"));
+    assertTrue(MaxComputeExceptionConverter.isSchemaNotFound("The project does 
not exist"));
+
+    assertFalse(MaxComputeExceptionConverter.isSchemaNotFound(null));
+    assertFalse(MaxComputeExceptionConverter.isSchemaNotFound(""));
+    assertFalse(MaxComputeExceptionConverter.isSchemaNotFound("   "));
+    assertFalse(MaxComputeExceptionConverter.isSchemaNotFound("Some other 
error"));
+  }
+
+  @Test
+  void testIsTableNotFound() {
+    assertTrue(MaxComputeExceptionConverter.isTableNotFound("table not found: 
test_table"));
+    assertTrue(MaxComputeExceptionConverter.isTableNotFound("Table not found 
in schema"));
+    assertTrue(MaxComputeExceptionConverter.isTableNotFound("Unknown table: 
test"));
+    assertTrue(MaxComputeExceptionConverter.isTableNotFound("unknown table 
specified"));
+
+    assertFalse(MaxComputeExceptionConverter.isTableNotFound(null));
+    assertFalse(MaxComputeExceptionConverter.isTableNotFound(""));
+    assertFalse(MaxComputeExceptionConverter.isTableNotFound("   "));
+    assertFalse(MaxComputeExceptionConverter.isTableNotFound("Some other 
error"));
+  }
+
+  @Test
+  void testIsConnectionFailure() {
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Connection 
refused by server"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Connection 
timed out"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Unable to 
connect to host"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Network is 
unreachable"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("No route to 
host"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Connection 
reset by peer"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Socket 
timeout occurred"));
+    assertTrue(MaxComputeExceptionConverter.isConnectionFailure("Read timed 
out"));
+
+    assertFalse(MaxComputeExceptionConverter.isConnectionFailure(null));
+    assertFalse(MaxComputeExceptionConverter.isConnectionFailure(""));
+    assertFalse(MaxComputeExceptionConverter.isConnectionFailure("   "));
+    assertFalse(MaxComputeExceptionConverter.isConnectionFailure("Some other 
error"));
+  }
+
+  @Test
+  void testToGravitinoExceptionAuthenticationFailure() {
+    SQLException se = new SQLException("Access denied for user 'test'");
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(UnauthorizedException.class, result);
+  }
+
+  @Test
+  void testToGravitinoExceptionPermissionDenied() {
+    SQLException se =
+        new SQLException("ODPS-0110011: Authorization exception - You are 
unauthorized");
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(UnauthorizedException.class, result);
+  }
+
+  @Test
+  void testToGravitinoExceptionSchemaNotFound() {
+    SQLException se = new SQLException("Project not found: test_project");
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(NoSuchSchemaException.class, result);
+  }
+
+  @Test
+  void testToGravitinoExceptionTableNotFound() {
+    SQLException se = new SQLException("Table not found: test_table");
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(NoSuchTableException.class, result);
+  }
+
+  @Test
+  void testToGravitinoExceptionConnectionFailure() {
+    SQLException se = new SQLException("Connection refused by server");
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(ConnectionFailedException.class, result);
+  }
+
+  @Test
+  void testToGravitinoExceptionGenericError() {
+    SQLException se = new SQLException("Some generic error occurred");
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(GravitinoRuntimeException.class, result);
+    assertFalse(result instanceof ConnectionFailedException);
+    assertFalse(result instanceof UnauthorizedException);
+    assertFalse(result instanceof NoSuchSchemaException);
+    assertFalse(result instanceof NoSuchTableException);
+  }
+
+  @Test
+  void testToGravitinoExceptionNullMessage() {
+    SQLException se = new SQLException((String) null);
+    GravitinoRuntimeException result = converter.toGravitinoException(se);
+    assertInstanceOf(GravitinoRuntimeException.class, result);
+  }
+}
diff --git 
a/catalogs/catalog-jdbc-maxcompute/src/test/java/org/apache/gravitino/catalog/maxcompute/operation/TestMaxComputeDatabaseOperations.java
 
b/catalogs/catalog-jdbc-maxcompute/src/test/java/org/apache/gravitino/catalog/maxcompute/operation/TestMaxComputeDatabaseOperations.java
new file mode 100644
index 0000000000..67fd8501eb
--- /dev/null
+++ 
b/catalogs/catalog-jdbc-maxcompute/src/test/java/org/apache/gravitino/catalog/maxcompute/operation/TestMaxComputeDatabaseOperations.java
@@ -0,0 +1,192 @@
+/*
+ * 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.maxcompute.operation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.sql.DataSource;
+import org.apache.gravitino.catalog.jdbc.JdbcSchema;
+import 
org.apache.gravitino.catalog.maxcompute.converter.MaxComputeExceptionConverter;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for MaxComputeDatabaseOperations. */
+class TestMaxComputeDatabaseOperations {
+
+  private MaxComputeDatabaseOperations operations;
+  private DataSource mockDataSource;
+  private Connection mockConnection;
+  private ResultSet mockResultSet;
+
+  @BeforeEach
+  void setUp() throws SQLException {
+    operations = new MaxComputeDatabaseOperations();
+    mockDataSource = mock(DataSource.class);
+    mockConnection = mock(Connection.class);
+    mockResultSet = mock(ResultSet.class);
+
+    Map<String, String> conf = new HashMap<>();
+    operations.initialize(mockDataSource, new MaxComputeExceptionConverter(), 
conf);
+  }
+
+  @Test
+  void testCreate() throws SQLException {
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+    java.sql.Statement mockStatement = mock(java.sql.Statement.class);
+    when(mockConnection.createStatement()).thenReturn(mockStatement);
+    when(mockStatement.executeUpdate("CREATE SCHEMA 
test_schema")).thenReturn(0);
+
+    // Should not throw exception
+    Map<String, String> properties = new HashMap<>();
+    operations.create("test_schema", null, properties);
+  }
+
+  @Test
+  void testDelete() throws SQLException {
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+    java.sql.Statement mockStatement = mock(java.sql.Statement.class);
+    when(mockConnection.createStatement()).thenReturn(mockStatement);
+    when(mockStatement.executeUpdate("DROP SCHEMA test_schema")).thenReturn(0);
+
+    // Should not throw exception
+    boolean result = operations.delete("test_schema", false);
+    assertTrue(result);
+  }
+
+  @Test
+  void testGenerateCreateDatabaseSql() {
+    Map<String, String> properties = new HashMap<>();
+    String sql = operations.generateCreateDatabaseSql("test_schema", 
"comment", properties);
+    assertEquals("CREATE SCHEMA test_schema", sql);
+  }
+
+  @Test
+  void testGenerateCreateDatabaseSqlWithPropertiesThrowsException() {
+    Map<String, String> properties = new HashMap<>();
+    properties.put("some_property", "value");
+    assertThrows(
+        UnsupportedOperationException.class,
+        () -> operations.generateCreateDatabaseSql("test_schema", "comment", 
properties));
+  }
+
+  @Test
+  void testGenerateDropDatabaseSql() throws SQLException {
+    // Mock for cascade check - empty schema
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+
+    String sql = operations.generateDropDatabaseSql("test_schema", false);
+    assertEquals("DROP SCHEMA test_schema", sql);
+  }
+
+  @Test
+  void testSupportSchemaComment() {
+    assertFalse(operations.supportSchemaComment());
+  }
+
+  @Test
+  void testCreateSysDatabaseNameSet() {
+    
assertTrue(operations.createSysDatabaseNameSet().contains("information_schema"));
+    assertEquals(1, operations.createSysDatabaseNameSet().size());
+  }
+
+  @Test
+  void testIsSystemDatabase() {
+    assertTrue(operations.isSystemDatabase("information_schema"));
+    assertTrue(operations.isSystemDatabase("INFORMATION_SCHEMA"));
+    assertFalse(operations.isSystemDatabase("my_project"));
+    assertFalse(operations.isSystemDatabase(null));
+  }
+
+  @Test
+  void testListDatabases() throws SQLException {
+    java.sql.Statement mockStatement = mock(java.sql.Statement.class);
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+    when(mockConnection.createStatement()).thenReturn(mockStatement);
+    when(mockStatement.executeQuery("SHOW SCHEMAS")).thenReturn(mockResultSet);
+    // Simulate space-separated format: "default information_schema 
test_schema"
+    when(mockResultSet.next()).thenReturn(true, false);
+    when(mockResultSet.getString(1)).thenReturn("default information_schema 
test_schema");
+
+    List<String> databases = operations.listDatabases();
+
+    assertEquals(2, databases.size());
+    assertTrue(databases.contains("default"));
+    assertTrue(databases.contains("test_schema"));
+    assertFalse(databases.contains("information_schema"));
+  }
+
+  @Test
+  void testListDatabasesMultipleRows() throws SQLException {
+    java.sql.Statement mockStatement = mock(java.sql.Statement.class);
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+    when(mockConnection.createStatement()).thenReturn(mockStatement);
+    when(mockStatement.executeQuery("SHOW SCHEMAS")).thenReturn(mockResultSet);
+    // Simulate one schema per row format
+    when(mockResultSet.next()).thenReturn(true, true, true, false);
+    when(mockResultSet.getString(1)).thenReturn("default", 
"information_schema", "test_schema");
+
+    List<String> databases = operations.listDatabases();
+
+    assertEquals(2, databases.size());
+    assertTrue(databases.contains("default"));
+    assertTrue(databases.contains("test_schema"));
+    assertFalse(databases.contains("information_schema"));
+  }
+
+  @Test
+  void testLoad() throws SQLException {
+    java.sql.Statement mockStatement = mock(java.sql.Statement.class);
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+    when(mockConnection.createStatement()).thenReturn(mockStatement);
+    when(mockStatement.executeQuery("SHOW SCHEMAS")).thenReturn(mockResultSet);
+    when(mockResultSet.next()).thenReturn(true, false);
+    when(mockResultSet.getString(1)).thenReturn("test_schema");
+
+    JdbcSchema schema = operations.load("test_schema");
+
+    assertNotNull(schema);
+    assertEquals("test_schema", schema.name());
+    assertNotNull(schema.properties());
+    assertTrue(schema.properties().isEmpty());
+  }
+
+  @Test
+  void testLoadThrowsNoSuchSchemaException() throws SQLException {
+    java.sql.Statement mockStatement = mock(java.sql.Statement.class);
+    when(mockDataSource.getConnection()).thenReturn(mockConnection);
+    when(mockConnection.createStatement()).thenReturn(mockStatement);
+    when(mockStatement.executeQuery("SHOW SCHEMAS")).thenReturn(mockResultSet);
+    when(mockResultSet.next()).thenReturn(false);
+
+    assertThrows(NoSuchSchemaException.class, () -> 
operations.load("non_existent_schema"));
+  }
+}


Reply via email to