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