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

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 26cd31495ed Refactor MCP database error and namespace handling (#39141)
26cd31495ed is described below

commit 26cd31495edabefd41b7b47365374ace2e61fda3
Author: Liang Zhang <[email protected]>
AuthorDate: Thu Jul 16 04:34:30 2026 +0800

    Refactor MCP database error and namespace handling (#39141)
    
    * Refactor MCP database error and namespace handling
    
    - centralize JDBC error classification with dialect SPI
    - derive database and schema boundaries from parsed metadata
    
    * Refactor MCP database error and namespace handling
    
    - centralize JDBC error classification with dialect SPI
    - derive database and schema boundaries from parsed metadata
---
 .../mcp/core/protocol/error/MCPErrorConverter.java |  21 ++-
 .../error/MCPQueryRecoveryPayloadFactory.java      |  76 +++--------
 .../handler/execute/MCPJdbcStatementExecutor.java  |  35 +++--
 .../handler/execute/MCPSQLExecutionFacade.java     |  66 ++--------
 .../execute/RuleDistSQLExecutionException.java     |   4 +-
 .../execute/SQLStatementObjectExtractor.java       |  40 ++++--
 .../handler/execute/SQLStatementObjectName.java    |  22 +++-
 .../core/workflow/WorkflowProxyQueryService.java   |  17 ++-
 .../core/protocol/error/MCPErrorConverterTest.java |   9 ++
 .../execute/MCPJdbcStatementExecutorTest.java      |  43 +++++--
 .../handler/execute/MCPSQLExecutionFacadeTest.java |   6 +-
 .../handler/execute/MCPStatementAnalyzerTest.java  |  14 ++
 .../workflow/WorkflowProxyQueryServiceTest.java    |  15 +++
 .../exception/MCPDatabaseQueryFailedException.java |  39 ++++++
 .../exception/MCPDatabaseSQLSyntaxException.java}  |  22 +---
 .../database/exception/MCPJDBCErrorCategory.java   |  33 +++++
 .../exception/MCPJDBCExceptionClassifier.java      | 141 +++++++++++++++++++++
 .../MySQLMCPDialectSQLExceptionClassifier.java     |  83 ++++++++++++
 .../jdbc/RuntimeDatabaseConnectionException.java   |  28 ++--
 .../spi/MCPDialectSQLExceptionClassifier.java      |  43 +++++++
 .../service/WorkflowDistSQLQueryUtils.java         |  22 +---
 ...t.database.spi.MCPDialectSQLExceptionClassifier |  18 +++
 .../MCPDatabaseQueryFailedExceptionTest.java       |  38 ++++++
 .../MCPDatabaseSQLSyntaxExceptionTest.java         |  37 ++++++
 .../exception/MCPJDBCExceptionClassifierTest.java  | 107 ++++++++++++++++
 .../MySQLMCPDialectSQLExceptionClassifierTest.java |  68 ++++++++++
 .../RuntimeDatabaseConnectionExceptionTest.java    |   6 +
 .../service/WorkflowDistSQLQueryUtilsTest.java     |   9 +-
 test/e2e/mcp/pom.xml                               |   5 +
 .../baseline-contract/model-contract/guidance.yaml |   2 +-
 30 files changed, 838 insertions(+), 231 deletions(-)

diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
index 5bca4525642..40372ede8f2 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
@@ -30,12 +30,11 @@ import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPToolCallLimitExc
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedResourceUriException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
 import 
org.apache.shardingsphere.mcp.core.tool.handler.execute.ExplainSQLSyntaxException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
 
 import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.sql.SQLSyntaxErrorException;
-import java.sql.SQLTimeoutException;
 import java.util.List;
 import java.util.Objects;
 
@@ -58,11 +57,7 @@ public final class MCPErrorConverter {
             new ErrorMapping(MCPToolCallLimitExceededException.class, "MCP 
tool call limit exceeded."),
             new ErrorMapping(MCPUnavailableException.class, "Service is 
temporarily unavailable."),
             new ErrorMapping(RuntimeDatabaseConnectionException.class, 
"Runtime database connection failed."),
-            new ErrorMapping(SQLSyntaxErrorException.class, "Invalid 
request."),
-            new ErrorMapping(SQLTimeoutException.class, "MCP operation 
timeout."),
-            new ErrorMapping(SQLFeatureNotSupportedException.class, 
"Unsupported MCP operation."),
             new ErrorMapping(UnsupportedOperationException.class, "Unsupported 
MCP operation."),
-            new ErrorMapping(SQLException.class, "MCP query failed."),
             new ErrorMapping(IllegalArgumentException.class, "Invalid 
request."),
             new ErrorMapping(IllegalStateException.class, "MCP operation 
failed."));
     
@@ -73,6 +68,9 @@ public final class MCPErrorConverter {
      * @return MCP error
      */
     public static MCPErrorPayload convert(final Throwable cause) {
+        if (cause instanceof SQLException) {
+            return createError(cause, 
getJDBCErrorMessage(MCPJDBCExceptionClassifier.classify(cause)));
+        }
         for (ErrorMapping each : ERROR_MAPPINGS) {
             if (each.matches(cause)) {
                 return createError(cause, each.defaultMessage());
@@ -81,6 +79,15 @@ public final class MCPErrorConverter {
         return createError(cause, "Service is temporarily unavailable.");
     }
     
+    private static String getJDBCErrorMessage(final MCPJDBCErrorCategory 
category) {
+        return switch (category) {
+            case SYNTAX -> "Invalid request.";
+            case TIMEOUT -> "MCP operation timeout.";
+            case FEATURE_NOT_SUPPORTED -> "Unsupported MCP operation.";
+            default -> "MCP query failed.";
+        };
+    }
+    
     private static MCPErrorPayload createError(final Throwable cause, final 
String defaultMessage) {
         String causeMessage = Objects.toString(cause.getMessage(), "").trim();
         String message = MCPQueryRecoveryPayloadFactory.isQueryFailure(cause) 
|| causeMessage.isEmpty() ? defaultMessage : causeMessage;
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
index 001e4151937..a4b9e4026ae 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
@@ -24,19 +24,15 @@ import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedExcept
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPTimeoutException;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedException;
 import 
org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import org.apache.shardingsphere.mcp.support.diagnostic.MCPDiagnosticCategory;
 import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
 import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
 
 import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.sql.SQLNonTransientConnectionException;
-import java.sql.SQLSyntaxErrorException;
-import java.sql.SQLTimeoutException;
-import java.sql.SQLTransientConnectionException;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 
 /**
  * MCP query recovery payload factory.
@@ -51,10 +47,10 @@ final class MCPQueryRecoveryPayloadFactory {
         if (cause instanceof MCPQueryFailedException || cause instanceof 
SQLException) {
             return true;
         }
-        Optional<SQLException> sqlException = findSQLException(cause);
-        return cause instanceof MCPTimeoutException && 
sqlException.filter(SQLTimeoutException.class::isInstance).isPresent()
-                || cause instanceof MCPUnsupportedException && 
sqlException.filter(SQLFeatureNotSupportedException.class::isInstance).isPresent()
-                || cause instanceof MCPInvalidRequestException && 
sqlException.filter(SQLSyntaxErrorException.class::isInstance).isPresent();
+        MCPJDBCErrorCategory category = 
MCPJDBCExceptionClassifier.classify(cause);
+        return cause instanceof MCPTimeoutException && 
MCPJDBCErrorCategory.TIMEOUT == category
+                || cause instanceof MCPUnsupportedException && 
MCPJDBCErrorCategory.FEATURE_NOT_SUPPORTED == category
+                || cause instanceof MCPInvalidRequestException && 
MCPJDBCErrorCategory.SYNTAX == category;
     }
     
     static Map<String, Object> create(final Throwable cause) {
@@ -69,57 +65,15 @@ final class MCPQueryRecoveryPayloadFactory {
     }
     
     private static String classify(final Throwable cause) {
-        Optional<SQLException> sqlException = findSQLException(cause);
-        if 
(sqlException.filter(SQLTimeoutException.class::isInstance).isPresent()) {
-            return MCPDiagnosticCategory.EXECUTION_TIMEOUT;
-        }
-        if 
(sqlException.filter(SQLFeatureNotSupportedException.class::isInstance).isPresent())
 {
-            return MCPDiagnosticCategory.UNSUPPORTED_DATABASE_CAPABILITY;
-        }
-        if (sqlException.filter(each -> each instanceof 
SQLTransientConnectionException || each instanceof 
SQLNonTransientConnectionException).isPresent()) {
-            return MCPDiagnosticCategory.CONNECTION_INTERRUPTED;
-        }
-        if 
(sqlException.map(SQLException::getSQLState).filter(MCPQueryRecoveryPayloadFactory::isConnectionInterruptedSQLState).isPresent())
 {
-            return MCPDiagnosticCategory.CONNECTION_INTERRUPTED;
-        }
-        if 
(sqlException.map(SQLException::getSQLState).filter(MCPQueryRecoveryPayloadFactory::isInsufficientPrivilegesSQLState).isPresent())
 {
-            return MCPDiagnosticCategory.INSUFFICIENT_PRIVILEGES;
-        }
-        if 
(sqlException.map(SQLException::getSQLState).filter(MCPQueryRecoveryPayloadFactory::isObjectNotVisibleSQLState).isPresent())
 {
-            return MCPDiagnosticCategory.OBJECT_NOT_VISIBLE;
-        }
-        if (cause instanceof SQLSyntaxErrorException || 
sqlException.filter(SQLSyntaxErrorException.class::isInstance).isPresent()
-                || 
sqlException.map(SQLException::getSQLState).filter(MCPQueryRecoveryPayloadFactory::isSyntaxErrorSQLState).isPresent())
 {
-            return MCPDiagnosticCategory.SQL_SYNTAX_ERROR;
-        }
-        return MCPDiagnosticCategory.QUERY_FAILED;
-    }
-    
-    private static Optional<SQLException> findSQLException(final Throwable 
cause) {
-        Throwable current = cause;
-        while (null != current) {
-            if (current instanceof SQLException) {
-                return Optional.of((SQLException) current);
-            }
-            current = current.getCause();
-        }
-        return Optional.empty();
-    }
-    
-    private static boolean isConnectionInterruptedSQLState(final String 
sqlState) {
-        return null != sqlState && sqlState.startsWith("08");
-    }
-    
-    private static boolean isInsufficientPrivilegesSQLState(final String 
sqlState) {
-        return null != sqlState && ("42501".equals(sqlState) || 
sqlState.startsWith("28"));
-    }
-    
-    private static boolean isObjectNotVisibleSQLState(final String sqlState) {
-        return null != sqlState && List.of("3F000", "42P01", "42703", "42704", 
"42S02", "42S22").contains(sqlState);
-    }
-    
-    private static boolean isSyntaxErrorSQLState(final String sqlState) {
-        return null != sqlState && sqlState.startsWith("42");
+        return switch (MCPJDBCExceptionClassifier.classify(cause)) {
+            case TIMEOUT -> MCPDiagnosticCategory.EXECUTION_TIMEOUT;
+            case FEATURE_NOT_SUPPORTED -> 
MCPDiagnosticCategory.UNSUPPORTED_DATABASE_CAPABILITY;
+            case CONNECTION -> MCPDiagnosticCategory.CONNECTION_INTERRUPTED;
+            case AUTHENTICATION, AUTHORIZATION -> 
MCPDiagnosticCategory.INSUFFICIENT_PRIVILEGES;
+            case OBJECT_NOT_VISIBLE -> 
MCPDiagnosticCategory.OBJECT_NOT_VISIBLE;
+            case SYNTAX -> MCPDiagnosticCategory.SQL_SYNTAX_ERROR;
+            default -> MCPDiagnosticCategory.QUERY_FAILED;
+        };
     }
     
     private static String createModelAction(final String category) {
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
index 447808d88f2..dd820964a8c 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
@@ -22,6 +22,10 @@ import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapa
 import 
org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
 import 
org.apache.shardingsphere.mcp.support.database.exception.QueryDidNotReturnResultSetException;
 import 
org.apache.shardingsphere.mcp.support.database.exception.StatementClassNotSupportedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseSQLSyntaxException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
 import 
org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
 import 
org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionColumnDefinition;
@@ -37,9 +41,6 @@ import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.sql.SQLSyntaxErrorException;
-import java.sql.SQLTimeoutException;
 import java.sql.Statement;
 import java.util.LinkedList;
 import java.util.List;
@@ -79,17 +80,25 @@ public final class MCPJdbcStatementExecutor {
                 return 
executeWithBorrowedConnection(transactionConnection.get(), executionRequest, 
classificationResult, databaseCapability);
             }
             return 
executeWithOwnedConnection(openOwnedConnection(executionRequest.getDatabase()), 
executionRequest, classificationResult, databaseCapability);
-        } catch (final SQLTimeoutException ex) {
-            throw new MCPTimeoutException(ex.getMessage(), ex);
-        } catch (final SQLFeatureNotSupportedException ex) {
-            throw new MCPUnsupportedException(ex.getMessage(), ex);
-        } catch (final SQLSyntaxErrorException ex) {
-            if (classificationResult.isRuleDistSQL()) {
-                throw new 
RuleDistSQLExecutionException(executionRequest.getDatabase(), 
classificationResult, ex);
-            }
-            throw new MCPInvalidRequestException(ex.getMessage(), ex);
         } catch (final SQLException ex) {
-            throw new MCPQueryFailedException(ex.getMessage(), ex);
+            throw createExecutionException(executionRequest, 
classificationResult, databaseCapability, ex);
+        }
+    }
+    
+    private RuntimeException createExecutionException(final 
SQLExecutionRequest executionRequest, final ClassificationResult 
classificationResult,
+                                                      final 
MCPDatabaseCapability databaseCapability, final SQLException cause) {
+        MCPJDBCErrorCategory category = 
MCPJDBCExceptionClassifier.classify(databaseCapability.getDatabaseType(), 
cause);
+        switch (category) {
+            case TIMEOUT:
+                return new MCPTimeoutException(cause.getMessage(), cause);
+            case FEATURE_NOT_SUPPORTED:
+                return new MCPUnsupportedException(cause.getMessage(), cause);
+            case SYNTAX:
+                return classificationResult.isRuleDistSQL()
+                        ? new 
RuleDistSQLExecutionException(executionRequest.getDatabase(), 
classificationResult, cause)
+                        : new MCPDatabaseSQLSyntaxException(cause);
+            default:
+                return new MCPDatabaseQueryFailedException(category, cause);
         }
     }
     
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacade.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacade.java
index b279b01c4c6..928010e1f38 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacade.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacade.java
@@ -34,14 +34,13 @@ import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapa
 import 
org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
 import 
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
 import 
org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import 
org.apache.shardingsphere.mcp.support.database.exception.StatementClassNotSupportedException;
 import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFacade;
 import 
org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
 import 
org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
 
-import java.sql.SQLException;
-import java.sql.SQLSyntaxErrorException;
-import java.util.Locale;
 import java.util.Optional;
 
 /**
@@ -60,15 +59,13 @@ public final class MCPSQLExecutionFacade implements 
MCPFeatureExecutionFacade {
     
     private final MCPStatementAnalyzer statementAnalyzer;
     
-    private final SQLStatementScanner scanner;
-    
     private final SQLExecutionTraceFactory sqlExecutionTraceFactory;
     
     public MCPSQLExecutionFacade(final MCPDatabaseCapabilityProvider 
databaseCapabilityProvider, final MCPSessionManager sessionManager) {
         this(databaseCapabilityProvider, new 
MCPSessionExecutionCoordinator(sessionManager),
                 new MCPJdbcTransactionStatementExecutor(sessionManager),
                 new 
MCPJdbcStatementExecutor(sessionManager.getTransactionResourceManager().getRuntimeDatabases(),
 sessionManager.getTransactionResourceManager()),
-                new MCPStatementAnalyzer(), new SQLStatementScanner(), new 
SQLExecutionTraceFactory());
+                new MCPStatementAnalyzer(), new SQLExecutionTraceFactory());
     }
     
     @Override
@@ -98,29 +95,13 @@ public final class MCPSQLExecutionFacade implements 
MCPFeatureExecutionFacade {
         try {
             return execute(executionRequest, classificationResult, 
databaseCapability);
         } catch (final MCPInvalidRequestException | MCPQueryFailedException 
ex) {
-            if (hasSQLSyntaxCause(ex)) {
+            if (MCPJDBCErrorCategory.SYNTAX == 
MCPJDBCExceptionClassifier.classify(databaseCapability.getDatabaseType(), ex)) {
                 throw new 
ExplainSQLSyntaxException(executionRequest.getDatabase(), 
executionRequest.getSchema(), sql, executionRequest.getSql(), ex);
             }
             throw ex;
         }
     }
     
-    private boolean hasSQLSyntaxCause(final Throwable cause) {
-        Throwable current = cause;
-        while (null != current) {
-            if (current instanceof SQLException && 
isSQLSyntaxError((SQLException) current)) {
-                return true;
-            }
-            current = current.getCause();
-        }
-        return false;
-    }
-    
-    private boolean isSQLSyntaxError(final SQLException cause) {
-        String sqlState = cause.getSQLState();
-        return cause instanceof SQLSyntaxErrorException || 
"37000".equals(sqlState) || "42000".equals(sqlState) || 
"42601".equals(sqlState);
-    }
-    
     private ClassificationResult classify(final SQLExecutionRequest 
executionRequest, final MCPDatabaseCapability databaseCapability) {
         try {
             return statementAnalyzer.analyze(executionRequest.getSql(), 
databaseCapability);
@@ -175,47 +156,16 @@ public final class MCPSQLExecutionFacade implements 
MCPFeatureExecutionFacade {
         }
         IdentifierCasePolicy identifierCasePolicy = 
databaseCapability.getIdentifierCasePolicySet().getPolicy(IdentifierScope.SCHEMA);
         for (SQLStatementObjectName each : 
classificationResult.getReferencedObjects()) {
-            if (isCrossSchemaReference(each, executionRequest.getDatabase(), 
classificationResult, identifierCasePolicy)) {
+            if (isCrossSchemaReference(each, executionRequest.getDatabase(), 
identifierCasePolicy)) {
                 throw recordFailure(executionRequest, 
classificationResult.getTraceStatementMarker(), new MCPInvalidRequestException(
                         String.format("Cross-schema SQL is not supported for 
database `%s`: `%s`.", executionRequest.getDatabase(), each.getObjectName())));
             }
         }
     }
     
-    private boolean isCrossSchemaReference(final SQLStatementObjectName 
objectName, final String databaseName, final ClassificationResult 
classificationResult,
-                                           final IdentifierCasePolicy 
identifierCasePolicy) {
-        if (objectName.isQualified()) {
-            return !identifierCasePolicy.matches(databaseName, 
objectName.getFirstIdentifier(), objectName.getFirstIdentifierQuoteCharacter());
-        }
-        return isDatabaseOrSchemaBoundaryReference(objectName, databaseName, 
classificationResult, identifierCasePolicy);
-    }
-    
-    private boolean isDatabaseOrSchemaBoundaryReference(final 
SQLStatementObjectName objectName, final String databaseName, final 
ClassificationResult classificationResult,
-                                                        final 
IdentifierCasePolicy identifierCasePolicy) {
-        if (identifierCasePolicy.matches(databaseName, 
objectName.getFirstIdentifier(), 
objectName.getFirstIdentifierQuoteCharacter())) {
-            return false;
-        }
-        String actualSql = classificationResult.getNormalizedSql();
-        String upperSql = 
actualSql.substring(scanner.skipInsignificant(actualSql, 
0)).toUpperCase(Locale.ENGLISH);
-        if (SupportedMCPStatement.DDL == 
classificationResult.getStatementClass()) {
-            return isDatabaseOrSchemaStatement(upperSql, 
classificationResult.getStatementType()) || containsSetSchemaClause(upperSql);
-        }
-        return SupportedMCPStatement.DCL == 
classificationResult.getStatementClass() && 
containsOnDatabaseOrSchemaClause(upperSql);
-    }
-    
-    private boolean isDatabaseOrSchemaStatement(final String upperSql, final 
String statementType) {
-        if (!"CREATE".equals(statementType) && !"ALTER".equals(statementType) 
&& !"DROP".equals(statementType)) {
-            return false;
-        }
-        return upperSql.startsWith(statementType + " DATABASE ") || 
upperSql.startsWith(statementType + " SCHEMA ");
-    }
-    
-    private boolean containsSetSchemaClause(final String upperSql) {
-        return upperSql.matches(".*\\bSET\\s+SCHEMA\\b.*");
-    }
-    
-    private boolean containsOnDatabaseOrSchemaClause(final String upperSql) {
-        return upperSql.matches(".*\\bON\\s+(DATABASE|SCHEMA)\\b.*");
+    private boolean isCrossSchemaReference(final SQLStatementObjectName 
objectName, final String databaseName, final IdentifierCasePolicy 
identifierCasePolicy) {
+        return (objectName.isQualified() || objectName.isNamespaceTarget())
+                && !identifierCasePolicy.matches(databaseName, 
objectName.getFirstIdentifier(), objectName.getFirstIdentifierQuoteCharacter());
     }
     
     private <T extends RuntimeException> T recordFailure(final 
SQLExecutionRequest executionRequest, final String statementMarker, final T ex) 
{
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
index dc2976637da..6a173a19ee1 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
@@ -20,7 +20,7 @@ package 
org.apache.shardingsphere.mcp.core.tool.handler.execute;
 import lombok.Getter;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
 
-import java.sql.SQLSyntaxErrorException;
+import java.sql.SQLException;
 
 /**
  * Exception for rule DistSQL execution failures that need workflow-aware 
recovery.
@@ -34,7 +34,7 @@ public final class RuleDistSQLExecutionException extends 
MCPInvalidRequestExcept
     
     private final ClassificationResult classificationResult;
     
-    public RuleDistSQLExecutionException(final String database, final 
ClassificationResult classificationResult, final SQLSyntaxErrorException cause) 
{
+    public RuleDistSQLExecutionException(final String database, final 
ClassificationResult classificationResult, final SQLException cause) {
         super(String.format("Rule DistSQL execution failed for database `%s`; 
check MCP runtime capability and workflow guidance before asking for corrected 
SQL.", database), cause);
         this.database = database;
         this.classificationResult = classificationResult;
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectExtractor.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectExtractor.java
index 5d159a84cb5..3b86896019f 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectExtractor.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectExtractor.java
@@ -93,13 +93,18 @@ final class SQLStatementObjectExtractor {
         }
         for (int index = 0; index < tokens.size(); index++) {
             if (scanner.isKeyword(tokens.get(index), "ON")) {
+                int objectTypeIndex = index + 1;
                 int objectStartIndex = skipDCLObjectType(tokens, index + 1);
-                addObjectName(tokens, objectStartIndex, 
findObjectNameEnd(tokens, objectStartIndex), result);
+                addObjectName(tokens, objectStartIndex, 
findObjectNameEnd(tokens, objectStartIndex), isNamespaceObjectType(tokens, 
objectTypeIndex), result);
                 return;
             }
         }
     }
     
+    private boolean isNamespaceObjectType(final List<SQLStatementToken> 
tokens, final int index) {
+        return index < tokens.size() && (scanner.isKeyword(tokens.get(index), 
"DATABASE") || scanner.isKeyword(tokens.get(index), "SCHEMA"));
+    }
+    
     private int skipDCLObjectType(final List<SQLStatementToken> tokens, final 
int startIndex) {
         return startIndex < tokens.size() && 
DCL_OBJECT_TYPE_KEYWORDS.contains(tokens.get(startIndex).upperText()) ? 
startIndex + 1 : startIndex;
     }
@@ -109,7 +114,7 @@ final class SQLStatementObjectExtractor {
         while (index < tokens.size()) {
             int objectNameEnd = findObjectNameEnd(tokens, index);
             if (objectNameEnd - index > 1 && objectNameEnd < tokens.size() && 
"(".equals(tokens.get(objectNameEnd).text())) {
-                addObjectName(tokens, index, objectNameEnd, result);
+                addObjectName(tokens, index, objectNameEnd, false, result);
                 index = objectNameEnd + 1;
             } else {
                 index++;
@@ -132,7 +137,8 @@ final class SQLStatementObjectExtractor {
         return token.identifier() || "*".equals(token.text());
     }
     
-    private void addObjectName(final List<SQLStatementToken> tokens, final int 
startIndex, final int stopIndex, final Collection<SQLStatementObjectName> 
result) {
+    private void addObjectName(final List<SQLStatementToken> tokens, final int 
startIndex, final int stopIndex, final boolean namespaceTarget,
+                               final Collection<SQLStatementObjectName> 
result) {
         if (startIndex >= stopIndex) {
             return;
         }
@@ -140,7 +146,7 @@ final class SQLStatementObjectExtractor {
         for (int index = startIndex; index < stopIndex; index += 2) {
             identifiers.add(new IdentifierValue(tokens.get(index).text()));
         }
-        result.add(SQLStatementObjectName.from(identifiers));
+        result.add(namespaceTarget ? 
SQLStatementObjectName.fromNamespace(identifiers) : 
SQLStatementObjectName.from(identifiers));
     }
     
     private void extractDirectTargets(final SQLStatement sqlStatement, final 
Collection<SQLStatementObjectName> result) {
@@ -188,24 +194,24 @@ final class SQLStatementObjectExtractor {
     
     private void extractDatabaseTargets(final SQLStatement sqlStatement, final 
Collection<SQLStatementObjectName> result) {
         if (sqlStatement instanceof CreateDatabaseStatement) {
-            addName(((CreateDatabaseStatement) 
sqlStatement).getDatabaseName(), result);
+            addNamespaceName(((CreateDatabaseStatement) 
sqlStatement).getDatabaseName(), result);
         } else if (sqlStatement instanceof AlterDatabaseStatement) {
-            addName(((AlterDatabaseStatement) sqlStatement).getDatabaseName(), 
result);
-            ((AlterDatabaseStatement) 
sqlStatement).getRenameDatabaseName().ifPresent(name -> addName(name, result));
+            addNamespaceName(((AlterDatabaseStatement) 
sqlStatement).getDatabaseName(), result);
+            ((AlterDatabaseStatement) 
sqlStatement).getRenameDatabaseName().ifPresent(name -> addNamespaceName(name, 
result));
         } else if (sqlStatement instanceof DropDatabaseStatement) {
-            addName(((DropDatabaseStatement) sqlStatement).getDatabaseName(), 
result);
+            addNamespaceName(((DropDatabaseStatement) 
sqlStatement).getDatabaseName(), result);
         }
     }
     
     private void extractSchemaTargets(final SQLStatement sqlStatement, final 
Collection<SQLStatementObjectName> result) {
         if (sqlStatement instanceof CreateSchemaStatement) {
-            ((CreateSchemaStatement) 
sqlStatement).getSchemaName().ifPresent(schema -> addIdentifier(schema, 
result));
+            ((CreateSchemaStatement) 
sqlStatement).getSchemaName().ifPresent(schema -> 
addNamespaceIdentifier(schema, result));
         } else if (sqlStatement instanceof AlterSchemaStatement) {
-            addIdentifier(((AlterSchemaStatement) 
sqlStatement).getSchemaName(), result);
-            ((AlterSchemaStatement) 
sqlStatement).getRenameSchema().ifPresent(schema -> addIdentifier(schema, 
result));
+            addNamespaceIdentifier(((AlterSchemaStatement) 
sqlStatement).getSchemaName(), result);
+            ((AlterSchemaStatement) 
sqlStatement).getRenameSchema().ifPresent(schema -> 
addNamespaceIdentifier(schema, result));
         } else if (sqlStatement instanceof DropSchemaStatement) {
             for (IdentifierValue each : ((DropSchemaStatement) 
sqlStatement).getSchemaNames()) {
-                addIdentifier(each, result);
+                addNamespaceIdentifier(each, result);
             }
         }
     }
@@ -319,9 +325,15 @@ final class SQLStatementObjectExtractor {
         }
     }
     
-    private void addIdentifier(final IdentifierValue identifier, final 
Collection<SQLStatementObjectName> result) {
+    private void addNamespaceName(final String name, final 
Collection<SQLStatementObjectName> result) {
+        if (null != name && !name.isEmpty()) {
+            
result.add(SQLStatementObjectName.fromNormalizedNamespaceName(name));
+        }
+    }
+    
+    private void addNamespaceIdentifier(final IdentifierValue identifier, 
final Collection<SQLStatementObjectName> result) {
         if (null != identifier) {
-            result.add(SQLStatementObjectName.from(Optional.empty(), 
identifier));
+            result.add(SQLStatementObjectName.fromNamespace(identifier));
         }
     }
 }
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectName.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectName.java
index 29c28a2bf27..8b5b900c47e 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectName.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementObjectName.java
@@ -44,10 +44,16 @@ final class SQLStatementObjectName {
     
     private final boolean qualified;
     
+    private final boolean namespaceTarget;
+    
     static SQLStatementObjectName fromNormalizedName(final String objectName) {
         int qualifierSeparatorIndex = objectName.indexOf('.');
         return new SQLStatementObjectName(objectName, -1 == 
qualifierSeparatorIndex ? objectName : objectName.substring(0, 
qualifierSeparatorIndex),
-                QuoteCharacter.NONE, -1 != qualifierSeparatorIndex);
+                QuoteCharacter.NONE, -1 != qualifierSeparatorIndex, false);
+    }
+    
+    static SQLStatementObjectName fromNormalizedNamespaceName(final String 
objectName) {
+        return new SQLStatementObjectName(objectName, objectName, 
QuoteCharacter.NONE, false, true);
     }
     
     static SQLStatementObjectName from(final Optional<OwnerSegment> owner, 
final IdentifierValue identifier) {
@@ -58,9 +64,21 @@ final class SQLStatementObjectName {
     }
     
     static SQLStatementObjectName from(final List<IdentifierValue> 
identifiers) {
+        return from(identifiers, false);
+    }
+    
+    private static SQLStatementObjectName from(final List<IdentifierValue> 
identifiers, final boolean namespaceTarget) {
         IdentifierValue firstIdentifier = identifiers.get(0);
         return new 
SQLStatementObjectName(identifiers.stream().map(IdentifierValue::getValue).collect(Collectors.joining(".")),
-                firstIdentifier.getValue(), 
firstIdentifier.getQuoteCharacter(), 1 < identifiers.size());
+                firstIdentifier.getValue(), 
firstIdentifier.getQuoteCharacter(), 1 < identifiers.size(), namespaceTarget);
+    }
+    
+    static SQLStatementObjectName fromNamespace(final List<IdentifierValue> 
identifiers) {
+        return from(identifiers, true);
+    }
+    
+    static SQLStatementObjectName fromNamespace(final IdentifierValue 
identifier) {
+        return new SQLStatementObjectName(identifier.getValue(), 
identifier.getValue(), identifier.getQuoteCharacter(), false, true);
     }
     
     private static void addOwnerIdentifiers(final OwnerSegment owner, final 
Collection<IdentifierValue> result) {
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryService.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryService.java
index eb302737a04..4f5d6ab42bc 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryService.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryService.java
@@ -19,12 +19,14 @@ package org.apache.shardingsphere.mcp.core.workflow;
 
 import lombok.RequiredArgsConstructor;
 import 
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierScope;
-import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnavailableException;
 import org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
 import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
 import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
 import 
org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
 import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
 import org.apache.shardingsphere.mcp.support.workflow.service.WorkflowSQLUtils;
@@ -39,6 +41,7 @@ import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Direct query service for Proxy-backed workflow reads.
@@ -61,7 +64,7 @@ public final class WorkflowProxyQueryService implements 
MCPFeatureQueryFacade {
                 ResultSet resultSet = executeQuery(connection, statement, 
schemaName, sql)) {
             return extractRows(resultSet);
         } catch (final SQLException ex) {
-            throw new MCPQueryFailedException(ex.getMessage(), ex);
+            throw createQueryFailedException(actualDatabaseName, ex);
         }
     }
     
@@ -101,10 +104,18 @@ public final class WorkflowProxyQueryService implements 
MCPFeatureQueryFacade {
             return formatColumnDefinition(resultSetMetaData.getColumnType(1), 
resultSetMetaData.getColumnTypeName(1),
                     resultSetMetaData.getPrecision(1), 
resultSetMetaData.getScale(1));
         } catch (final SQLException ex) {
-            throw new MCPQueryFailedException(ex.getMessage(), ex);
+            throw new 
MCPDatabaseQueryFailedException(MCPJDBCExceptionClassifier.classify(databaseType,
 ex), ex);
         }
     }
     
+    private MCPDatabaseQueryFailedException createQueryFailedException(final 
String databaseName, final SQLException cause) {
+        Optional<MCPDatabaseCapability> databaseCapability = 
databaseCapabilityProvider.provide(databaseName);
+        MCPJDBCErrorCategory category = databaseCapability.isPresent()
+                ? 
MCPJDBCExceptionClassifier.classify(databaseCapability.get().getDatabaseType(), 
cause)
+                : MCPJDBCExceptionClassifier.classify(cause);
+        return new MCPDatabaseQueryFailedException(category, cause);
+    }
+    
     private Connection openConnection(final String databaseName) throws 
SQLException {
         RuntimeDatabaseConfiguration runtimeDatabaseConfig = 
sessionManager.getTransactionResourceManager().getRuntimeDatabases().get(databaseName);
         if (null == runtimeDatabaseConfig) {
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverterTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverterTest.java
index 5f6fe3a52bb..16dd8519d52 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverterTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverterTest.java
@@ -44,6 +44,9 @@ import 
org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecut
 import 
org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
 import 
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
 import 
org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseSQLSyntaxException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -522,9 +525,15 @@ class MCPErrorConverterTest {
     static Stream<Arguments> assertConvertQueryFailureWithRecoveryCases() {
         return Stream.of(
                 Arguments.of("sql syntax", new SQLSyntaxErrorException("Bad 
SQL.", "42601"), "Invalid request.", "sql_syntax_error", "ask_user"),
+                Arguments.of("classified SQL syntax", new 
MCPDatabaseSQLSyntaxException(new SQLException("Bad SQL.", "42000", 1064)),
+                        "Invalid request.", "sql_syntax_error", "ask_user"),
                 Arguments.of("object not visible", new 
MCPQueryFailedException("Query failed.", new SQLException("Missing table.", 
"42P01")), "MCP query failed.", "object_not_visible",
                         "resource_read"),
                 Arguments.of("insufficient privileges", new 
SQLException("Permission denied.", "42501"), "MCP query failed.", 
"insufficient_privileges", "ask_user"),
+                Arguments.of("classified insufficient privileges", new 
MCPDatabaseQueryFailedException(
+                        MCPJDBCErrorCategory.AUTHORIZATION, new 
SQLSyntaxErrorException("Permission denied.", "42000", 1044)),
+                        "MCP query failed.", "insufficient_privileges", 
"ask_user"),
+                Arguments.of("ambiguous class 42", new SQLException("Query 
failed.", "42000"), "MCP query failed.", "query_failed", "resource_read"),
                 Arguments.of("execution timeout", new 
MCPTimeoutException("Timed out.", new SQLTimeoutException("Timed out.")), "MCP 
operation timeout.", "execution_timeout",
                         "resource_read"),
                 Arguments.of("connection interrupted", new 
MCPQueryFailedException("Query failed.", new 
SQLTransientConnectionException("Connection lost.", "08006")), "MCP query 
failed.",
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
index c0f8eab8869..b5dac4e2f08 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
@@ -22,12 +22,15 @@ import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapa
 import 
org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
 import 
org.apache.shardingsphere.mcp.support.database.exception.QueryDidNotReturnResultSetException;
 import 
org.apache.shardingsphere.mcp.support.database.exception.StatementClassNotSupportedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseSQLSyntaxException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
 import 
org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
 import 
org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionColumnDefinition;
 import 
org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
 import 
org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResultKind;
-import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.ShardingSphereMCPException;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPTimeoutException;
@@ -285,7 +288,7 @@ class MCPJdbcStatementExecutorTest {
     
     @Test
     void assertExecuteRuleDistSQLSyntaxError() throws SQLException {
-        SQLSyntaxErrorException cause = new SQLSyntaxErrorException("syntax 
error");
+        SQLException cause = new SQLException("syntax error", "42601");
         Statement statement = mock(Statement.class);
         when(statement.execute(anyString())).thenThrow(cause);
         MCPJdbcTransactionResourceManager transactionResourceManager = 
mock(MCPJdbcTransactionResourceManager.class);
@@ -308,8 +311,8 @@ class MCPJdbcStatementExecutorTest {
     
     @ParameterizedTest(name = "{0}")
     @MethodSource("assertExecuteWithSQLExceptionCases")
-    void assertExecuteWithSQLException(final String name, final SQLException 
sqlException, final Class<? extends ShardingSphereMCPException> 
expectedExceptionClass,
-                                       final String expectedMessage) throws 
SQLException {
+    void assertExecuteWithSQLException(final String name, final String 
databaseType, final SQLException sqlException,
+                                       final Class<? extends 
ShardingSphereMCPException> expectedExceptionClass, final MCPJDBCErrorCategory 
expectedCategory) throws SQLException {
         Statement statement = mock(Statement.class);
         when(statement.execute(anyString())).thenThrow(sqlException);
         doThrow(new SQLException("statement close 
failed")).when(statement).close();
@@ -319,13 +322,16 @@ class MCPJdbcStatementExecutorTest {
         RuntimeDatabaseConfiguration databaseConfig = 
mock(RuntimeDatabaseConfiguration.class);
         
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
         MCPJdbcStatementExecutor statementExecutor = new 
MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), 
transactionResourceManager);
+        MCPDatabaseCapability databaseCapability = 
createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE);
+        when(databaseCapability.getDatabaseType()).thenReturn(databaseType);
         ShardingSphereMCPException actual = 
assertThrows(expectedExceptionClass, () -> statementExecutor.execute(new 
SQLExecutionRequest("session-1",
                 "logic_db", "public", "SELECT status FROM orders", 10, 1000),
                 new ClassificationResult(SupportedMCPStatement.QUERY, 
"SELECT", "SELECT status FROM orders", "", List.of(), false),
-                
createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE)));
+                databaseCapability));
         assertThat(actual.getClass(), is(expectedExceptionClass));
-        assertThat(actual.getMessage(), is(expectedMessage));
+        assertThat(actual.getMessage(), is(sqlException.getMessage()));
         assertThat(actual.getCause(), is(sqlException));
+        assertThat(MCPJDBCExceptionClassifier.classify(databaseType, actual), 
is(expectedCategory));
     }
     
     @Test
@@ -421,6 +427,7 @@ class MCPJdbcStatementExecutorTest {
     private static MCPDatabaseCapability createDatabaseCapability(final 
SchemaExecutionSemantics schemaExecutionSemantics) {
         MCPDatabaseCapability result = mock(MCPDatabaseCapability.class);
         
when(result.getSchemaExecutionSemantics()).thenReturn(schemaExecutionSemantics);
+        when(result.getDatabaseType()).thenReturn("PostgreSQL");
         return result;
     }
     
@@ -482,13 +489,23 @@ class MCPJdbcStatementExecutorTest {
     
     private static Stream<Arguments> assertExecuteWithSQLExceptionCases() {
         return Stream.of(
-                Arguments.of("timeout", new SQLTimeoutException("timeout"), 
MCPTimeoutException.class, "timeout"),
-                Arguments.of("unsupported feature", new 
SQLFeatureNotSupportedException("unsupported feature"), 
MCPUnsupportedException.class, "unsupported feature"),
-                Arguments.of("syntax error", new 
SQLSyntaxErrorException("syntax error"), MCPInvalidRequestException.class, 
"syntax error"),
-                Arguments.of("object not visible", new SQLException("missing 
table", "42P01"), MCPQueryFailedException.class, "missing table"),
-                Arguments.of("insufficient privileges", new 
SQLException("permission denied", "42501"), MCPQueryFailedException.class, 
"permission denied"),
-                Arguments.of("connection interrupted", new 
SQLTransientConnectionException("connection lost", "08006"), 
MCPQueryFailedException.class, "connection lost"),
-                Arguments.of("query failed", new SQLException("query failed"), 
MCPQueryFailedException.class, "query failed"));
+                Arguments.of("timeout", "PostgreSQL", new 
SQLTimeoutException("timeout"), MCPTimeoutException.class, 
MCPJDBCErrorCategory.TIMEOUT),
+                Arguments.of("unsupported feature", "PostgreSQL", new 
SQLFeatureNotSupportedException("unsupported feature"), 
MCPUnsupportedException.class,
+                        MCPJDBCErrorCategory.FEATURE_NOT_SUPPORTED),
+                Arguments.of("typed syntax error", "PostgreSQL", new 
SQLSyntaxErrorException("syntax error"), MCPDatabaseSQLSyntaxException.class, 
MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("SQLState syntax error", "PostgreSQL", new 
SQLException("syntax error", "42601"), MCPDatabaseSQLSyntaxException.class, 
MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("MySQL syntax error", "MySQL", new 
SQLException("syntax error", "42000", 1064), 
MCPDatabaseSQLSyntaxException.class, MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("object not visible", "PostgreSQL", new 
SQLException("missing table", "42P01"), MCPDatabaseQueryFailedException.class,
+                        MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE),
+                Arguments.of("insufficient privileges", "PostgreSQL", new 
SQLException("permission denied", "42501"), 
MCPDatabaseQueryFailedException.class,
+                        MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("MySQL insufficient privileges", "MySQL", new 
SQLException("permission denied", "42000", 1044), 
MCPDatabaseQueryFailedException.class,
+                        MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("connection interrupted", "PostgreSQL", new 
SQLTransientConnectionException("connection lost", "08006"), 
MCPDatabaseQueryFailedException.class,
+                        MCPJDBCErrorCategory.CONNECTION),
+                Arguments.of("ambiguous MySQL error", "MySQL", new 
SQLSyntaxErrorException("query failed", "42000", 1055), 
MCPDatabaseQueryFailedException.class,
+                        MCPJDBCErrorCategory.QUERY_FAILED),
+                Arguments.of("query failed", "PostgreSQL", new 
SQLException("query failed"), MCPDatabaseQueryFailedException.class, 
MCPJDBCErrorCategory.QUERY_FAILED));
     }
     
     private static List<SQLExecutionColumnDefinition> createColumns() {
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeTest.java
index 3bb16e950bb..a1946fd057f 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeTest.java
@@ -472,7 +472,7 @@ class MCPSQLExecutionFacadeTest {
     private MCPSQLExecutionFacade createFacade(final 
MCPDatabaseCapabilityProvider capabilityProvider, final 
MCPSessionExecutionCoordinator coordinator,
                                                final 
MCPJdbcTransactionStatementExecutor transactionExecutor, final 
MCPJdbcStatementExecutor statementExecutor,
                                                final SQLExecutionTraceFactory 
traceFactory, final MCPStatementAnalyzer statementAnalyzer) {
-        return new MCPSQLExecutionFacade(capabilityProvider, coordinator, 
transactionExecutor, statementExecutor, statementAnalyzer, new 
SQLStatementScanner(), traceFactory);
+        return new MCPSQLExecutionFacade(capabilityProvider, coordinator, 
transactionExecutor, statementExecutor, statementAnalyzer, traceFactory);
     }
     
     @SuppressWarnings("unchecked")
@@ -520,6 +520,7 @@ class MCPSQLExecutionFacadeTest {
                 Arguments.of("qualified function", "SELECT 
other_db.foo_refresh_orders()", "other_db.foo_refresh_orders", "QUERY"),
                 Arguments.of("create database target", "CREATE DATABASE 
other_db", "other_db", "DDL"),
                 Arguments.of("commented create database target", "/* guard */ 
CREATE DATABASE other_db", "other_db", "DDL"),
+                Arguments.of("create schema target", "CREATE SCHEMA other_db", 
"other_db", "DDL"),
                 Arguments.of("drop table object list", "DROP TABLE IF EXISTS 
logic_db.orders, other_db.items", "other_db.items", "DDL"),
                 Arguments.of("truncate table", "TRUNCATE TABLE 
other_db.items", "other_db.items", "DDL"));
     }
@@ -540,7 +541,8 @@ class MCPSQLExecutionFacadeTest {
     private static Stream<Arguments> 
assertExecuteExplainWithSyntaxFailureCases() {
         return Stream.of(
                 Arguments.of("JDBC syntax exception", new 
MCPInvalidRequestException("bad explain", new SQLSyntaxErrorException("bad 
explain"))),
-                Arguments.of("SQLState syntax exception", new 
MCPQueryFailedException("bad explain", new SQLException("bad explain", 
"42601"))));
+                Arguments.of("SQLState syntax exception", new 
MCPQueryFailedException("bad explain", new SQLException("bad explain", 
"42601"))),
+                Arguments.of("MySQL vendor syntax exception", new 
MCPQueryFailedException("bad explain", new SQLException("bad explain", "42000", 
1064))));
     }
     
     private SQLExecutionRequest createExecutionRequest(final String sql) {
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPStatementAnalyzerTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPStatementAnalyzerTest.java
index 2092dac90aa..9d17190db1d 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPStatementAnalyzerTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPStatementAnalyzerTest.java
@@ -129,6 +129,14 @@ class MCPStatementAnalyzerTest {
         assertThat(actual.getReferencedObjectNames(), contains("*.*"));
     }
     
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("dclNamespaceCases")
+    void assertAnalyzeDCLNamespaceReference(final String name, final String 
sql) {
+        ClassificationResult actual = analyzer.analyze(sql, 
createCapability("PostgreSQL"));
+        assertThat(actual.getReferencedObjectNames(), contains("other_db"));
+        
assertTrue(actual.getReferencedObjects().iterator().next().isNamespaceTarget());
+    }
+    
     @Test
     void assertAnalyzeQualifiedFunctionReference() {
         ClassificationResult actual = analyzer.analyze("SELECT 
other_db.foo_refresh_orders()", createCapability("MySQL"));
@@ -170,6 +178,12 @@ class MCPStatementAnalyzerTest {
                 Arguments.of("Presto", "SELECT 1"));
     }
     
+    private static Stream<Arguments> dclNamespaceCases() {
+        return Stream.of(
+                Arguments.of("database", "GRANT CONNECT ON DATABASE other_db 
TO PUBLIC"),
+                Arguments.of("schema", "GRANT USAGE ON SCHEMA other_db TO 
PUBLIC"));
+    }
+    
     private static Stream<Arguments> statementCases() {
         return Stream.of(
                 Arguments.of("trim trailing semicolon", "MySQL", "  SELECT * 
FROM orders ;  ", SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM 
orders", "orders", ""),
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
index ab01955888b..7e9f57b927b 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
@@ -32,6 +32,8 @@ import 
org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
 import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
 import 
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
 import 
org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
@@ -118,6 +120,19 @@ class WorkflowProxyQueryServiceTest {
         assertThat(actual.getMessage(), is("Database `logic_db` is not 
configured."));
     }
     
+    @Test
+    void assertQueryClassifiesDatabaseFailure() throws SQLException {
+        RuntimeDatabaseConfiguration runtimeDatabaseConfig = 
mock(RuntimeDatabaseConfiguration.class);
+        Connection connection = mock(Connection.class);
+        Statement statement = mock(Statement.class);
+        
when(runtimeDatabaseConfig.openConnection("logic_db")).thenReturn(connection);
+        when(connection.createStatement()).thenReturn(statement);
+        when(statement.executeQuery("SHOW MASK RULES")).thenThrow(new 
SQLException("access denied", "42000", 1044));
+        WorkflowProxyQueryService service = createService(Map.of("logic_db", 
runtimeDatabaseConfig));
+        MCPDatabaseQueryFailedException actual = 
assertThrows(MCPDatabaseQueryFailedException.class, () -> 
service.query("logic_db", "", "SHOW MASK RULES"));
+        assertThat(actual.getCategory(), 
is(MCPJDBCErrorCategory.AUTHORIZATION));
+    }
+    
     @Test
     void assertCheckDatabaseCapabilityWithoutCapability() {
         WorkflowProxyQueryService service = new WorkflowProxyQueryService(new 
MCPSessionManager(Map.of()), mock(MCPDatabaseCapabilityProvider.class));
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseQueryFailedException.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseQueryFailedException.java
new file mode 100644
index 00000000000..3a867600aa1
--- /dev/null
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseQueryFailedException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception;
+
+import lombok.Getter;
+import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
+
+import java.sql.SQLException;
+
+/**
+ * Classified MCP database query failure.
+ */
+@Getter
+public final class MCPDatabaseQueryFailedException extends 
MCPQueryFailedException {
+    
+    private static final long serialVersionUID = 6131347880409413845L;
+    
+    private final MCPJDBCErrorCategory category;
+    
+    public MCPDatabaseQueryFailedException(final MCPJDBCErrorCategory 
category, final SQLException cause) {
+        super(cause.getMessage(), cause);
+        this.category = category;
+    }
+}
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseSQLSyntaxException.java
similarity index 51%
copy from 
mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
copy to 
mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseSQLSyntaxException.java
index dc2976637da..81c15525a06 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseSQLSyntaxException.java
@@ -15,28 +15,20 @@
  * limitations under the License.
  */
 
-package org.apache.shardingsphere.mcp.core.tool.handler.execute;
+package org.apache.shardingsphere.mcp.support.database.exception;
 
-import lombok.Getter;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
 
-import java.sql.SQLSyntaxErrorException;
+import java.sql.SQLException;
 
 /**
- * Exception for rule DistSQL execution failures that need workflow-aware 
recovery.
+ * MCP database SQL syntax exception.
  */
-@Getter
-public final class RuleDistSQLExecutionException extends 
MCPInvalidRequestException {
+public final class MCPDatabaseSQLSyntaxException extends 
MCPInvalidRequestException {
     
-    private static final long serialVersionUID = -5256057044313402728L;
+    private static final long serialVersionUID = 8170209785725635050L;
     
-    private final String database;
-    
-    private final ClassificationResult classificationResult;
-    
-    public RuleDistSQLExecutionException(final String database, final 
ClassificationResult classificationResult, final SQLSyntaxErrorException cause) 
{
-        super(String.format("Rule DistSQL execution failed for database `%s`; 
check MCP runtime capability and workflow guidance before asking for corrected 
SQL.", database), cause);
-        this.database = database;
-        this.classificationResult = classificationResult;
+    public MCPDatabaseSQLSyntaxException(final SQLException cause) {
+        super(cause.getMessage(), cause);
     }
 }
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCErrorCategory.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCErrorCategory.java
new file mode 100644
index 00000000000..628099c5058
--- /dev/null
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCErrorCategory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception;
+
+/**
+ * MCP JDBC error category.
+ */
+public enum MCPJDBCErrorCategory {
+    
+    TIMEOUT,
+    FEATURE_NOT_SUPPORTED,
+    CONNECTION,
+    AUTHENTICATION,
+    AUTHORIZATION,
+    OBJECT_NOT_VISIBLE,
+    SYNTAX,
+    QUERY_FAILED
+}
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCExceptionClassifier.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCExceptionClassifier.java
new file mode 100644
index 00000000000..7a198c2c58e
--- /dev/null
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCExceptionClassifier.java
@@ -0,0 +1,141 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader;
+import 
org.apache.shardingsphere.mcp.support.database.spi.MCPDialectSQLExceptionClassifier;
+
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLNonTransientConnectionException;
+import java.sql.SQLSyntaxErrorException;
+import java.sql.SQLTimeoutException;
+import java.sql.SQLTransientConnectionException;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * MCP JDBC exception classifier.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class MCPJDBCExceptionClassifier {
+    
+    private static final Set<String> OBJECT_NOT_VISIBLE_SQL_STATES = 
Set.of("3F000", "42P01", "42703", "42704", "42S02", "42S22");
+    
+    /**
+     * Classify JDBC exception without database dialect evidence.
+     *
+     * @param cause cause
+     * @return error category
+     */
+    public static MCPJDBCErrorCategory classify(final Throwable cause) {
+        return classify(Optional.empty(), cause);
+    }
+    
+    /**
+     * Classify JDBC exception for a database type.
+     *
+     * @param databaseType database type
+     * @param cause cause
+     * @return error category
+     */
+    public static MCPJDBCErrorCategory classify(final String databaseType, 
final Throwable cause) {
+        return 
classify(TypedSPILoader.findService(MCPDialectSQLExceptionClassifier.class, 
databaseType), cause);
+    }
+    
+    private static MCPJDBCErrorCategory classify(final 
Optional<MCPDialectSQLExceptionClassifier> dialectClassifier, final Throwable 
cause) {
+        Deque<Throwable> pending = new ArrayDeque<>();
+        pending.add(cause);
+        Set<Throwable> visited = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        while (!pending.isEmpty()) {
+            Throwable current = pending.removeFirst();
+            if (!visited.add(current)) {
+                continue;
+            }
+            if (current instanceof MCPDatabaseQueryFailedException) {
+                return ((MCPDatabaseQueryFailedException) 
current).getCategory();
+            }
+            if (current instanceof MCPDatabaseSQLSyntaxException) {
+                return MCPJDBCErrorCategory.SYNTAX;
+            }
+            if (current instanceof SQLException) {
+                Optional<MCPJDBCErrorCategory> category = 
classifySQLException(dialectClassifier, (SQLException) current);
+                if (category.isPresent() && MCPJDBCErrorCategory.QUERY_FAILED 
!= category.get()) {
+                    return category.get();
+                }
+                addIfPresent(pending, ((SQLException) 
current).getNextException());
+            }
+            addIfPresent(pending, current.getCause());
+        }
+        return MCPJDBCErrorCategory.QUERY_FAILED;
+    }
+    
+    private static Optional<MCPJDBCErrorCategory> classifySQLException(final 
Optional<MCPDialectSQLExceptionClassifier> dialectClassifier, final 
SQLException cause) {
+        Optional<MCPJDBCErrorCategory> standardCategory = 
classifyStandard(cause);
+        if (standardCategory.isPresent()) {
+            return standardCategory;
+        }
+        Optional<MCPJDBCErrorCategory> dialectCategory = 
dialectClassifier.flatMap(classifier -> classifier.classify(cause));
+        return dialectCategory.isPresent() ? dialectCategory : 
classifySyntax(cause);
+    }
+    
+    private static Optional<MCPJDBCErrorCategory> classifyStandard(final 
SQLException cause) {
+        if (cause instanceof SQLTimeoutException) {
+            return Optional.of(MCPJDBCErrorCategory.TIMEOUT);
+        }
+        String sqlState = cause.getSQLState();
+        if (cause instanceof SQLFeatureNotSupportedException || 
startsWith(sqlState, "0A")) {
+            return Optional.of(MCPJDBCErrorCategory.FEATURE_NOT_SUPPORTED);
+        }
+        if (cause instanceof SQLTransientConnectionException || cause 
instanceof SQLNonTransientConnectionException || startsWith(sqlState, "08")) {
+            return Optional.of(MCPJDBCErrorCategory.CONNECTION);
+        }
+        if (startsWith(sqlState, "28")) {
+            return Optional.of(MCPJDBCErrorCategory.AUTHENTICATION);
+        }
+        if ("42501".equals(sqlState)) {
+            return Optional.of(MCPJDBCErrorCategory.AUTHORIZATION);
+        }
+        if (null != sqlState && 
OBJECT_NOT_VISIBLE_SQL_STATES.contains(sqlState)) {
+            return Optional.of(MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE);
+        }
+        return Optional.empty();
+    }
+    
+    private static Optional<MCPJDBCErrorCategory> classifySyntax(final 
SQLException cause) {
+        return "42601".equals(cause.getSQLState()) || cause instanceof 
SQLSyntaxErrorException
+                ? Optional.of(MCPJDBCErrorCategory.SYNTAX)
+                : Optional.empty();
+    }
+    
+    private static boolean startsWith(final String value, final String prefix) 
{
+        return null != value && value.startsWith(prefix);
+    }
+    
+    private static void addIfPresent(final Deque<Throwable> pending, final 
Throwable cause) {
+        if (null != cause) {
+            pending.addLast(cause);
+        }
+    }
+}
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/dialect/MySQLMCPDialectSQLExceptionClassifier.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/dialect/MySQLMCPDialectSQLExceptionClassifier.java
new file mode 100644
index 00000000000..3324f229da4
--- /dev/null
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/exception/dialect/MySQLMCPDialectSQLExceptionClassifier.java
@@ -0,0 +1,83 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception.dialect;
+
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.spi.MCPDialectSQLExceptionClassifier;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * MCP SQL exception classifier for MySQL protocol databases.
+ */
+public final class MySQLMCPDialectSQLExceptionClassifier implements 
MCPDialectSQLExceptionClassifier {
+    
+    private static final int ER_DBACCESS_DENIED_ERROR = 1044;
+    
+    private static final int ER_BAD_DB_ERROR = 1049;
+    
+    private static final int ER_PARSE_ERROR = 1064;
+    
+    private static final int ER_TABLEACCESS_DENIED_ERROR = 1142;
+    
+    private static final int ER_COLUMNACCESS_DENIED_ERROR = 1143;
+    
+    private static final int ER_SYNTAX_ERROR = 1149;
+    
+    private static final int ER_SPECIFIC_ACCESS_DENIED_ERROR = 1227;
+    
+    private static final int ER_SP_DOES_NOT_EXIST = 1305;
+    
+    private static final int ER_PROCACCESS_DENIED_ERROR = 1370;
+    
+    @Override
+    public Optional<MCPJDBCErrorCategory> classify(final SQLException cause) {
+        if (!"42000".equals(cause.getSQLState())) {
+            return Optional.empty();
+        }
+        switch (cause.getErrorCode()) {
+            case ER_DBACCESS_DENIED_ERROR:
+            case ER_TABLEACCESS_DENIED_ERROR:
+            case ER_COLUMNACCESS_DENIED_ERROR:
+            case ER_SPECIFIC_ACCESS_DENIED_ERROR:
+            case ER_PROCACCESS_DENIED_ERROR:
+                return Optional.of(MCPJDBCErrorCategory.AUTHORIZATION);
+            case ER_BAD_DB_ERROR:
+            case ER_SP_DOES_NOT_EXIST:
+                return Optional.of(MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE);
+            case ER_PARSE_ERROR:
+            case ER_SYNTAX_ERROR:
+                return Optional.of(MCPJDBCErrorCategory.SYNTAX);
+            default:
+                return Optional.of(MCPJDBCErrorCategory.QUERY_FAILED);
+        }
+    }
+    
+    @Override
+    public String getType() {
+        return "MySQL";
+    }
+    
+    @Override
+    public Collection<Object> getTypeAliases() {
+        return List.of("MariaDB");
+    }
+}
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
index ee892dc7a77..437416741de 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
@@ -18,11 +18,10 @@
 package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
 
 import lombok.Getter;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 
 import java.sql.SQLException;
-import java.sql.SQLTimeoutException;
-import java.util.Locale;
-import java.util.Objects;
 
 /**
  * Runtime database connection exception with safe model-facing category.
@@ -103,20 +102,13 @@ public final class RuntimeDatabaseConnectionException 
extends RuntimeException {
     }
     
     private static String resolveCategory(final SQLException cause) {
-        String sqlState = Objects.toString(cause.getSQLState(), "");
-        String message = Objects.toString(cause.getMessage(), 
"").toLowerCase(Locale.ENGLISH);
-        if (cause instanceof SQLTimeoutException || 
message.contains("timeout") || message.contains("timed out")) {
-            return CATEGORY_CONNECTION_TIMEOUT;
-        }
-        if (sqlState.startsWith("42501") || message.contains("permission 
denied") || message.contains("insufficient privilege") || message.contains("not 
authorized")) {
-            return CATEGORY_AUTHORIZATION_FAILED;
-        }
-        if (sqlState.startsWith("28") || message.contains("authentication") || 
message.contains("access denied") || message.contains("password")) {
-            return CATEGORY_AUTHENTICATION_FAILED;
-        }
-        if (sqlState.startsWith("08") || message.contains("no suitable 
driver")) {
-            return CATEGORY_DATABASE_UNAVAILABLE;
-        }
-        return CATEGORY_CONNECTION_FAILED;
+        MCPJDBCErrorCategory category = 
MCPJDBCExceptionClassifier.classify(cause);
+        return switch (category) {
+            case TIMEOUT -> CATEGORY_CONNECTION_TIMEOUT;
+            case AUTHORIZATION -> CATEGORY_AUTHORIZATION_FAILED;
+            case AUTHENTICATION -> CATEGORY_AUTHENTICATION_FAILED;
+            case CONNECTION -> CATEGORY_DATABASE_UNAVAILABLE;
+            default -> CATEGORY_CONNECTION_FAILED;
+        };
     }
 }
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPDialectSQLExceptionClassifier.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPDialectSQLExceptionClassifier.java
new file mode 100644
index 00000000000..8694a286572
--- /dev/null
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPDialectSQLExceptionClassifier.java
@@ -0,0 +1,43 @@
+/*
+ * 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.shardingsphere.mcp.support.database.spi;
+
+import org.apache.shardingsphere.infra.spi.annotation.SingletonSPI;
+import org.apache.shardingsphere.infra.spi.type.typed.TypedSPI;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+
+import java.sql.SQLException;
+import java.util.Optional;
+
+/**
+ * Database dialect SQL exception classifier for MCP.
+ */
+@SingletonSPI
+public interface MCPDialectSQLExceptionClassifier extends TypedSPI {
+    
+    /**
+     * Classify SQL exception when JDBC-standard evidence is ambiguous.
+     *
+     * @param cause SQL exception
+     * @return classified category
+     */
+    Optional<MCPJDBCErrorCategory> classify(SQLException cause);
+    
+    @Override
+    String getType();
+}
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
index ccf50d0d449..9b488b8b3d5 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
@@ -20,13 +20,12 @@ package 
org.apache.shardingsphere.mcp.support.workflow.service;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCExceptionClassifier;
 import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
 
-import java.sql.SQLSyntaxErrorException;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.Objects;
 
 /**
  * DistSQL query utilities for workflow planning.
@@ -41,7 +40,7 @@ public final class WorkflowDistSQLQueryUtils {
      * @return whether the backend does not support DistSQL syntax
      */
     public static boolean isUnsupportedDistSQLQueryFailure(final 
MCPQueryFailedException ex) {
-        return hasSyntaxErrorCause(ex) || hasUnsupportedDistSQLMessage(ex);
+        return MCPJDBCErrorCategory.SYNTAX == 
MCPJDBCExceptionClassifier.classify(ex);
     }
     
     /**
@@ -63,19 +62,4 @@ public final class WorkflowDistSQLQueryUtils {
         }
     }
     
-    private static boolean hasSyntaxErrorCause(final Throwable throwable) {
-        Throwable current = throwable;
-        while (null != current) {
-            if (current instanceof SQLSyntaxErrorException) {
-                return true;
-            }
-            current = current.getCause();
-        }
-        return false;
-    }
-    
-    private static boolean hasUnsupportedDistSQLMessage(final 
MCPQueryFailedException ex) {
-        String message = Objects.toString(ex.getMessage(), 
"").toLowerCase(Locale.ENGLISH);
-        return message.contains("syntax") && (message.contains("distsql") || 
message.contains(" rule") || message.contains("algorithm plugins"));
-    }
 }
diff --git 
a/mcp/support/src/main/resources/META-INF/services/org.apache.shardingsphere.mcp.support.database.spi.MCPDialectSQLExceptionClassifier
 
b/mcp/support/src/main/resources/META-INF/services/org.apache.shardingsphere.mcp.support.database.spi.MCPDialectSQLExceptionClassifier
new file mode 100644
index 00000000000..c99ccb282fc
--- /dev/null
+++ 
b/mcp/support/src/main/resources/META-INF/services/org.apache.shardingsphere.mcp.support.database.spi.MCPDialectSQLExceptionClassifier
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.apache.shardingsphere.mcp.support.database.exception.dialect.MySQLMCPDialectSQLExceptionClassifier
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseQueryFailedExceptionTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseQueryFailedExceptionTest.java
new file mode 100644
index 00000000000..5da0b6b4d46
--- /dev/null
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseQueryFailedExceptionTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+class MCPDatabaseQueryFailedExceptionTest {
+    
+    @Test
+    void assertErrorContract() {
+        SQLException cause = new SQLException("missing table", "42P01");
+        MCPDatabaseQueryFailedException actual = new 
MCPDatabaseQueryFailedException(MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE, cause);
+        assertThat(actual.getMessage(), is("missing table"));
+        assertThat(actual.getCategory(), 
is(MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE));
+        assertThat(actual.getCause(), sameInstance(cause));
+    }
+}
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseSQLSyntaxExceptionTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseSQLSyntaxExceptionTest.java
new file mode 100644
index 00000000000..d654b3231b9
--- /dev/null
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPDatabaseSQLSyntaxExceptionTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+class MCPDatabaseSQLSyntaxExceptionTest {
+    
+    @Test
+    void assertErrorContract() {
+        SQLException cause = new SQLException("syntax error", "42000", 1064);
+        MCPDatabaseSQLSyntaxException actual = new 
MCPDatabaseSQLSyntaxException(cause);
+        assertThat(actual.getMessage(), is("syntax error"));
+        assertThat(actual.getCause(), sameInstance(cause));
+    }
+}
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCExceptionClassifierTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCExceptionClassifierTest.java
new file mode 100644
index 00000000000..272af78c9fc
--- /dev/null
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/MCPJDBCExceptionClassifierTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLNonTransientConnectionException;
+import java.sql.SQLSyntaxErrorException;
+import java.sql.SQLTimeoutException;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class MCPJDBCExceptionClassifierTest {
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("assertClassifyCases")
+    void assertClassify(final String name, final SQLException cause, final 
MCPJDBCErrorCategory expected) {
+        assertThat(MCPJDBCExceptionClassifier.classify(cause), is(expected));
+    }
+    
+    @Test
+    void assertClassifyCauseChain() {
+        assertThat(MCPJDBCExceptionClassifier.classify(new 
IllegalStateException(new SQLException("missing table", "42P01"))),
+                is(MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE));
+    }
+    
+    @Test
+    void assertClassifyNextExceptionChain() {
+        SQLException cause = new SQLException("batch failed");
+        cause.setNextException(new SQLException("syntax error", "42601"));
+        assertThat(MCPJDBCExceptionClassifier.classify(cause), 
is(MCPJDBCErrorCategory.SYNTAX));
+    }
+    
+    @Test
+    void assertClassifyNextExceptionAfterDialectFallback() {
+        SQLException cause = new SQLException("batch failed", "42000", 1055);
+        cause.setNextException(new SQLException("syntax error", "42000", 
1064));
+        assertThat(MCPJDBCExceptionClassifier.classify("MySQL", cause), 
is(MCPJDBCErrorCategory.SYNTAX));
+    }
+    
+    @Test
+    void assertClassifyIgnoresSuppressedException() {
+        SQLException cause = new SQLException("query failed");
+        cause.addSuppressed(new SQLTimeoutException("timeout"));
+        assertThat(MCPJDBCExceptionClassifier.classify(cause), 
is(MCPJDBCErrorCategory.QUERY_FAILED));
+    }
+    
+    @Test
+    void assertClassifyClassifiedQueryFailure() {
+        MCPDatabaseQueryFailedException cause = new 
MCPDatabaseQueryFailedException(
+                MCPJDBCErrorCategory.AUTHORIZATION, new 
SQLSyntaxErrorException("access denied", "42000", 1044));
+        assertThat(MCPJDBCExceptionClassifier.classify(cause), 
is(MCPJDBCErrorCategory.AUTHORIZATION));
+    }
+    
+    @Test
+    void assertClassifyDatabaseSyntaxFailure() {
+        assertThat(MCPJDBCExceptionClassifier.classify(new 
MCPDatabaseSQLSyntaxException(new SQLException("syntax error", "42000", 1064))),
+                is(MCPJDBCErrorCategory.SYNTAX));
+    }
+    
+    @Test
+    void assertClassifyUnambiguousJDBCTypeBeforeDialect() {
+        assertThat(MCPJDBCExceptionClassifier.classify("MySQL", new 
SQLTimeoutException("timeout", "42000", 1064)), 
is(MCPJDBCErrorCategory.TIMEOUT));
+    }
+    
+    @Test
+    void assertClassifyMariaDBAlias() {
+        assertThat(MCPJDBCExceptionClassifier.classify("MariaDB", new 
SQLException("syntax error", "42000", 1064)), is(MCPJDBCErrorCategory.SYNTAX));
+    }
+    
+    private static Stream<Arguments> assertClassifyCases() {
+        return Stream.of(
+                Arguments.of("timeout", new SQLTimeoutException(), 
MCPJDBCErrorCategory.TIMEOUT),
+                Arguments.of("feature not supported", new 
SQLFeatureNotSupportedException(), MCPJDBCErrorCategory.FEATURE_NOT_SUPPORTED),
+                Arguments.of("connection subtype", new 
SQLNonTransientConnectionException(), MCPJDBCErrorCategory.CONNECTION),
+                Arguments.of("connection SQLState", new SQLException("", 
"08006"), MCPJDBCErrorCategory.CONNECTION),
+                Arguments.of("authentication", new SQLException("", "28000"), 
MCPJDBCErrorCategory.AUTHENTICATION),
+                Arguments.of("authorization", new SQLException("", "42501"), 
MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("object not visible", new 
SQLSyntaxErrorException("", "42P01"), MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE),
+                Arguments.of("syntax subtype", new SQLSyntaxErrorException(), 
MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("syntax SQLState", new SQLException("", "42601"), 
MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("ambiguous class 42", new SQLException("", 
"42000"), MCPJDBCErrorCategory.QUERY_FAILED),
+                Arguments.of("unknown", new SQLException(), 
MCPJDBCErrorCategory.QUERY_FAILED));
+    }
+}
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/dialect/MySQLMCPDialectSQLExceptionClassifierTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/dialect/MySQLMCPDialectSQLExceptionClassifierTest.java
new file mode 100644
index 00000000000..661d0cecd51
--- /dev/null
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/exception/dialect/MySQLMCPDialectSQLExceptionClassifierTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.shardingsphere.mcp.support.database.exception.dialect;
+
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.sql.SQLException;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+
+class MySQLMCPDialectSQLExceptionClassifierTest {
+    
+    private final MySQLMCPDialectSQLExceptionClassifier classifier = new 
MySQLMCPDialectSQLExceptionClassifier();
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("assertClassifyCases")
+    void assertClassify(final String name, final int errorCode, final 
MCPJDBCErrorCategory expected) {
+        assertThat(classifier.classify(new SQLException("error", "42000", 
errorCode)), is(Optional.of(expected)));
+    }
+    
+    @Test
+    void assertClassifyUnrelatedSQLState() {
+        assertThat(classifier.classify(new SQLException("missing table", 
"42S02", 1146)), is(Optional.empty()));
+    }
+    
+    @Test
+    void assertTypes() {
+        assertThat(classifier.getType(), is("MySQL"));
+        assertThat(classifier.getTypeAliases(), contains("MariaDB"));
+    }
+    
+    private static Stream<Arguments> assertClassifyCases() {
+        return Stream.of(
+                Arguments.of("database access denied", 1044, 
MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("unknown database", 1049, 
MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE),
+                Arguments.of("parse error", 1064, MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("table access denied", 1142, 
MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("column access denied", 1143, 
MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("syntax error", 1149, 
MCPJDBCErrorCategory.SYNTAX),
+                Arguments.of("specific access denied", 1227, 
MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("missing routine", 1305, 
MCPJDBCErrorCategory.OBJECT_NOT_VISIBLE),
+                Arguments.of("routine access denied", 1370, 
MCPJDBCErrorCategory.AUTHORIZATION),
+                Arguments.of("ambiguous error", 1055, 
MCPJDBCErrorCategory.QUERY_FAILED));
+    }
+}
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
index 3dbc4d787e1..4e7b79d6ab8 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
@@ -73,6 +73,12 @@ class RuntimeDatabaseConnectionExceptionTest {
         assertThat(actual.getCategory(), 
is(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED));
     }
     
+    @Test
+    void assertConnectionFailedDoesNotInspectMessage() {
+        RuntimeDatabaseConnectionException actual = 
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new 
SQLException("Access denied because the operation timed out"));
+        assertThat(actual.getCategory(), 
is(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED));
+    }
+    
     @Test
     void assertDatabaseNotVisible() {
         RuntimeDatabaseConnectionException actual = 
RuntimeDatabaseConnectionException.databaseNotVisible("logic_db", new 
IllegalStateException("not visible"));
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
index db3b44c10ab..b1172dab94a 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
@@ -18,6 +18,8 @@
 package org.apache.shardingsphere.mcp.support.workflow.service;
 
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPDatabaseQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.exception.MCPJDBCErrorCategory;
 import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
 import org.junit.jupiter.api.Test;
 
@@ -45,9 +47,9 @@ class WorkflowDistSQLQueryUtilsTest {
     }
     
     @Test
-    void assertIsUnsupportedDistSQLQueryFailureWithDistSQLMessage() {
+    void assertIsUnsupportedDistSQLQueryFailureWithoutJDBCEvidence() {
         MCPQueryFailedException actualException = new 
MCPQueryFailedException("DistSQL syntax is unsupported by this runtime 
backend.");
-        
assertTrue(WorkflowDistSQLQueryUtils.isUnsupportedDistSQLQueryFailure(actualException));
+        
assertFalse(WorkflowDistSQLQueryUtils.isUnsupportedDistSQLQueryFailure(actualException));
     }
     
     @Test
@@ -67,7 +69,8 @@ class WorkflowDistSQLQueryUtilsTest {
     @Test
     void assertQueryRuleRowsWithUnsupportedDistSQL() {
         MCPFeatureQueryFacade queryFacade = mock(MCPFeatureQueryFacade.class);
-        when(queryFacade.query("logic_db", "", "SHOW MASK 
RULES")).thenThrow(new MCPQueryFailedException("DistSQL syntax is unsupported 
by this runtime backend."));
+        when(queryFacade.query("logic_db", "", "SHOW MASK RULES")).thenThrow(
+                new 
MCPDatabaseQueryFailedException(MCPJDBCErrorCategory.SYNTAX, new 
SQLException("syntax error", "42601")));
         assertTrue(WorkflowDistSQLQueryUtils.queryRuleRows(queryFacade, 
"logic_db", "SHOW MASK RULES").isEmpty());
     }
     
diff --git a/test/e2e/mcp/pom.xml b/test/e2e/mcp/pom.xml
index 1c4993c4512..fd36f94330e 100644
--- a/test/e2e/mcp/pom.xml
+++ b/test/e2e/mcp/pom.xml
@@ -95,6 +95,11 @@
             <artifactId>mysql-connector-j</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hive</groupId>
+            <artifactId>hive-jdbc</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>jul-to-slf4j</artifactId>
diff --git 
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
 
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
index 1e3e2788886..09bb854f254 100644
--- 
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
+++ 
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
@@ -104,7 +104,7 @@ model_contract:
   metadata_first_resource: shardingsphere://databases
   preflight_rule: Use database_gateway_validate_runtime_database with a 
configured
     database name before onboarding or troubleshooting runtime connectivity.
-  sql_tool_selection: {read_only: Use database_gateway_execute_query for one 
classifier-approved
+  sql_tool_selection: {read_only: Use database_gateway_execute_query for one 
parser-approved
       SELECT statement., explain: Use database_gateway_execute_explain_query 
with
       the original SELECT and a model-generated database-native EXPLAIN SQL; 
do not
       use EXPLAIN ANALYZE., side_effecting: Use 
database_gateway_execute_update with

Reply via email to