You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by zh...@apache.org on 2026/07/14 06:55:41 UTC
(shardingsphere) branch master updated: Refactor MCP execution result and response boundaries (#39094)
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 b83fb4d8895 Refactor MCP execution result and response boundaries (#39094)
b83fb4d8895 is described below
commit b83fb4d8895a415d3e1192a414bee9eb4bef2dea
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Tue Jul 14 14:55:35 2026 +0800
Refactor MCP execution result and response boundaries (#39094)
* Refactor MCP execution result and response boundaries
- separate runtime validation and SQL execution facts from MCP payload mapping
- clarify MCPMapResponse usage and execute-update ResultSet schema
- prevent automatic replay of truncated side-effecting SQL
* Refactor MCP execution result and response boundaries
- separate runtime validation and SQL execution facts from MCP payload mapping
- clarify MCPMapResponse usage and execute-update ResultSet schema
- prevent automatic replay of truncated side-effecting SQL
---
AGENTS.md | 14 +-
.../tool/MCPCallToolResultFactoryTest.java | 50 ++++
.../error/MCPBasicRecoveryPayloadFactory.java | 56 ----
.../protocol/error/MCPRecoveryPayloadFactory.java | 2 +-
.../MCPRuntimeDatabaseRecoveryPayloadFactory.java | 111 ++++++++
.../handler/execute/ExecuteExplainToolHandler.java | 3 +-
.../handler/execute/ExecuteQueryToolHandler.java | 7 +-
.../handler/execute/ExecuteUpdateToolHandler.java | 7 +-
.../handler/execute/MCPJdbcStatementExecutor.java | 55 ++--
.../MCPJdbcTransactionStatementExecutor.java | 62 ++--
.../handler/execute/MCPSQLExecutionFacade.java | 14 +-
.../execute/SQLExecutionToolHandlerSupport.java | 2 +-
.../ValidateRuntimeDatabaseToolHandler.java | 15 +-
.../RuntimeDatabaseValidationResponse.java} | 75 +++--
.../core/tool/response/SQLExecutionResponse.java | 204 ++++++++++++++
.../mcp-descriptors/mcp-descriptor-core.yaml | 35 ++-
...PRuntimeDatabaseRecoveryPayloadFactoryTest.java | 59 ++++
.../capability/ServerCapabilitiesHandlerTest.java | 17 ++
.../execute/ExecuteExplainToolHandlerTest.java | 10 +-
.../execute/ExecuteQueryToolHandlerTest.java | 13 +-
.../execute/ExecuteUpdateToolHandlerTest.java | 11 +-
.../execute/MCPJdbcStatementExecutorTest.java | 55 ++--
.../MCPJdbcTransactionStatementExecutorTest.java | 30 +-
.../handler/execute/MCPSQLExecutionFacadeTest.java | 63 ++---
.../ValidateRuntimeDatabaseToolHandlerTest.java | 8 +-
.../RuntimeDatabaseValidationResponseTest.java | 83 ++++++
.../tool/response/SQLExecutionResponseTest.java | 148 ++++++++++
.../workflow/WorkflowExecutionServiceTest.java | 12 +-
.../database/spi/MCPFeatureExecutionFacade.java | 10 +-
.../tool/response/SQLExecutionResponse.java | 313 ---------------------
.../RuntimeDatabaseValidationCheckResult.java | 19 +-
.../result/RuntimeDatabaseValidationResult.java | 63 +++++
.../result/SQLExecutionColumnDefinition.java} | 6 +-
.../database/tool/result/SQLExecutionResult.java | 106 +++++++
.../result/SQLExecutionResultKind.java} | 6 +-
.../service/RuntimeDatabaseValidationService.java | 43 +--
.../MCPToolDescriptorCatalogValidator.java | 4 +-
.../support/protocol/response/MCPMapResponse.java | 5 +-
.../security/MCPRuntimeProtectionPolicy.java | 3 +-
.../RuntimeDatabaseValidationResultTest.java | 76 -----
.../tool/response/SQLExecutionResponseTest.java | 161 -----------
.../RuntimeDatabaseValidationCheckResultTest.java | 14 +-
.../RuntimeDatabaseValidationResultTest.java | 49 ++++
.../result/SQLExecutionColumnDefinitionTest.java} | 14 +-
.../tool/result/SQLExecutionResultTest.java | 79 ++++++
.../RuntimeDatabaseValidationServiceTest.java | 90 +++---
.../MCPDescriptorCatalogValidatorTest.java | 29 ++
.../protocol/response/MCPMapResponseTest.java | 4 +-
.../security/MCPRuntimeProtectionPolicyTest.java | 3 +
.../baseline-contract/model-contract/guidance.yaml | 6 +-
50 files changed, 1340 insertions(+), 984 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 228e3f4067c..21ebcf9c2f8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -230,16 +230,22 @@ Dangerous operation detected! Operation type: [specific action] Scope of impact:
## Coding Skill Guidance
- When the named third-party skills are available in the current environment, use them for the matching work:
`source-driven-development` for official-source checks, `api-and-interface-design` for public contracts,
- `doubt-driven-development` for non-trivial decisions, `code-simplification` after implementation, and `code-review-and-quality` before handoff.
+ `code-simplification` after implementation, and `code-review-and-quality` before handoff.
- If a named third-party skill is unavailable, do not install it automatically or block the task.
Apply an equivalent manual checklist for the same intent, record the fallback in the plan or final response, and continue.
+### Cross-Model Review Prohibition
+- Do not use `doubt-driven-development` in this repository when it requires an external or cross-model review offer.
+- Unless the user explicitly requests a specific external model in the current task, never ask whether to run a cross-model review, never pause work for that choice,
+ and never invoke Gemini CLI, Codex CLI, Claude CLI, or any other external model or model-review service.
+- When an independent challenge is useful, use a fresh-context sub-agent within the current session or perform a bounded adversarial self-review, then continue to completion without asking the user to choose a reviewer.
+
## Workflow
- Use Sequential Thinking when tasks need decomposition: 6-10 steps (fallback 3-5), one sentence each, actionable.
- Intake: choose the strategy for the task, confirm tool availability/fallbacks, capture constraints (forbidden APIs, output format, coverage/test expectations),
and use `source-driven-development` when available, or equivalent source-checking, to verify facts that depend on authoritative sources.
-- Plan: inspect existing code with tools before edits, finish the plan before coding, use `doubt-driven-development` when available for non-trivial decisions,
- and set the quality/verification bar.
+- Plan: inspect existing code with tools before edits, finish the plan before coding, use a bounded fresh-context or adversarial self-review for non-trivial decisions,
+ and set the quality/verification bar without invoking or offering cross-model review.
- Implement: keep scope minimal, follow quality standards, record decisions, and handle edge cases; honor instruction precedence from Core Principle #7.
- Validate: run the narrowest meaningful checks (see Verification & Commands) and prefer scoped runs; note any sandbox or limit blocks and alternatives.
- Report & self-check: share intent, edits, verification results, and next steps; ensure all required instructions, coverage, and mocking rules are satisfied, with remaining risks called out.
@@ -332,7 +338,7 @@ Dangerous operation detected! Operation type: [specific action] Scope of impact:
- Before finishing an authorized implementation task, ask whether this task created in-scope legacy or dead code that can be safely removed,
whether the changed implementation can be simpler without changing behavior,
whether existing public behavior and contracts are preserved, and whether `code-review-and-quality` or equivalent review still has in-scope required findings.
-- Use `doubt-driven-development` when available, or equivalent adversarial self-review, to keep raising and resolving valuable in-scope questions until the stop condition is met.
+- Use a bounded fresh-context or adversarial self-review to keep raising and resolving valuable in-scope questions until the stop condition is met.
Stop when no actionable findings remain, the same findings repeat, 3 doubt cycles complete, or the user explicitly overrides.
- Before finishing any implementation task, compare the final diff against the declared or confirmed change boundary.
Use `git diff` to verify the final changed file set and changed hunks against the declared or confirmed boundary.
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
index 1e6eb8bb5c5..80b2a7cf2e8 100644
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
@@ -24,9 +24,14 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceLink;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import org.apache.shardingsphere.infra.util.json.JsonUtils;
import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.apache.shardingsphere.mcp.core.protocol.error.MCPErrorConverter;
import org.apache.shardingsphere.mcp.core.protocol.response.MCPErrorResponse;
+import org.apache.shardingsphere.mcp.core.tool.handler.metadata.ValidateRuntimeDatabaseToolHandler;
+import org.apache.shardingsphere.mcp.core.tool.response.RuntimeDatabaseValidationResponse;
+import org.apache.shardingsphere.mcp.core.tool.response.SQLExecutionResponse;
import org.apache.shardingsphere.mcp.core.tool.handler.MCPToolDefinition;
import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
import org.apache.shardingsphere.mcp.feature.encrypt.EncryptFeatureDefinition;
@@ -34,6 +39,12 @@ import org.apache.shardingsphere.mcp.feature.encrypt.tool.model.EncryptWorkflowR
import org.apache.shardingsphere.mcp.feature.encrypt.tool.service.EncryptAlgorithmPropertyTemplateService;
import org.apache.shardingsphere.mcp.feature.mask.MaskFeatureDefinition;
import org.apache.shardingsphere.mcp.feature.mask.tool.service.MaskAlgorithmPropertyTemplateService;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationCheckResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationResult;
+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.descriptor.MCPShardingSphereMetadataKeys;
import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
@@ -95,6 +106,45 @@ class MCPCallToolResultFactoryTest extends AbstractMCPToolSpecificationFactoryTe
assertFalse(actual.isError());
}
+ @Test
+ void assertCreateWithRuntimeValidationDiagnostic() {
+ MCPResponse response = RuntimeDatabaseValidationResponse.from(RuntimeDatabaseValidationResult.failed("",
+ List.of(RuntimeDatabaseValidationCheckResult.failed("configuration", RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION,
+ "The requested database is not configured for this MCP runtime.")),
+ RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION));
+ CallToolResult actual = createRealDescriptorCallToolResult(ValidateRuntimeDatabaseToolHandler.TOOL_NAME, response);
+ Map<String, Object> actualPayload = getStructuredContent(actual);
+ assertFalse(actual.isError());
+ assertThat(actualPayload.get("status"), is("failed"));
+ assertFalse(((Map<?, ?>) actualPayload.get("recovery")).containsKey("database"));
+ assertFalse(((Map<?, ?>) actualPayload.get("recovery")).containsKey("request_id"));
+ assertThat(((TextContent) actual.content().getFirst()).text(), is(JsonUtils.toJsonString(actualPayload)));
+ }
+
+ @Test
+ void assertCreateWithRuntimeValidationException() {
+ MCPResponse response = MCPErrorConverter.convert(RuntimeDatabaseConnectionException.invalidConfiguration(
+ "logic_db", new IllegalStateException("Invalid runtime database configuration.")));
+ CallToolResult actual = createRealDescriptorCallToolResult(ValidateRuntimeDatabaseToolHandler.TOOL_NAME, response);
+ assertTrue(actual.isError());
+ assertThat(getStructuredContent(actual).get("response_mode"), is("recovery"));
+ }
+
+ @Test
+ void assertCreateWithExecutedResultSet() {
+ SQLExecutionColumnDefinition column = new SQLExecutionColumnDefinition("order_id", "BIGINT", "BIGINT", false);
+ MCPResponse response = SQLExecutionResponse.executed(SQLExecutionResult.resultSet(SupportedMCPStatement.DML, "SELECT",
+ List.of(column), List.of(List.of(1L)), true, 1, 0, "WITH changed AS (...) SELECT order_id FROM changed"));
+ CallToolResult actual = createRealDescriptorCallToolResult("database_gateway_execute_update", response);
+ Map<String, Object> actualPayload = getStructuredContent(actual);
+ assertFalse(actual.isError(), () -> String.valueOf(actualPayload));
+ assertThat(actualPayload.get("columns"), is(List.of(column)));
+ assertThat(actualPayload.get("rows"), is(List.of(List.of(1L))));
+ assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("next_actions")).getFirst()).get("reason"), is(
+ "The side-effecting statement already executed and returned truncated rows; do not replay it automatically. "
+ + "Use a separate read-only query if more data is needed."));
+ }
+
@Test
void assertCreateWithMaskWorkflowPlanSchema() {
CallToolResult actual = createRealDescriptorCallToolResult(MaskFeatureDefinition.PLAN_TOOL_NAME, createMaskPlanResponse());
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
index 45114190f83..f4c24e3dbbd 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
@@ -25,7 +25,6 @@ import org.apache.shardingsphere.mcp.core.protocol.exception.MCPToolArgumentCont
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPToolCallLimitExceededException;
import org.apache.shardingsphere.mcp.core.resource.handler.ResourceDefinitionRegistry;
import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
-import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
@@ -66,61 +65,6 @@ final class MCPBasicRecoveryPayloadFactory {
return result;
}
- static Map<String, Object> createRuntimeDatabaseConnectionRecovery(final RuntimeDatabaseConnectionException cause) {
- Map<String, Object> result = MCPRecoveryPayloadSupport.createBaseRecovery(cause.getCategory(), createRuntimeDatabaseConnectionModelAction(cause));
- result.put(WorkflowFieldNames.DATABASE, cause.getDatabaseName());
- result.put(MCPPayloadFieldNames.RESOURCES_TO_READ, createRuntimeDatabaseConnectionResourcesToRead(cause));
- result.put(MCPPayloadFieldNames.NEXT_ACTIONS, createRuntimeDatabaseConnectionNextActions(cause));
- result.put("ask_user_when_uncertain", true);
- return result;
- }
-
- private static String createRuntimeDatabaseConnectionModelAction(final RuntimeDatabaseConnectionException cause) {
- if (RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER.equals(cause.getCategory())) {
- return "Install or configure the JDBC driver for the MCP runtime database, then retry.";
- }
- if (RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED.equals(cause.getCategory())) {
- return "Check the runtime database credentials outside MCP, then retry.";
- }
- if (RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED.equals(cause.getCategory())) {
- return "Check runtime database account privileges outside MCP, then retry.";
- }
- if (RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT.equals(cause.getCategory())) {
- return "Check database reachability and timeout settings, then retry.";
- }
- if (RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION.equals(cause.getCategory())) {
- return "Fix the MCP runtime database configuration outside MCP, then retry.";
- }
- if (RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(cause.getCategory())) {
- return "Connect to the intended logical database or update the expected database name before retrying.";
- }
- return "Check the runtime database availability and configuration, then retry.";
- }
-
- private static List<Map<String, Object>> createRuntimeDatabaseConnectionResourcesToRead(final RuntimeDatabaseConnectionException cause) {
- Map<String, Object> runtimeResource = MCPResourceHintUtils.create(
- "shardingsphere://runtime", "runtime", "read_first", "Read current MCP runtime status before retrying.", MCPPayloadFieldNames.RESOURCES_TO_READ);
- if (!RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(cause.getCategory())) {
- return List.of(runtimeResource);
- }
- return List.of(runtimeResource, MCPResourceHintUtils.create(
- "shardingsphere://databases", "logical-database", "read_first", "Read visible logical databases before retrying.", MCPPayloadFieldNames.RESOURCES_TO_READ));
- }
-
- private static List<Map<String, Object>> createRuntimeDatabaseConnectionNextActions(final RuntimeDatabaseConnectionException cause) {
- Map<String, Object> readRuntime = MCPNextActionUtils.readResource("shardingsphere://runtime", "Read current MCP runtime status and configured database visibility.");
- if (!RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(cause.getCategory())) {
- return MCPNextActionUtils.ordered(readRuntime,
- MCPNextActionUtils.dependsOn(MCPNextActionUtils.askUser(
- "Ask the operator to fix the MCP runtime database configuration before retrying.", List.of("runtime_database_configuration")), 1));
- }
- return MCPNextActionUtils.ordered(readRuntime,
- MCPNextActionUtils.dependsOn(MCPNextActionUtils.readResource("shardingsphere://databases", "Read visible logical databases before retrying."), 1),
- MCPNextActionUtils.dependsOn(MCPNextActionUtils.askUser(
- "Ask the operator to choose a visible logical database or update runtimeDatabases before retrying.",
- List.of(WorkflowFieldNames.DATABASE, "runtimeDatabases")), 2));
- }
-
static Map<String, Object> createToolCallLimitRecovery(final MCPToolCallLimitExceededException cause) {
Map<String, Object> result = MCPRecoveryPayloadSupport.createBaseRecovery(
"tool_call_limit_exceeded", "Stop the current loop, summarize progress, or start a new MCP session before retrying.");
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
index 0b8c6e09ed8..2928660630f 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
@@ -69,7 +69,7 @@ final class MCPRecoveryPayloadFactory {
new RecoveryMapping(UnsupportedResourceUriException.class,
cause -> MCPBasicRecoveryPayloadFactory.createUnsupportedResourceRecovery(((UnsupportedResourceUriException) cause).getResourceUri())),
new RecoveryMapping(RuntimeDatabaseConnectionException.class,
- cause -> MCPBasicRecoveryPayloadFactory.createRuntimeDatabaseConnectionRecovery((RuntimeDatabaseConnectionException) cause)),
+ cause -> MCPRuntimeDatabaseRecoveryPayloadFactory.create((RuntimeDatabaseConnectionException) cause)),
new RecoveryMapping(MCPToolCallLimitExceededException.class,
cause -> MCPBasicRecoveryPayloadFactory.createToolCallLimitRecovery((MCPToolCallLimitExceededException) cause)),
new RecoveryMapping(MCPInvalidToolArgumentException.class, cause -> MCPBasicRecoveryPayloadFactory.createInvalidToolArgumentRecovery((MCPInvalidToolArgumentException) cause)),
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRuntimeDatabaseRecoveryPayloadFactory.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRuntimeDatabaseRecoveryPayloadFactory.java
new file mode 100644
index 00000000000..a7f6295dac3
--- /dev/null
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRuntimeDatabaseRecoveryPayloadFactory.java
@@ -0,0 +1,111 @@
+/*
+ * 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.core.protocol.error;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowFieldNames;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * MCP runtime database recovery payload factory.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class MCPRuntimeDatabaseRecoveryPayloadFactory {
+
+ /**
+ * Create a runtime database recovery payload for a validation diagnostic.
+ *
+ * @param database database name
+ * @param category recovery category
+ * @return recovery payload
+ */
+ public static Map<String, Object> create(final String database, final String category) {
+ Map<String, Object> result = create(category);
+ if (!database.isBlank()) {
+ result.put(WorkflowFieldNames.DATABASE, database);
+ }
+ return result;
+ }
+
+ static Map<String, Object> create(final RuntimeDatabaseConnectionException cause) {
+ Map<String, Object> result = create(cause.getCategory());
+ result.put(WorkflowFieldNames.DATABASE, cause.getDatabaseName());
+ return result;
+ }
+
+ private static Map<String, Object> create(final String category) {
+ Map<String, Object> result = MCPRecoveryPayloadSupport.createBaseRecovery(category, createModelAction(category));
+ result.put(MCPPayloadFieldNames.RESOURCES_TO_READ, createResourcesToRead(category));
+ result.put(MCPPayloadFieldNames.NEXT_ACTIONS, createNextActions(category));
+ result.put("ask_user_when_uncertain", true);
+ return result;
+ }
+
+ private static String createModelAction(final String category) {
+ if (RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER.equals(category)) {
+ return "Install or configure the JDBC driver for the MCP runtime database, then retry.";
+ }
+ if (RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED.equals(category)) {
+ return "Check the runtime database credentials outside MCP, then retry.";
+ }
+ if (RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED.equals(category)) {
+ return "Check runtime database account privileges outside MCP, then retry.";
+ }
+ if (RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT.equals(category)) {
+ return "Check database reachability and timeout settings, then retry.";
+ }
+ if (RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION.equals(category)) {
+ return "Fix the MCP runtime database configuration outside MCP, then retry.";
+ }
+ if (RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(category)) {
+ return "Connect to the intended logical database or update the expected database name before retrying.";
+ }
+ return "Check the runtime database availability and configuration, then retry.";
+ }
+
+ private static List<Map<String, Object>> createResourcesToRead(final String category) {
+ Map<String, Object> runtimeResource = MCPResourceHintUtils.create(
+ "shardingsphere://runtime", "runtime", "read_first", "Read current MCP runtime status before retrying.", MCPPayloadFieldNames.RESOURCES_TO_READ);
+ if (!RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(category)) {
+ return List.of(runtimeResource);
+ }
+ return List.of(runtimeResource, MCPResourceHintUtils.create(
+ "shardingsphere://databases", "logical-database", "read_first", "Read visible logical databases before retrying.", MCPPayloadFieldNames.RESOURCES_TO_READ));
+ }
+
+ private static List<Map<String, Object>> createNextActions(final String category) {
+ Map<String, Object> readRuntime = MCPNextActionUtils.readResource("shardingsphere://runtime", "Read current MCP runtime status and configured database visibility.");
+ if (!RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(category)) {
+ return MCPNextActionUtils.ordered(readRuntime,
+ MCPNextActionUtils.dependsOn(MCPNextActionUtils.askUser(
+ "Ask the operator to fix the MCP runtime database configuration before retrying.", List.of("runtime_database_configuration")), 1));
+ }
+ return MCPNextActionUtils.ordered(readRuntime,
+ MCPNextActionUtils.dependsOn(MCPNextActionUtils.readResource("shardingsphere://databases", "Read visible logical databases before retrying."), 1),
+ MCPNextActionUtils.dependsOn(MCPNextActionUtils.askUser(
+ "Ask the operator to choose a visible logical database or update runtimeDatabases before retrying.",
+ List.of(WorkflowFieldNames.DATABASE, "runtimeDatabases")), 2));
+ }
+}
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandler.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandler.java
index 6a518d63184..a125d491ce3 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandler.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandler.java
@@ -21,6 +21,7 @@ import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
import org.apache.shardingsphere.mcp.core.tool.request.MCPToolArguments;
+import org.apache.shardingsphere.mcp.core.tool.response.SQLExecutionResponse;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
@@ -50,6 +51,6 @@ public final class ExecuteExplainToolHandler implements MCPToolHandler<MCPDataba
String schema = SQLExecutionToolHandlerSupport.resolveSchema(databaseContext, toolArguments);
SQLExecutionRequest executionRequest = SQLExecutionToolHandlerSupport.createReadOnlyExecutionRequest(toolCall, toolArguments,
schema, explainSql, TOOL_NAME);
- return databaseContext.getExecutionFacade().executeExplain(executionRequest, sql);
+ return SQLExecutionResponse.query(databaseContext.getExecutionFacade().executeExplain(executionRequest, sql));
}
}
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandler.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandler.java
index ac6505ecb5c..cef69c42082 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandler.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandler.java
@@ -21,6 +21,7 @@ import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
import org.apache.shardingsphere.mcp.core.tool.request.MCPToolArguments;
+import org.apache.shardingsphere.mcp.core.tool.response.SQLExecutionResponse;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
@@ -50,13 +51,13 @@ public final class ExecuteQueryToolHandler implements MCPToolHandler<MCPDatabase
String sql = toolArguments.getStringArgument("sql");
checkReadOnlyQuery(toolArguments, sql);
SQLExecutionToolHandlerSupport.checkExecutionArguments(toolArguments, TOOL_NAME);
- return databaseContext.getExecutionFacade().execute(SQLExecutionToolHandlerSupport.createReadOnlyExecutionRequest(toolCall, toolArguments,
- SQLExecutionToolHandlerSupport.resolveSchema(databaseContext, toolArguments), sql, TOOL_NAME));
+ return SQLExecutionResponse.query(databaseContext.getExecutionFacade().execute(SQLExecutionToolHandlerSupport.createReadOnlyExecutionRequest(toolCall, toolArguments,
+ SQLExecutionToolHandlerSupport.resolveSchema(databaseContext, toolArguments), sql, TOOL_NAME)));
}
private void checkReadOnlyQuery(final MCPToolArguments toolArguments, final String sql) {
ClassificationResult classificationResult = new StatementClassifier().classify(sql);
- if (!SQLExecutionToolHandlerSupport.isReadOnlyStatement(classificationResult)) {
+ if (!SQLExecutionToolHandlerSupport.isQueryStatement(classificationResult)) {
throw new SQLToolMismatchException(
"database_gateway_execute_query only supports classifier-approved QUERY statements. "
+ "Use database_gateway_execute_explain_query for EXPLAIN diagnostics or database_gateway_execute_update for side-effecting SQL.",
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
index 7880cc97569..726fef84bbe 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
@@ -22,6 +22,7 @@ import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPExecutionModeRequiredException;
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidExecutionModeException;
import org.apache.shardingsphere.mcp.core.tool.request.MCPToolArguments;
+import org.apache.shardingsphere.mcp.core.tool.response.SQLExecutionResponse;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
@@ -81,13 +82,13 @@ public final class ExecuteUpdateToolHandler implements MCPToolHandler<MCPDatabas
if (EXECUTION_MODE_PREVIEW.equals(executionMode)) {
return createPreviewResponse(toolArguments, classificationResult);
}
- return databaseContext.getExecutionFacade().execute(SQLExecutionToolHandlerSupport.createExecutionRequest(toolCall, toolArguments, sql, TOOL_NAME))
- .withExecutionMode(EXECUTION_MODE_EXECUTE);
+ return SQLExecutionResponse.executed(databaseContext.getExecutionFacade().execute(
+ SQLExecutionToolHandlerSupport.createExecutionRequest(toolCall, toolArguments, sql, TOOL_NAME)));
}
private ClassificationResult checkUpdateStatement(final MCPToolArguments toolArguments, final String sql) {
ClassificationResult classificationResult = new StatementClassifier().classify(sql);
- if (SQLExecutionToolHandlerSupport.isReadOnlyStatement(classificationResult)) {
+ if (SQLExecutionToolHandlerSupport.isQueryStatement(classificationResult)) {
throw new SQLToolMismatchException("database_gateway_execute_update does not accept read-only SQL. Use database_gateway_execute_query for read-only SQL.",
TOOL_NAME, "database_gateway_execute_query", classificationResult,
createQuerySuggestedArguments(toolArguments, classificationResult));
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 098d86d017f..447808d88f2 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
@@ -23,9 +23,9 @@ import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecution
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.metadata.jdbc.RuntimeDatabaseConfiguration;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryColumnDefinition;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+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.api.protocol.exception.MCPInvalidRequestException;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPTimeoutException;
@@ -63,7 +63,7 @@ public final class MCPJdbcStatementExecutor {
* @param executionRequest execution request
* @param classificationResult classification result
* @param databaseCapability database capability
- * @return execution response
+ * @return execution result
* @throws MCPTransactionStateException when the current transaction state blocks execution
* @throws MCPTimeoutException when the JDBC execution times out
* @throws MCPUnsupportedException when the JDBC driver or statement class is unsupported
@@ -72,7 +72,7 @@ public final class MCPJdbcStatementExecutor {
* @throws MCPQueryFailedException when query execution fails
* @throws MCPUnavailableException when the runtime database configuration is unavailable
*/
- public SQLExecutionResponse execute(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult, final MCPDatabaseCapability databaseCapability) {
+ public SQLExecutionResult execute(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult, final MCPDatabaseCapability databaseCapability) {
try {
Optional<Connection> transactionConnection = findTransactionConnection(executionRequest);
if (transactionConnection.isPresent()) {
@@ -109,14 +109,14 @@ public final class MCPJdbcStatementExecutor {
}
}
- private SQLExecutionResponse executeWithBorrowedConnection(final Connection connection, final SQLExecutionRequest executionRequest,
- final ClassificationResult classificationResult, final MCPDatabaseCapability databaseCapability) throws SQLException {
+ private SQLExecutionResult executeWithBorrowedConnection(final Connection connection, final SQLExecutionRequest executionRequest,
+ final ClassificationResult classificationResult, final MCPDatabaseCapability databaseCapability) throws SQLException {
applySchema(connection, executionRequest.getSchema(), databaseCapability.getSchemaExecutionSemantics());
return executeWithStatement(connection, executionRequest, classificationResult);
}
- private SQLExecutionResponse executeWithOwnedConnection(final Connection connection, final SQLExecutionRequest executionRequest,
- final ClassificationResult classificationResult, final MCPDatabaseCapability databaseCapability) throws SQLException {
+ private SQLExecutionResult executeWithOwnedConnection(final Connection connection, final SQLExecutionRequest executionRequest,
+ final ClassificationResult classificationResult, final MCPDatabaseCapability databaseCapability) throws SQLException {
try {
applySchema(connection, executionRequest.getSchema(), databaseCapability.getSchemaExecutionSemantics());
return executionRequest.isReadOnlyExecution()
@@ -127,8 +127,8 @@ public final class MCPJdbcStatementExecutor {
}
}
- private SQLExecutionResponse executeWithStatement(final Connection connection, final SQLExecutionRequest executionRequest,
- final ClassificationResult classificationResult) throws SQLException {
+ private SQLExecutionResult executeWithStatement(final Connection connection, final SQLExecutionRequest executionRequest,
+ final ClassificationResult classificationResult) throws SQLException {
try (Statement statement = connection.createStatement()) {
configureStatement(statement, executionRequest);
return executeStatement(statement, executionRequest, classificationResult);
@@ -142,8 +142,8 @@ public final class MCPJdbcStatementExecutor {
}
}
- private SQLExecutionResponse executeWithReadOnlyConnection(final Connection connection, final SQLExecutionRequest executionRequest,
- final ClassificationResult classificationResult) throws SQLException {
+ private SQLExecutionResult executeWithReadOnlyConnection(final Connection connection, final SQLExecutionRequest executionRequest,
+ final ClassificationResult classificationResult) throws SQLException {
boolean originalReadOnly = connection.isReadOnly();
boolean originalAutoCommit = connection.getAutoCommit();
boolean autoCommitDisabled = false;
@@ -219,8 +219,8 @@ public final class MCPJdbcStatementExecutor {
}
}
- private SQLExecutionResponse executeStatement(final Statement statement, final SQLExecutionRequest executionRequest,
- final ClassificationResult classificationResult) throws SQLException {
+ private SQLExecutionResult executeStatement(final Statement statement, final SQLExecutionRequest executionRequest,
+ final ClassificationResult classificationResult) throws SQLException {
boolean hasResultSet = statement.execute(classificationResult.getNormalizedSql());
switch (classificationResult.getStatementClass()) {
case QUERY:
@@ -228,35 +228,31 @@ public final class MCPJdbcStatementExecutor {
if (!hasResultSet) {
throw new QueryDidNotReturnResultSetException();
}
- return withExecutionHints(createResultSetResponse(statement.getResultSet(), executionRequest.getMaxRows(), classificationResult), executionRequest, classificationResult);
+ return createResultSetResult(statement.getResultSet(), executionRequest, classificationResult);
case DML:
return hasResultSet
- ? withExecutionHints(createResultSetResponse(statement.getResultSet(), executionRequest.getMaxRows(), classificationResult), executionRequest, classificationResult)
- : withExecutionHints(SQLExecutionResponse.updateCount(
- classificationResult.getStatementClass(), classificationResult.getStatementType(), statement.getUpdateCount()), executionRequest, classificationResult);
+ ? createResultSetResult(statement.getResultSet(), executionRequest, classificationResult)
+ : SQLExecutionResult.updateCount(classificationResult.getStatementClass(), classificationResult.getStatementType(), statement.getUpdateCount(),
+ executionRequest.getMaxRows(), executionRequest.getTimeoutMs(), classificationResult.getNormalizedSql());
case DDL:
case DCL:
- return withExecutionHints(SQLExecutionResponse.statementAck(
- classificationResult.getStatementClass(), classificationResult.getStatementType(), "Statement executed."), executionRequest, classificationResult);
+ return SQLExecutionResult.statementAck(classificationResult.getStatementClass(), classificationResult.getStatementType(),
+ executionRequest.getMaxRows(), executionRequest.getTimeoutMs(), classificationResult.getNormalizedSql());
default:
throw new StatementClassNotSupportedException();
}
}
- private SQLExecutionResponse withExecutionHints(final SQLExecutionResponse response, final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) {
- return response.withExecutionHints(executionRequest.getMaxRows(), executionRequest.getTimeoutMs()).withNormalizedSql(classificationResult.getNormalizedSql());
- }
-
- private SQLExecutionResponse createResultSetResponse(final ResultSet resultSet, final int maxRows, final ClassificationResult classificationResult) throws SQLException {
+ private SQLExecutionResult createResultSetResult(final ResultSet resultSet, final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) throws SQLException {
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
- LinkedList<ExecuteQueryColumnDefinition> columns = new LinkedList<>();
+ LinkedList<SQLExecutionColumnDefinition> columns = new LinkedList<>();
for (int index = 1; index <= resultSetMetaData.getColumnCount(); index++) {
- columns.add(new ExecuteQueryColumnDefinition(resultSetMetaData.getColumnLabel(index), resultSetMetaData.getColumnTypeName(index),
+ columns.add(new SQLExecutionColumnDefinition(resultSetMetaData.getColumnLabel(index), resultSetMetaData.getColumnTypeName(index),
resultSetMetaData.getColumnTypeName(index), ResultSetMetaData.columnNoNulls != resultSetMetaData.isNullable(index)));
}
LinkedList<List<Object>> rows = new LinkedList<>();
boolean truncated = false;
- int effectiveMaxRows = 0 >= maxRows ? Integer.MAX_VALUE : maxRows;
+ int effectiveMaxRows = 0 >= executionRequest.getMaxRows() ? Integer.MAX_VALUE : executionRequest.getMaxRows();
while (resultSet.next()) {
if (rows.size() >= effectiveMaxRows) {
truncated = true;
@@ -268,7 +264,8 @@ public final class MCPJdbcStatementExecutor {
}
rows.add(row);
}
- return SQLExecutionResponse.resultSet(classificationResult.getStatementClass(), classificationResult.getStatementType(), columns, rows, truncated);
+ return SQLExecutionResult.resultSet(classificationResult.getStatementClass(), classificationResult.getStatementType(), columns, rows, truncated,
+ executionRequest.getMaxRows(), executionRequest.getTimeoutMs(), classificationResult.getNormalizedSql());
}
private int resolveStatementMaxRows(final int maxRows) {
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutor.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutor.java
index 4f762772a72..d552e5b567b 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutor.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutor.java
@@ -19,14 +19,13 @@ package org.apache.shardingsphere.mcp.core.tool.handler.execute;
import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
-import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPTransactionStateException;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedException;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
import org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
import org.apache.shardingsphere.mcp.core.session.MCPSessionNotExistedException;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
/**
* MCP JDBC transaction statement executor.
@@ -43,27 +42,28 @@ public final class MCPJdbcTransactionStatementExecutor {
* @param databaseName logical database name
* @param databaseCapability resolved database capability
* @param classificationResult statement classification result
- * @return execution response
+ * @return execution result
* @throws MCPSessionNotExistedException when the session does not exist
* @throws MCPInvalidRequestException when the statement is not a transaction command
* @throws MCPTransactionStateException when the transaction state does not allow the operation
* @throws MCPUnsupportedException when the database does not support the requested transaction feature
*/
- public SQLExecutionResponse execute(final String sessionId, final String databaseName, final MCPDatabaseCapability databaseCapability, final ClassificationResult classificationResult) {
+ public SQLExecutionResult execute(final String sessionId, final String databaseName, final MCPDatabaseCapability databaseCapability,
+ final ClassificationResult classificationResult) {
String statementType = classificationResult.getStatementType();
try {
ShardingSpherePreconditions.checkState(sessionManager.hasSession(sessionId), MCPSessionNotExistedException::new);
- SQLExecutionResponse result;
if ("BEGIN".equals(statementType) || "START TRANSACTION".equals(statementType)) {
- result = executeBeginTransaction(sessionId, databaseName, databaseCapability, statementType);
+ executeBeginTransaction(sessionId, databaseName, databaseCapability);
} else if ("COMMIT".equals(statementType)) {
- result = executeCommit(sessionId, databaseCapability);
+ executeCommit(sessionId, databaseCapability);
} else if ("ROLLBACK".equals(statementType)) {
- result = executeRollback(sessionId, databaseCapability);
+ executeRollback(sessionId, databaseCapability);
} else {
- result = executeSavepointStatement(sessionId, databaseCapability, classificationResult);
+ executeSavepointStatement(sessionId, databaseCapability, classificationResult);
}
- return result.withNormalizedSql(classificationResult.getNormalizedSql());
+ return SQLExecutionResult.statementAck(classificationResult.getStatementClass(), statementType,
+ 0, 0, classificationResult.getNormalizedSql());
} catch (final IllegalArgumentException ex) {
throw new MCPInvalidRequestException(ex.getMessage(), ex);
} catch (final IllegalStateException ex) {
@@ -71,20 +71,18 @@ public final class MCPJdbcTransactionStatementExecutor {
}
}
- private SQLExecutionResponse executeSavepointStatement(final String sessionId, final MCPDatabaseCapability databaseCapability,
- final ClassificationResult classificationResult) {
+ private void executeSavepointStatement(final String sessionId, final MCPDatabaseCapability databaseCapability, final ClassificationResult classificationResult) {
String statementType = classificationResult.getStatementType();
String savepointName = classificationResult.getSavepointName().orElse("");
if ("SAVEPOINT".equals(statementType)) {
- return executeSavepoint(sessionId, databaseCapability, getRequiredSavepointName(savepointName));
+ executeSavepoint(sessionId, databaseCapability, getRequiredSavepointName(savepointName));
+ } else if ("ROLLBACK TO SAVEPOINT".equals(statementType)) {
+ executeRollbackSavepoint(sessionId, databaseCapability, getRequiredSavepointName(savepointName));
+ } else if ("RELEASE SAVEPOINT".equals(statementType)) {
+ executeReleaseSavepoint(sessionId, databaseCapability, getRequiredSavepointName(savepointName));
+ } else {
+ throw new MCPInvalidRequestException("Statement is not a transaction command.");
}
- if ("ROLLBACK TO SAVEPOINT".equals(statementType)) {
- return executeRollbackSavepoint(sessionId, databaseCapability, getRequiredSavepointName(savepointName));
- }
- if ("RELEASE SAVEPOINT".equals(statementType)) {
- return executeReleaseSavepoint(sessionId, databaseCapability, getRequiredSavepointName(savepointName));
- }
- throw new MCPInvalidRequestException("Statement is not a transaction command.");
}
private String getRequiredSavepointName(final String savepointName) {
@@ -93,45 +91,33 @@ public final class MCPJdbcTransactionStatementExecutor {
return result;
}
- private SQLExecutionResponse executeBeginTransaction(final String sessionId, final String databaseName, final MCPDatabaseCapability databaseCapability, final String statementType) {
+ private void executeBeginTransaction(final String sessionId, final String databaseName, final MCPDatabaseCapability databaseCapability) {
ShardingSpherePreconditions.checkState(databaseCapability.supportsTransactionControl(), () -> new MCPUnsupportedException("Transaction control is not supported."));
sessionManager.getTransactionResourceManager().beginTransaction(sessionId, databaseName);
- return SQLExecutionResponse.statementAck(classifyTransactionStatement(statementType), statementType, "Transaction started.");
}
- private SQLExecutionResponse executeCommit(final String sessionId, final MCPDatabaseCapability databaseCapability) {
+ private void executeCommit(final String sessionId, final MCPDatabaseCapability databaseCapability) {
ShardingSpherePreconditions.checkState(databaseCapability.supportsTransactionControl(), () -> new MCPUnsupportedException("Transaction control is not supported."));
sessionManager.getTransactionResourceManager().commitTransaction(sessionId);
- return SQLExecutionResponse.statementAck(classifyTransactionStatement("COMMIT"), "COMMIT", "Transaction committed.");
}
- private SQLExecutionResponse executeRollback(final String sessionId, final MCPDatabaseCapability databaseCapability) {
+ private void executeRollback(final String sessionId, final MCPDatabaseCapability databaseCapability) {
ShardingSpherePreconditions.checkState(databaseCapability.supportsTransactionControl(), () -> new MCPUnsupportedException("Transaction control is not supported."));
sessionManager.getTransactionResourceManager().rollbackTransaction(sessionId);
- return SQLExecutionResponse.statementAck(classifyTransactionStatement("ROLLBACK"), "ROLLBACK", "Transaction rolled back.");
}
- private SQLExecutionResponse executeSavepoint(final String sessionId, final MCPDatabaseCapability databaseCapability, final String savepointName) {
+ private void executeSavepoint(final String sessionId, final MCPDatabaseCapability databaseCapability, final String savepointName) {
ShardingSpherePreconditions.checkState(databaseCapability.supportsSavepoint(), () -> new MCPUnsupportedException("Savepoint is not supported."));
sessionManager.getTransactionResourceManager().createSavepoint(sessionId, savepointName);
- return SQLExecutionResponse.statementAck(classifyTransactionStatement("SAVEPOINT"), "SAVEPOINT", "Savepoint created.");
}
- private SQLExecutionResponse executeRollbackSavepoint(final String sessionId, final MCPDatabaseCapability databaseCapability, final String savepointName) {
+ private void executeRollbackSavepoint(final String sessionId, final MCPDatabaseCapability databaseCapability, final String savepointName) {
ShardingSpherePreconditions.checkState(databaseCapability.supportsSavepoint(), () -> new MCPUnsupportedException("Savepoint is not supported."));
sessionManager.getTransactionResourceManager().rollbackToSavepoint(sessionId, savepointName);
- return SQLExecutionResponse.statementAck(classifyTransactionStatement("ROLLBACK TO SAVEPOINT"), "ROLLBACK TO SAVEPOINT", "Savepoint rolled back.");
}
- private SQLExecutionResponse executeReleaseSavepoint(final String sessionId, final MCPDatabaseCapability databaseCapability, final String savepointName) {
+ private void executeReleaseSavepoint(final String sessionId, final MCPDatabaseCapability databaseCapability, final String savepointName) {
ShardingSpherePreconditions.checkState(databaseCapability.supportsSavepoint(), () -> new MCPUnsupportedException("Savepoint is not supported."));
sessionManager.getTransactionResourceManager().releaseSavepoint(sessionId, savepointName);
- return SQLExecutionResponse.statementAck(classifyTransactionStatement("RELEASE SAVEPOINT"), "RELEASE SAVEPOINT", "Savepoint released.");
- }
-
- private SupportedMCPStatement classifyTransactionStatement(final String statementType) {
- return "SAVEPOINT".equals(statementType) || "ROLLBACK TO SAVEPOINT".equals(statementType) || "RELEASE SAVEPOINT".equals(statementType)
- ? SupportedMCPStatement.SAVEPOINT
- : SupportedMCPStatement.TRANSACTION_CONTROL;
}
}
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 1b8d03dd74a..969b3cd949e 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
@@ -37,7 +37,7 @@ import org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabili
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.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
@@ -72,7 +72,7 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
}
@Override
- public SQLExecutionResponse execute(final SQLExecutionRequest executionRequest) {
+ public SQLExecutionResult execute(final SQLExecutionRequest executionRequest) {
try {
return sessionExecutionCoordinator.executeWithSessionLock(executionRequest.getSessionId(), () -> executeInternal(executionRequest, classify(executionRequest)));
} catch (final MCPSessionNotExistedException ex) {
@@ -80,7 +80,7 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
}
}
- private SQLExecutionResponse execute(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) {
+ private SQLExecutionResult execute(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) {
try {
return sessionExecutionCoordinator.executeWithSessionLock(executionRequest.getSessionId(), () -> executeInternal(executionRequest, classificationResult));
} catch (final MCPSessionNotExistedException ex) {
@@ -89,7 +89,7 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
}
@Override
- public SQLExecutionResponse executeExplain(final SQLExecutionRequest executionRequest, final String sql) {
+ public SQLExecutionResult executeExplain(final SQLExecutionRequest executionRequest, final String sql) {
ClassificationResult classificationResult = new ExplainSQLCandidateValidator().validate(sql, executionRequest.getSql());
try {
return execute(executionRequest, classificationResult);
@@ -125,7 +125,7 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
}
}
- private SQLExecutionResponse executeInternal(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) {
+ private SQLExecutionResult executeInternal(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) {
Optional<MCPDatabaseCapability> databaseCapability = databaseCapabilityProvider.provide(executionRequest.getDatabase());
ShardingSpherePreconditions.checkState(databaseCapability.isPresent(), () -> recordFailure(executionRequest, "QUERY", new DatabaseCapabilityNotFoundException()));
MCPDatabaseCapability actualDatabaseCapability = databaseCapability.orElseThrow();
@@ -154,9 +154,9 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
}
}
- private SQLExecutionResponse recordSuccess(final SQLExecutionRequest executionRequest, final SQLExecutionResponse response, final String statementMarker) {
+ private SQLExecutionResult recordSuccess(final SQLExecutionRequest executionRequest, final SQLExecutionResult result, final String statementMarker) {
sqlExecutionTraceFactory.create(executionRequest.getSessionId(), executionRequest.getDatabase(), executionRequest.getSql(), true, statementMarker);
- return response;
+ return result;
}
private void checkCrossSchemaSql(final SQLExecutionRequest executionRequest, final MCPDatabaseCapability databaseCapability, final ClassificationResult classificationResult) {
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
index e1e68c517a6..12e9a9372f9 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
@@ -35,7 +35,7 @@ import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class SQLExecutionToolHandlerSupport {
- static boolean isReadOnlyStatement(final ClassificationResult classificationResult) {
+ static boolean isQueryStatement(final ClassificationResult classificationResult) {
return SupportedMCPStatement.QUERY == classificationResult.getStatementClass();
}
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandler.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandler.java
index 51e545b8688..bd1eb2d5409 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandler.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandler.java
@@ -20,14 +20,10 @@ package org.apache.shardingsphere.mcp.core.tool.handler.metadata;
import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
-import org.apache.shardingsphere.mcp.core.protocol.error.MCPErrorConverter;
+import org.apache.shardingsphere.mcp.core.tool.response.RuntimeDatabaseValidationResponse;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
-import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
import org.apache.shardingsphere.mcp.support.database.tool.request.RuntimeDatabaseValidationRequest;
import org.apache.shardingsphere.mcp.support.database.tool.service.RuntimeDatabaseValidationService;
-import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
-
-import java.util.Map;
/**
* Handler for runtime database validation tool.
@@ -50,12 +46,7 @@ public final class ValidateRuntimeDatabaseToolHandler implements MCPToolHandler<
@Override
public MCPResponse handle(final MCPDatabaseHandlerContext databaseContext, final MCPToolCall toolCall) {
- return validationService.validate(RuntimeDatabaseValidationRequest.from(toolCall.getArguments()), databaseContext::findRuntimeDatabaseConfiguration, this::createRecoveryPayload);
- }
-
- @SuppressWarnings("unchecked")
- private Map<String, Object> createRecoveryPayload(final RuntimeDatabaseConnectionException cause) {
- Object result = MCPErrorConverter.convert(cause).toPayload().get(MCPPayloadFieldNames.RECOVERY);
- return result instanceof Map ? (Map<String, Object>) result : Map.of();
+ return RuntimeDatabaseValidationResponse.from(
+ validationService.validate(RuntimeDatabaseValidationRequest.from(toolCall.getArguments()), databaseContext::findRuntimeDatabaseConfiguration));
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/response/RuntimeDatabaseValidationResponse.java
similarity index 50%
rename from mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java
rename to mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/response/RuntimeDatabaseValidationResponse.java
index d689df4addb..c51485cb270 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/response/RuntimeDatabaseValidationResponse.java
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.tool.response;
+package org.apache.shardingsphere.mcp.core.tool.response;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.core.protocol.error.MCPRuntimeDatabaseRecoveryPayloadFactory;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationCheckResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationResult;
import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
@@ -28,47 +28,33 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
/**
- * Runtime database validation result.
+ * MCP response for runtime database validation.
*/
-@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
-@Getter
-public final class RuntimeDatabaseValidationResult implements MCPResponse {
+public final class RuntimeDatabaseValidationResponse implements MCPResponse {
- private final String status;
+ private static final String STATUS_READY = "ready";
- private final String database;
-
- private final List<RuntimeDatabaseValidationCheckResult> checks;
-
- private final String category;
+ private final RuntimeDatabaseValidationResult validationResult;
private final Map<String, Object> recovery;
- /**
- * Create a success result.
- *
- * @param database database name
- * @param checks check results
- * @return validation result
- */
- public static RuntimeDatabaseValidationResult ready(final String database, final List<RuntimeDatabaseValidationCheckResult> checks) {
- return new RuntimeDatabaseValidationResult("ready", Objects.toString(database, ""), checks, "ready", Map.of());
+ private RuntimeDatabaseValidationResponse(final RuntimeDatabaseValidationResult validationResult) {
+ this.validationResult = validationResult;
+ recovery = isReady()
+ ? Map.of()
+ : MCPRuntimeDatabaseRecoveryPayloadFactory.create(validationResult.getDatabase(), validationResult.getCategory());
}
/**
- * Create a failure result.
+ * Create a runtime database validation response.
*
- * @param database database name
- * @param checks check results
- * @param category failure category
- * @param recovery recovery payload
- * @return validation result
+ * @param validationResult validation result
+ * @return validation response
*/
- public static RuntimeDatabaseValidationResult failed(final String database, final List<RuntimeDatabaseValidationCheckResult> checks, final String category, final Map<String, Object> recovery) {
- return new RuntimeDatabaseValidationResult("failed", Objects.toString(database, ""), checks, category, null == recovery ? Map.of() : recovery);
+ public static RuntimeDatabaseValidationResponse from(final RuntimeDatabaseValidationResult validationResult) {
+ return new RuntimeDatabaseValidationResponse(validationResult);
}
@Override
@@ -76,10 +62,10 @@ public final class RuntimeDatabaseValidationResult implements MCPResponse {
Map<String, Object> result = new LinkedHashMap<>(8, 1F);
result.put("response_mode", MCPResponseMode.VALIDATION);
result.put(MCPPayloadFieldNames.SUMMARY, createSummary());
- result.put("status", status);
- result.put("database", database);
+ result.put("status", validationResult.getStatus());
+ result.put("database", validationResult.getDatabase());
result.put("checks", createChecksPayload());
- result.put("category", category);
+ result.put("category", validationResult.getCategory());
result.put(MCPPayloadFieldNames.RECOVERY, recovery);
if (recovery.containsKey(MCPPayloadFieldNames.NEXT_ACTIONS)) {
result.put(MCPPayloadFieldNames.NEXT_ACTIONS, recovery.get(MCPPayloadFieldNames.NEXT_ACTIONS));
@@ -88,15 +74,24 @@ public final class RuntimeDatabaseValidationResult implements MCPResponse {
}
private String createSummary() {
- return "ready".equals(status)
- ? String.format("Runtime database `%s` passed validation.", database)
- : String.format("Runtime database `%s` failed validation with category `%s`.", database, category);
+ return isReady()
+ ? String.format("Runtime database `%s` passed validation.", validationResult.getDatabase())
+ : String.format("Runtime database `%s` failed validation with category `%s`.", validationResult.getDatabase(), validationResult.getCategory());
+ }
+
+ private boolean isReady() {
+ return STATUS_READY.equals(validationResult.getStatus());
}
private List<Map<String, Object>> createChecksPayload() {
List<Map<String, Object>> result = new LinkedList<>();
- for (RuntimeDatabaseValidationCheckResult each : checks) {
- result.add(each.toPayload());
+ for (RuntimeDatabaseValidationCheckResult each : validationResult.getChecks()) {
+ Map<String, Object> check = new LinkedHashMap<>(4, 1F);
+ check.put("name", each.getName());
+ check.put("status", each.getStatus());
+ check.put("category", each.getCategory());
+ check.put("message", each.getMessage());
+ result.add(check);
}
return result;
}
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/response/SQLExecutionResponse.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/response/SQLExecutionResponse.java
new file mode 100644
index 00000000000..1ac9fcd37f1
--- /dev/null
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/response/SQLExecutionResponse.java
@@ -0,0 +1,204 @@
+/*
+ * 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.core.tool.response;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+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.support.protocol.MCPNextActionUtils;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * MCP response for SQL execution results.
+ */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public final class SQLExecutionResponse implements MCPResponse {
+
+ private static final int ROW_OBJECT_LIMIT = 100;
+
+ private static final String EXECUTED_TRUNCATION_SUMMARY = " Returned rows were truncated; do not replay the statement automatically.";
+
+ private static final String EXECUTED_TRUNCATION_ACTION = "The side-effecting statement already executed and returned truncated rows; do not replay it automatically. "
+ + "Use a separate read-only query if more data is needed.";
+
+ private final SQLExecutionResult executionResult;
+
+ private final String responseMode;
+
+ /**
+ * Create a response for a read-only query result.
+ *
+ * @param executionResult SQL execution result
+ * @return query response
+ */
+ public static SQLExecutionResponse query(final SQLExecutionResult executionResult) {
+ return new SQLExecutionResponse(executionResult, MCPResponseMode.QUERY);
+ }
+
+ /**
+ * Create a response for an executed side-effecting statement result.
+ *
+ * @param executionResult SQL execution result
+ * @return executed response
+ */
+ public static SQLExecutionResponse executed(final SQLExecutionResult executionResult) {
+ return new SQLExecutionResponse(executionResult, MCPResponseMode.EXECUTED);
+ }
+
+ @Override
+ public Map<String, Object> toPayload() {
+ Map<String, Object> result = new LinkedHashMap<>(33, 1F);
+ result.put("response_mode", responseMode);
+ result.put("result_kind", executionResult.getResultKind().name().toLowerCase(Locale.ENGLISH));
+ if (isExecuted()) {
+ result.put(MCPPayloadFieldNames.EXECUTION_MODE, "execute");
+ }
+ result.put("statement_class", executionResult.getStatementClass().name().toLowerCase(Locale.ENGLISH));
+ result.put("statement_type", executionResult.getStatementType());
+ result.put("status", "OK");
+ result.put(MCPPayloadFieldNames.SUMMARY, createSummary());
+ if (!executionResult.getNormalizedSql().isEmpty()) {
+ result.put("normalized_sql", executionResult.getNormalizedSql());
+ }
+ if (SQLExecutionResultKind.RESULT_SET == executionResult.getResultKind()) {
+ result.put("columns", executionResult.getColumns());
+ result.put("rows", executionResult.getRows());
+ appendRowObjects(result);
+ result.put("returned_row_count", executionResult.getRows().size());
+ }
+ if (SQLExecutionResultKind.UPDATE_COUNT == executionResult.getResultKind()) {
+ result.put("affected_rows", executionResult.getAffectedRows());
+ }
+ if (SQLExecutionResultKind.STATEMENT_ACK == executionResult.getResultKind()) {
+ result.put(MCPPayloadFieldNames.MESSAGE, createStatementAcknowledgement());
+ }
+ result.put("applied_max_rows", executionResult.getAppliedMaxRows());
+ result.put("applied_timeout_ms", executionResult.getAppliedTimeoutMs());
+ result.put("truncated", executionResult.isTruncated());
+ result.put(MCPPayloadFieldNames.NEXT_ACTIONS, createNextActions());
+ return result;
+ }
+
+ private boolean isExecuted() {
+ return MCPResponseMode.EXECUTED.equals(responseMode);
+ }
+
+ private String createSummary() {
+ return switch (executionResult.getResultKind()) {
+ case RESULT_SET -> createResultSetSummary();
+ case UPDATE_COUNT -> String.format("Executed %s statement and affected %d row(s).", executionResult.getStatementType(), executionResult.getAffectedRows());
+ case STATEMENT_ACK -> createStatementAcknowledgement();
+ };
+ }
+
+ private String createResultSetSummary() {
+ String result = isExecuted()
+ ? String.format("Executed side-effecting SQL (statement type %s) and returned %d row(s).", executionResult.getStatementType(), executionResult.getRows().size())
+ : String.format("Executed %s statement and returned %d row(s).", executionResult.getStatementType(), executionResult.getRows().size());
+ if (!executionResult.isTruncated()) {
+ return result;
+ }
+ return result + (isExecuted() ? EXECUTED_TRUNCATION_SUMMARY : " Result was truncated.");
+ }
+
+ private String createStatementAcknowledgement() {
+ switch (executionResult.getStatementType()) {
+ case "BEGIN", "START TRANSACTION":
+ return "Transaction started.";
+ case "COMMIT":
+ return "Transaction committed.";
+ case "ROLLBACK":
+ return "Transaction rolled back.";
+ case "SAVEPOINT":
+ return "Savepoint created.";
+ case "ROLLBACK TO SAVEPOINT":
+ return "Savepoint rolled back.";
+ case "RELEASE SAVEPOINT":
+ return "Savepoint released.";
+ default:
+ return "Statement executed.";
+ }
+ }
+
+ private void appendRowObjects(final Map<String, Object> payload) {
+ String rowObjectStatus = getRowObjectStatus();
+ payload.put("row_object_status", rowObjectStatus);
+ if ("available".equals(rowObjectStatus)) {
+ payload.put("row_objects", createRowObjects());
+ }
+ }
+
+ private String getRowObjectStatus() {
+ String unavailableStatus = getUnavailableRowObjectStatus();
+ if (!unavailableStatus.isEmpty()) {
+ return unavailableStatus;
+ }
+ return ROW_OBJECT_LIMIT < executionResult.getRows().size() ? "omitted_large_result" : "available";
+ }
+
+ private String getUnavailableRowObjectStatus() {
+ if (executionResult.getColumns().isEmpty()) {
+ return executionResult.getRows().isEmpty() ? "" : "unnamed_columns";
+ }
+ Set<String> columnNames = new LinkedHashSet<>(executionResult.getColumns().size());
+ for (SQLExecutionColumnDefinition each : executionResult.getColumns()) {
+ if (null == each.getColumnName() || each.getColumnName().isBlank()) {
+ return "unnamed_columns";
+ }
+ if (!columnNames.add(each.getColumnName())) {
+ return "duplicate_column_labels";
+ }
+ }
+ return "";
+ }
+
+ private List<Map<String, Object>> createRowObjects() {
+ return executionResult.getRows().stream().map(this::createRowObject).toList();
+ }
+
+ private Map<String, Object> createRowObject(final List<Object> row) {
+ Map<String, Object> result = new LinkedHashMap<>(executionResult.getColumns().size(), 1F);
+ for (int i = 0; i < executionResult.getColumns().size(); i++) {
+ result.put(executionResult.getColumns().get(i).getColumnName(), i < row.size() ? row.get(i) : null);
+ }
+ return result;
+ }
+
+ private List<Map<String, Object>> createNextActions() {
+ if (SQLExecutionResultKind.RESULT_SET == executionResult.getResultKind() && executionResult.isTruncated()) {
+ return isExecuted()
+ ? List.of(MCPNextActionUtils.stop(EXECUTED_TRUNCATION_ACTION))
+ : List.of(MCPNextActionUtils.askUser(
+ "The result was truncated by max_rows. Ask for a narrower SELECT, stronger WHERE clause, or smaller projection before retrying.", List.of("sql")));
+ }
+ return List.of(MCPNextActionUtils.stop(SQLExecutionResultKind.RESULT_SET == executionResult.getResultKind()
+ ? "Return the result rows to the user or ask a follow-up question if the user requested more analysis."
+ : "Report the execution status to the user and stop unless the user asks for another operation."));
+ }
+}
diff --git a/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml b/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
index 7e8bb7d5bf7..9c11dcc770c 100644
--- a/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
+++ b/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
@@ -1636,13 +1636,19 @@ tools:
description: "Execution or preview status returned by the server."
normalized_sql:
type: string
- description: "Classifier-normalized SQL returned by preview or recovery paths."
+ description: "Classifier-normalized SQL returned by preview or executed responses."
affected_rows:
type: integer
description: "Affected row count when the database reports one."
message:
type: string
description: "Execution message for statements without row results."
+ columns:
+ type: array
+ description: "Returned column definitions when result_kind is result_set."
+ rows:
+ type: array
+ description: "Returned result rows when result_kind is result_set."
returned_row_count:
type: integer
description: "Number of rows returned when result_kind is result_set."
@@ -1761,6 +1767,33 @@ tools:
depends_on:
- 1
reason: Execute only after reviewing normalized_sql and side_effect_scope; preview did not validate runtime executability.
+ - response_mode: executed
+ summary: Executed side-effecting SQL (statement type SELECT) and returned 1 row(s). Returned rows were truncated; do not replay the statement automatically.
+ result_kind: result_set
+ execution_mode: execute
+ statement_class: dml
+ statement_type: SELECT
+ status: OK
+ normalized_sql: "WITH updated AS (UPDATE orders SET status = 'PAID' WHERE order_id = 1 RETURNING order_id) SELECT order_id FROM updated"
+ columns:
+ - columnName: order_id
+ logicalType: BIGINT
+ nativeType: BIGINT
+ nullable: false
+ rows:
+ - - 1
+ row_objects:
+ - order_id: 1
+ row_object_status: available
+ returned_row_count: 1
+ applied_max_rows: 1
+ applied_timeout_ms: 0
+ truncated: true
+ next_actions:
+ - order: 1
+ type: terminal
+ title: Stop
+ reason: The side-effecting statement already executed and returned truncated rows; do not replay it automatically. Use a separate read-only query if more data is needed.
annotations:
title: Preview or Execute Side-Effecting SQL
readOnlyHint: false
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRuntimeDatabaseRecoveryPayloadFactoryTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRuntimeDatabaseRecoveryPayloadFactoryTest.java
new file mode 100644
index 00000000000..540073b661d
--- /dev/null
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRuntimeDatabaseRecoveryPayloadFactoryTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.core.protocol.error;
+
+import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MCPRuntimeDatabaseRecoveryPayloadFactoryTest {
+
+ @Test
+ void assertCreateValidationRecoveryWithoutDatabase() {
+ Map<String, Object> actual = MCPRuntimeDatabaseRecoveryPayloadFactory.create("", RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION);
+ assertThat(actual.get("category"), is("invalid_configuration"));
+ assertThat(actual.get("recovery_category"), is("unavailable_runtime"));
+ assertFalse(actual.containsKey("database"));
+ assertFalse(actual.containsKey("request_id"));
+ }
+
+ @Test
+ void assertCreateValidationRecoveryForInvisibleDatabase() {
+ Map<String, Object> actual = MCPRuntimeDatabaseRecoveryPayloadFactory.create("logic_db", RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE);
+ assertThat(actual.get("database"), is("logic_db"));
+ assertThat(actual.get("recovery_category"), is("validation"));
+ assertThat(((Map<?, ?>) ((List<?>) actual.get("resources_to_read")).get(1)).get("uri"), is("shardingsphere://databases"));
+ assertThat(((Map<?, ?>) ((List<?>) actual.get("next_actions")).get(2)).get("depends_on"), is(List.of(2)));
+ }
+
+ @Test
+ void assertCreateExceptionRecoveryPreservesDatabase() {
+ Map<String, Object> actual = MCPRuntimeDatabaseRecoveryPayloadFactory.create(
+ new RuntimeDatabaseConnectionException("", RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED, new SQLException("Connection failed.")));
+ assertTrue(actual.containsKey("database"));
+ assertThat(actual.get("database"), is(""));
+ }
+}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
index ea1e6d51731..354f4837088 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
@@ -302,7 +302,24 @@ class ServerCapabilitiesHandlerTest {
assertFalse(((String) executeUpdateStatementClass.get("description")).contains("explain_analyze"));
assertTrue(executeUpdateOutputProperties.containsKey("preview_semantics"));
assertTrue(executeUpdateOutputProperties.containsKey("review_summary"));
+ assertTrue(executeUpdateOutputProperties.containsKey("columns"));
+ assertTrue(executeUpdateOutputProperties.containsKey("rows"));
assertFalse(executeUpdateOutputProperties.containsKey("approval_summary"));
+ assertThat(((Map<?, ?>) executeUpdateOutputProperties.get("normalized_sql")).get("description"),
+ is("Classifier-normalized SQL returned by preview or executed responses."));
+ Map<?, ?> executedResultSetExample = findByKey((List<?>) ((Map<?, ?>) executeUpdateTool.get("outputSchema")).get("examples"), "response_mode", "executed");
+ assertThat(executedResultSetExample.get("result_kind"), is("result_set"));
+ assertThat(executedResultSetExample.get("statement_class"), is("dml"));
+ assertThat(executedResultSetExample.get("statement_type"), is("SELECT"));
+ assertThat(executedResultSetExample.get("summary"),
+ is("Executed side-effecting SQL (statement type SELECT) and returned 1 row(s). Returned rows were truncated; do not replay the statement automatically."));
+ assertTrue(executedResultSetExample.containsKey("columns"));
+ assertTrue(executedResultSetExample.containsKey("rows"));
+ assertTrue((Boolean) executedResultSetExample.get("truncated"));
+ Map<?, ?> executedResultSetNextAction = (Map<?, ?>) ((List<?>) executedResultSetExample.get("next_actions")).get(0);
+ assertThat(executedResultSetNextAction.get("type"), is("terminal"));
+ assertThat(executedResultSetNextAction.get("reason"), is(
+ "The side-effecting statement already executed and returned truncated rows; do not replay it automatically. Use a separate read-only query if more data is needed."));
assertThat(executeUpdateTool.get("title"), is("Preview or Execute Side-Effecting SQL"));
assertTrue(((String) executeUpdateTool.get("description")).contains("Preview is classification-only, not a database dry run."));
assertThat(findInputSchema(executeUpdateTool, "execution_mode").get("description"),
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandlerTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandlerTest.java
index 1f7fd5d674e..dc3916233ab 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandlerTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandlerTest.java
@@ -26,7 +26,7 @@ import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPSta
import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFacade;
import org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -47,7 +47,7 @@ class ExecuteExplainToolHandlerTest {
@Test
void assertHandleExplainQuery() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.executeExplain(any(), any())).thenReturn(SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN, "EXPLAIN", List.of(), List.of(), false));
+ when(executionFacade.executeExplain(any(), any())).thenReturn(createExplainResult());
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
MCPResponse actual = new ExecuteExplainToolHandler().handle(databaseContext, new MCPToolCall("session-1",
@@ -64,7 +64,7 @@ class ExecuteExplainToolHandlerTest {
@Test
void assertHandleExplainQueryWithSingleSchemaDefault() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.executeExplain(any(), any())).thenReturn(SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN, "EXPLAIN", List.of(), List.of(), false));
+ when(executionFacade.executeExplain(any(), any())).thenReturn(createExplainResult());
MCPMetadataQueryFacade metadataQueryFacade = mock(MCPMetadataQueryFacade.class);
when(metadataQueryFacade.querySchemas("logic_db")).thenReturn(List.of(new ShardingSphereSchema("public", mock(DatabaseType.class))));
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
@@ -76,4 +76,8 @@ class ExecuteExplainToolHandlerTest {
verify(executionFacade).executeExplain(requestCaptor.capture(), eq("SELECT * FROM orders"));
assertThat(requestCaptor.getValue().getSchema(), is("public"));
}
+
+ private SQLExecutionResult createExplainResult() {
+ return SQLExecutionResult.resultSet(SupportedMCPStatement.EXPLAIN, "EXPLAIN", List.of(), List.of(), false, 100, 0, "EXPLAIN SELECT * FROM orders");
+ }
}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandlerTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandlerTest.java
index 5fd32c7f831..053716cc0cc 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandlerTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteQueryToolHandlerTest.java
@@ -24,10 +24,11 @@ import org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedExcept
import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFacade;
import org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -49,7 +50,7 @@ class ExecuteQueryToolHandlerTest {
@Test
void assertHandleReadOnlyQuery() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.resultSet(List.of(), List.of(), false));
+ when(executionFacade.execute(any())).thenReturn(createQueryResult());
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
MCPResponse actual = new ExecuteQueryToolHandler().handle(databaseContext, new MCPToolCall("session-1",
@@ -68,7 +69,7 @@ class ExecuteQueryToolHandlerTest {
@Test
void assertHandleReadOnlyQueryWithSingleSchemaDefault() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.resultSet(List.of(), List.of(), false));
+ when(executionFacade.execute(any())).thenReturn(createQueryResult());
MCPMetadataQueryFacade metadataQueryFacade = mock(MCPMetadataQueryFacade.class);
when(metadataQueryFacade.querySchemas("logic_db")).thenReturn(List.of(new ShardingSphereSchema("public", mock(DatabaseType.class))));
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
@@ -127,7 +128,7 @@ class ExecuteQueryToolHandlerTest {
@Test
void assertHandleZeroMaxRowsAsDefault() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.resultSet(List.of(), List.of(), false));
+ when(executionFacade.execute(any())).thenReturn(createQueryResult());
MCPMetadataQueryFacade metadataQueryFacade = mock(MCPMetadataQueryFacade.class);
when(metadataQueryFacade.querySchemas("logic_db")).thenReturn(List.of(new ShardingSphereSchema("public", mock(DatabaseType.class))));
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
@@ -139,4 +140,8 @@ class ExecuteQueryToolHandlerTest {
verify(executionFacade).execute(requestCaptor.capture());
assertThat(requestCaptor.getValue().getMaxRows(), is(100));
}
+
+ private SQLExecutionResult createQueryResult() {
+ return SQLExecutionResult.resultSet(SupportedMCPStatement.QUERY, "SELECT", List.of(), List.of(), false, 100, 0, "SELECT * FROM orders");
+ }
}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
index 8907b14fd5a..867f7263f7b 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
@@ -23,9 +23,10 @@ import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidToolArgumentException;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
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.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -47,7 +48,7 @@ class ExecuteUpdateToolHandlerTest {
@Test
void assertHandleUpdateStatement() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.updateCount("UPDATE", 1));
+ when(executionFacade.execute(any())).thenReturn(createUpdateResult());
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
MCPResponse actual = new ExecuteUpdateToolHandler().handle(databaseContext, new MCPToolCall("session-1",
@@ -67,7 +68,7 @@ class ExecuteUpdateToolHandlerTest {
@Test
void assertHandleExecutionWithoutApprovalArgument() {
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.updateCount("UPDATE", 1));
+ when(executionFacade.execute(any())).thenReturn(createUpdateResult());
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
MCPResponse actual = new ExecuteUpdateToolHandler().handle(databaseContext, new MCPToolCall("session-1",
@@ -184,4 +185,8 @@ class ExecuteUpdateToolHandlerTest {
verifyNoInteractions(executionFacade);
}
+ private SQLExecutionResult createUpdateResult() {
+ return SQLExecutionResult.updateCount(SupportedMCPStatement.DML, "UPDATE", 1, 100, 0, "UPDATE orders SET status = 'PAID'");
+ }
+
}
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 d5e97c08911..af5a33fa906 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
@@ -23,10 +23,10 @@ import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecution
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.metadata.jdbc.RuntimeDatabaseConfiguration;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryColumnDefinition;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryResultKind;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+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;
@@ -72,20 +72,19 @@ class MCPJdbcStatementExecutorTest {
@ParameterizedTest(name = "{0}")
@MethodSource("assertExecuteCases")
void assertExecute(final String name, final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult, final Connection connection,
- final ExecuteQueryResultKind expectedResultKind, final String expectedStatementType, final int expectedRowCount, final int expectedAffectedRows,
- final String expectedMessage, final boolean expectedTruncated) throws SQLException {
+ final SQLExecutionResultKind expectedResultKind, final String expectedStatementType, final int expectedRowCount, final int expectedAffectedRows,
+ final boolean expectedTruncated) throws SQLException {
RuntimeDatabaseConfiguration runtimeDatabaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(runtimeDatabaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcTransactionResourceManager transactionResourceManager = mock(MCPJdbcTransactionResourceManager.class);
when(transactionResourceManager.findTransactionConnection(anyString(), anyString())).thenReturn(Optional.empty());
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", runtimeDatabaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(executionRequest, classificationResult, createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
+ SQLExecutionResult actual = statementExecutor.execute(executionRequest, classificationResult, createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
assertThat(actual.getResultKind(), is(expectedResultKind));
assertThat(actual.getStatementClass(), is(classificationResult.getStatementClass()));
assertThat(actual.getStatementType(), is(expectedStatementType));
assertThat(actual.getRows().size(), is(expectedRowCount));
assertThat(actual.getAffectedRows(), is(expectedAffectedRows));
- assertThat(actual.getMessage(), is(expectedMessage));
assertThat(actual.isTruncated(), is(expectedTruncated));
assertThat(actual.getAppliedMaxRows(), is(executionRequest.getMaxRows()));
assertThat(actual.getAppliedTimeoutMs(), is(executionRequest.getTimeoutMs()));
@@ -99,7 +98,7 @@ class MCPJdbcStatementExecutorTest {
MCPJdbcTransactionResourceManager transactionResourceManager = mock(MCPJdbcTransactionResourceManager.class);
when(transactionResourceManager.findTransactionConnection(anyString(), anyString())).thenReturn(Optional.of(connection));
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Collections.emptyMap(), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "public", "UPDATE orders SET status = 'DONE'", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DML, "UPDATE", "UPDATE orders SET status = 'DONE'", "", List.of()),
createDatabaseCapability(SchemaExecutionSemantics.BEST_EFFORT));
@@ -122,7 +121,7 @@ class MCPJdbcStatementExecutorTest {
RuntimeDatabaseConfiguration databaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "public", "SELECT status FROM orders", 10, 1000, true),
new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT status FROM orders", "", List.of()), createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
assertThat(actual.getRows().size(), is(1));
@@ -140,7 +139,7 @@ class MCPJdbcStatementExecutorTest {
MCPJdbcTransactionResourceManager transactionResourceManager = mock(MCPJdbcTransactionResourceManager.class);
when(transactionResourceManager.findTransactionConnection(anyString(), anyString())).thenReturn(Optional.of(connection));
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Collections.emptyMap(), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "public", "SELECT status FROM orders", 10, 1000, true),
new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT status FROM orders", "", List.of()), createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
assertThat(actual.getRows().size(), is(1));
@@ -163,7 +162,7 @@ class MCPJdbcStatementExecutorTest {
RuntimeDatabaseConfiguration databaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", " ", "SELECT status FROM orders", 0, 0),
new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT status FROM orders", "", List.of()), createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
assertThat(actual.getRows().size(), is(2));
@@ -185,7 +184,7 @@ class MCPJdbcStatementExecutorTest {
RuntimeDatabaseConfiguration databaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "public", "SELECT status FROM orders", Integer.MAX_VALUE, 1000),
new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT status FROM orders", "", List.of()), createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
assertThat(actual.getRows().size(), is(0));
@@ -196,7 +195,7 @@ class MCPJdbcStatementExecutorTest {
@Test
void assertExecuteWithDmlResultSet() throws SQLException {
Statement statement = mock(Statement.class);
- ResultSet resultSet = createResultSet(List.of(new ExecuteQueryColumnDefinition("order_id", "INTEGER", "INTEGER", false)), List.of(List.of(1)));
+ ResultSet resultSet = createResultSet(List.of(new SQLExecutionColumnDefinition("order_id", "INTEGER", "INTEGER", false)), List.of(List.of(1)));
when(statement.execute(anyString())).thenReturn(true);
when(statement.getResultSet()).thenReturn(resultSet);
Connection connection = createStatementConnection(statement);
@@ -205,12 +204,12 @@ class MCPJdbcStatementExecutorTest {
RuntimeDatabaseConfiguration databaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "public", "WITH updated_orders AS (UPDATE orders SET status = 'DONE' RETURNING *) SELECT * FROM updated_orders", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DML, "SELECT",
"WITH updated_orders AS (UPDATE orders SET status = 'DONE' RETURNING *) SELECT * FROM updated_orders", "", List.of()),
createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
- assertThat(actual.getResultKind(), is(ExecuteQueryResultKind.RESULT_SET));
+ assertThat(actual.getResultKind(), is(SQLExecutionResultKind.RESULT_SET));
assertThat(actual.getStatementClass(), is(SupportedMCPStatement.DML));
assertThat(actual.getStatementType(), is("SELECT"));
assertThat(actual.getRows().size(), is(1));
@@ -224,7 +223,7 @@ class MCPJdbcStatementExecutorTest {
RuntimeDatabaseConfiguration databaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "logic_db", "UPDATE orders SET status = 'DONE'", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DML, "UPDATE", "UPDATE orders SET status = 'DONE'", "", List.of()),
createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
@@ -374,7 +373,7 @@ class MCPJdbcStatementExecutorTest {
RuntimeDatabaseConfiguration databaseConfig = mock(RuntimeDatabaseConfiguration.class);
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
MCPJdbcStatementExecutor statementExecutor = new MCPJdbcStatementExecutor(Map.of("logic_db", databaseConfig), transactionResourceManager);
- SQLExecutionResponse actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
+ SQLExecutionResult actual = statementExecutor.execute(new SQLExecutionRequest("session-1",
"logic_db", "public", "UPDATE orders SET status = 'DONE'", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DML, "UPDATE", "UPDATE orders SET status = 'DONE'", "", List.of()),
createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE));
@@ -431,7 +430,7 @@ class MCPJdbcStatementExecutorTest {
return result;
}
- private static ResultSet createResultSet(final List<ExecuteQueryColumnDefinition> columns, final List<List<Object>> rows) throws SQLException {
+ private static ResultSet createResultSet(final List<SQLExecutionColumnDefinition> columns, final List<List<Object>> rows) throws SQLException {
ResultSet result = mock(ResultSet.class);
ResultSetMetaData resultSetMetaData = mock(ResultSetMetaData.class);
AtomicInteger rowIndex = new AtomicInteger(-1);
@@ -440,7 +439,7 @@ class MCPJdbcStatementExecutorTest {
when(result.getObject(anyInt())).thenAnswer(invocation -> rows.get(rowIndex.get()).get(invocation.getArgument(0, Integer.class) - 1));
when(resultSetMetaData.getColumnCount()).thenReturn(columns.size());
for (int index = 0; index < columns.size(); index++) {
- ExecuteQueryColumnDefinition column = columns.get(index);
+ SQLExecutionColumnDefinition column = columns.get(index);
int columnIndex = index + 1;
when(resultSetMetaData.getColumnLabel(columnIndex)).thenReturn(column.getColumnName());
when(resultSetMetaData.getColumnTypeName(columnIndex)).thenReturn(column.getNativeType());
@@ -454,20 +453,20 @@ class MCPJdbcStatementExecutorTest {
Arguments.of("query result set", new SQLExecutionRequest("session-1", "logic_db", "public", "SELECT status FROM orders ORDER BY order_id", 1, 1000),
new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT status FROM orders ORDER BY order_id", "", List.of()),
createStatementConnection(true, 0, createResultSet(createColumns(), List.of(List.of(1, "NEW"), List.of(2, "DONE")))),
- ExecuteQueryResultKind.RESULT_SET, "SELECT", 1, 0, "", true),
+ SQLExecutionResultKind.RESULT_SET, "SELECT", 1, 0, true),
Arguments.of("explain result set", new SQLExecutionRequest("session-1", "logic_db", "public", "EXPLAIN SELECT * FROM orders", 10, 1000),
new ClassificationResult(SupportedMCPStatement.EXPLAIN, "EXPLAIN", "EXPLAIN SELECT * FROM orders", "", List.of()),
- createStatementConnection(true, 0, createResultSet(List.of(new ExecuteQueryColumnDefinition("plan", "VARCHAR", "VARCHAR", true)), List.of(List.of("plan")))),
- ExecuteQueryResultKind.RESULT_SET, "EXPLAIN", 1, 0, "", false),
+ createStatementConnection(true, 0, createResultSet(List.of(new SQLExecutionColumnDefinition("plan", "VARCHAR", "VARCHAR", true)), List.of(List.of("plan")))),
+ SQLExecutionResultKind.RESULT_SET, "EXPLAIN", 1, 0, false),
Arguments.of("dml update count", new SQLExecutionRequest("session-1", "logic_db", "public", "UPDATE orders SET status = 'DONE'", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DML, "UPDATE", "UPDATE orders SET status = 'DONE'", "", List.of()), createStatementConnection(false, 3, null),
- ExecuteQueryResultKind.UPDATE_COUNT, "UPDATE", 0, 3, "", false),
+ SQLExecutionResultKind.UPDATE_COUNT, "UPDATE", 0, 3, false),
Arguments.of("ddl ack", new SQLExecutionRequest("session-1", "logic_db", "public", "CREATE TABLE orders_archive", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DDL, "CREATE", "CREATE TABLE orders_archive", "", List.of()), createStatementConnection(false, 0, null),
- ExecuteQueryResultKind.STATEMENT_ACK, "CREATE", 0, 0, "Statement executed.", false),
+ SQLExecutionResultKind.STATEMENT_ACK, "CREATE", 0, 0, false),
Arguments.of("dcl ack", new SQLExecutionRequest("session-1", "logic_db", "public", "GRANT SELECT ON orders TO app_user", 10, 1000),
new ClassificationResult(SupportedMCPStatement.DCL, "GRANT", "GRANT SELECT ON orders TO app_user", "", List.of()), createStatementConnection(false, 0, null),
- ExecuteQueryResultKind.STATEMENT_ACK, "GRANT", 0, 0, "Statement executed.", false));
+ SQLExecutionResultKind.STATEMENT_ACK, "GRANT", 0, 0, false));
}
private static Stream<Arguments> assertExecuteWithSQLExceptionCases() {
@@ -481,9 +480,9 @@ class MCPJdbcStatementExecutorTest {
Arguments.of("query failed", new SQLException("query failed"), MCPQueryFailedException.class, "query failed"));
}
- private static List<ExecuteQueryColumnDefinition> createColumns() {
+ private static List<SQLExecutionColumnDefinition> createColumns() {
return List.of(
- new ExecuteQueryColumnDefinition("order_id", "INTEGER", "INTEGER", false),
- new ExecuteQueryColumnDefinition("status", "VARCHAR", "VARCHAR", true));
+ new SQLExecutionColumnDefinition("order_id", "INTEGER", "INTEGER", false),
+ new SQLExecutionColumnDefinition("status", "VARCHAR", "VARCHAR", true));
}
}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutorTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutorTest.java
index 10595118777..c5e5dede040 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutorTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcTransactionStatementExecutorTest.java
@@ -17,8 +17,6 @@
package org.apache.shardingsphere.mcp.core.tool.handler.execute;
-import org.apache.shardingsphere.mcp.support.database.metadata.TransactionCapability;
-
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPTransactionStateException;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedException;
@@ -26,8 +24,10 @@ import org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
import org.apache.shardingsphere.mcp.core.session.MCPSessionNotExistedException;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+import org.apache.shardingsphere.mcp.support.database.metadata.TransactionCapability;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResultKind;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -54,7 +54,7 @@ class MCPJdbcTransactionStatementExecutorTest {
@ParameterizedTest(name = "{0}")
@MethodSource("assertExecuteCases")
- void assertExecute(final String name, final String sql, final String expectedStatementType, final String expectedMessage) throws SQLException {
+ void assertExecute(final String name, final String sql, final String expectedStatementType) throws SQLException {
Connection connection = mock(Connection.class);
Savepoint savepoint = mock(Savepoint.class);
RuntimeDatabaseConfiguration runtimeDatabaseConfig = mock(RuntimeDatabaseConfiguration.class);
@@ -63,22 +63,26 @@ class MCPJdbcTransactionStatementExecutorTest {
sessionManager.createSession("session-1");
prepareTransactionState(sql, sessionManager, connection, savepoint);
MCPJdbcTransactionStatementExecutor executor = new MCPJdbcTransactionStatementExecutor(sessionManager);
- SQLExecutionResponse actual = executor.execute("session-1", "logic_db", createCapability(), new StatementClassifier().classify(sql));
+ ClassificationResult classificationResult = new StatementClassifier().classify(sql);
+ SQLExecutionResult actual = executor.execute("session-1", "logic_db", createCapability(), classificationResult);
+ assertThat(actual.getResultKind(), is(SQLExecutionResultKind.STATEMENT_ACK));
+ assertThat(actual.getStatementClass(), is(classificationResult.getStatementClass()));
assertThat(actual.getStatementType(), is(expectedStatementType));
- assertThat(actual.getMessage(), is(expectedMessage));
+ assertThat(actual.getAppliedMaxRows(), is(0));
+ assertThat(actual.getAppliedTimeoutMs(), is(0));
assertThat(actual.getNormalizedSql(), is(sql));
assertDatabaseExecution(sql, sessionManager, runtimeDatabaseConfig, connection, savepoint);
}
static Stream<Arguments> assertExecuteCases() {
return Stream.of(
- Arguments.of("begin", "BEGIN", "BEGIN", "Transaction started."),
- Arguments.of("start transaction", "START TRANSACTION", "START TRANSACTION", "Transaction started."),
- Arguments.of("commit", "COMMIT", "COMMIT", "Transaction committed."),
- Arguments.of("rollback", "ROLLBACK", "ROLLBACK", "Transaction rolled back."),
- Arguments.of("savepoint", "SAVEPOINT sp_1", "SAVEPOINT", "Savepoint created."),
- Arguments.of("rollback to savepoint", "ROLLBACK TO SAVEPOINT sp_1", "ROLLBACK TO SAVEPOINT", "Savepoint rolled back."),
- Arguments.of("release savepoint", "RELEASE SAVEPOINT sp_1", "RELEASE SAVEPOINT", "Savepoint released."));
+ Arguments.of("begin", "BEGIN", "BEGIN"),
+ Arguments.of("start transaction", "START TRANSACTION", "START TRANSACTION"),
+ Arguments.of("commit", "COMMIT", "COMMIT"),
+ Arguments.of("rollback", "ROLLBACK", "ROLLBACK"),
+ Arguments.of("savepoint", "SAVEPOINT sp_1", "SAVEPOINT"),
+ Arguments.of("rollback to savepoint", "ROLLBACK TO SAVEPOINT sp_1", "ROLLBACK TO SAVEPOINT"),
+ Arguments.of("release savepoint", "RELEASE SAVEPOINT sp_1", "RELEASE SAVEPOINT"));
}
@Test
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 c6671b35894..64d3d4a1c99 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
@@ -32,7 +32,7 @@ import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPSta
import org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
import org.apache.shardingsphere.mcp.support.database.exception.StatementClassNotSupportedException;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -41,7 +41,6 @@ import org.mockito.ArgumentCaptor;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -90,16 +89,16 @@ class MCPSQLExecutionFacadeTest {
SQLExecutionRequest request = createExecutionRequest("SELECT 1");
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT 1", "", List.of());
- SQLExecutionResponse response = mock(SQLExecutionResponse.class);
- ArgumentCaptor<Supplier<SQLExecutionResponse>> lockedExecution = ArgumentCaptor.forClass(Supplier.class);
- when(coordinator.executeWithSessionLock(eq("session-1"), lockedExecution.capture())).thenReturn(response);
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
+ ArgumentCaptor<Supplier<SQLExecutionResult>> lockedExecution = ArgumentCaptor.forClass(Supplier.class);
+ when(coordinator.executeWithSessionLock(eq("session-1"), lockedExecution.capture())).thenReturn(expectedResult);
when(statementClassifier.classify("SELECT 1")).thenReturn(classification);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
- when(statementExecutor.execute(request, classification, capability)).thenReturn(response);
+ when(statementExecutor.execute(request, classification, capability)).thenReturn(expectedResult);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, statementClassifier);
- assertThat(facade.execute(request), is(response));
+ assertThat(facade.execute(request), is(expectedResult));
verifyNoInteractions(capabilityProvider, transactionExecutor, statementExecutor, statementClassifier, traceFactory);
- assertThat(lockedExecution.getValue().get(), is(response));
+ assertThat(lockedExecution.getValue().get(), is(expectedResult));
verify(statementClassifier).classify("SELECT 1");
verify(capabilityProvider).provide("logic_db");
verify(statementExecutor).execute(request, classification, capability);
@@ -171,16 +170,16 @@ class MCPSQLExecutionFacadeTest {
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.TRANSACTION_CONTROL));
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.TRANSACTION_CONTROL, "BEGIN", "BEGIN", "", List.of());
- SQLExecutionResponse response = SQLExecutionResponse.statementAck(SupportedMCPStatement.TRANSACTION_CONTROL, "BEGIN", "Transaction started.");
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
- when(transactionExecutor.execute("session-1", "logic_db", capability, classification)).thenReturn(response);
+ when(transactionExecutor.execute("session-1", "logic_db", capability, classification)).thenReturn(expectedResult);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
SQLExecutionRequest request = createExecutionRequest("BEGIN");
- SQLExecutionResponse actual = facade.execute(request);
- assertThat(actual, is(response));
+ SQLExecutionResult actual = facade.execute(request);
+ assertThat(actual, is(expectedResult));
verify(transactionExecutor).execute("session-1", "logic_db", capability, classification);
verify(traceFactory).create("session-1", "logic_db", "BEGIN", true, "BEGIN");
verifyNoInteractions(statementExecutor);
@@ -194,16 +193,16 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT 1", "",
List.of(SQLStatementObjectName.fromNormalizedName("orders")));
- SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
SQLExecutionRequest request = createExecutionRequest("SELECT 1");
- when(statementExecutor.execute(request, classification, capability)).thenReturn(response);
+ when(statementExecutor.execute(request, classification, capability)).thenReturn(expectedResult);
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
- SQLExecutionResponse actual = facade.execute(request);
- assertThat(actual, is(response));
+ SQLExecutionResult actual = facade.execute(request);
+ assertThat(actual, is(expectedResult));
verify(statementExecutor).execute(request, classification, capability);
verify(traceFactory).create("session-1", "logic_db", "SELECT 1", true, "QUERY");
verifyNoInteractions(transactionExecutor);
@@ -294,16 +293,16 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM logic_db.orders", "",
List.of(SQLStatementObjectName.fromNormalizedName("logic_db.orders")));
- SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
SQLExecutionRequest request = createExecutionRequest("SELECT * FROM logic_db.orders");
- when(statementExecutor.execute(request, classification, capability)).thenReturn(response);
+ when(statementExecutor.execute(request, classification, capability)).thenReturn(expectedResult);
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
- SQLExecutionResponse actual = facade.execute(request);
- assertThat(actual, is(response));
+ SQLExecutionResult actual = facade.execute(request);
+ assertThat(actual, is(expectedResult));
verify(statementExecutor).execute(request, classification, capability);
verify(traceFactory).create("session-1", "logic_db", "SELECT * FROM logic_db.orders", true, "QUERY");
verifyNoInteractions(transactionExecutor);
@@ -315,14 +314,14 @@ class MCPSQLExecutionFacadeTest {
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
- SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
SQLExecutionRequest request = createExecutionRequest("SELECT * FROM Logic_DB.orders");
- when(statementExecutor.execute(eq(request), any(), eq(capability))).thenReturn(response);
+ when(statementExecutor.execute(eq(request), any(), eq(capability))).thenReturn(expectedResult);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, mock(MCPJdbcTransactionStatementExecutor.class), statementExecutor,
mock(SQLExecutionTraceFactory.class));
- assertThat(facade.execute(request), is(response));
+ assertThat(facade.execute(request), is(expectedResult));
}
@ParameterizedTest(name = "{0}")
@@ -348,16 +347,16 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), SchemaExecutionSemantics.BEST_EFFORT);
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM other_db.orders", "",
List.of(SQLStatementObjectName.fromNormalizedName("other_db.orders")));
- SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
SQLExecutionRequest request = createExecutionRequest("SELECT * FROM other_db.orders");
- when(statementExecutor.execute(request, classification, capability)).thenReturn(response);
+ when(statementExecutor.execute(request, classification, capability)).thenReturn(expectedResult);
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
- SQLExecutionResponse actual = facade.execute(request);
- assertThat(actual, is(response));
+ SQLExecutionResult actual = facade.execute(request);
+ assertThat(actual, is(expectedResult));
verify(statementExecutor).execute(request, classification, capability);
verify(traceFactory).create("session-1", "logic_db", "SELECT * FROM other_db.orders", true, "QUERY");
verifyNoInteractions(transactionExecutor);
@@ -390,16 +389,16 @@ class MCPSQLExecutionFacadeTest {
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.EXPLAIN));
SQLExecutionRequest request = createExecutionRequest("EXPLAIN SELECT * FROM orders");
- SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN, "EXPLAIN", Collections.emptyList(), Collections.emptyList(), false);
+ SQLExecutionResult expectedResult = mock(SQLExecutionResult.class);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
- when(statementExecutor.execute(eq(request), any(), eq(capability))).thenReturn(response);
+ when(statementExecutor.execute(eq(request), any(), eq(capability))).thenReturn(expectedResult);
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory);
- SQLExecutionResponse actual = facade.executeExplain(request, "SELECT * FROM orders");
- assertThat(actual, is(response));
+ SQLExecutionResult actual = facade.executeExplain(request, "SELECT * FROM orders");
+ assertThat(actual, is(expectedResult));
ArgumentCaptor<ClassificationResult> classificationCaptor = ArgumentCaptor.forClass(ClassificationResult.class);
verify(statementExecutor).execute(eq(request), classificationCaptor.capture(), eq(capability));
assertThat(classificationCaptor.getValue().getStatementClass(), is(SupportedMCPStatement.EXPLAIN));
@@ -459,7 +458,7 @@ class MCPSQLExecutionFacadeTest {
@SuppressWarnings("unchecked")
private void mockSessionLock(final MCPSessionExecutionCoordinator coordinator) {
- when(coordinator.executeWithSessionLock(eq("session-1"), any())).thenAnswer(invocation -> ((Supplier<SQLExecutionResponse>) invocation.getArgument(1, Supplier.class)).get());
+ when(coordinator.executeWithSessionLock(eq("session-1"), any())).thenAnswer(invocation -> ((Supplier<SQLExecutionResult>) invocation.getArgument(1, Supplier.class)).get());
}
private MCPDatabaseCapability createCapability(final Set<SupportedMCPStatement> supportedStatementClasses) {
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandlerTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandlerTest.java
index 34c67f244c6..a27e1b79e5c 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandlerTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateRuntimeDatabaseToolHandlerTest.java
@@ -22,8 +22,8 @@ import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
import org.apache.shardingsphere.mcp.support.database.tool.request.RuntimeDatabaseValidationRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.RuntimeDatabaseValidationCheckResult;
-import org.apache.shardingsphere.mcp.support.database.tool.response.RuntimeDatabaseValidationResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationCheckResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationResult;
import org.apache.shardingsphere.mcp.support.database.tool.service.RuntimeDatabaseValidationService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -49,7 +49,7 @@ class ValidateRuntimeDatabaseToolHandlerTest {
void assertHandle() {
try (
MockedConstruction<RuntimeDatabaseValidationService> mocked =
- mockConstruction(RuntimeDatabaseValidationService.class, (mock, context) -> when(mock.validate(any(), any(), any())).thenReturn(RuntimeDatabaseValidationResult.ready(
+ mockConstruction(RuntimeDatabaseValidationService.class, (mock, context) -> when(mock.validate(any(), any())).thenReturn(RuntimeDatabaseValidationResult.ready(
"logic_db", List.of(RuntimeDatabaseValidationCheckResult.passed("configuration", "Validated the request.")))))) {
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
RuntimeDatabaseConfiguration runtimeDatabaseConfig = mock(RuntimeDatabaseConfiguration.class);
@@ -58,7 +58,7 @@ class ValidateRuntimeDatabaseToolHandlerTest {
assertThat(actual.toPayload().get("response_mode"), is("validation"));
ArgumentCaptor<RuntimeDatabaseValidationRequest> requestCaptor = ArgumentCaptor.forClass(RuntimeDatabaseValidationRequest.class);
ArgumentCaptor<Function<String, Optional<RuntimeDatabaseConfiguration>>> resolverCaptor = ArgumentCaptor.forClass(Function.class);
- verify(mocked.constructed().getFirst()).validate(requestCaptor.capture(), resolverCaptor.capture(), any());
+ verify(mocked.constructed().getFirst()).validate(requestCaptor.capture(), resolverCaptor.capture());
assertThat(requestCaptor.getValue().getDatabase(), is("logic_db"));
assertThat(resolverCaptor.getValue().apply("logic_db"), is(Optional.of(runtimeDatabaseConfig)));
}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/response/RuntimeDatabaseValidationResponseTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/response/RuntimeDatabaseValidationResponseTest.java
new file mode 100644
index 00000000000..06dd45c2644
--- /dev/null
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/response/RuntimeDatabaseValidationResponseTest.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.core.tool.response;
+
+import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationCheckResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationResult;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class RuntimeDatabaseValidationResponseTest {
+
+ @Test
+ void assertReady() {
+ Map<String, Object> actual = RuntimeDatabaseValidationResponse.from(RuntimeDatabaseValidationResult.ready(
+ "logic_db", List.of(RuntimeDatabaseValidationCheckResult.passed("configuration", "Resolved the configured runtime database.")))).toPayload();
+ assertThat(actual, is(Map.of(
+ "response_mode", "validation",
+ "summary", "Runtime database `logic_db` passed validation.",
+ "status", "ready",
+ "database", "logic_db",
+ "checks", List.of(Map.of(
+ "name", "configuration",
+ "status", "passed",
+ "category", "ready",
+ "message", "Resolved the configured runtime database.")),
+ "category", "ready",
+ "recovery", Map.of())));
+ }
+
+ @Test
+ void assertFailed() {
+ Map<String, Object> actual = RuntimeDatabaseValidationResponse.from(RuntimeDatabaseValidationResult.failed(
+ "logic_db", List.of(RuntimeDatabaseValidationCheckResult.failed("configuration", "invalid_configuration", "Database is not configured.")),
+ RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION)).toPayload();
+ assertThat(actual.get("summary"), is("Runtime database `logic_db` failed validation with category `invalid_configuration`."));
+ assertThat(actual.get("status"), is("failed"));
+ Map<?, ?> actualRecovery = (Map<?, ?>) actual.get("recovery");
+ assertThat(actualRecovery.get("database"), is("logic_db"));
+ assertThat(actualRecovery.get("category"), is("invalid_configuration"));
+ assertFalse(actualRecovery.containsKey("request_id"));
+ assertThat(actual.get("next_actions"), is(actualRecovery.get("next_actions")));
+ }
+
+ @Test
+ void assertFailedWithoutDatabase() {
+ Map<String, Object> actual = RuntimeDatabaseValidationResponse.from(RuntimeDatabaseValidationResult.failed(
+ "", List.of(RuntimeDatabaseValidationCheckResult.failed("configuration", "invalid_configuration", "Database is required.")),
+ RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION)).toPayload();
+ assertThat(actual.get("database"), is(""));
+ assertFalse(((Map<?, ?>) actual.get("recovery")).containsKey("database"));
+ }
+
+ @Test
+ void assertStablePayload() {
+ RuntimeDatabaseValidationResponse response = RuntimeDatabaseValidationResponse.from(RuntimeDatabaseValidationResult.failed(
+ "logic_db", List.of(RuntimeDatabaseValidationCheckResult.failed("database_visibility", "database_not_visible", "Database is not visible.")),
+ RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE));
+ Map<String, Object> actual = response.toPayload();
+ assertThat(response.toPayload(), is(actual));
+ }
+}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/response/SQLExecutionResponseTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/response/SQLExecutionResponseTest.java
new file mode 100644
index 00000000000..8e574fdafdd
--- /dev/null
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/response/SQLExecutionResponseTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.core.tool.response;
+
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+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.protocol.MCPNextActionUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class SQLExecutionResponseTest {
+
+ @Test
+ void assertQueryResultSet() {
+ SQLExecutionColumnDefinition column = new SQLExecutionColumnDefinition("order_id", "INT", "INT", false);
+ Map<String, Object> actual = SQLExecutionResponse.query(SQLExecutionResult.resultSet(
+ SupportedMCPStatement.QUERY, "SELECT", List.of(column), List.of(List.of(1)), false, 10, 5000, "SELECT order_id FROM orders")).toPayload();
+ assertThat(actual, is(Map.ofEntries(
+ Map.entry("response_mode", "query"), Map.entry("result_kind", "result_set"), Map.entry("statement_class", "query"), Map.entry("statement_type", "SELECT"),
+ Map.entry("status", "OK"), Map.entry("summary", "Executed SELECT statement and returned 1 row(s)."),
+ Map.entry("normalized_sql", "SELECT order_id FROM orders"), Map.entry("columns", List.of(column)), Map.entry("rows", List.of(List.of(1))),
+ Map.entry("row_object_status", "available"), Map.entry("row_objects", List.of(Map.of("order_id", 1))), Map.entry("returned_row_count", 1),
+ Map.entry("applied_max_rows", 10), Map.entry("applied_timeout_ms", 5000), Map.entry("truncated", false),
+ Map.entry("next_actions", List.of(MCPNextActionUtils.stop(
+ "Return the result rows to the user or ask a follow-up question if the user requested more analysis."))))));
+ }
+
+ @Test
+ void assertTruncatedQueryResultSet() {
+ Map<String, Object> actual = SQLExecutionResponse.query(SQLExecutionResult.resultSet(
+ SupportedMCPStatement.QUERY, "SELECT", List.of(), List.of(), true, 10, 0, "SELECT * FROM orders")).toPayload();
+ assertThat(actual.get("summary"), is("Executed SELECT statement and returned 0 row(s). Result was truncated."));
+ assertThat(actual.get("next_actions"), is(List.of(MCPNextActionUtils.askUser(
+ "The result was truncated by max_rows. Ask for a narrower SELECT, stronger WHERE clause, or smaller projection before retrying.", List.of("sql")))));
+ }
+
+ @Test
+ void assertExecutedUpdateCount() {
+ Map<String, Object> actual = SQLExecutionResponse.executed(SQLExecutionResult.updateCount(
+ SupportedMCPStatement.DML, "UPDATE", 2, 100, 0, "UPDATE orders SET status = 'PAID'")).toPayload();
+ assertThat(actual.get("response_mode"), is("executed"));
+ assertThat(actual.get("execution_mode"), is("execute"));
+ assertThat(actual.get("result_kind"), is("update_count"));
+ assertThat(actual.get("affected_rows"), is(2));
+ assertThat(actual.get("summary"), is("Executed UPDATE statement and affected 2 row(s)."));
+ }
+
+ @Test
+ void assertExecutedResultSet() {
+ Map<String, Object> actual = SQLExecutionResponse.executed(SQLExecutionResult.resultSet(
+ SupportedMCPStatement.DML, "SELECT", List.of(), List.of(List.of(1)), false, 100, 0, "WITH changed AS (...) SELECT * FROM changed")).toPayload();
+ assertThat(actual.get("summary"), is("Executed side-effecting SQL (statement type SELECT) and returned 1 row(s)."));
+ assertThat(actual.get("response_mode"), is("executed"));
+ assertThat(actual.get("execution_mode"), is("execute"));
+ }
+
+ @Test
+ void assertTruncatedExecutedResultSet() {
+ Map<String, Object> actual = SQLExecutionResponse.executed(SQLExecutionResult.resultSet(
+ SupportedMCPStatement.DML, "SELECT", List.of(), List.of(List.of(1)), true, 1, 0, "WITH changed AS (...) SELECT * FROM changed")).toPayload();
+ assertThat(actual.get("summary"), is("Executed side-effecting SQL (statement type SELECT) and returned 1 row(s). "
+ + "Returned rows were truncated; do not replay the statement automatically."));
+ assertThat(actual.get("next_actions"), is(List.of(MCPNextActionUtils.stop(
+ "The side-effecting statement already executed and returned truncated rows; do not replay it automatically. "
+ + "Use a separate read-only query if more data is needed."))));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @CsvSource({
+ "TRANSACTION_CONTROL, BEGIN, Transaction started.",
+ "TRANSACTION_CONTROL, START TRANSACTION, Transaction started.",
+ "TRANSACTION_CONTROL, COMMIT, Transaction committed.",
+ "TRANSACTION_CONTROL, ROLLBACK, Transaction rolled back.",
+ "SAVEPOINT, SAVEPOINT, Savepoint created.",
+ "SAVEPOINT, ROLLBACK TO SAVEPOINT, Savepoint rolled back.",
+ "SAVEPOINT, RELEASE SAVEPOINT, Savepoint released.",
+ "DDL, CREATE, Statement executed."
+ })
+ void assertStatementAcknowledgement(final SupportedMCPStatement statementClass, final String statementType, final String expectedMessage) {
+ Map<String, Object> actual = SQLExecutionResponse.executed(SQLExecutionResult.statementAck(
+ statementClass, statementType, 100, 0, statementType)).toPayload();
+ assertThat(actual.get("message"), is(expectedMessage));
+ assertThat(actual.get("summary"), is(expectedMessage));
+ }
+
+ @Test
+ void assertUnnamedColumns() {
+ Map<String, Object> actual = SQLExecutionResponse.query(SQLExecutionResult.resultSet(
+ SupportedMCPStatement.QUERY, "SELECT", List.of(), List.of(List.of(1)), false, 100, 0, "SELECT 1")).toPayload();
+ assertThat(actual.get("row_object_status"), is("unnamed_columns"));
+ assertFalse(actual.containsKey("row_objects"));
+ }
+
+ @Test
+ void assertBlankColumnName() {
+ Map<String, Object> actual = SQLExecutionResponse.query(SQLExecutionResult.resultSet(SupportedMCPStatement.QUERY, "SELECT",
+ List.of(new SQLExecutionColumnDefinition("", "INT", "INT", false)), List.of(List.of(1)), false, 100, 0, "SELECT 1")).toPayload();
+ assertThat(actual.get("row_object_status"), is("unnamed_columns"));
+ assertFalse(actual.containsKey("row_objects"));
+ }
+
+ @Test
+ void assertDuplicateColumnLabels() {
+ List<SQLExecutionColumnDefinition> columns = List.of(
+ new SQLExecutionColumnDefinition("order_id", "INT", "INT", false), new SQLExecutionColumnDefinition("order_id", "INT", "INT", false));
+ Map<String, Object> actual = SQLExecutionResponse.query(SQLExecutionResult.resultSet(
+ SupportedMCPStatement.QUERY, "SELECT", columns, List.of(List.of(1, 2)), false, 100, 0, "SELECT order_id, order_id FROM orders")).toPayload();
+ assertThat(actual.get("row_object_status"), is("duplicate_column_labels"));
+ assertFalse(actual.containsKey("row_objects"));
+ }
+
+ @Test
+ void assertLargeRowObjectsOmitted() {
+ List<List<Object>> rows = new LinkedList<>();
+ for (int i = 0; i < 101; i++) {
+ rows.add(List.of(i));
+ }
+ Map<String, Object> actual = SQLExecutionResponse.query(SQLExecutionResult.resultSet(SupportedMCPStatement.QUERY, "SELECT",
+ List.of(new SQLExecutionColumnDefinition("order_id", "INT", "INT", false)), rows, false, 100, 0, "SELECT order_id FROM orders")).toPayload();
+ assertThat(actual.get("row_object_status"), is("omitted_large_result"));
+ assertFalse(actual.containsKey("row_objects"));
+ }
+}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowExecutionServiceTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowExecutionServiceTest.java
index 1195f7a618b..d9139a693d4 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowExecutionServiceTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowExecutionServiceTest.java
@@ -21,7 +21,7 @@ import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestExc
import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFacade;
import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
import org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
import org.apache.shardingsphere.mcp.support.diagnostic.MCPDiagnosticCategory;
import org.apache.shardingsphere.mcp.support.workflow.WorkflowSessionContext;
import org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmPropertyRequirement;
@@ -169,7 +169,7 @@ class WorkflowExecutionServiceTest {
snapshot.setStatus("previewed");
snapshot.getDdlArtifacts().add(new DDLArtifact("add-column", "ALTER TABLE orders ADD COLUMN order_id_cipher VARCHAR(32)", 10));
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResponse.class));
+ when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResult.class));
Map<String, Object> actualResponse = apply(snapshot, executionFacade, MCPWorkflowApplySynchronizationHandler.NO_OP, List.of(), "review-then-execute");
assertThat(actualResponse.get("status"), is("failed"));
assertThat(((Map<?, ?>) ((List<?>) actualResponse.get("issues")).getFirst()).get("code"), is(WorkflowIssueCode.WORKFLOW_STATUS_INVALID));
@@ -324,7 +324,7 @@ class WorkflowExecutionServiceTest {
snapshot.getRuleArtifacts().add(new RuleArtifact("create", "CREATE MASK RULE orders"));
WorkflowSessionContext workflowSessionContext = createWorkflowSessionContext(snapshot);
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResponse.class));
+ when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResult.class));
Map<String, Object> actualResponse = apply(workflowSessionContext, snapshot, executionFacade, MCPWorkflowApplySynchronizationHandler.NO_OP,
List.of("ddl", "index_ddl", "rule_distsql"), "review-then-execute");
assertThat(actualResponse.get("status"), is("completed"));
@@ -344,7 +344,7 @@ class WorkflowExecutionServiceTest {
snapshot.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "aes-key-value", true, true, "AES key.", ""));
snapshot.getRuleArtifacts().add(new RuleArtifact("create", "CREATE ENCRYPT RULE orders (PROPERTIES('aes-key-value'='123456'))"));
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResponse.class));
+ when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResult.class));
Map<String, Object> actualResponse = apply(snapshot, executionFacade, MCPWorkflowApplySynchronizationHandler.NO_OP, List.of("rule_distsql"), "review-then-execute");
assertThat(actualResponse.get("status"), is("completed"));
assertThat(((List<?>) actualResponse.get("executed_distsql")).getFirst(), is("CREATE ENCRYPT RULE orders (PROPERTIES('aes-key-value'='******'))"));
@@ -379,7 +379,7 @@ class WorkflowExecutionServiceTest {
snapshot.getRuleArtifacts().add(new RuleArtifact("create", "CREATE MASK RULE orders"));
WorkflowSessionContext workflowSessionContext = createWorkflowSessionContext(snapshot);
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResponse.class));
+ when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResult.class));
MCPWorkflowApplySynchronizationHandler workflowApplySynchronizationHandler = mock(MCPWorkflowApplySynchronizationHandler.class);
Map<String, Object> actualResponse = apply(workflowSessionContext, snapshot, executionFacade, workflowApplySynchronizationHandler, List.of("ddl"), "review-then-execute");
List<?> actualStepResults = (List<?>) actualResponse.get("step_results");
@@ -399,7 +399,7 @@ class WorkflowExecutionServiceTest {
snapshot.getRuleArtifacts().add(new RuleArtifact("create", "ALTER MASK RULE orders"));
WorkflowSessionContext workflowSessionContext = createWorkflowSessionContext(snapshot);
MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResponse.class));
+ when(executionFacade.execute(any())).thenReturn(mock(SQLExecutionResult.class));
MCPWorkflowApplySynchronizationHandler workflowApplySynchronizationHandler = mock(MCPWorkflowApplySynchronizationHandler.class);
doThrow(new WorkflowSynchronizationException(WorkflowIssueCode.RULE_STATE_MISMATCH, "Mask rule is missing.",
List.of(Map.of("code", WorkflowIssueCode.RULE_STATE_MISMATCH)))).when(workflowApplySynchronizationHandler).synchronize(any(), any(), any(), any(), any());
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPFeatureExecutionFacade.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPFeatureExecutionFacade.java
index 3780f7f4d43..ae1f9bddf06 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPFeatureExecutionFacade.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/spi/MCPFeatureExecutionFacade.java
@@ -18,7 +18,7 @@
package org.apache.shardingsphere.mcp.support.database.spi;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import org.apache.shardingsphere.mcp.support.database.tool.result.SQLExecutionResult;
/**
* MCP feature execution facade.
@@ -29,16 +29,16 @@ public interface MCPFeatureExecutionFacade {
* Execute SQL request.
*
* @param executionRequest SQL execution request
- * @return execution response
+ * @return execution result
*/
- SQLExecutionResponse execute(SQLExecutionRequest executionRequest);
+ SQLExecutionResult execute(SQLExecutionRequest executionRequest);
/**
* Execute database-native EXPLAIN SQL for an original query.
*
* @param executionRequest EXPLAIN SQL execution request
* @param sql original query SQL
- * @return execution response
+ * @return execution result
*/
- SQLExecutionResponse executeExplain(SQLExecutionRequest executionRequest, String sql);
+ SQLExecutionResult executeExplain(SQLExecutionRequest executionRequest, String sql);
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java
deleted file mode 100644
index 7fe2e4e9e41..00000000000
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * 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.tool.response;
-
-import lombok.AccessLevel;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
-import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryColumnDefinition;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryResultKind;
-import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
-import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
-import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * SQL execution response.
- */
-@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
-@Builder(access = AccessLevel.PRIVATE, toBuilder = true)
-@Getter
-public final class SQLExecutionResponse implements MCPResponse {
-
- private static final int ROW_OBJECT_LIMIT = 100;
-
- private final ExecuteQueryResultKind resultKind;
-
- private final SupportedMCPStatement statementClass;
-
- private final List<ExecuteQueryColumnDefinition> columns;
-
- private final List<List<Object>> rows;
-
- private final int affectedRows;
-
- private final String statementType;
-
- private final String message;
-
- private final boolean truncated;
-
- private final int appliedMaxRows;
-
- private final int appliedTimeoutMs;
-
- private final String normalizedSql;
-
- private final String responseMode;
-
- private final String executionMode;
-
- /**
- * Create a result-set response.
- *
- * @param columns column definitions
- * @param rows result rows
- * @param truncated truncation flag
- * @return result-set response
- */
- public static SQLExecutionResponse resultSet(final List<ExecuteQueryColumnDefinition> columns, final List<List<Object>> rows, final boolean truncated) {
- return resultSet(SupportedMCPStatement.QUERY, "SELECT", columns, rows, truncated);
- }
-
- /**
- * Create a result-set response.
- *
- * @param statementClass statement class
- * @param statementType statement type
- * @param columns column definitions
- * @param rows result rows
- * @param truncated truncation flag
- * @return result-set response
- */
- public static SQLExecutionResponse resultSet(final SupportedMCPStatement statementClass, final String statementType,
- final List<ExecuteQueryColumnDefinition> columns, final List<List<Object>> rows, final boolean truncated) {
- return SQLExecutionResponse.builder()
- .resultKind(ExecuteQueryResultKind.RESULT_SET)
- .statementClass(statementClass)
- .columns(normalizeColumns(columns))
- .rows(normalizeRows(rows))
- .statementType(statementType)
- .message("")
- .truncated(truncated)
- .normalizedSql("")
- .responseMode(MCPResponseMode.QUERY)
- .executionMode("")
- .build();
- }
-
- /**
- * Create an update-count response.
- *
- * @param statementType statement type
- * @param affectedRows affected row count
- * @return update-count response
- */
- public static SQLExecutionResponse updateCount(final String statementType, final int affectedRows) {
- return updateCount(SupportedMCPStatement.DML, statementType, affectedRows);
- }
-
- /**
- * Create an update-count response.
- *
- * @param statementClass statement class
- * @param statementType statement type
- * @param affectedRows affected row count
- * @return update-count response
- */
- public static SQLExecutionResponse updateCount(final SupportedMCPStatement statementClass, final String statementType, final int affectedRows) {
- return SQLExecutionResponse.builder()
- .resultKind(ExecuteQueryResultKind.UPDATE_COUNT)
- .statementClass(statementClass)
- .columns(Collections.emptyList())
- .rows(Collections.emptyList())
- .affectedRows(affectedRows)
- .statementType(statementType)
- .message("")
- .normalizedSql("")
- .responseMode(MCPResponseMode.EXECUTED)
- .executionMode("")
- .build();
- }
-
- /**
- * Create a statement acknowledgement response.
- *
- * @param statementClass statement class
- * @param statementType statement type
- * @param message acknowledgement message
- * @return acknowledgement response
- */
- public static SQLExecutionResponse statementAck(final SupportedMCPStatement statementClass, final String statementType, final String message) {
- return SQLExecutionResponse.builder()
- .resultKind(ExecuteQueryResultKind.STATEMENT_ACK)
- .statementClass(statementClass)
- .columns(Collections.emptyList())
- .rows(Collections.emptyList())
- .statementType(statementType)
- .message(message)
- .normalizedSql("")
- .responseMode(MCPResponseMode.EXECUTED)
- .executionMode("")
- .build();
- }
-
- /**
- * Create a copy with applied execution hints.
- *
- * @param appliedMaxRows applied max rows argument
- * @param appliedTimeoutMs applied timeout milliseconds argument
- * @return response with execution hints
- */
- public SQLExecutionResponse withExecutionHints(final int appliedMaxRows, final int appliedTimeoutMs) {
- return toBuilder().appliedMaxRows(appliedMaxRows).appliedTimeoutMs(appliedTimeoutMs).build();
- }
-
- /**
- * Create a copy with normalized SQL.
- *
- * @param normalizedSql normalized SQL
- * @return response with normalized SQL
- */
- public SQLExecutionResponse withNormalizedSql(final String normalizedSql) {
- return toBuilder().normalizedSql(null == normalizedSql ? "" : normalizedSql).build();
- }
-
- /**
- * Create a copy with side-effect tool execution mode markers.
- *
- * @param executionMode execution mode
- * @return response with execution mode markers
- */
- public SQLExecutionResponse withExecutionMode(final String executionMode) {
- return toBuilder().responseMode(MCPResponseMode.EXECUTED).executionMode(null == executionMode ? "" : executionMode).build();
- }
-
- @Override
- public Map<String, Object> toPayload() {
- Map<String, Object> result = new LinkedHashMap<>(33, 1F);
- result.put("response_mode", responseMode);
- result.put("result_kind", resultKind.name().toLowerCase(Locale.ENGLISH));
- if (!executionMode.isEmpty()) {
- result.put(MCPPayloadFieldNames.EXECUTION_MODE, executionMode);
- }
- result.put("statement_class", statementClass.name().toLowerCase(Locale.ENGLISH));
- result.put("statement_type", statementType);
- result.put("status", "OK");
- result.put(MCPPayloadFieldNames.SUMMARY, createSummary());
- if (!normalizedSql.isEmpty()) {
- result.put("normalized_sql", normalizedSql);
- }
- if (ExecuteQueryResultKind.RESULT_SET == resultKind) {
- result.put("columns", columns);
- result.put("rows", rows);
- appendRowObjects(result);
- result.put("returned_row_count", rows.size());
- }
- if (ExecuteQueryResultKind.UPDATE_COUNT == resultKind) {
- result.put("affected_rows", affectedRows);
- }
- if (ExecuteQueryResultKind.STATEMENT_ACK == resultKind) {
- result.put(MCPPayloadFieldNames.MESSAGE, message);
- }
- result.put("applied_max_rows", appliedMaxRows);
- result.put("applied_timeout_ms", appliedTimeoutMs);
- result.put("truncated", truncated);
- result.put(MCPPayloadFieldNames.NEXT_ACTIONS, createNextActions());
- return result;
- }
-
- private String createSummary() {
- return switch (resultKind) {
- case RESULT_SET -> createResultSetSummary();
- case UPDATE_COUNT -> String.format("Executed %s statement and affected %d row(s).", statementType, affectedRows);
- case STATEMENT_ACK -> message.isEmpty() ? String.format("Executed %s statement.", statementType) : message;
- };
- }
-
- private String createResultSetSummary() {
- String result = String.format("Executed %s statement and returned %d row(s).", statementType, rows.size());
- return truncated ? result + " Result was truncated." : result;
- }
-
- private List<Map<String, Object>> createNextActions() {
- if (ExecuteQueryResultKind.RESULT_SET == resultKind && truncated) {
- return List.of(MCPNextActionUtils.askUser("The result was truncated by max_rows. Ask for a narrower SELECT, stronger WHERE clause, or smaller projection before retrying.",
- List.of("sql")));
- }
- return List.of(createStopAction(ExecuteQueryResultKind.RESULT_SET == resultKind
- ? "Return the result rows to the user or ask a follow-up question if the user requested more analysis."
- : "Report the execution status to the user and stop unless the user asks for another operation."));
- }
-
- private Map<String, Object> createStopAction(final String reason) {
- return MCPNextActionUtils.stop(reason);
- }
-
- private void appendRowObjects(final Map<String, Object> payload) {
- String rowObjectStatus = getRowObjectStatus();
- payload.put("row_object_status", rowObjectStatus);
- if ("available".equals(rowObjectStatus)) {
- payload.put("row_objects", createRowObjects());
- }
- }
-
- private String getRowObjectStatus() {
- String unavailableStatus = getUnavailableRowObjectStatus();
- if (!unavailableStatus.isEmpty()) {
- return unavailableStatus;
- }
- return ROW_OBJECT_LIMIT < rows.size() ? "omitted_large_result" : "available";
- }
-
- private String getUnavailableRowObjectStatus() {
- if (columns.isEmpty()) {
- return rows.isEmpty() ? "" : "unnamed_columns";
- }
- Set<String> columnNames = new LinkedHashSet<>(columns.size());
- for (ExecuteQueryColumnDefinition each : columns) {
- if (null == each.getColumnName() || each.getColumnName().isBlank()) {
- return "unnamed_columns";
- }
- if (!columnNames.add(each.getColumnName())) {
- return "duplicate_column_labels";
- }
- }
- return "";
- }
-
- private List<Map<String, Object>> createRowObjects() {
- return rows.stream().map(this::createRowObject).toList();
- }
-
- private Map<String, Object> createRowObject(final List<Object> row) {
- Map<String, Object> result = new LinkedHashMap<>(columns.size(), 1F);
- for (int i = 0; i < columns.size(); i++) {
- result.put(columns.get(i).getColumnName(), i < row.size() ? row.get(i) : null);
- }
- return result;
- }
-
- private static List<ExecuteQueryColumnDefinition> normalizeColumns(final List<ExecuteQueryColumnDefinition> columns) {
- return null == columns ? Collections.emptyList() : columns;
- }
-
- private static List<List<Object>> normalizeRows(final List<List<Object>> rows) {
- return null == rows ? Collections.emptyList() : rows;
- }
-}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationCheckResult.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationCheckResult.java
similarity index 83%
rename from mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationCheckResult.java
rename to mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationCheckResult.java
index e6a68aadab3..1b66f843133 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationCheckResult.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationCheckResult.java
@@ -15,15 +15,12 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.tool.response;
+package org.apache.shardingsphere.mcp.support.database.tool.result;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
/**
* Runtime database validation check result.
*/
@@ -78,18 +75,4 @@ public final class RuntimeDatabaseValidationCheckResult {
public static RuntimeDatabaseValidationCheckResult skipped(final String name, final String message) {
return new RuntimeDatabaseValidationCheckResult(name, STATUS_SKIPPED, "skipped", message);
}
-
- /**
- * Convert to payload.
- *
- * @return payload
- */
- public Map<String, Object> toPayload() {
- Map<String, Object> result = new LinkedHashMap<>(4, 1F);
- result.put("name", name);
- result.put("status", status);
- result.put("category", category);
- result.put("message", message);
- return result;
- }
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationResult.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationResult.java
new file mode 100644
index 00000000000..febe02c5778
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationResult.java
@@ -0,0 +1,63 @@
+/*
+ * 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.tool.result;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Runtime database validation result.
+ */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public final class RuntimeDatabaseValidationResult {
+
+ private final String status;
+
+ private final String database;
+
+ private final List<RuntimeDatabaseValidationCheckResult> checks;
+
+ private final String category;
+
+ /**
+ * Create a ready validation result.
+ *
+ * @param database database name
+ * @param checks check results
+ * @return validation result
+ */
+ public static RuntimeDatabaseValidationResult ready(final String database, final List<RuntimeDatabaseValidationCheckResult> checks) {
+ return new RuntimeDatabaseValidationResult("ready", database, checks, "ready");
+ }
+
+ /**
+ * Create a failed validation result.
+ *
+ * @param database database name
+ * @param checks check results
+ * @param category failure category
+ * @return validation result
+ */
+ public static RuntimeDatabaseValidationResult failed(final String database, final List<RuntimeDatabaseValidationCheckResult> checks, final String category) {
+ return new RuntimeDatabaseValidationResult("failed", database, checks, category);
+ }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/protocol/ExecuteQueryColumnDefinition.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionColumnDefinition.java
similarity index 87%
rename from mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/protocol/ExecuteQueryColumnDefinition.java
rename to mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionColumnDefinition.java
index 1a4ae7bd3e8..dd6b9c5d128 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/protocol/ExecuteQueryColumnDefinition.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionColumnDefinition.java
@@ -15,17 +15,17 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.protocol;
+package org.apache.shardingsphere.mcp.support.database.tool.result;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
- * Execute query column definition.
+ * SQL execution column definition.
*/
@RequiredArgsConstructor
@Getter
-public final class ExecuteQueryColumnDefinition {
+public final class SQLExecutionColumnDefinition {
private final String columnName;
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResult.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResult.java
new file mode 100644
index 00000000000..48527542fae
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResult.java
@@ -0,0 +1,106 @@
+/*
+ * 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.tool.result;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+
+import java.util.List;
+
+/**
+ * SQL execution result.
+ */
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public final class SQLExecutionResult {
+
+ private final SQLExecutionResultKind resultKind;
+
+ private final SupportedMCPStatement statementClass;
+
+ private final String statementType;
+
+ private final List<SQLExecutionColumnDefinition> columns;
+
+ private final List<List<Object>> rows;
+
+ private final int affectedRows;
+
+ private final boolean truncated;
+
+ private final int appliedMaxRows;
+
+ private final int appliedTimeoutMs;
+
+ private final String normalizedSql;
+
+ /**
+ * Create a result-set result.
+ *
+ * @param statementClass statement class
+ * @param statementType statement type
+ * @param columns column definitions
+ * @param rows result rows
+ * @param truncated truncation flag
+ * @param appliedMaxRows applied max rows argument
+ * @param appliedTimeoutMs applied timeout milliseconds argument
+ * @param normalizedSql normalized SQL
+ * @return result-set result
+ */
+ public static SQLExecutionResult resultSet(final SupportedMCPStatement statementClass, final String statementType,
+ final List<SQLExecutionColumnDefinition> columns, final List<List<Object>> rows, final boolean truncated,
+ final int appliedMaxRows, final int appliedTimeoutMs, final String normalizedSql) {
+ return new SQLExecutionResult(SQLExecutionResultKind.RESULT_SET, statementClass, statementType, columns, rows, 0, truncated,
+ appliedMaxRows, appliedTimeoutMs, normalizedSql);
+ }
+
+ /**
+ * Create an update-count result.
+ *
+ * @param statementClass statement class
+ * @param statementType statement type
+ * @param affectedRows affected row count
+ * @param appliedMaxRows applied max rows argument
+ * @param appliedTimeoutMs applied timeout milliseconds argument
+ * @param normalizedSql normalized SQL
+ * @return update-count result
+ */
+ public static SQLExecutionResult updateCount(final SupportedMCPStatement statementClass, final String statementType, final int affectedRows,
+ final int appliedMaxRows, final int appliedTimeoutMs, final String normalizedSql) {
+ return new SQLExecutionResult(SQLExecutionResultKind.UPDATE_COUNT, statementClass, statementType, List.of(), List.of(), affectedRows, false,
+ appliedMaxRows, appliedTimeoutMs, normalizedSql);
+ }
+
+ /**
+ * Create a statement-acknowledgement result.
+ *
+ * @param statementClass statement class
+ * @param statementType statement type
+ * @param appliedMaxRows applied max rows argument
+ * @param appliedTimeoutMs applied timeout milliseconds argument
+ * @param normalizedSql normalized SQL
+ * @return statement-acknowledgement result
+ */
+ public static SQLExecutionResult statementAck(final SupportedMCPStatement statementClass, final String statementType,
+ final int appliedMaxRows, final int appliedTimeoutMs, final String normalizedSql) {
+ return new SQLExecutionResult(SQLExecutionResultKind.STATEMENT_ACK, statementClass, statementType, List.of(), List.of(), 0, false,
+ appliedMaxRows, appliedTimeoutMs, normalizedSql);
+ }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/protocol/ExecuteQueryResultKind.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResultKind.java
similarity index 86%
rename from mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/protocol/ExecuteQueryResultKind.java
rename to mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResultKind.java
index 70358b5f3f8..f06f6c4353d 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/protocol/ExecuteQueryResultKind.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResultKind.java
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.protocol;
+package org.apache.shardingsphere.mcp.support.database.tool.result;
/**
- * Execute query result kind.
+ * SQL execution result kind.
*/
-public enum ExecuteQueryResultKind {
+public enum SQLExecutionResultKind {
RESULT_SET, UPDATE_COUNT, STATEMENT_ACK
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationService.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationService.java
index 454c9f3fb9c..f26a45ef90c 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationService.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationService.java
@@ -28,8 +28,8 @@ import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatab
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseProfile;
import org.apache.shardingsphere.mcp.support.database.tool.request.RuntimeDatabaseValidationRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.RuntimeDatabaseValidationCheckResult;
-import org.apache.shardingsphere.mcp.support.database.tool.response.RuntimeDatabaseValidationResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationCheckResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationResult;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@@ -38,7 +38,6 @@ import java.sql.SQLException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
@@ -48,8 +47,6 @@ import java.util.function.Function;
*/
public final class RuntimeDatabaseValidationService {
- private static final String VALIDATION_BINDING_DATABASE = "__preflight_validation__";
-
private final MCPJdbcDatabaseProfileLoader profileLoader = new MCPJdbcDatabaseProfileLoader();
private final MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
@@ -59,23 +56,21 @@ public final class RuntimeDatabaseValidationService {
*
* @param request validation request
* @param runtimeDatabaseResolver runtime database resolver
- * @param recoveryFactory runtime recovery factory
* @return validation result
*/
public RuntimeDatabaseValidationResult validate(final RuntimeDatabaseValidationRequest request,
- final Function<String, Optional<RuntimeDatabaseConfiguration>> runtimeDatabaseResolver,
- final Function<RuntimeDatabaseConnectionException, Map<String, Object>> recoveryFactory) {
+ final Function<String, Optional<RuntimeDatabaseConfiguration>> runtimeDatabaseResolver) {
List<RuntimeDatabaseValidationCheckResult> checks = new LinkedList<>();
String database = normalize(request.getDatabase());
Optional<RuntimeDatabaseConfiguration> runtimeDatabaseConfig = findRuntimeDatabaseConfiguration(database, runtimeDatabaseResolver);
if (runtimeDatabaseConfig.isEmpty()) {
- RuntimeDatabaseConnectionException ex = createMissingRuntimeDatabaseException(database);
- checks.add(RuntimeDatabaseValidationCheckResult.failed("configuration", ex.getCategory(), "The requested database is not configured for this MCP runtime."));
+ checks.add(RuntimeDatabaseValidationCheckResult.failed("configuration", RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION,
+ "The requested database is not configured for this MCP runtime."));
appendSkippedChecks(checks, "jdbc_driver", "configuration validation did not finish");
appendSkippedChecks(checks, "jdbc_connectivity", "configuration validation did not finish");
appendSkippedChecks(checks, "metadata_read", "configuration validation did not finish");
appendSkippedChecks(checks, "database_visibility", "configuration validation did not finish");
- return createFailureResult(database, checks, ex, recoveryFactory);
+ return RuntimeDatabaseValidationResult.failed(database, checks, RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION);
}
checks.add(RuntimeDatabaseValidationCheckResult.passed("configuration", "Resolved the configured runtime database."));
RuntimeDatabaseProfile databaseProfile;
@@ -87,7 +82,7 @@ public final class RuntimeDatabaseValidationService {
appendProfileFailureChecks(checks, ex);
appendSkippedChecks(checks, "metadata_read", "driver or connectivity validation failed");
appendSkippedChecks(checks, "database_visibility", "driver or connectivity validation failed");
- return createFailureResult(database, checks, ex, recoveryFactory);
+ return RuntimeDatabaseValidationResult.failed(database, checks, ex.getCategory());
}
Collection<ShardingSphereSchema> schemas;
try {
@@ -96,14 +91,14 @@ public final class RuntimeDatabaseValidationService {
} catch (final RuntimeDatabaseConnectionException ex) {
checks.add(RuntimeDatabaseValidationCheckResult.failed("metadata_read", ex.getCategory(), "Failed to read metadata through the configured JDBC connection."));
appendSkippedChecks(checks, "database_visibility", "metadata validation failed");
- return createFailureResult(database, checks, ex, recoveryFactory);
+ return RuntimeDatabaseValidationResult.failed(database, checks, ex.getCategory());
}
try {
validateDatabaseVisibility(database, runtimeDatabaseConfig.get(), schemas, databaseProfile.getIdentifierCasePolicySet());
checks.add(RuntimeDatabaseValidationCheckResult.passed("database_visibility", "Validated the requested database name against visible JDBC metadata and connection context."));
} catch (final RuntimeDatabaseConnectionException ex) {
checks.add(RuntimeDatabaseValidationCheckResult.failed("database_visibility", ex.getCategory(), "The requested database name is not visible to the configured JDBC connection."));
- return createFailureResult(database, checks, ex, recoveryFactory);
+ return RuntimeDatabaseValidationResult.failed(database, checks, ex.getCategory());
}
return RuntimeDatabaseValidationResult.ready(database, checks);
}
@@ -113,11 +108,6 @@ public final class RuntimeDatabaseValidationService {
return database.isEmpty() ? Optional.empty() : runtimeDatabaseResolver.apply(database);
}
- private RuntimeDatabaseConnectionException createMissingRuntimeDatabaseException(final String database) {
- return RuntimeDatabaseConnectionException.invalidConfiguration(resolveExceptionDatabaseName(database),
- new IllegalStateException("Runtime database validation requires one configured runtime database."));
- }
-
private void appendProfileFailureChecks(final List<RuntimeDatabaseValidationCheckResult> checks, final RuntimeDatabaseConnectionException ex) {
if (RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER.equals(ex.getCategory())) {
checks.add(RuntimeDatabaseValidationCheckResult.failed("jdbc_driver", ex.getCategory(), "Failed to load the configured JDBC driver."));
@@ -132,24 +122,19 @@ public final class RuntimeDatabaseValidationService {
checks.add(RuntimeDatabaseValidationCheckResult.skipped(name, String.format("Skipped because %s.", reason)));
}
- private RuntimeDatabaseValidationResult createFailureResult(final String database, final List<RuntimeDatabaseValidationCheckResult> checks, final RuntimeDatabaseConnectionException cause,
- final Function<RuntimeDatabaseConnectionException, Map<String, Object>> recoveryFactory) {
- return RuntimeDatabaseValidationResult.failed(database, checks, cause.getCategory(), recoveryFactory.apply(cause));
- }
-
private void validateDatabaseVisibility(final String database, final RuntimeDatabaseConfiguration runtimeDatabaseConfig, final Collection<ShardingSphereSchema> schemas,
final IdentifierCasePolicySet identifierCasePolicySet) {
if (containsVisibleSchema(schemas, database, identifierCasePolicySet.getPolicy(IdentifierScope.SCHEMA))) {
return;
}
- try (Connection connection = runtimeDatabaseConfig.openConnection(resolveExceptionDatabaseName(database))) {
+ try (Connection connection = runtimeDatabaseConfig.openConnection(database)) {
if (isVisibleDatabase(connection, database, identifierCasePolicySet)) {
return;
}
} catch (final SQLException ex) {
- throw RuntimeDatabaseConnectionException.connectionFailed(resolveExceptionDatabaseName(database), ex);
+ throw RuntimeDatabaseConnectionException.connectionFailed(database, ex);
}
- throw RuntimeDatabaseConnectionException.databaseNotVisible(resolveExceptionDatabaseName(database),
+ throw RuntimeDatabaseConnectionException.databaseNotVisible(database,
new IllegalStateException(String.format("Requested database `%s` is not visible to the configured JDBC connection.", database)));
}
@@ -198,10 +183,6 @@ public final class RuntimeDatabaseValidationService {
return !actualStoredName.isEmpty() && identifierCasePolicy.matches(actualStoredName, identifier, QuoteCharacter.NONE);
}
- private String resolveExceptionDatabaseName(final String database) {
- return database.isEmpty() ? VALIDATION_BINDING_DATABASE : database;
- }
-
private String normalize(final String value) {
return Objects.toString(value, "").trim();
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
index ee6ee7d16c7..eca5789b56c 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
@@ -216,8 +216,8 @@ public final class MCPToolDescriptorCatalogValidator {
}
private static void validateExecuteUpdateDescriptor(final MCPToolDescriptor descriptor) {
- MCPToolDescriptorValidationUtils.validateRequiredOutputFields(descriptor, List.of("response_mode", "result_kind", "statement_class", "statement_type", "status", "returned_row_count",
- "applied_max_rows", "applied_timeout_ms", "suggested_arguments", MCPPayloadFieldNames.NEXT_ACTIONS));
+ MCPToolDescriptorValidationUtils.validateRequiredOutputFields(descriptor, List.of("response_mode", "result_kind", "statement_class", "statement_type", "status", "columns", "rows",
+ "returned_row_count", "applied_max_rows", "applied_timeout_ms", "suggested_arguments", MCPPayloadFieldNames.NEXT_ACTIONS));
Map<?, ?> executionMode = MCPToolDescriptorValidationUtils.findToolInputProperty(descriptor, MCPPayloadFieldNames.EXECUTION_MODE).orElseThrow(
() -> new IllegalStateException("Tool `database_gateway_execute_update` must declare execution_mode."));
ShardingSpherePreconditions.checkState(MCPToolDescriptorValidationUtils.isRequiredToolInput(descriptor, MCPPayloadFieldNames.EXECUTION_MODE),
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponse.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponse.java
index c56a6f2df36..580f5aebd03 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponse.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponse.java
@@ -23,7 +23,10 @@ import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import java.util.Map;
/**
- * MCP map response.
+ * MCP response adapter for a fully assembled successful payload.
+ *
+ * <p>This thin adapter returns the supplied payload as-is. It does not copy, merge, normalize, validate against a tool schema,
+ * classify errors, or build a transport envelope. Callers must finish constructing the payload before wrapping it and must not mutate it afterwards.</p>
*/
@RequiredArgsConstructor
public final class MCPMapResponse implements MCPResponse {
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java
index 4c90eff6fd5..250aa6d9e8b 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java
@@ -91,7 +91,8 @@ public final class MCPRuntimeProtectionPolicy {
result.put("maximum_value", MAX_ROWS_LIMIT);
result.put("applied_field", "applied_max_rows");
result.put("truncation_field", "truncated");
- result.put(MCPPayloadFieldNames.RECOVERY, "Retry with a narrower SELECT, stronger WHERE clause, or smaller projection when rows are truncated.");
+ result.put(MCPPayloadFieldNames.RECOVERY, "For read-only queries, retry with a narrower SELECT, stronger WHERE clause, or smaller projection. "
+ + "If rows came from a side-effecting statement, do not replay that statement automatically; use a separate read-only query when more data is needed.");
return result;
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java
deleted file mode 100644
index 2a42235c6cb..00000000000
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.tool.response;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Map;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-class RuntimeDatabaseValidationResultTest {
-
- @Test
- void assertReady() {
- RuntimeDatabaseValidationResult actual = RuntimeDatabaseValidationResult.ready("logic_db", List.of(RuntimeDatabaseValidationCheckResult.passed("configuration", "Validated the request.")));
- assertThat(actual.getStatus(), is("ready"));
- assertThat(actual.getDatabase(), is("logic_db"));
- assertThat(actual.getCategory(), is("ready"));
- assertThat(actual.getRecovery(), is(Map.of()));
- }
-
- @Test
- void assertFailed() {
- Map<String, Object> recovery = Map.of("category", "connection_failed");
- RuntimeDatabaseValidationResult actual = RuntimeDatabaseValidationResult.failed("logic_db",
- List.of(RuntimeDatabaseValidationCheckResult.failed("jdbc_connectivity", "connection_failed", "Failed to open a JDBC connection.")), "connection_failed", recovery);
- assertThat(actual.getStatus(), is("failed"));
- assertThat(actual.getDatabase(), is("logic_db"));
- assertThat(actual.getCategory(), is("connection_failed"));
- assertThat(actual.getRecovery(), is(recovery));
- }
-
- @Test
- void assertToPayload() {
- Map<String, Object> actual = RuntimeDatabaseValidationResult.failed("logic_db",
- List.of(RuntimeDatabaseValidationCheckResult.failed("metadata_read", "connection_failed", "Failed to read metadata.")),
- "connection_failed", Map.of("category", "connection_failed")).toPayload();
- assertThat(actual.get("response_mode"), is("validation"));
- assertThat(actual.get("summary"), is("Runtime database `logic_db` failed validation with category `connection_failed`."));
- assertThat(actual.get("status"), is("failed"));
- assertThat(actual.get("database"), is("logic_db"));
- assertThat(actual.get("category"), is("connection_failed"));
- assertThat(actual.get("recovery"), is(Map.of("category", "connection_failed")));
- assertThat(actual.get("checks"), is(List.of(Map.of(
- "name", "metadata_read",
- "status", "failed",
- "category", "connection_failed",
- "message", "Failed to read metadata."))));
- }
-
- @Test
- void assertToPayloadWithTopLevelNextActions() {
- Map<String, Object> recovery = Map.of("category", "connection_failed", "next_actions", List.of(Map.of("order", 1, "type", "resource_read", "title", "Read resource",
- "resource_uri", "shardingsphere://runtime")));
- Map<String, Object> actual = RuntimeDatabaseValidationResult.failed("logic_db",
- List.of(RuntimeDatabaseValidationCheckResult.failed("metadata_read", "connection_failed", "Failed to read metadata.")), "connection_failed", recovery).toPayload();
- assertThat(actual.get("next_actions"), is(recovery.get("next_actions")));
- }
-}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java
deleted file mode 100644
index 2e19a2620cb..00000000000
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.tool.response;
-
-import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryColumnDefinition;
-import org.apache.shardingsphere.mcp.support.database.protocol.ExecuteQueryResultKind;
-import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
-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.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Supplier;
-import java.util.stream.Stream;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-class SQLExecutionResponseTest {
-
- @ParameterizedTest(name = "{0}")
- @MethodSource("assertToPayloadCases")
- void assertToPayload(final String name, final Supplier<SQLExecutionResponse> responseSupplier, final Map<String, Object> expectedPayload) {
- SQLExecutionResponse actual = responseSupplier.get();
- assertThat(actual.toPayload(), is(expectedPayload));
- }
-
- @Test
- void assertResultSetWithNullRows() {
- assertThat(SQLExecutionResponse.resultSet(List.of(), null, false).getRows(), is(List.of()));
- }
-
- @Test
- void assertToPayloadWithUnnamedColumns() {
- Map<String, Object> actual = SQLExecutionResponse.resultSet(List.of(), List.of(List.of(1)), false).toPayload();
- assertThat(actual.get("row_object_status"), is("unnamed_columns"));
- assertFalse(actual.containsKey("row_objects"));
- }
-
- @Test
- void assertToPayloadWithBlankColumnName() {
- Map<String, Object> actual = SQLExecutionResponse.resultSet(List.of(new ExecuteQueryColumnDefinition("", "INT", "INT", false)), List.of(List.of(1)), false).toPayload();
- assertThat(actual.get("row_object_status"), is("unnamed_columns"));
- assertFalse(actual.containsKey("row_objects"));
- }
-
- @Test
- void assertToPayloadWithDuplicateColumnLabels() {
- List<ExecuteQueryColumnDefinition> columns = List.of(
- new ExecuteQueryColumnDefinition("order_id", "INT", "INT", false), new ExecuteQueryColumnDefinition("order_id", "INT", "INT", false));
- Map<String, Object> actual = SQLExecutionResponse.resultSet(columns, List.of(List.of(1, 2)), false).toPayload();
- assertThat(actual.get("row_object_status"), is("duplicate_column_labels"));
- assertFalse(actual.containsKey("row_objects"));
- }
-
- @Test
- void assertToPayloadOmitsLargeRowObjects() {
- List<ExecuteQueryColumnDefinition> columns = List.of(new ExecuteQueryColumnDefinition("order_id", "INT", "INT", false));
- List<List<Object>> rows = new LinkedList<>();
- for (int i = 0; i < 101; i++) {
- rows.add(List.of(i));
- }
- Map<String, Object> actual = SQLExecutionResponse.resultSet(columns, rows, false).toPayload();
- assertThat(actual.get("row_object_status"), is("omitted_large_result"));
- assertFalse(actual.containsKey("row_objects"));
- }
-
- @Test
- void assertResultSetWithDmlStatementClass() {
- SQLExecutionResponse actual = SQLExecutionResponse.resultSet(SupportedMCPStatement.DML, "SELECT", List.of(), List.of(List.of(1)), false);
- assertThat(actual.getStatementClass(), is(SupportedMCPStatement.DML));
- assertThat(actual.getStatementType(), is("SELECT"));
- assertThat(actual.getResultKind(), is(ExecuteQueryResultKind.RESULT_SET));
- }
-
- @Test
- void assertUpdateCountWithZeroAffectedRows() {
- SQLExecutionResponse actual = SQLExecutionResponse.updateCount("UPDATE", 0);
- assertThat(actual.toPayload().get("affected_rows"), is(0));
- }
-
- @Test
- void assertWithNormalizedSql() {
- SQLExecutionResponse actual = SQLExecutionResponse.updateCount("UPDATE", 1).withNormalizedSql("UPDATE orders SET status = 'DONE'");
- assertThat(actual.getNormalizedSql(), is("UPDATE orders SET status = 'DONE'"));
- assertThat(actual.toPayload().get("normalized_sql"), is("UPDATE orders SET status = 'DONE'"));
- }
-
- @Test
- void assertWithExecutionMode() {
- SQLExecutionResponse actual = SQLExecutionResponse.updateCount("UPDATE", 1).withExecutionMode("execute");
- assertThat(actual.getResponseMode(), is("executed"));
- assertThat(actual.getExecutionMode(), is("execute"));
- assertThat(actual.toPayload().get("response_mode"), is("executed"));
- assertThat(actual.toPayload().get("execution_mode"), is("execute"));
- }
-
- private static Stream<Arguments> assertToPayloadCases() {
- ExecuteQueryColumnDefinition orderIdColumn = new ExecuteQueryColumnDefinition("order_id", "INT", "INT", false);
- List<ExecuteQueryColumnDefinition> columns = List.of(orderIdColumn);
- List<List<Object>> rows = List.of(List.of(1));
- List<Map<String, Object>> rowObjects = List.of(Map.of("order_id", 1));
- List<Map<String, Object>> truncatedResultSetNextActions = createTruncatedResultSetNextActions();
- List<Map<String, Object>> resultSetNextActions = createNextActions("Return the result rows to the user or ask a follow-up question if the user requested more analysis.");
- List<Map<String, Object>> executionNextActions = createNextActions("Report the execution status to the user and stop unless the user asks for another operation.");
- return Stream.of(
- Arguments.of("result set with rows", (Supplier<SQLExecutionResponse>) () -> SQLExecutionResponse.resultSet(columns, rows, true).withExecutionHints(10, 5000),
- Map.ofEntries(
- Map.entry("result_kind", "result_set"), Map.entry("statement_class", "query"), Map.entry("statement_type", "SELECT"), Map.entry("status", "OK"),
- Map.entry("response_mode", "query"), Map.entry("summary", "Executed SELECT statement and returned 1 row(s). Result was truncated."),
- Map.entry("columns", columns), Map.entry("rows", rows), Map.entry("row_object_status", "available"), Map.entry("row_objects", rowObjects),
- Map.entry("returned_row_count", 1), Map.entry("applied_max_rows", 10), Map.entry("applied_timeout_ms", 5000), Map.entry("truncated", true),
- Map.entry("next_actions", truncatedResultSetNextActions))),
- Arguments.of("result set with null rows", (Supplier<SQLExecutionResponse>) () -> SQLExecutionResponse.resultSet(List.of(), null, false),
- Map.ofEntries(
- Map.entry("result_kind", "result_set"), Map.entry("statement_class", "query"), Map.entry("statement_type", "SELECT"), Map.entry("status", "OK"),
- Map.entry("response_mode", "query"), Map.entry("summary", "Executed SELECT statement and returned 0 row(s)."),
- Map.entry("columns", List.of()), Map.entry("rows", List.of()), Map.entry("row_object_status", "available"), Map.entry("row_objects", List.of()),
- Map.entry("returned_row_count", 0), Map.entry("applied_max_rows", 0), Map.entry("applied_timeout_ms", 0), Map.entry("truncated", false),
- Map.entry("next_actions", resultSetNextActions))),
- Arguments.of("update count", (Supplier<SQLExecutionResponse>) () -> SQLExecutionResponse.updateCount("UPDATE", 2),
- Map.ofEntries(Map.entry("response_mode", "executed"), Map.entry("result_kind", "update_count"), Map.entry("statement_class", "dml"),
- Map.entry("statement_type", "UPDATE"), Map.entry("status", "OK"), Map.entry("summary", "Executed UPDATE statement and affected 2 row(s)."),
- Map.entry("affected_rows", 2), Map.entry("truncated", false),
- Map.entry("applied_max_rows", 0), Map.entry("applied_timeout_ms", 0), Map.entry("next_actions", executionNextActions))),
- Arguments.of("statement acknowledgement",
- (Supplier<SQLExecutionResponse>) () -> SQLExecutionResponse.statementAck(SupportedMCPStatement.TRANSACTION_CONTROL, "COMMIT", "Transaction committed."),
- Map.ofEntries(Map.entry("response_mode", "executed"), Map.entry("result_kind", "statement_ack"), Map.entry("statement_class", "transaction_control"),
- Map.entry("statement_type", "COMMIT"), Map.entry("status", "OK"), Map.entry("summary", "Transaction committed."), Map.entry("message", "Transaction committed."),
- Map.entry("truncated", false), Map.entry("applied_max_rows", 0), Map.entry("applied_timeout_ms", 0), Map.entry("next_actions", executionNextActions))));
- }
-
- private static List<Map<String, Object>> createNextActions(final String reason) {
- return List.of(MCPNextActionUtils.stop(reason));
- }
-
- private static List<Map<String, Object>> createTruncatedResultSetNextActions() {
- return List.of(MCPNextActionUtils.askUser("The result was truncated by max_rows. Ask for a narrower SELECT, stronger WHERE clause, or smaller projection before retrying.",
- List.of("sql")));
- }
-}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationCheckResultTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationCheckResultTest.java
similarity index 82%
rename from mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationCheckResultTest.java
rename to mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationCheckResultTest.java
index e30e816af9a..3a43f4e70d9 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationCheckResultTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationCheckResultTest.java
@@ -15,12 +15,10 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.tool.response;
+package org.apache.shardingsphere.mcp.support.database.tool.result;
import org.junit.jupiter.api.Test;
-import java.util.Map;
-
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@@ -52,14 +50,4 @@ class RuntimeDatabaseValidationCheckResultTest {
assertThat(actual.getCategory(), is("skipped"));
assertThat(actual.getMessage(), is("Skipped because no database was provided."));
}
-
- @Test
- void assertToPayload() {
- Map<String, Object> actual = RuntimeDatabaseValidationCheckResult.failed("metadata_read", "connection_failed", "Failed to read metadata.").toPayload();
- assertThat(actual, is(Map.of(
- "name", "metadata_read",
- "status", "failed",
- "category", "connection_failed",
- "message", "Failed to read metadata.")));
- }
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationResultTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationResultTest.java
new file mode 100644
index 00000000000..6945c09e5ad
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/RuntimeDatabaseValidationResultTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.tool.result;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class RuntimeDatabaseValidationResultTest {
+
+ @Test
+ void assertReady() {
+ List<RuntimeDatabaseValidationCheckResult> checks = List.of(RuntimeDatabaseValidationCheckResult.passed("configuration", "Validated the request."));
+ RuntimeDatabaseValidationResult actual = RuntimeDatabaseValidationResult.ready("logic_db", checks);
+ assertThat(actual.getStatus(), is("ready"));
+ assertThat(actual.getDatabase(), is("logic_db"));
+ assertThat(actual.getChecks(), is(checks));
+ assertThat(actual.getCategory(), is("ready"));
+ }
+
+ @Test
+ void assertFailed() {
+ List<RuntimeDatabaseValidationCheckResult> checks = List.of(
+ RuntimeDatabaseValidationCheckResult.failed("jdbc_connectivity", "connection_failed", "Failed to open a JDBC connection."));
+ RuntimeDatabaseValidationResult actual = RuntimeDatabaseValidationResult.failed("logic_db", checks, "connection_failed");
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getDatabase(), is("logic_db"));
+ assertThat(actual.getChecks(), is(checks));
+ assertThat(actual.getCategory(), is("connection_failed"));
+ }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionColumnDefinitionTest.java
similarity index 68%
copy from mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java
copy to mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionColumnDefinitionTest.java
index 7fcba054667..3f32e3e582a 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionColumnDefinitionTest.java
@@ -15,21 +15,19 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.protocol.response;
+package org.apache.shardingsphere.mcp.support.database.tool.result;
+import org.apache.shardingsphere.infra.util.json.JsonUtils;
import org.junit.jupiter.api.Test;
-import java.util.Map;
-
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
-class MCPMapResponseTest {
+class SQLExecutionColumnDefinitionTest {
@Test
- void assertToPayload() {
- Map<String, Object> expectedPayload = Map.of("foo_key", "bar_value");
- Map<String, Object> actual = new MCPMapResponse(expectedPayload).toPayload();
- assertThat(actual, is(expectedPayload));
+ void assertJsonFieldNames() {
+ assertThat(JsonUtils.toJsonString(new SQLExecutionColumnDefinition("order_id", "INTEGER", "int4", false)),
+ is("{\"columnName\":\"order_id\",\"logicalType\":\"INTEGER\",\"nativeType\":\"int4\",\"nullable\":false}"));
}
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResultTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResultTest.java
new file mode 100644
index 00000000000..41b00c1531a
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/result/SQLExecutionResultTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.tool.result;
+
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SQLExecutionResultTest {
+
+ @Test
+ void assertResultSet() {
+ SQLExecutionColumnDefinition column = new SQLExecutionColumnDefinition("order_id", "INTEGER", "int4", false);
+ SQLExecutionResult actual = SQLExecutionResult.resultSet(SupportedMCPStatement.QUERY, "SELECT", List.of(column), List.of(List.of(1)), true,
+ 10, 5000, "SELECT order_id FROM orders");
+ assertThat(actual.getResultKind(), is(SQLExecutionResultKind.RESULT_SET));
+ assertThat(actual.getStatementClass(), is(SupportedMCPStatement.QUERY));
+ assertThat(actual.getStatementType(), is("SELECT"));
+ assertThat(actual.getColumns(), is(List.of(column)));
+ assertThat(actual.getRows(), is(List.of(List.of(1))));
+ assertThat(actual.getAffectedRows(), is(0));
+ assertTrue(actual.isTruncated());
+ assertThat(actual.getAppliedMaxRows(), is(10));
+ assertThat(actual.getAppliedTimeoutMs(), is(5000));
+ assertThat(actual.getNormalizedSql(), is("SELECT order_id FROM orders"));
+ }
+
+ @Test
+ void assertUpdateCount() {
+ SQLExecutionResult actual = SQLExecutionResult.updateCount(SupportedMCPStatement.DML, "UPDATE", 2, 100, 3000,
+ "UPDATE orders SET status = 'DONE'");
+ assertThat(actual.getResultKind(), is(SQLExecutionResultKind.UPDATE_COUNT));
+ assertThat(actual.getStatementClass(), is(SupportedMCPStatement.DML));
+ assertThat(actual.getStatementType(), is("UPDATE"));
+ assertThat(actual.getColumns(), is(List.of()));
+ assertThat(actual.getRows(), is(List.of()));
+ assertThat(actual.getAffectedRows(), is(2));
+ assertFalse(actual.isTruncated());
+ assertThat(actual.getAppliedMaxRows(), is(100));
+ assertThat(actual.getAppliedTimeoutMs(), is(3000));
+ assertThat(actual.getNormalizedSql(), is("UPDATE orders SET status = 'DONE'"));
+ }
+
+ @Test
+ void assertStatementAck() {
+ SQLExecutionResult actual = SQLExecutionResult.statementAck(SupportedMCPStatement.DDL, "CREATE", 100, 0, "CREATE TABLE orders (id INT)");
+ assertThat(actual.getResultKind(), is(SQLExecutionResultKind.STATEMENT_ACK));
+ assertThat(actual.getStatementClass(), is(SupportedMCPStatement.DDL));
+ assertThat(actual.getStatementType(), is("CREATE"));
+ assertThat(actual.getColumns(), is(List.of()));
+ assertThat(actual.getRows(), is(List.of()));
+ assertThat(actual.getAffectedRows(), is(0));
+ assertFalse(actual.isTruncated());
+ assertThat(actual.getAppliedMaxRows(), is(100));
+ assertThat(actual.getAppliedTimeoutMs(), is(0));
+ assertThat(actual.getNormalizedSql(), is("CREATE TABLE orders (id INT)"));
+ }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationServiceTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationServiceTest.java
index 0831adf4080..d049b46451e 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationServiceTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/RuntimeDatabaseValidationServiceTest.java
@@ -30,7 +30,7 @@ import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatab
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseProfile;
import org.apache.shardingsphere.mcp.support.database.tool.request.RuntimeDatabaseValidationRequest;
-import org.apache.shardingsphere.mcp.support.database.tool.response.RuntimeDatabaseValidationResult;
+import org.apache.shardingsphere.mcp.support.database.tool.result.RuntimeDatabaseValidationResult;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -86,26 +86,22 @@ class RuntimeDatabaseValidationServiceTest {
@Test
void assertValidateWithMissingDatabase() {
RuntimeDatabaseValidationService service = new RuntimeDatabaseValidationService();
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest(""),
- ignored -> Optional.empty(), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- assertThat(actualPayload.get("status"), is("failed"));
- assertThat(actualPayload.get("category"), is("invalid_configuration"));
- assertThat(actualPayload.get("recovery"), is(Map.of("category", "invalid_configuration")));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(0)).get("name"), is("configuration"));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(1)).get("status"), is("skipped"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest(""), ignored -> Optional.empty());
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getDatabase(), is(""));
+ assertThat(actual.getCategory(), is("invalid_configuration"));
+ assertThat(actual.getChecks().get(0).getName(), is("configuration"));
+ assertThat(actual.getChecks().get(1).getStatus(), is("skipped"));
verifyNoInteractions(getProfileLoader(), getMetadataLoader());
}
@Test
void assertValidateWithUnknownDatabase() {
RuntimeDatabaseValidationService service = new RuntimeDatabaseValidationService();
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.empty(), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- assertThat(actualPayload.get("status"), is("failed"));
- assertThat(actualPayload.get("category"), is("invalid_configuration"));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(0)).get("status"), is("failed"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.empty());
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getCategory(), is("invalid_configuration"));
+ assertThat(actual.getChecks().get(0).getStatus(), is("failed"));
verifyNoInteractions(getProfileLoader(), getMetadataLoader());
}
@@ -116,11 +112,8 @@ class RuntimeDatabaseValidationServiceTest {
RuntimeDatabaseConfiguration runtimeDatabaseConfig = createRuntimeDatabaseConfiguration();
when(profileLoader.load(any(), any(RuntimeDatabaseConfiguration.class))).thenReturn(createProfile());
when(getMetadataLoader().load(any(), any(RuntimeDatabaseConfiguration.class), any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("logic_db"));
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.of(runtimeDatabaseConfig),
- RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- assertThat(actualPayload.get("status"), is("ready"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.of(runtimeDatabaseConfig));
+ assertThat(actual.getStatus(), is("ready"));
ArgumentCaptor<RuntimeDatabaseConfiguration> configurationCaptor = ArgumentCaptor.forClass(RuntimeDatabaseConfiguration.class);
verify(profileLoader).load(any(), configurationCaptor.capture());
assertThat(configurationCaptor.getValue(), is(runtimeDatabaseConfig));
@@ -138,11 +131,9 @@ class RuntimeDatabaseValidationServiceTest {
when(profileLoader.load(any(), any(RuntimeDatabaseConfiguration.class)))
.thenReturn(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", TransactionCapability.LOCAL_WITH_SAVEPOINT, identifierCasePolicySet));
when(metadataLoader.load(any(), any(RuntimeDatabaseConfiguration.class), any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("Logic_DB"));
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.of(runtimeDatabaseConfig), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- assertThat(actualPayload.get("status"), is("ready"));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(4)).get("status"), is("passed"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.of(runtimeDatabaseConfig));
+ assertThat(actual.getStatus(), is("ready"));
+ assertThat(actual.getChecks().get(4).getStatus(), is("passed"));
}
@Test
@@ -154,13 +145,10 @@ class RuntimeDatabaseValidationServiceTest {
when(profileLoader.load(any(), any(RuntimeDatabaseConfiguration.class))).thenReturn(
new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", TransactionCapability.LOCAL_WITH_SAVEPOINT, IdentifierCasePolicyFactory.newSensitivePolicySet()));
when(metadataLoader.load(any(), any(RuntimeDatabaseConfiguration.class), any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("Logic_DB"));
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.of(runtimeDatabaseConfig), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- assertThat(actualPayload.get("status"), is("failed"));
- assertThat(actualPayload.get("category"), is(RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE));
- assertThat(actualPayload.get("recovery"), is(Map.of("category", RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE)));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(4)).get("status"), is("failed"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.of(runtimeDatabaseConfig));
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getCategory(), is(RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE));
+ assertThat(actual.getChecks().get(4).getStatus(), is("failed"));
}
@Test
@@ -176,9 +164,8 @@ class RuntimeDatabaseValidationServiceTest {
when(getProfileLoader().load(any(), any(RuntimeDatabaseConfiguration.class)))
.thenReturn(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", TransactionCapability.LOCAL_WITH_SAVEPOINT, identifierCasePolicySet));
when(getMetadataLoader().load(any(), any(RuntimeDatabaseConfiguration.class), any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("public"));
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.of(runtimeDatabaseConfig), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- assertThat(actual.toPayload().get("status"), is("ready"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.of(runtimeDatabaseConfig));
+ assertThat(actual.getStatus(), is("ready"));
}
@Test
@@ -190,13 +177,11 @@ class RuntimeDatabaseValidationServiceTest {
when(profileLoader.load(any(), any(RuntimeDatabaseConfiguration.class))).thenReturn(createProfile());
when(metadataLoader.load(any(), any(RuntimeDatabaseConfiguration.class), any(RuntimeDatabaseProfile.class)))
.thenThrow(RuntimeDatabaseConnectionException.connectionFailed("logic_db", new SQLException("Broken connection")));
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.of(runtimeDatabaseConfig), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- assertThat(actualPayload.get("status"), is("failed"));
- assertThat(actualPayload.get("category"), is(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(3)).get("status"), is("failed"));
- assertThat(((Map<?, ?>) ((List<?>) actualPayload.get("checks")).get(4)).get("status"), is("skipped"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.of(runtimeDatabaseConfig));
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getCategory(), is(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED));
+ assertThat(actual.getChecks().get(3).getStatus(), is("failed"));
+ assertThat(actual.getChecks().get(4).getStatus(), is("skipped"));
}
@ParameterizedTest(name = "{0}")
@@ -207,16 +192,13 @@ class RuntimeDatabaseValidationServiceTest {
MCPJdbcDatabaseProfileLoader profileLoader = getProfileLoader();
RuntimeDatabaseConfiguration runtimeDatabaseConfig = createRuntimeDatabaseConfiguration();
when(profileLoader.load(any(), any(RuntimeDatabaseConfiguration.class))).thenThrow(cause);
- RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"),
- ignored -> Optional.of(runtimeDatabaseConfig), RuntimeDatabaseValidationServiceTest::createRecoveryPayload);
- Map<String, Object> actualPayload = actual.toPayload();
- List<?> checks = (List<?>) actualPayload.get("checks");
- assertThat(actualPayload.get("status"), is("failed"));
- assertThat(actualPayload.get("category"), is(expectedCategory));
- assertThat(((Map<?, ?>) checks.get(1)).get("status"), is(expectedDriverStatus));
- assertThat(((Map<?, ?>) checks.get(2)).get("status"), is(expectedConnectivityStatus));
- assertThat(((Map<?, ?>) checks.get(3)).get("status"), is("skipped"));
- assertThat(((Map<?, ?>) checks.get(4)).get("status"), is("skipped"));
+ RuntimeDatabaseValidationResult actual = service.validate(new RuntimeDatabaseValidationRequest("logic_db"), ignored -> Optional.of(runtimeDatabaseConfig));
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getCategory(), is(expectedCategory));
+ assertThat(actual.getChecks().get(1).getStatus(), is(expectedDriverStatus));
+ assertThat(actual.getChecks().get(2).getStatus(), is(expectedConnectivityStatus));
+ assertThat(actual.getChecks().get(3).getStatus(), is("skipped"));
+ assertThat(actual.getChecks().get(4).getStatus(), is("skipped"));
verifyNoInteractions(getMetadataLoader());
}
@@ -254,10 +236,6 @@ class RuntimeDatabaseValidationServiceTest {
return List.of(new ShardingSphereSchema(schemaName, mock(DatabaseType.class)));
}
- private static Map<String, Object> createRecoveryPayload(final RuntimeDatabaseConnectionException cause) {
- return Map.of("category", cause.getCategory());
- }
-
private static final class InvisibleDatabaseDriver implements Driver {
private static final String JDBC_URL = "jdbc:runtime-validation:invisible";
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidatorTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidatorTest.java
index 973be440cc7..c3463ff371b 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidatorTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidatorTest.java
@@ -27,6 +27,8 @@ import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
import org.apache.shardingsphere.mcp.support.workflow.descriptor.WorkflowToolDescriptors;
import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowFieldNames;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.LinkedHashMap;
import java.util.List;
@@ -194,6 +196,13 @@ class MCPDescriptorCatalogValidatorTest {
"Tool `database_gateway_search_metadata` outputSchema must declare `response_mode`.");
}
+ @ParameterizedTest(name = "{0}")
+ @ValueSource(strings = {"columns", "rows"})
+ void assertValidateRejectsExecuteUpdateDescriptorWithoutResultSetField(final String fieldName) {
+ assertValidationError(createCatalog(List.of(), List.of(createExecuteUpdateDescriptorWithout(fieldName))),
+ String.format("Tool `database_gateway_execute_update` outputSchema must declare `%s`.", fieldName));
+ }
+
@Test
@SuppressWarnings("unchecked")
void assertValidateRejectsApplyDescriptorWithoutSecretReferenceSummary() {
@@ -310,6 +319,26 @@ class MCPDescriptorCatalogValidatorTest {
return new MCPToolDescriptor(toolName, "Test Tool", "Run a test tool.", createInputSchema(), outputSchema, annotations, meta);
}
+ private MCPToolDescriptor createExecuteUpdateDescriptorWithout(final String fieldName) {
+ Map<String, Object> outputProperties = new LinkedHashMap<>();
+ for (String each : List.of("response_mode", "result_kind", "statement_class", "statement_type", "status")) {
+ outputProperties.put(each, Map.of("type", "string", "description", "Execution result field."));
+ }
+ for (String each : List.of("columns", "rows")) {
+ outputProperties.put(each, Map.of("type", "array", "description", "Result-set field."));
+ }
+ for (String each : List.of("returned_row_count", "applied_max_rows", "applied_timeout_ms")) {
+ outputProperties.put(each, Map.of("type", "integer", "description", "Execution limit field."));
+ }
+ outputProperties.put("suggested_arguments", Map.of("type", "object", "description", "Suggested execution arguments."));
+ outputProperties.put(MCPPayloadFieldNames.NEXT_ACTIONS, createNextActionsSchema());
+ outputProperties.remove(fieldName);
+ Map<String, Object> inputSchema = createInputSchema(Map.of(MCPPayloadFieldNames.EXECUTION_MODE,
+ Map.of("type", "string", "description", "Execution mode.", "enum", List.of("execute", "preview"))), List.of(MCPPayloadFieldNames.EXECUTION_MODE));
+ return new MCPToolDescriptor("database_gateway_execute_update", "Execute Update", "Execute a side-effecting statement.", inputSchema,
+ createOutputSchema(outputProperties, List.of(Map.of("response_mode", "preview"))), createReadOnlyToolAnnotations("Execute Update"), Map.of());
+ }
+
private MCPToolAnnotations createPlanningToolAnnotations() {
return MCPToolAnnotations.builder().title("Test Tool").readOnlyHint(false).destructiveHint(false).idempotentHint(false).openWorldHint(true).build();
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java
index 7fcba054667..54e51f653a5 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/response/MCPMapResponseTest.java
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
class MCPMapResponseTest {
@@ -30,6 +30,6 @@ class MCPMapResponseTest {
void assertToPayload() {
Map<String, Object> expectedPayload = Map.of("foo_key", "bar_value");
Map<String, Object> actual = new MCPMapResponse(expectedPayload).toPayload();
- assertThat(actual, is(expectedPayload));
+ assertThat(actual, sameInstance(expectedPayload));
}
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java
index 8da426a91a9..cd7373b58ae 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java
@@ -72,6 +72,9 @@ class MCPRuntimeProtectionPolicyTest {
assertThat(actualMaxRows.get("maximum_value"), is(MCPRuntimeProtectionPolicy.MAX_ROWS_LIMIT));
assertThat(actualMaxRows.get("applied_field"), is("applied_max_rows"));
assertThat(actualMaxRows.get("truncation_field"), is("truncated"));
+ assertThat(actualMaxRows.get("recovery"), is(
+ "For read-only queries, retry with a narrower SELECT, stronger WHERE clause, or smaller projection. "
+ + "If rows came from a side-effecting statement, do not replay that statement automatically; use a separate read-only query when more data is needed."));
Map<?, ?> actualTimeout = (Map<?, ?>) actualSQLExecutionLimits.get("timeout_ms");
assertThat(actualTimeout.get("default_value"), is(MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS));
assertThat(actualTimeout.get("maximum_value"), is(MCPRuntimeProtectionPolicy.MAX_TIMEOUT_MILLISECONDS));
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 bdf630f7677..b083ebe98f9 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
@@ -245,8 +245,10 @@ security_hints:
recovery: Close and recreate the MCP session after the quota is exhausted.}
sql_execution_limits:
max_rows: {default_value: 100, maximum_value: 5000, applied_field: applied_max_rows,
- truncation_field: truncated, recovery: 'Retry with a narrower SELECT, stronger
- WHERE clause, or smaller projection when rows are truncated.'}
+ truncation_field: truncated, recovery: 'For read-only queries, retry with
+ a narrower SELECT, stronger WHERE clause, or smaller projection. If rows
+ came from a side-effecting statement, do not replay that statement automatically;
+ use a separate read-only query when more data is needed.'}
timeout_ms: {default_value: 0, maximum_value: 300000, applied_field: applied_timeout_ms,
zero_means: server_default, recovery: Retry with a bounded timeout_ms value
or omit timeout_ms to use the server default.}