You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by su...@apache.org on 2026/07/11 12:41:42 UTC
(shardingsphere) branch master updated: Refactor MCP database capabilities to connector dialect metadata (#39070)
This is an automated email from the ASF dual-hosted git repository.
taojintianxia 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 25d65170b19 Refactor MCP database capabilities to connector dialect metadata (#39070)
25d65170b19 is described below
commit 25d65170b191a1ee56dd8268dfca227985d59878
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Sat Jul 11 20:41:34 2026 +0800
Refactor MCP database capabilities to connector dialect metadata (#39070)
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
* Refactor MCP database capabilities to connector dialect metadata
---
AGENTS.md | 7 +
.../database/metadata/DialectDatabaseMetaData.java | 3 +-
.../option/schema/DefaultSchemaOption.java | 8 +-
.../option/schema/DialectSchemaOption.java | 7 +
.../option/schema/DialectSchemaSemantics.java | 10 +-
.../fixture/TrunkDialectDatabaseMetaData.java | 3 +-
.../metadata/DialectDatabaseMetaDataTest.java | 2 +
.../option/schema/DefaultSchemaOptionTest.java | 19 ++-
.../core/type/DatabaseTypeRegistryTest.java | 3 +-
.../database/ClickHouseDatabaseMetaData.java | 8 +
.../database/ClickHouseDatabaseMetaDataTest.java | 8 +
.../database/option/FirebirdSchemaOption.java | 8 +-
.../database/FirebirdDatabaseMetaDataTest.java | 2 +
.../metadata/database/HiveDatabaseMetaData.java | 3 +-
.../database/HiveDatabaseMetaDataTest.java | 2 +
.../metadata/database/MariaDBDatabaseMetaData.java | 6 +
.../database/MariaDBDatabaseMetaDataTest.java | 8 +
.../metadata/database/MySQLDatabaseMetaData.java | 8 +
.../database/MySQLDatabaseMetaDataTest.java | 8 +
.../database/option/OpenGaussSchemaOption.java | 6 +
.../database/OpenGaussDatabaseMetaDataTest.java | 2 +
.../database/option/OracleSchemaOption.java | 8 +-
.../database/OracleDatabaseMetaDataTest.java | 6 +-
.../database/option/PostgreSQLSchemaOption.java | 8 +-
.../database/PostgreSQLDatabaseMetaDataTest.java | 2 +
.../metadata/database/PrestoDatabaseMetaData.java | 3 +-
.../database/PrestoDatabaseMetaDataTest.java | 2 +
.../database/SQLServerDatabaseMetaData.java | 3 +-
.../database/SQLServerDatabaseMetaDataTest.java | 2 +
.../content/reference/mcp/protocol-surface.cn.md | 10 +-
.../content/reference/mcp/protocol-surface.en.md | 10 +-
.../shardingsphere-mcp/developer-appendix.cn.md | 3 +-
.../shardingsphere-mcp/developer-appendix.en.md | 3 +-
.../instructions/server-instructions.md | 3 +-
.../MCPCompletionSpecificationFactoryTest.java | 4 +-
.../prompt/MCPPromptSpecificationFactoryTest.java | 4 +-
.../mcp/core/handler/core/CoreToolHandlers.java | 4 +-
.../mcp/core/protocol/error/MCPErrorConverter.java | 2 +
.../protocol/error/MCPRecoveryPayloadFactory.java | 2 +
.../error/MCPSQLRecoveryPayloadFactory.java | 31 +++-
.../handler/capability/RuntimeStatusHandler.java | 2 +-
.../tool/handler/execute/ClassificationResult.java | 24 +--
.../handler/execute/ExecuteExplainToolHandler.java | 55 +++++++
.../handler/execute/ExecuteQueryToolHandler.java | 22 +--
.../execute/ExplainSQLCandidateValidator.java | 110 +++++++++++++
.../execute/ExplainSQLSyntaxException.java} | 25 ++-
.../handler/execute/MCPJdbcStatementExecutor.java | 2 +-
.../handler/execute/MCPSQLExecutionFacade.java | 64 ++++++--
.../execute/SQLExecutionToolHandlerSupport.java | 22 ++-
.../execute/SQLStatementSafetyValidator.java | 2 +-
.../tool/handler/execute/SQLStatementScanner.java | 11 +-
.../tool/handler/execute/StatementClassifier.java | 10 +-
.../mcp-descriptors/mcp-descriptor-core.yaml | 175 ++++++++++++++++++++-
.../core/completion/MCPCompletionServiceTest.java | 4 +-
.../provider/MetadataCompletionProviderTest.java | 4 +-
.../core/handler/core/CoreHandlerProviderTest.java | 4 +-
.../mcp/core/protocol/MCPErrorConverterTest.java | 20 ++-
.../mcp/core/resource/ResourceTestDataFactory.java | 3 +
.../handler/CoreResourceHandlerSurfaceTest.java | 11 +-
.../capability/ServerCapabilitiesHandlerTest.java | 24 ++-
.../metadata/MetadataResourceHandlerTest.java | 6 +-
.../MetadataResourcePayloadMapperTest.java | 2 +-
.../MCPJdbcTransactionStatementExecutorTest.java | 19 +--
.../tool/handler/ToolDefinitionRegistryTest.java | 26 +--
.../execute/ExecuteExplainToolHandlerTest.java | 79 ++++++++++
.../execute/ExecuteQueryToolHandlerTest.java | 27 +---
.../execute/ExecuteUpdateToolHandlerTest.java | 25 ---
.../execute/ExplainSQLCandidateValidatorTest.java | 101 ++++++++++++
.../execute/MCPJdbcStatementExecutorTest.java | 18 +--
.../MCPSQLExecutionFacadeConcurrencyTest.java | 16 +-
.../handler/execute/MCPSQLExecutionFacadeTest.java | 130 +++++++++------
.../handler/execute/StatementClassifierTest.java | 19 +--
.../metadata/SearchMetadataPayloadBuilderTest.java | 2 +-
.../workflow/WorkflowProxyQueryServiceTest.java | 12 +-
.../EncryptWorkflowPlanningServiceTest.java | 2 +-
.../service/MaskWorkflowPlanningServiceTest.java | 2 +-
.../database/capability/DatabaseVersionUtil.java | 60 -------
.../database/capability/MCPDatabaseCapability.java | 62 +++++---
.../capability/MCPDatabaseCapabilityOption.java | 42 +----
.../capability/MCPDatabaseCapabilityProvider.java | 2 +-
.../database/capability/MCPDatabaseDialect.java | 23 ++-
.../database/capability/SupportedMCPStatement.java | 2 +-
.../AbstractMCPDatabaseCapabilityOption.java | 74 +--------
.../ClickHouseMCPDatabaseCapabilityOption.java | 6 +-
.../FirebirdMCPDatabaseCapabilityOption.java | 7 +-
.../dialect/HiveMCPDatabaseCapabilityOption.java | 6 +-
.../MariaDBMCPDatabaseCapabilityOption.java | 6 +-
.../dialect/MySQLMCPDatabaseCapabilityOption.java | 12 +-
.../OpenGaussMCPDatabaseCapabilityOption.java | 12 +-
.../dialect/OracleMCPDatabaseCapabilityOption.java | 7 +-
.../PostgreSQLMCPDatabaseCapabilityOption.java | 12 +-
.../dialect/PrestoMCPDatabaseCapabilityOption.java | 11 +-
.../SQLServerMCPDatabaseCapabilityOption.java | 7 +-
.../jdbc/MCPJdbcDatabaseProfileLoader.java | 3 +-
.../metadata/jdbc/MCPJdbcMetadataLoader.java | 17 +-
.../metadata/jdbc/RuntimeDatabaseProfile.java | 4 +
.../response/MCPDatabaseCapabilityResponse.java | 9 +-
.../database/spi/MCPFeatureExecutionFacade.java | 9 ++
.../descriptor/MCPGuidancePayloadBuilder.java | 23 ++-
.../MCPToolDescriptorCatalogValidator.java | 20 +++
.../descriptor/MCPToolOutputSchemaValidator.java | 2 +-
.../prompts/safe-sql-execution.md | 12 +-
.../capability/DatabaseVersionUtilTest.java | 48 ------
.../MCPDatabaseCapabilityProviderTest.java | 110 +++++++------
.../capability/MCPDatabaseDialectTest.java | 36 +++--
.../jdbc/AbstractMCPJdbcMetadataLoaderTest.java | 25 ++-
.../jdbc/MCPJdbcDatabaseProfileLoaderTest.java | 24 ++-
.../jdbc/MCPJdbcMetadataLoaderFailureTest.java | 2 +-
.../MCPJdbcMetadataLoaderSchemaFilteringTest.java | 11 +-
.../metadata/jdbc/MCPJdbcMetadataLoaderTest.java | 21 +++
.../metadata/query/MetadataQueryServiceTest.java | 13 +-
.../MCPDatabaseCapabilityResponseTest.java | 55 ++++---
.../RuntimeDatabaseValidationServiceTest.java | 2 +-
.../descriptor/MCPGuidancePayloadBuilderTest.java | 5 +-
.../WorkflowPlanningContextValidatorTest.java | 2 +-
.../service/WorkflowPlanningSupportTest.java | 2 +-
.../DatabaseMetaDataPersistFacadeTest.java | 3 +-
.../llm/conversation/LLMMCPSafetyValidator.java | 22 ++-
.../conversation/LLMMCPSafetyValidatorTest.java | 20 ++-
.../production/PackagedDistributionE2ETest.java | 3 +-
.../ProductionMySQLReadOnlySQLRuntimeE2ETest.java | 21 ++-
.../production/ProductionMySQLRuntimeE2ETest.java | 2 +-
.../model-contract/capabilities.yaml | 9 +-
.../baseline-contract/model-contract/guidance.yaml | 27 +++-
.../llm/evaluation/mcp-builder-evaluation.xml | 13 +-
125 files changed, 1474 insertions(+), 773 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index df71826f786..228e3f4067c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -125,6 +125,13 @@ This guide is written **for AI coding agents only**. Follow it literally; improv
Record the reason in the plan, review note, final response, or nearby code rationale.
- **Complete Implementation**: no MVPs/placeholders/TODOs—deliver fully runnable solutions.
+### Constructor Design Rules
+- Before adding or changing a constructor, inspect nearby production code and follow the module's existing conventions for visibility, Lombok, validation, and tests.
+- Keep only constructors with distinct production semantics or real framework, SPI, reflection, or serialization requirements.
+- Do not add or keep constructor overloads only for fewer arguments, test convenience, new-code backward compatibility, or future flexibility.
+- When a new semantic parameter is required, update callers to pass it explicitly instead of adding a default-forwarding overload.
+- Before handoff, scan changed files and call sites for unused or compatibility constructors and delete them.
+
### Performance Standards
- **Algorithm Awareness**: account for time and space complexity.
- **Resource Management**: optimize memory usage and I/O behavior.
diff --git a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java
index 323319e0ee5..60791671b96 100644
--- a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java
+++ b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java
@@ -33,6 +33,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.pagination.DialectPaginationOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sqlbatch.DialectSQLBatchOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.table.DialectDriverQuerySystemCatalogOption;
@@ -106,7 +107,7 @@ public interface DialectDatabaseMetaData extends DatabaseTypedSPI {
* @return schema option
*/
default DialectSchemaOption getSchemaOption() {
- return new DefaultSchemaOption(false, null);
+ return new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA);
}
/**
diff --git a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOption.java b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOption.java
index f28c9e523a9..dc3b05f6923 100644
--- a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOption.java
+++ b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOption.java
@@ -17,6 +17,7 @@
package org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -28,13 +29,16 @@ import java.util.Optional;
* Default schema option.
*/
@RequiredArgsConstructor
+@Getter
public final class DefaultSchemaOption implements DialectSchemaOption {
- @Getter
- private final boolean isSchemaAvailable;
+ private final boolean schemaAvailable;
+ @Getter(AccessLevel.NONE)
private final String defaultSchema;
+ private final DialectSchemaSemantics schemaSemantics;
+
@Override
@SuppressWarnings("ReturnOfNull")
public String getSchema(final Connection connection) {
diff --git a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaOption.java b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaOption.java
index 88f8a7817f4..6927cd21af6 100644
--- a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaOption.java
+++ b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaOption.java
@@ -53,4 +53,11 @@ public interface DialectSchemaOption {
* @return default system schema name
*/
Optional<String> getDefaultSystemSchema();
+
+ /**
+ * Get schema semantics.
+ *
+ * @return schema semantics
+ */
+ DialectSchemaSemantics getSchemaSemantics();
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SchemaSemantics.java b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaSemantics.java
similarity index 79%
rename from mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SchemaSemantics.java
rename to database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaSemantics.java
index 171e3d1240f..6b2cefc420d 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SchemaSemantics.java
+++ b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DialectSchemaSemantics.java
@@ -15,12 +15,14 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.capability;
+package org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema;
/**
- * Default schema semantics exposed by database-level capability.
+ * Dialect schema semantics.
*/
-public enum SchemaSemantics {
+public enum DialectSchemaSemantics {
- NATIVE_SCHEMA, DATABASE_AS_SCHEMA
+ NATIVE_SCHEMA,
+
+ DATABASE_AS_SCHEMA
}
diff --git a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/fixture/TrunkDialectDatabaseMetaData.java b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/fixture/TrunkDialectDatabaseMetaData.java
index 64481e5d51e..f11f6d24dda 100644
--- a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/fixture/TrunkDialectDatabaseMetaData.java
+++ b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/fixture/TrunkDialectDatabaseMetaData.java
@@ -23,6 +23,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
public final class TrunkDialectDatabaseMetaData implements DialectDatabaseMetaData {
@@ -43,7 +44,7 @@ public final class TrunkDialectDatabaseMetaData implements DialectDatabaseMetaDa
@Override
public DialectSchemaOption getSchemaOption() {
- return new DefaultSchemaOption(false, "test");
+ return new DefaultSchemaOption(false, "test", DialectSchemaSemantics.NATIVE_SCHEMA);
}
@Override
diff --git a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaDataTest.java b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaDataTest.java
index c76ee531b8a..d0c19856848 100644
--- a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaDataTest.java
+++ b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaDataTest.java
@@ -23,6 +23,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.join.DialectJoinOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.pagination.DialectPaginationOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.junit.jupiter.api.Test;
@@ -61,6 +62,7 @@ class DialectDatabaseMetaDataTest {
DialectSchemaOption actual = dialectDatabaseMetaData.getSchemaOption();
assertFalse(actual.isSchemaAvailable());
assertThat(actual.getDefaultSchema(), is(Optional.empty()));
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOptionTest.java b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOptionTest.java
index fcf9e8c49a7..fb98af7b0b8 100644
--- a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOptionTest.java
+++ b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/option/schema/DefaultSchemaOptionTest.java
@@ -37,40 +37,45 @@ class DefaultSchemaOptionTest {
void assertGetSchema() throws SQLException {
Connection successConnection = mock(Connection.class);
when(successConnection.getSchema()).thenReturn("actual_schema");
- assertThat(new DefaultSchemaOption(true, "foo_schema").getSchema(successConnection), is("actual_schema"));
+ assertThat(new DefaultSchemaOption(true, "foo_schema", DialectSchemaSemantics.NATIVE_SCHEMA).getSchema(successConnection), is("actual_schema"));
}
@Test
void assertGetSchemaWithException() throws SQLException {
Connection connection = mock(Connection.class);
when(connection.getSchema()).thenThrow(SQLException.class);
- assertNull(new DefaultSchemaOption(false, null).getSchema(connection));
+ assertNull(new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA).getSchema(connection));
}
@Test
void assertGetDefaultSchemaWithSchemaAvailable() {
- Optional<String> defaultSchema = new DefaultSchemaOption(true, "foo_schema").getDefaultSchema();
+ Optional<String> defaultSchema = new DefaultSchemaOption(true, "foo_schema", DialectSchemaSemantics.NATIVE_SCHEMA).getDefaultSchema();
assertTrue(defaultSchema.isPresent());
assertThat(defaultSchema.get(), is("foo_schema"));
}
@Test
void assertGetDefaultSchemaWithSchemaUnavailable() {
- assertFalse(new DefaultSchemaOption(false, null).getDefaultSchema().isPresent());
+ assertFalse(new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA).getDefaultSchema().isPresent());
}
@Test
void assertGetDefaultSystemSchema() {
- assertFalse(new DefaultSchemaOption(true, "foo_schema").getDefaultSystemSchema().isPresent());
+ assertFalse(new DefaultSchemaOption(true, "foo_schema", DialectSchemaSemantics.NATIVE_SCHEMA).getDefaultSystemSchema().isPresent());
}
@Test
void assertIsSchemaAvailable() {
- assertTrue(new DefaultSchemaOption(true, "foo_schema").isSchemaAvailable());
+ assertTrue(new DefaultSchemaOption(true, "foo_schema", DialectSchemaSemantics.NATIVE_SCHEMA).isSchemaAvailable());
}
@Test
void assertIsSchemaUnavailable() {
- assertFalse(new DefaultSchemaOption(false, "foo_schema").isSchemaAvailable());
+ assertFalse(new DefaultSchemaOption(false, "foo_schema", DialectSchemaSemantics.NATIVE_SCHEMA).isSchemaAvailable());
+ }
+
+ @Test
+ void assertGetSchemaSemantics() {
+ assertThat(new DefaultSchemaOption(false, "foo_schema", DialectSchemaSemantics.DATABASE_AS_SCHEMA).getSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
}
}
diff --git a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/type/DatabaseTypeRegistryTest.java b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/type/DatabaseTypeRegistryTest.java
index 6f05d1d4a4f..248d4d2209e 100644
--- a/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/type/DatabaseTypeRegistryTest.java
+++ b/database/connector/core/src/test/java/org/apache/shardingsphere/database/connector/core/type/DatabaseTypeRegistryTest.java
@@ -22,6 +22,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.enums
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -110,7 +111,7 @@ class DatabaseTypeRegistryTest {
private FixtureDialectDatabaseMetaData(final IdentifierPatternType identifierPatternType, final String defaultSchema) {
this.identifierPatternType = identifierPatternType;
- schemaOption = new DefaultSchemaOption(false, defaultSchema);
+ schemaOption = new DefaultSchemaOption(false, defaultSchema, DialectSchemaSemantics.NATIVE_SCHEMA);
}
@Override
diff --git a/database/connector/dialect/clickhouse/src/main/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaData.java b/database/connector/dialect/clickhouse/src/main/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaData.java
index d9b68e10e00..3bf198c8dbd 100644
--- a/database/connector/dialect/clickhouse/src/main/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaData.java
+++ b/database/connector/dialect/clickhouse/src/main/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaData.java
@@ -21,6 +21,9 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.enums
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
/**
* Database meta data of ClickHouse.
@@ -47,6 +50,11 @@ public final class ClickHouseDatabaseMetaData implements DialectDatabaseMetaData
return true;
}
+ @Override
+ public DialectSchemaOption getSchemaOption() {
+ return new DefaultSchemaOption(false, null, DialectSchemaSemantics.DATABASE_AS_SCHEMA);
+ }
+
@Override
public String getDatabaseType() {
return "ClickHouse";
diff --git a/database/connector/dialect/clickhouse/src/test/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaDataTest.java b/database/connector/dialect/clickhouse/src/test/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaDataTest.java
index 95fea65e966..e4d5fbca79a 100644
--- a/database/connector/dialect/clickhouse/src/test/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaDataTest.java
+++ b/database/connector/dialect/clickhouse/src/test/java/org/apache/shardingsphere/database/connector/clickhouse/database/ClickHouseDatabaseMetaDataTest.java
@@ -21,6 +21,8 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.enums
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader;
@@ -53,4 +55,10 @@ class ClickHouseDatabaseMetaDataTest {
void assertIsCaseSensitive() {
assertTrue(dialectDatabaseMetaData.isCaseSensitive());
}
+
+ @Test
+ void assertGetSchemaOption() {
+ DialectSchemaOption actual = dialectDatabaseMetaData.getSchemaOption();
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ }
}
diff --git a/database/connector/dialect/firebird/src/main/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/option/FirebirdSchemaOption.java b/database/connector/dialect/firebird/src/main/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/option/FirebirdSchemaOption.java
index cc0dd636c58..8827d662fd7 100644
--- a/database/connector/dialect/firebird/src/main/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/option/FirebirdSchemaOption.java
+++ b/database/connector/dialect/firebird/src/main/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/option/FirebirdSchemaOption.java
@@ -19,6 +19,7 @@ package org.apache.shardingsphere.database.connector.firebird.metadata.database.
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import java.sql.Connection;
import java.util.Optional;
@@ -28,7 +29,7 @@ import java.util.Optional;
*/
public final class FirebirdSchemaOption implements DialectSchemaOption {
- private final DialectSchemaOption delegate = new DefaultSchemaOption(false, null);
+ private final DialectSchemaOption delegate = new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA);
@Override
public boolean isSchemaAvailable() {
@@ -49,4 +50,9 @@ public final class FirebirdSchemaOption implements DialectSchemaOption {
public Optional<String> getDefaultSystemSchema() {
return Optional.of("system_tables");
}
+
+ @Override
+ public DialectSchemaSemantics getSchemaSemantics() {
+ return delegate.getSchemaSemantics();
+ }
}
diff --git a/database/connector/dialect/firebird/src/test/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/FirebirdDatabaseMetaDataTest.java b/database/connector/dialect/firebird/src/test/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/FirebirdDatabaseMetaDataTest.java
index da00d2e8978..a09a0b42e1d 100644
--- a/database/connector/dialect/firebird/src/test/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/FirebirdDatabaseMetaDataTest.java
+++ b/database/connector/dialect/firebird/src/test/java/org/apache/shardingsphere/database/connector/firebird/metadata/database/FirebirdDatabaseMetaDataTest.java
@@ -22,6 +22,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.enums
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -65,6 +66,7 @@ class FirebirdDatabaseMetaDataTest {
assertFalse(actual.isSchemaAvailable());
assertTrue(actual.getDefaultSystemSchema().isPresent());
assertThat(actual.getDefaultSystemSchema().get(), is("system_tables"));
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/database/connector/dialect/hive/src/main/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaData.java b/database/connector/dialect/hive/src/main/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaData.java
index bbc54051c44..f7f8ec01582 100644
--- a/database/connector/dialect/hive/src/main/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaData.java
+++ b/database/connector/dialect/hive/src/main/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaData.java
@@ -24,6 +24,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.function.DialectFunctionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sqlbatch.DialectSQLBatchOption;
import org.apache.shardingsphere.database.connector.hive.metadata.database.option.HiveFunctionOption;
@@ -49,7 +50,7 @@ public final class HiveDatabaseMetaData implements DialectDatabaseMetaData {
@Override
public DialectSchemaOption getSchemaOption() {
- return new DefaultSchemaOption(false, "default");
+ return new DefaultSchemaOption(false, "default", DialectSchemaSemantics.DATABASE_AS_SCHEMA);
}
@Override
diff --git a/database/connector/dialect/hive/src/test/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaDataTest.java b/database/connector/dialect/hive/src/test/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaDataTest.java
index 04ff520e07a..8f86e4fada0 100644
--- a/database/connector/dialect/hive/src/test/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaDataTest.java
+++ b/database/connector/dialect/hive/src/test/java/org/apache/shardingsphere/database/connector/hive/metadata/database/HiveDatabaseMetaDataTest.java
@@ -23,6 +23,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
import org.apache.shardingsphere.database.connector.hive.metadata.database.option.HiveFunctionOption;
@@ -62,6 +63,7 @@ class HiveDatabaseMetaDataTest {
assertTrue(actual.getDefaultSchema().isPresent());
assertThat(actual.getDefaultSchema().get(), is("default"));
assertFalse(actual.getDefaultSystemSchema().isPresent());
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
}
@Test
diff --git a/database/connector/dialect/mariadb/src/main/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaData.java b/database/connector/dialect/mariadb/src/main/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaData.java
index b9f89d9d4c1..250b2015f78 100644
--- a/database/connector/dialect/mariadb/src/main/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaData.java
+++ b/database/connector/dialect/mariadb/src/main/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaData.java
@@ -26,6 +26,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.datatype.DialectDataTypeOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.join.DialectJoinOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.keygen.DialectGeneratedKeyOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.version.DialectProtocolVersionOption;
@@ -67,6 +68,11 @@ public final class MariaDBDatabaseMetaData implements DialectDatabaseMetaData {
return delegate.getColumnOption();
}
+ @Override
+ public DialectSchemaOption getSchemaOption() {
+ return delegate.getSchemaOption();
+ }
+
@Override
public DialectConnectionOption getConnectionOption() {
return delegate.getConnectionOption();
diff --git a/database/connector/dialect/mariadb/src/test/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaDataTest.java b/database/connector/dialect/mariadb/src/test/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaDataTest.java
index 5421bf80374..95af32458b7 100644
--- a/database/connector/dialect/mariadb/src/test/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaDataTest.java
+++ b/database/connector/dialect/mariadb/src/test/java/org/apache/shardingsphere/database/connector/mariadb/metadata/database/MariaDBDatabaseMetaDataTest.java
@@ -24,6 +24,8 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.connection.DialectConnectionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.join.DialectJoinOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.keygen.DialectGeneratedKeyOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -72,6 +74,12 @@ class MariaDBDatabaseMetaDataTest {
assertFalse(metaData.getColumnOption().isColumnNameEqualsLabelInColumnProjection());
}
+ @Test
+ void assertGetSchemaOption() {
+ DialectSchemaOption actual = metaData.getSchemaOption();
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ }
+
@Test
void assertGetConnectionOption() {
DialectConnectionOption actual = metaData.getConnectionOption();
diff --git a/database/connector/dialect/mysql/src/main/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaData.java b/database/connector/dialect/mysql/src/main/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaData.java
index 623112cbed7..391bb392e78 100644
--- a/database/connector/dialect/mysql/src/main/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaData.java
+++ b/database/connector/dialect/mysql/src/main/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaData.java
@@ -27,6 +27,9 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.function.DialectFunctionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.join.DialectJoinOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.keygen.DialectGeneratedKeyOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.version.DialectProtocolVersionOption;
import org.apache.shardingsphere.database.connector.mysql.metadata.database.option.MySQLDataTypeOption;
@@ -67,6 +70,11 @@ public final class MySQLDatabaseMetaData implements DialectDatabaseMetaData {
return new DialectColumnOption(false);
}
+ @Override
+ public DialectSchemaOption getSchemaOption() {
+ return new DefaultSchemaOption(false, null, DialectSchemaSemantics.DATABASE_AS_SCHEMA);
+ }
+
@Override
public DialectConnectionOption getConnectionOption() {
return new DialectConnectionOption(true, true);
diff --git a/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaDataTest.java b/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaDataTest.java
index 14f8bbe4ef4..773c18638f9 100644
--- a/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaDataTest.java
+++ b/database/connector/dialect/mysql/src/test/java/org/apache/shardingsphere/database/connector/mysql/metadata/database/MySQLDatabaseMetaDataTest.java
@@ -24,6 +24,8 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.connection.DialectConnectionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.join.DialectJoinOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.keygen.DialectGeneratedKeyOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
@@ -72,6 +74,12 @@ class MySQLDatabaseMetaDataTest {
assertFalse(dialectDatabaseMetaData.getColumnOption().isColumnNameEqualsLabelInColumnProjection());
}
+ @Test
+ void assertGetSchemaOption() {
+ DialectSchemaOption actual = dialectDatabaseMetaData.getSchemaOption();
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ }
+
@Test
void assertGetConnectionOption() {
DialectConnectionOption actual = dialectDatabaseMetaData.getConnectionOption();
diff --git a/database/connector/dialect/opengauss/src/main/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/option/OpenGaussSchemaOption.java b/database/connector/dialect/opengauss/src/main/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/option/OpenGaussSchemaOption.java
index 2d183086a92..aebeb79e6f8 100644
--- a/database/connector/dialect/opengauss/src/main/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/option/OpenGaussSchemaOption.java
+++ b/database/connector/dialect/opengauss/src/main/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/option/OpenGaussSchemaOption.java
@@ -18,6 +18,7 @@
package org.apache.shardingsphere.database.connector.opengauss.metadata.database.option;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.postgresql.metadata.database.option.PostgreSQLSchemaOption;
import java.sql.Connection;
@@ -49,4 +50,9 @@ public final class OpenGaussSchemaOption implements DialectSchemaOption {
public Optional<String> getDefaultSystemSchema() {
return delegate.getDefaultSystemSchema();
}
+
+ @Override
+ public DialectSchemaSemantics getSchemaSemantics() {
+ return delegate.getSchemaSemantics();
+ }
}
diff --git a/database/connector/dialect/opengauss/src/test/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/OpenGaussDatabaseMetaDataTest.java b/database/connector/dialect/opengauss/src/test/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/OpenGaussDatabaseMetaDataTest.java
index 6240ed4b68a..14ff52b9933 100644
--- a/database/connector/dialect/opengauss/src/test/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/OpenGaussDatabaseMetaDataTest.java
+++ b/database/connector/dialect/opengauss/src/test/java/org/apache/shardingsphere/database/connector/opengauss/metadata/database/OpenGaussDatabaseMetaDataTest.java
@@ -23,6 +23,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.index.DialectIndexOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.table.DialectDriverQuerySystemCatalogOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
@@ -89,6 +90,7 @@ class OpenGaussDatabaseMetaDataTest {
assertTrue(actual.isSchemaAvailable());
assertThat(actual.getDefaultSchema(), is(Optional.of("public")));
assertThat(actual.getDefaultSystemSchema(), is(Optional.of("pg_catalog")));
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/database/connector/dialect/oracle/src/main/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/option/OracleSchemaOption.java b/database/connector/dialect/oracle/src/main/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/option/OracleSchemaOption.java
index 9e1391b9aa8..e18ba798b64 100644
--- a/database/connector/dialect/oracle/src/main/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/option/OracleSchemaOption.java
+++ b/database/connector/dialect/oracle/src/main/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/option/OracleSchemaOption.java
@@ -19,6 +19,7 @@ package org.apache.shardingsphere.database.connector.oracle.metadata.database.op
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import java.sql.Connection;
import java.sql.SQLException;
@@ -29,7 +30,7 @@ import java.util.Optional;
*/
public final class OracleSchemaOption implements DialectSchemaOption {
- private final DialectSchemaOption delegate = new DefaultSchemaOption(false, null);
+ private final DialectSchemaOption delegate = new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA);
@Override
public boolean isSchemaAvailable() {
@@ -54,4 +55,9 @@ public final class OracleSchemaOption implements DialectSchemaOption {
public Optional<String> getDefaultSystemSchema() {
return delegate.getDefaultSystemSchema();
}
+
+ @Override
+ public DialectSchemaSemantics getSchemaSemantics() {
+ return delegate.getSchemaSemantics();
+ }
}
diff --git a/database/connector/dialect/oracle/src/test/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/OracleDatabaseMetaDataTest.java b/database/connector/dialect/oracle/src/test/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/OracleDatabaseMetaDataTest.java
index bb1935b458a..4725c73dec9 100644
--- a/database/connector/dialect/oracle/src/test/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/OracleDatabaseMetaDataTest.java
+++ b/database/connector/dialect/oracle/src/test/java/org/apache/shardingsphere/database/connector/oracle/metadata/database/OracleDatabaseMetaDataTest.java
@@ -24,6 +24,8 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.connection.DialectConnectionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.index.DialectIndexOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.pagination.DialectPaginationOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
@@ -71,7 +73,9 @@ class OracleDatabaseMetaDataTest {
@Test
void assertGetSchemaOption() {
- assertThat(dialectDatabaseMetaData.getSchemaOption(), isA(OracleSchemaOption.class));
+ DialectSchemaOption actual = dialectDatabaseMetaData.getSchemaOption();
+ assertThat(actual, isA(OracleSchemaOption.class));
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/database/connector/dialect/postgresql/src/main/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/option/PostgreSQLSchemaOption.java b/database/connector/dialect/postgresql/src/main/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/option/PostgreSQLSchemaOption.java
index 88bc2f1ad9a..80c92f2849d 100644
--- a/database/connector/dialect/postgresql/src/main/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/option/PostgreSQLSchemaOption.java
+++ b/database/connector/dialect/postgresql/src/main/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/option/PostgreSQLSchemaOption.java
@@ -19,6 +19,7 @@ package org.apache.shardingsphere.database.connector.postgresql.metadata.databas
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import java.sql.Connection;
import java.util.Optional;
@@ -28,7 +29,7 @@ import java.util.Optional;
*/
public final class PostgreSQLSchemaOption implements DialectSchemaOption {
- private final DialectSchemaOption delegate = new DefaultSchemaOption(true, "public");
+ private final DialectSchemaOption delegate = new DefaultSchemaOption(true, "public", DialectSchemaSemantics.NATIVE_SCHEMA);
@Override
public boolean isSchemaAvailable() {
@@ -49,4 +50,9 @@ public final class PostgreSQLSchemaOption implements DialectSchemaOption {
public Optional<String> getDefaultSystemSchema() {
return Optional.of("pg_catalog");
}
+
+ @Override
+ public DialectSchemaSemantics getSchemaSemantics() {
+ return delegate.getSchemaSemantics();
+ }
}
diff --git a/database/connector/dialect/postgresql/src/test/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/PostgreSQLDatabaseMetaDataTest.java b/database/connector/dialect/postgresql/src/test/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/PostgreSQLDatabaseMetaDataTest.java
index b3f0adb4d81..bb4411a4f4d 100644
--- a/database/connector/dialect/postgresql/src/test/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/PostgreSQLDatabaseMetaDataTest.java
+++ b/database/connector/dialect/postgresql/src/test/java/org/apache/shardingsphere/database/connector/postgresql/metadata/database/PostgreSQLDatabaseMetaDataTest.java
@@ -22,6 +22,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.enums
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.index.DialectIndexOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -74,6 +75,7 @@ class PostgreSQLDatabaseMetaDataTest {
@Test
void assertGetDefaultSchema() {
assertThat(dialectDatabaseMetaData.getSchemaOption().getDefaultSchema(), is(Optional.of("public")));
+ assertThat(dialectDatabaseMetaData.getSchemaOption().getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/database/connector/dialect/presto/src/main/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaData.java b/database/connector/dialect/presto/src/main/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaData.java
index b4a9a359154..d83c40ec461 100644
--- a/database/connector/dialect/presto/src/main/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaData.java
+++ b/database/connector/dialect/presto/src/main/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaData.java
@@ -24,6 +24,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.function.DialectFunctionOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.presto.metadata.database.option.PrestoFunctionOption;
/**
@@ -48,7 +49,7 @@ public final class PrestoDatabaseMetaData implements DialectDatabaseMetaData {
@Override
public DialectSchemaOption getSchemaOption() {
- return new DefaultSchemaOption(false, "default");
+ return new DefaultSchemaOption(false, "default", DialectSchemaSemantics.NATIVE_SCHEMA);
}
@Override
diff --git a/database/connector/dialect/presto/src/test/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaDataTest.java b/database/connector/dialect/presto/src/test/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaDataTest.java
index 651f1d1746b..997ca50aa7e 100644
--- a/database/connector/dialect/presto/src/test/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaDataTest.java
+++ b/database/connector/dialect/presto/src/test/java/org/apache/shardingsphere/database/connector/presto/metadata/database/PrestoDatabaseMetaDataTest.java
@@ -23,6 +23,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
import org.apache.shardingsphere.database.connector.presto.metadata.database.option.PrestoFunctionOption;
@@ -64,6 +65,7 @@ class PrestoDatabaseMetaDataTest {
assertTrue(actual.getDefaultSchema().isPresent());
assertThat(actual.getDefaultSchema().get(), is("default"));
assertFalse(actual.getDefaultSystemSchema().isPresent());
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java b/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java
index 255230cd14d..a7b3332e2e5 100644
--- a/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java
+++ b/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java
@@ -27,6 +27,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.pagination.DialectPaginationOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.sql92.sqlserver.metadata.database.option.SQLServerFunctionOption;
@@ -57,7 +58,7 @@ public final class SQLServerDatabaseMetaData implements DialectDatabaseMetaData
@Override
public DialectSchemaOption getSchemaOption() {
- return new DefaultSchemaOption(false, "dbo");
+ return new DefaultSchemaOption(false, "dbo", DialectSchemaSemantics.NATIVE_SCHEMA);
}
@Override
diff --git a/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java b/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java
index f3de4bccfa0..0fa896b4658 100644
--- a/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java
+++ b/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java
@@ -24,6 +24,7 @@ import org.apache.shardingsphere.database.connector.core.metadata.database.metad
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.altertable.DialectAlterTableOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.pagination.DialectPaginationOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.transaction.DialectTransactionOption;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -68,6 +69,7 @@ class SQLServerDatabaseMetaDataTest {
assertFalse(actual.isSchemaAvailable());
assertThat(actual.getDefaultSchema(), is(Optional.of("dbo")));
assertThat(actual.getDefaultSystemSchema(), is(Optional.empty()));
+ assertThat(actual.getSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
}
@Test
diff --git a/docs/document/content/reference/mcp/protocol-surface.cn.md b/docs/document/content/reference/mcp/protocol-surface.cn.md
index 7572cc2f70a..b36103adb05 100644
--- a/docs/document/content/reference/mcp/protocol-surface.cn.md
+++ b/docs/document/content/reference/mcp/protocol-surface.cn.md
@@ -52,11 +52,19 @@ ShardingSphere-MCP 不要求 roots,也不会发送 `sampling/createMessage`
`database_gateway_execute_query`
-- 执行一个 classifier 允许的 `SELECT` 或 `EXPLAIN ANALYZE`。
+- 执行一个 classifier 允许的 `SELECT`。
- 拒绝 DML、DDL、DCL、事务控制、savepoint 和已知有副作用的查询形态。
- `max_rows` 范围是 `0..5000`,省略或 `0` 使用服务端默认值 `100`。
- `timeout_ms` 范围是 `0..300000`,`0` 表示不设置显式 timeout。
+`database_gateway_execute_explain_query`
+
+- 执行一个模型生成的数据库原生 `EXPLAIN`,用于诊断一个 classifier 允许的 `SELECT`。
+- 必须把原始 `SELECT` 放在 `sql`,把生成的 `EXPLAIN` 放在 `explain_sql`。
+- 拒绝 `EXPLAIN ANALYZE`、`EXPLAIN PLAN FOR`、多语句和有副作用 SQL。
+- `max_rows` 范围是 `0..5000`,省略或 `0` 使用服务端默认值 `100`。
+- `timeout_ms` 范围是 `0..300000`,`0` 表示不设置显式 timeout。
+
`database_gateway_execute_update`
- 预览或执行一个支持的有副作用 SQL。
diff --git a/docs/document/content/reference/mcp/protocol-surface.en.md b/docs/document/content/reference/mcp/protocol-surface.en.md
index fcf02622ca9..6a72cfbc2aa 100644
--- a/docs/document/content/reference/mcp/protocol-surface.en.md
+++ b/docs/document/content/reference/mcp/protocol-surface.en.md
@@ -52,11 +52,19 @@ ShardingSphere-MCP does not require roots and does not send `sampling/createMess
`database_gateway_execute_query`
-- Executes one classifier-approved `SELECT` or `EXPLAIN ANALYZE`.
+- Executes one classifier-approved `SELECT`.
- Rejects DML, DDL, DCL, transaction control, savepoints, and known side-effecting query forms.
- `max_rows` range is `0..5000`; omitted or `0` uses the server default `100`.
- `timeout_ms` range is `0..300000`; `0` means no explicit timeout.
+`database_gateway_execute_explain_query`
+
+- Executes one model-generated database-native `EXPLAIN` for one classifier-approved `SELECT`.
+- Requires the original `SELECT` as `sql` and the generated `EXPLAIN` as `explain_sql`.
+- Rejects `EXPLAIN ANALYZE`, `EXPLAIN PLAN FOR`, multiple statements, and side-effecting SQL.
+- `max_rows` range is `0..5000`; omitted or `0` uses the server default `100`.
+- `timeout_ms` range is `0..300000`; `0` means no explicit timeout.
+
`database_gateway_execute_update`
- Previews or executes one supported side-effecting SQL statement.
diff --git a/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.cn.md b/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.cn.md
index 27a25b746ca..5e98d1c2d10 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.cn.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.cn.md
@@ -85,7 +85,8 @@ Workflow resources 和 workflow tools 会包含简短的 `summary` 以及结构
|------------------------------------------------|--------------------------------------------|----------------------------------------------|
| `database_gateway_search_metadata` | 按名称片段和对象类型搜索运行时数据库元数据,并返回后续资源读取提示。 | 无。 |
| `database_gateway_validate_runtime_database` | 校验运行时数据库配置是否可用,用于接入失败时定位连接问题。 | 无。 |
-| `database_gateway_execute_query` | 执行一个已判定为查询类的 `SELECT` 或 `EXPLAIN ANALYZE`。 | 无;拒绝 DML、DDL、DCL、事务控制、savepoint 和其他有副作用 SQL。 |
+| `database_gateway_execute_query` | 执行一个已判定为查询类的 `SELECT`。 | 无;拒绝 DML、DDL、DCL、事务控制、savepoint 和其他有副作用 SQL。 |
+| `database_gateway_execute_explain_query` | 为一个 classifier 允许的 `SELECT` 执行模型生成的数据库原生 `EXPLAIN`。 | 无;拒绝 `EXPLAIN ANALYZE`、`EXPLAIN PLAN FOR`、多语句和有副作用 SQL。 |
| `database_gateway_execute_update` | 预览或执行一个可能修改数据、元数据、规则或事务状态的 SQL。 | 有;应先预览并确认。 |
| `database_gateway_apply_workflow` | 预览、执行或导出功能插件生成的治理变更计划。 | 取决于执行方式;预览和人工执行包不修改运行时状态。 |
| `database_gateway_validate_workflow` | 规则变更执行后,按功能插件自身边界校验规则状态或 workflow 执行结果。 | 无。 |
diff --git a/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.en.md b/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.en.md
index 3a8cf7f7faa..3a75f550075 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.en.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/developer-appendix.en.md
@@ -85,7 +85,8 @@ Workflow resources and workflow tools include a short `summary` plus structured
|------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `database_gateway_search_metadata` | Search runtime database metadata by name fragment and object type, and return resource hints for follow-up reads. | None. |
| `database_gateway_validate_runtime_database` | Validate whether runtime database configuration is reachable, mainly for diagnosing integration failures. | None. |
-| `database_gateway_execute_query` | Execute a statement classified as query, such as `SELECT` or `EXPLAIN ANALYZE`. | None. Rejects DML, DDL, DCL, transaction control, savepoint, and other side-effecting SQL. |
+| `database_gateway_execute_query` | Execute a statement classified as query, such as `SELECT`. | None. Rejects DML, DDL, DCL, transaction control, savepoint, and other side-effecting SQL. |
+| `database_gateway_execute_explain_query` | Execute a model-generated database-native `EXPLAIN` for one classifier-approved `SELECT`. | None. Rejects `EXPLAIN ANALYZE`, `EXPLAIN PLAN FOR`, multiple statements, and side-effecting SQL. |
| `database_gateway_execute_update` | Preview or execute SQL that may change data, metadata, rules, or transaction state. | Yes. Preview and confirmation are recommended. |
| `database_gateway_apply_workflow` | Preview, execute, or export a governance change plan created by a feature plugin. | Depends on the execution choice. Preview and manual packages do not change runtime state. |
| `database_gateway_validate_workflow` | After rule change execution, validate rule state or workflow execution results according to the feature plugin boundary. | None. |
diff --git a/mcp/bootstrap/src/main/resources/META-INF/shardingsphere-mcp/instructions/server-instructions.md b/mcp/bootstrap/src/main/resources/META-INF/shardingsphere-mcp/instructions/server-instructions.md
index b9f84933a26..245e574a2d4 100644
--- a/mcp/bootstrap/src/main/resources/META-INF/shardingsphere-mcp/instructions/server-instructions.md
+++ b/mcp/bootstrap/src/main/resources/META-INF/shardingsphere-mcp/instructions/server-instructions.md
@@ -21,7 +21,8 @@ Use MCP list methods (`tools/list`, `resources/list`, `resources/templates/list`
Use `completion/complete` for supported argument values. Read `shardingsphere://capabilities` when ShardingSphere domain capability guidance, workflow guidance, or side-effect notes are needed.
-Use `database_gateway_execute_query` only for classifier-approved `SELECT` or `EXPLAIN ANALYZE`.
+Use `database_gateway_execute_query` only for classifier-approved `SELECT`.
+Use `database_gateway_execute_explain_query` for execution-plan diagnostics.
Use `database_gateway_execute_update` with `execution_mode=preview` before side effects.
Treat `database_gateway_execute_update` preview as classification-only, not as a database dry run.
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/completion/MCPCompletionSpecificationFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/completion/MCPCompletionSpecificationFactoryTest.java
index 9e4d105dee5..f19438b2cd1 100644
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/completion/MCPCompletionSpecificationFactoryTest.java
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/completion/MCPCompletionSpecificationFactoryTest.java
@@ -198,8 +198,8 @@ class MCPCompletionSpecificationFactoryTest {
private MCPRuntimeContext createRuntimeContext(final WorkflowSessionContext workflowSessionContext) {
MCPDatabaseCapabilityProvider databaseCapabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
when(databaseCapabilityProvider.getDatabaseProfiles()).thenReturn(List.of(
- new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0"),
- new RuntimeDatabaseProfile("warehouse", "FixtureWarehouseDB", "2.0")));
+ new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true),
+ new RuntimeDatabaseProfile("warehouse", "FixtureWarehouseDB", "2.0", true, true)));
MCPRuntimeContext result = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
when(result.getDatabaseCapabilityProvider()).thenReturn(databaseCapabilityProvider);
when(result.getWorkflowSessionContext()).thenReturn(workflowSessionContext);
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/prompt/MCPPromptSpecificationFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/prompt/MCPPromptSpecificationFactoryTest.java
index 3a1ee6ad2ed..cb9b415ada2 100644
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/prompt/MCPPromptSpecificationFactoryTest.java
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/prompt/MCPPromptSpecificationFactoryTest.java
@@ -70,9 +70,9 @@ class MCPPromptSpecificationFactoryTest {
assertRenderedLines(actualText, List.of(
"- database: logic_db",
"- sql_intent: count orders",
- "2. Use database_gateway_execute_query only for one classifier-approved SELECT or EXPLAIN ANALYZE statement.",
+ "2. Use database_gateway_execute_query only for one classifier-approved SELECT statement.",
" Treat preview as classification-only, not as a database dry run.",
- "5. Never split or batch multiple SQL statements into one MCP call."));
+ "6. Never split or batch multiple SQL statements into one MCP call."));
}
@Test
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
index 121948c9728..653c8f59ebe 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
@@ -20,13 +20,14 @@ package org.apache.shardingsphere.mcp.core.handler.core;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
-import org.apache.shardingsphere.mcp.core.workflow.WorkflowRuntimeDefinitionRegistry;
+import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExecuteExplainToolHandler;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExecuteQueryToolHandler;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExecuteUpdateToolHandler;
import org.apache.shardingsphere.mcp.core.tool.handler.metadata.SearchMetadataToolHandler;
import org.apache.shardingsphere.mcp.core.tool.handler.metadata.ValidateRuntimeDatabaseToolHandler;
import org.apache.shardingsphere.mcp.core.tool.handler.workflow.WorkflowExecutionToolHandler;
import org.apache.shardingsphere.mcp.core.tool.handler.workflow.WorkflowValidationToolHandler;
+import org.apache.shardingsphere.mcp.core.workflow.WorkflowRuntimeDefinitionRegistry;
import java.util.Collection;
import java.util.List;
@@ -43,6 +44,7 @@ final class CoreToolHandlers {
new SearchMetadataToolHandler(),
new ValidateRuntimeDatabaseToolHandler(),
new ExecuteQueryToolHandler(),
+ new ExecuteExplainToolHandler(),
new ExecuteUpdateToolHandler(),
new WorkflowExecutionToolHandler(workflowRuntimeDefinitionRegistry),
new WorkflowValidationToolHandler(workflowRuntimeDefinitionRegistry));
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
index 4aa11fb52b9..25684107b1f 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
@@ -30,6 +30,7 @@ import org.apache.shardingsphere.mcp.core.protocol.exception.MCPToolCallLimitExc
import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedResourceUriException;
import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
import org.apache.shardingsphere.mcp.core.protocol.response.MCPErrorResponse;
+import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExplainSQLSyntaxException;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
import java.sql.SQLException;
@@ -48,6 +49,7 @@ public final class MCPErrorConverter {
private static final List<ErrorMapping> ERROR_MAPPINGS = List.of(
new ErrorMapping(UnsupportedToolException.class, "Unsupported tool."),
new ErrorMapping(UnsupportedResourceUriException.class, "Unsupported resource URI."),
+ new ErrorMapping(ExplainSQLSyntaxException.class, "Generated explain_sql is not valid for the target database."),
new ErrorMapping(MCPInvalidRequestException.class, "Invalid request."),
new ErrorMapping(MCPNotFoundException.class, "MCP operation not found."),
new ErrorMapping(MCPUnsupportedException.class, "Unsupported MCP operation."),
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 84b03f12fbf..0b8c6e09ed8 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
@@ -33,6 +33,7 @@ import org.apache.shardingsphere.mcp.core.protocol.exception.MCPUnsupportedSQLSt
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPWorkflowStateException;
import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedResourceUriException;
import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
+import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExplainSQLSyntaxException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospectionSQLStatementException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
@@ -50,6 +51,7 @@ final class MCPRecoveryPayloadFactory {
private static final List<RecoveryMapping> RECOVERY_MAPPINGS = List.of(
new RecoveryMapping(SQLToolMismatchException.class, cause -> MCPSQLRecoveryPayloadFactory.createSQLToolMismatchRecovery((SQLToolMismatchException) cause)),
+ new RecoveryMapping(ExplainSQLSyntaxException.class, cause -> MCPSQLRecoveryPayloadFactory.createExplainSQLSyntaxRecovery((ExplainSQLSyntaxException) cause)),
new RecoveryMapping(RuleDistSQLExecutionException.class, cause -> MCPSQLRecoveryPayloadFactory.createRuleDistSQLExecutionRecovery((RuleDistSQLExecutionException) cause)),
new RecoveryMapping(MetadataIntrospectionSQLStatementException.class,
cause -> MCPSQLRecoveryPayloadFactory.createMetadataIntrospectionSQLRecovery((MetadataIntrospectionSQLStatementException) cause)),
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
index 12031d21ec5..bcd5783e9bd 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
@@ -20,6 +20,7 @@ package org.apache.shardingsphere.mcp.core.protocol.error;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.ClassificationResult;
+import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExplainSQLSyntaxException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospectionSQLStatementException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
@@ -28,6 +29,7 @@ import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
import org.apache.shardingsphere.mcp.support.resource.MCPUriPathSegmentUtils;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -55,6 +57,33 @@ final class MCPSQLRecoveryPayloadFactory {
return result;
}
+ static Map<String, Object> createExplainSQLSyntaxRecovery(final ExplainSQLSyntaxException cause) {
+ Map<String, Object> result = MCPRecoveryPayloadSupport.createBaseRecovery("invalid_explain_sql",
+ "Regenerate explain_sql for the same sql using database-native EXPLAIN syntax, then retry database_gateway_execute_explain_query. Do not use EXPLAIN ANALYZE.");
+ result.put("database", cause.getDatabase());
+ result.put("schema", cause.getSchema());
+ result.put("sql", cause.getSql());
+ result.put("rejected_explain_sql", cause.getExplainSql());
+ result.put("suggested_arguments", createExplainRetryArguments(cause));
+ result.put(MCPPayloadFieldNames.NEXT_ACTIONS, MCPNextActionUtils.ordered(
+ MCPNextActionUtils.readResource(createDatabaseCapabilityUri(cause.getDatabase()), "Read explain tool semantics before regenerating explain_sql."),
+ MCPNextActionUtils.dependsOn(MCPNextActionUtils.callTool("database_gateway_execute_explain_query",
+ "Regenerate explain_sql from sql without changing sql, then retry the explain tool with the generated explain_sql.",
+ createExplainRetryArguments(cause)), 1)));
+ result.put("ask_user_when_uncertain", false);
+ return result;
+ }
+
+ private static Map<String, Object> createExplainRetryArguments(final ExplainSQLSyntaxException cause) {
+ Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+ result.put("database", cause.getDatabase());
+ if (!cause.getSchema().isEmpty()) {
+ result.put("schema", cause.getSchema());
+ }
+ result.put("sql", cause.getSql());
+ return result;
+ }
+
static Map<String, Object> createRuleDistSQLExecutionRecovery(final RuleDistSQLExecutionException cause) {
Map<String, Object> result = MCPRecoveryPayloadSupport.createBaseRecovery("rule_distsql_execution_failed",
"Do not ask the user to rewrite the SQL yet; read workflow guidance and verify the runtime database can execute ShardingSphere rule DistSQL.");
@@ -172,7 +201,7 @@ final class MCPSQLRecoveryPayloadFactory {
static Map<String, Object> createUnsupportedStatementRecovery() {
Map<String, Object> result = MCPRecoveryPayloadSupport.createBaseRecovery(
- "unsupported_sql_statement", "Ask the user for a supported SELECT, EXPLAIN ANALYZE, DML, DDL, DCL, transaction, or savepoint statement.");
+ "unsupported_sql_statement", "Ask the user for a supported SELECT, EXPLAIN, DML, DDL, DCL, transaction, or savepoint statement.");
result.put(MCPPayloadFieldNames.RESOURCES_TO_READ, MCPRecoveryPayloadSupport.createResourceHintList(
"shardingsphere://capabilities", "capability", "Read supported SQL statement classes before retrying."));
result.put(MCPPayloadFieldNames.NEXT_ACTIONS, List.of(MCPNextActionUtils.readResource("shardingsphere://capabilities", "Read supported statement classes before retrying.")));
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
index 566a5cc509e..57c8f7d2a3d 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
@@ -183,7 +183,7 @@ public final class RuntimeStatusHandler implements MCPResourceHandler<MCPDatabas
private Map<String, Object> createCapabilityStatus(final MCPDatabaseCapability capability) {
Map<String, Object> result = new LinkedHashMap<>(4, 1F);
result.put("available", true);
- result.put("supports_explain_analyze", capability.isSupportsExplainAnalyze());
+ result.put("supports_explain", capability.isSupportsExplain());
result.put("supported_statement_classes", capability.getSupportedStatementClasses().stream().map(Enum::name).toList());
result.put("supported_metadata_object_types", capability.getSupportedMetadataObjectTypes().stream().map(Enum::name).toList());
return result;
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
index f0b64a30350..20a0d471842 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
@@ -53,36 +53,18 @@ public final class ClassificationResult {
private final String savepointName;
- private final Optional<SupportedMCPStatement> analyzedStatementClass;
-
public ClassificationResult(final SupportedMCPStatement statementClass, final String statementType, final String normalizedSql, final String targetObjectName, final String savepointName) {
- this(statementClass, statementType, normalizedSql, targetObjectName, savepointName, null);
- }
-
- /**
- * Create statement classification result with the inner statement class analyzed by EXPLAIN ANALYZE.
- *
- * @param statementClass statement class
- * @param statementType statement type
- * @param normalizedSql normalized SQL
- * @param targetObjectName target object name
- * @param savepointName savepoint name
- * @param analyzedStatementClass inner statement class analyzed by EXPLAIN ANALYZE
- */
- public ClassificationResult(final SupportedMCPStatement statementClass, final String statementType, final String normalizedSql, final String targetObjectName, final String savepointName,
- final SupportedMCPStatement analyzedStatementClass) {
- this(statementClass, statementType, normalizedSql, targetObjectName, savepointName, analyzedStatementClass, targetObjectName.isEmpty() ? List.of() : List.of(targetObjectName));
+ this(statementClass, statementType, normalizedSql, targetObjectName, savepointName, targetObjectName.isEmpty() ? List.of() : List.of(targetObjectName));
}
ClassificationResult(final SupportedMCPStatement statementClass, final String statementType, final String normalizedSql, final String targetObjectName, final String savepointName,
- final SupportedMCPStatement analyzedStatementClass, final Collection<String> referencedObjectNames) {
+ final Collection<String> referencedObjectNames) {
this.statementClass = statementClass;
this.statementType = statementType;
this.normalizedSql = normalizedSql;
this.targetObjectName = targetObjectName;
this.referencedObjectNames = referencedObjectNames;
this.savepointName = savepointName;
- this.analyzedStatementClass = Optional.ofNullable(analyzedStatementClass);
}
/**
@@ -130,7 +112,7 @@ public final class ClassificationResult {
if (isRuleDistSQL()) {
return "rule-metadata";
}
- return switch (analyzedStatementClass.orElse(statementClass)) {
+ return switch (statementClass) {
case DML -> "physical-data";
case DDL -> "physical-structure";
case DCL -> "privilege-metadata";
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
new file mode 100644
index 00000000000..6a518d63184
--- /dev/null
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.handler.execute;
+
+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.support.database.MCPDatabaseHandlerContext;
+import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
+
+/**
+ * Execute model-assisted EXPLAIN SQL tool handler.
+ */
+public final class ExecuteExplainToolHandler implements MCPToolHandler<MCPDatabaseHandlerContext> {
+
+ private static final String TOOL_NAME = "database_gateway_execute_explain_query";
+
+ @Override
+ public Class<MCPDatabaseHandlerContext> getContextType() {
+ return MCPDatabaseHandlerContext.class;
+ }
+
+ @Override
+ public String getToolName() {
+ return TOOL_NAME;
+ }
+
+ @Override
+ public MCPResponse handle(final MCPDatabaseHandlerContext databaseContext, final MCPToolCall toolCall) {
+ MCPToolArguments toolArguments = new MCPToolArguments(toolCall.getArguments());
+ String sql = toolArguments.getStringArgument("sql");
+ String explainSql = toolArguments.getStringArgument("explain_sql");
+ SQLExecutionToolHandlerSupport.checkExecutionArguments(toolArguments, TOOL_NAME);
+ String schema = SQLExecutionToolHandlerSupport.resolveSchema(databaseContext, toolArguments);
+ SQLExecutionRequest executionRequest = SQLExecutionToolHandlerSupport.createReadOnlyExecutionRequest(toolCall, toolArguments,
+ schema, explainSql, TOOL_NAME);
+ return 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 e9a2d7420a9..ac6505ecb5c 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
@@ -19,14 +19,12 @@ package org.apache.shardingsphere.mcp.core.tool.handler.execute;
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.support.database.MCPDatabaseHandlerContext;
-import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
-import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereSchema;
import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
/**
@@ -53,27 +51,15 @@ public final class ExecuteQueryToolHandler implements MCPToolHandler<MCPDatabase
checkReadOnlyQuery(toolArguments, sql);
SQLExecutionToolHandlerSupport.checkExecutionArguments(toolArguments, TOOL_NAME);
return databaseContext.getExecutionFacade().execute(SQLExecutionToolHandlerSupport.createReadOnlyExecutionRequest(toolCall, toolArguments,
- resolveSchema(databaseContext, toolArguments), sql, TOOL_NAME));
- }
-
- private String resolveSchema(final MCPDatabaseHandlerContext databaseContext, final MCPToolArguments toolArguments) {
- String result = toolArguments.getStringArgument("schema");
- if (!result.isEmpty()) {
- return result;
- }
- String database = toolArguments.getStringArgument("database");
- if (database.isEmpty()) {
- return "";
- }
- List<ShardingSphereSchema> schemas = databaseContext.getMetadataQueryFacade().querySchemas(database);
- return 1 == schemas.size() ? schemas.iterator().next().getName() : "";
+ 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)) {
throw new SQLToolMismatchException(
- "database_gateway_execute_query only supports classifier-approved QUERY and EXPLAIN_ANALYZE statements. Use database_gateway_execute_update for side-effecting SQL.",
+ "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.",
TOOL_NAME, "database_gateway_execute_update", classificationResult,
createSuggestedArguments(toolArguments, classificationResult));
}
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLCandidateValidator.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLCandidateValidator.java
new file mode 100644
index 00000000000..1e624dc04e8
--- /dev/null
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLCandidateValidator.java
@@ -0,0 +1,110 @@
+/*
+ * 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.handler.execute;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+
+import java.util.List;
+import java.util.Set;
+
+final class ExplainSQLCandidateValidator {
+
+ private static final Set<String> STATEMENT_START_KEYWORDS = Set.of(
+ "EXPLAIN", "SELECT", "WITH", "VALUES", "INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE", "UPSERT",
+ "CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME", "ATTACH", "DETACH", "EXCHANGE", "UNDROP", "OPTIMIZE",
+ "GRANT", "REVOKE", "CALL", "EXEC", "EXECUTE", "DECLARE", "PREPARE", "DEALLOCATE", "DESCRIBE", "SHOW",
+ "DO", "COPY", "LOAD", "UNLOAD", "IMPORT", "EXPORT", "BEGIN", "START", "COMMIT", "ROLLBACK", "SAVEPOINT", "RELEASE",
+ "SET", "RESET", "USE", "PARALLEL");
+
+ private final StatementClassifier statementClassifier = new StatementClassifier();
+
+ private final SQLStatementScanner scanner = new SQLStatementScanner();
+
+ ClassificationResult validate(final String sql, final String explainSql) {
+ ClassificationResult explainedStatement = statementClassifier.classify(sql);
+ ShardingSpherePreconditions.checkState(SupportedMCPStatement.QUERY == explainedStatement.getStatementClass(),
+ () -> new MCPInvalidRequestException("database_gateway_execute_explain_query only supports QUERY statements as the explained SQL."));
+ String actualExplainSql = scanner.normalizeSingleStatement(explainSql);
+ ShardingSpherePreconditions.checkState(!scanner.containsExecutableComment(actualExplainSql),
+ () -> new MCPInvalidRequestException("Executable comments are not supported by the MCP explain query tool."));
+ List<SQLStatementToken> tokens = scanner.tokenize(actualExplainSql);
+ checkExplainCandidate(tokens, explainedStatement.getNormalizedSql(), actualExplainSql);
+ return new ClassificationResult(SupportedMCPStatement.EXPLAIN, "EXPLAIN", actualExplainSql, explainedStatement.getTargetObjectName().orElse(""), "",
+ explainedStatement.getReferencedObjectNames());
+ }
+
+ private void checkExplainCandidate(final List<SQLStatementToken> tokens, final String sql, final String explainSql) {
+ ShardingSpherePreconditions.checkState(!tokens.isEmpty() && scanner.isKeyword(tokens.get(0), "EXPLAIN"),
+ () -> new MCPInvalidRequestException("explain_sql must start with EXPLAIN."));
+ String explainPrefix = extractExplainPrefix(explainSql, sql);
+ List<SQLStatementToken> explainPrefixTokens = scanner.tokenize(explainPrefix);
+ ShardingSpherePreconditions.checkState(!containsKeywordSequence(explainPrefixTokens, "EXPLAIN", "PLAN", "FOR"),
+ () -> new MCPInvalidRequestException("EXPLAIN PLAN FOR workflows are not supported by the MCP explain query tool."));
+ ShardingSpherePreconditions.checkState(!containsKeyword(explainPrefixTokens, "ANALYZE", "ANALYSE"),
+ () -> new MCPInvalidRequestException("EXPLAIN ANALYZE is not supported by the MCP explain query tool."));
+ ShardingSpherePreconditions.checkState(!containsKeyword(explainPrefixTokens, "INTO"),
+ () -> new MCPInvalidRequestException("EXPLAIN output redirection is not supported by the MCP explain query tool."));
+ ShardingSpherePreconditions.checkState(!containsStatementStartKeyword(explainPrefixTokens),
+ () -> new MCPInvalidRequestException("explain_sql must not wrap the original sql argument in another statement."));
+ }
+
+ private String extractExplainPrefix(final String explainSql, final String sql) {
+ ShardingSpherePreconditions.checkState(explainSql.endsWith(sql),
+ () -> new MCPInvalidRequestException("explain_sql must include the original sql argument without rewriting it."));
+ return explainSql.substring(0, explainSql.length() - sql.length());
+ }
+
+ private boolean containsStatementStartKeyword(final List<SQLStatementToken> tokens) {
+ for (SQLStatementToken each : tokens.subList(1, tokens.size())) {
+ if (!each.quotedIdentifier() && STATEMENT_START_KEYWORDS.contains(each.upperText())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean containsKeyword(final List<SQLStatementToken> tokens, final String... keywords) {
+ for (SQLStatementToken each : tokens) {
+ if (scanner.isKeyword(each, keywords)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean containsKeywordSequence(final List<SQLStatementToken> tokens, final String... keywords) {
+ for (int index = 0; index + keywords.length <= tokens.size(); index++) {
+ if (containsKeywordSequence(tokens, index, keywords)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean containsKeywordSequence(final List<SQLStatementToken> tokens, final int startIndex, final String... keywords) {
+ for (int index = 0; index < keywords.length; index++) {
+ if (!scanner.isKeyword(tokens.get(startIndex + index), keywords[index])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLSyntaxException.java
similarity index 51%
copy from mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java
copy to mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLSyntaxException.java
index 20000fee87a..bc1a177cd96 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLSyntaxException.java
@@ -15,21 +15,32 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
+package org.apache.shardingsphere.mcp.core.tool.handler.execute;
import lombok.Getter;
-import lombok.RequiredArgsConstructor;
+import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
/**
- * Runtime database profile.
+ * Exception for rejected model-generated EXPLAIN SQL syntax.
*/
-@RequiredArgsConstructor
@Getter
-public final class RuntimeDatabaseProfile {
+public final class ExplainSQLSyntaxException extends MCPInvalidRequestException {
+
+ private static final long serialVersionUID = 2267318188070094144L;
private final String database;
- private final String databaseType;
+ private final String schema;
+
+ private final String sql;
+
+ private final String explainSql;
- private final String databaseVersion;
+ public ExplainSQLSyntaxException(final String database, final String schema, final String sql, final String explainSql, final RuntimeException cause) {
+ super("Generated explain_sql is not valid for the target database.", cause);
+ this.database = database;
+ this.schema = schema;
+ this.sql = sql;
+ this.explainSql = explainSql;
+ }
}
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 3c45261e16b..0fbfe47818e 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
@@ -206,7 +206,7 @@ public final class MCPJdbcStatementExecutor {
boolean hasResultSet = statement.execute(classificationResult.getNormalizedSql());
switch (classificationResult.getStatementClass()) {
case QUERY:
- case EXPLAIN_ANALYZE:
+ case EXPLAIN:
if (!hasResultSet) {
throw new QueryDidNotReturnResultSetException();
}
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 d0a19287ea1..a0216a082ee 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
@@ -21,6 +21,7 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
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.MCPUnsupportedException;
import org.apache.shardingsphere.mcp.core.session.MCPSessionExecutionCoordinator;
import org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
@@ -28,6 +29,7 @@ import org.apache.shardingsphere.mcp.core.session.MCPSessionNotExistedException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.trace.SQLExecutionTraceFactory;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
+import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
import org.apache.shardingsphere.mcp.support.database.exception.StatementClassNotSupportedException;
@@ -35,6 +37,8 @@ import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFac
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
+import java.sql.SQLException;
+import java.sql.SQLSyntaxErrorException;
import java.util.Locale;
import java.util.Optional;
@@ -68,22 +72,61 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
@Override
public SQLExecutionResponse execute(final SQLExecutionRequest executionRequest) {
try {
- return sessionExecutionCoordinator.executeWithSessionLock(executionRequest.getSessionId(), () -> executeInternal(executionRequest));
+ return sessionExecutionCoordinator.executeWithSessionLock(executionRequest.getSessionId(), () -> executeInternal(executionRequest, classify(executionRequest)));
} catch (final MCPSessionNotExistedException ex) {
throw recordFailure(executionRequest, SupportedMCPStatement.QUERY.name(), ex);
}
}
- private SQLExecutionResponse executeInternal(final SQLExecutionRequest executionRequest) {
- Optional<MCPDatabaseCapability> databaseCapability = databaseCapabilityProvider.provide(executionRequest.getDatabase());
- ShardingSpherePreconditions.checkState(databaseCapability.isPresent(), () -> recordFailure(executionRequest, "QUERY", new DatabaseCapabilityNotFoundException()));
- MCPDatabaseCapability actualDatabaseCapability = databaseCapability.orElseThrow();
- ClassificationResult classificationResult;
+ private SQLExecutionResponse execute(final SQLExecutionRequest executionRequest, final ClassificationResult classificationResult) {
+ try {
+ return sessionExecutionCoordinator.executeWithSessionLock(executionRequest.getSessionId(), () -> executeInternal(executionRequest, classificationResult));
+ } catch (final MCPSessionNotExistedException ex) {
+ throw recordFailure(executionRequest, classificationResult.getTraceStatementMarker(), ex);
+ }
+ }
+
+ @Override
+ public SQLExecutionResponse executeExplain(final SQLExecutionRequest executionRequest, final String sql) {
+ ClassificationResult classificationResult = new ExplainSQLCandidateValidator().validate(sql, executionRequest.getSql());
+ try {
+ return execute(executionRequest, classificationResult);
+ } catch (final MCPInvalidRequestException | MCPQueryFailedException ex) {
+ if (hasSQLSyntaxCause(ex)) {
+ throw new ExplainSQLSyntaxException(executionRequest.getDatabase(), executionRequest.getSchema(), sql, executionRequest.getSql(), ex);
+ }
+ throw ex;
+ }
+ }
+
+ private boolean hasSQLSyntaxCause(final Throwable cause) {
+ Throwable current = cause;
+ while (null != current) {
+ if (current instanceof SQLException && isSQLSyntaxError((SQLException) current)) {
+ return true;
+ }
+ current = current.getCause();
+ }
+ return false;
+ }
+
+ private boolean isSQLSyntaxError(final SQLException cause) {
+ String sqlState = cause.getSQLState();
+ return cause instanceof SQLSyntaxErrorException || "37000".equals(sqlState) || "42000".equals(sqlState) || "42601".equals(sqlState);
+ }
+
+ private ClassificationResult classify(final SQLExecutionRequest executionRequest) {
try {
- classificationResult = statementClassifier.classify(executionRequest.getSql());
+ return statementClassifier.classify(executionRequest.getSql());
} catch (final MCPUnsupportedException | IllegalArgumentException ex) {
throw recordFailure(executionRequest, SupportedMCPStatement.QUERY.name(), ex);
}
+ }
+
+ private SQLExecutionResponse 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();
ShardingSpherePreconditions.checkContains(actualDatabaseCapability.getSupportedStatementClasses(), classificationResult.getStatementClass(),
() -> recordFailure(executionRequest, classificationResult.getTraceStatementMarker(), new StatementClassNotSupportedException()));
checkCrossSchemaSql(executionRequest, actualDatabaseCapability, classificationResult);
@@ -94,14 +137,11 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
return recordSuccess(executionRequest, transactionStatementExecutor.execute(
executionRequest.getSessionId(), executionRequest.getDatabase(), actualDatabaseCapability, classificationResult), classificationResult.getTraceStatementMarker());
case QUERY:
+ case EXPLAIN:
case DML:
case DDL:
case DCL:
return recordSuccess(executionRequest, statementExecutor.execute(executionRequest, classificationResult, actualDatabaseCapability), classificationResult.getTraceStatementMarker());
- case EXPLAIN_ANALYZE:
- ShardingSpherePreconditions.checkState(actualDatabaseCapability.isSupportsExplainAnalyze(), () -> new MCPUnsupportedException("EXPLAIN ANALYZE is not supported."));
- return recordSuccess(executionRequest, statementExecutor.execute(executionRequest, classificationResult, actualDatabaseCapability),
- classificationResult.getTraceStatementMarker());
default:
throw new StatementClassNotSupportedException();
}
@@ -118,7 +158,7 @@ public final class MCPSQLExecutionFacade implements MCPFeatureExecutionFacade {
}
private void checkCrossSchemaSql(final SQLExecutionRequest executionRequest, final MCPDatabaseCapability databaseCapability, final ClassificationResult classificationResult) {
- if (databaseCapability.isSupportsCrossSchemaSql()) {
+ if (SchemaExecutionSemantics.BEST_EFFORT == databaseCapability.getSchemaExecutionSemantics()) {
return;
}
for (String each : classificationResult.getReferencedObjectNames()) {
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 97ba784e7ce..e1e68c517a6 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
@@ -19,25 +19,24 @@ package org.apache.shardingsphere.mcp.core.tool.handler.execute;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
+import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereSchema;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidToolArgumentException;
import org.apache.shardingsphere.mcp.core.tool.request.MCPToolArguments;
+import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
import org.apache.shardingsphere.mcp.support.security.MCPRuntimeProtectionPolicy;
+import java.util.List;
import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class SQLExecutionToolHandlerSupport {
static boolean isReadOnlyStatement(final ClassificationResult classificationResult) {
- if (SupportedMCPStatement.QUERY == classificationResult.getStatementClass()) {
- return true;
- }
- return SupportedMCPStatement.EXPLAIN_ANALYZE == classificationResult.getStatementClass()
- && SupportedMCPStatement.QUERY == classificationResult.getAnalyzedStatementClass().orElse(SupportedMCPStatement.QUERY);
+ return SupportedMCPStatement.QUERY == classificationResult.getStatementClass();
}
static void checkExecutionArguments(final MCPToolArguments toolArguments, final String sourceTool) {
@@ -67,6 +66,19 @@ final class SQLExecutionToolHandlerSupport {
return createExecutionRequest(toolCall, toolArguments, schema, sql, sourceTool, true);
}
+ static String resolveSchema(final MCPDatabaseHandlerContext databaseContext, final MCPToolArguments toolArguments) {
+ String result = toolArguments.getStringArgument("schema");
+ if (!result.isEmpty()) {
+ return result;
+ }
+ String database = toolArguments.getStringArgument("database");
+ if (database.isEmpty()) {
+ return "";
+ }
+ List<ShardingSphereSchema> schemas = databaseContext.getMetadataQueryFacade().querySchemas(database);
+ return 1 == schemas.size() ? schemas.iterator().next().getName() : "";
+ }
+
private static int resolveMaxRows(final MCPToolArguments toolArguments, final String sourceTool) {
int result = getIntegerArgument(toolArguments, sourceTool, "max_rows", MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS, 0, MCPRuntimeProtectionPolicy.MAX_ROWS_LIMIT,
MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS);
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementSafetyValidator.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementSafetyValidator.java
index 599f19ac25c..82b6c98f3a0 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementSafetyValidator.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementSafetyValidator.java
@@ -65,7 +65,7 @@ final class SQLStatementSafetyValidator {
|| upperSql.startsWith("COPY ")
|| upperSql.startsWith("LOAD ")
|| upperSql.startsWith("CALL ")
- || scanner.containsMySQLExecutableComment(sql)
+ || scanner.containsExecutableComment(sql)
|| scanner.containsUserVariableAssignment(sql)
|| containsBannedDialectPattern(scanner.tokenize(sql));
}
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementScanner.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementScanner.java
index 2cf63a4d8d5..93dca5fa767 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementScanner.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLStatementScanner.java
@@ -211,7 +211,7 @@ final class SQLStatementScanner {
return normalizeIdentifier(identifier).toUpperCase(Locale.ENGLISH);
}
- boolean containsMySQLExecutableComment(final String sql) {
+ boolean containsExecutableComment(final String sql) {
int currentIndex = 0;
while (currentIndex < sql.length()) {
if (isLineCommentStart(sql, currentIndex)) {
@@ -219,7 +219,7 @@ final class SQLStatementScanner {
continue;
}
if (isBlockCommentStart(sql, currentIndex)) {
- if (currentIndex + 2 < sql.length() && '!' == sql.charAt(currentIndex + 2)) {
+ if (isExecutableCommentStart(sql, currentIndex)) {
return true;
}
currentIndex = skipBlockComment(sql, currentIndex) + 1;
@@ -234,6 +234,13 @@ final class SQLStatementScanner {
return false;
}
+ private boolean isExecutableCommentStart(final String sql, final int startIndex) {
+ if (startIndex + 2 < sql.length() && '!' == sql.charAt(startIndex + 2)) {
+ return true;
+ }
+ return startIndex + 3 < sql.length() && 'M' == Character.toUpperCase(sql.charAt(startIndex + 2)) && '!' == sql.charAt(startIndex + 3);
+ }
+
boolean containsUserVariableAssignment(final String sql) {
int currentIndex = 0;
while (currentIndex < sql.length()) {
diff --git a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifier.java b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifier.java
index 8620e197245..db4a778ac8e 100644
--- a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifier.java
+++ b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifier.java
@@ -48,14 +48,6 @@ public final class StatementClassifier {
String leadingSql = actualSql.substring(scanner.skipInsignificant(actualSql, 0)).trim();
String upperLeadingSql = leadingSql.toUpperCase(Locale.ENGLISH);
safetyValidator.checkLeadingStatement(upperLeadingSql, actualSql);
- if (upperLeadingSql.startsWith("EXPLAIN ANALYZE")) {
- String explainedSql = leadingSql.substring("EXPLAIN ANALYZE".length()).trim();
- SQLStatementStructure explainedStatementStructure = structureResolver.resolve(explainedSql);
- SupportedMCPStatement explainedStatementClass = statementClassResolver.resolve(explainedStatementStructure);
- safetyValidator.checkStructuredStatement(explainedStatementClass, explainedStatementStructure);
- return new ClassificationResult(SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE", actualSql, targetResolver.resolve(explainedStatementStructure), "",
- explainedStatementClass, targetResolver.resolveAll(explainedStatementStructure));
- }
if (isSavepointStatement(upperLeadingSql)) {
String statementType = extractStatementType(upperLeadingSql);
String savepointName = extractSavepointName(leadingSql);
@@ -68,7 +60,7 @@ public final class StatementClassifier {
SQLStatementStructure statementStructure = structureResolver.resolve(actualSql);
SupportedMCPStatement statementClass = statementClassResolver.resolve(statementStructure);
safetyValidator.checkStructuredStatement(statementClass, statementStructure);
- return new ClassificationResult(statementClass, statementStructure.statementType(), actualSql, targetResolver.resolve(statementStructure), "", null,
+ return new ClassificationResult(statementClass, statementStructure.statementType(), actualSql, targetResolver.resolve(statementStructure), "",
targetResolver.resolveAll(statementStructure));
}
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 b8e575fe86d..7e8bb7d5bf7 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
@@ -74,6 +74,7 @@ resources:
relatedTools:
- database_gateway_search_metadata
- database_gateway_execute_query
+ - database_gateway_execute_explain_query
resourceTemplates:
- uriTemplate: shardingsphere://databases/{database}
name: logical-database-detail
@@ -92,7 +93,7 @@ resourceTemplates:
- uriTemplate: shardingsphere://databases/{database}/capabilities
name: database-capabilities
title: Database Capabilities
- description: "Read statement, transaction, schema, and metadata-object capabilities for one logical database. Use before executing SQL or deciding whether EXPLAIN ANALYZE is available."
+ description: "Read statement, transaction, schema, and metadata-object capabilities for one logical database. Use before executing SQL or preparing a database-native EXPLAIN candidate."
mimeType: application/json
shardingSphereMetadata:
uriVariables:
@@ -104,6 +105,7 @@ resourceTemplates:
resourceKind: detail
relatedTools:
- database_gateway_execute_query
+ - database_gateway_execute_explain_query
- database_gateway_execute_update
- uriTemplate: shardingsphere://databases/{database}/storage-units
name: storage-units
@@ -1227,7 +1229,7 @@ tools:
- name: database_gateway_execute_query
title: Execute Query SQL
description: >-
- Execute exactly one classifier-approved SELECT or EXPLAIN ANALYZE statement against a ShardingSphere logical database. Use for inspection only;
+ Execute exactly one classifier-approved SELECT statement against a ShardingSphere logical database. Use for inspection only;
DML, DDL, DCL, transaction control, savepoints, and known side-effecting query forms are rejected.
inputSchema:
type: object
@@ -1240,7 +1242,7 @@ tools:
description: "Optional schema or namespace hint for unqualified object names."
sql:
type: string
- description: "One classifier-approved SELECT or EXPLAIN ANALYZE statement. Do not include multiple statements or mutating SQL."
+ description: "One classifier-approved SELECT statement. Do not include EXPLAIN, multiple statements, or mutating SQL."
max_rows:
type: integer
description: "Optional maximum number of rows to return. Omit or pass 0 to use the server default of 100."
@@ -1279,10 +1281,9 @@ tools:
- result_set
statement_class:
type: string
- description: "Statement class: query or explain_analyze."
+ description: "Statement class: query."
enum:
- query
- - explain_analyze
statement_type:
type: string
description: "Database statement type reported by the execution classifier."
@@ -1382,6 +1383,167 @@ tools:
- DCL
- TRANSACTION_CONTROL
- SAVEPOINT
+ - name: database_gateway_execute_explain_query
+ title: Execute Explain Query SQL
+ description: >-
+ Execute exactly one model-generated database-native EXPLAIN SQL for one classifier-approved SELECT statement. Pass the original SELECT as sql
+ and the generated EXPLAIN as explain_sql. EXPLAIN ANALYZE and EXPLAIN PLAN FOR workflows are rejected.
+ inputSchema:
+ type: object
+ properties:
+ database:
+ type: string
+ description: "ShardingSphere logical database name that provides the execution boundary."
+ schema:
+ type: string
+ description: "Optional schema or namespace hint for unqualified object names."
+ sql:
+ type: string
+ description: "Original classifier-approved SELECT statement to explain. Do not include EXPLAIN, multiple statements, or mutating SQL."
+ explain_sql:
+ type: string
+ description: "Database-native EXPLAIN SQL generated for sql. It must start with EXPLAIN, include sql without rewriting it, and must not use ANALYZE."
+ max_rows:
+ type: integer
+ description: "Optional maximum number of plan rows to return. Omit or pass 0 to use the server default of 100."
+ default: 100
+ minimum: 0
+ maximum: 5000
+ examples:
+ - 100
+ timeout_ms:
+ type: integer
+ description: "Optional execution timeout in milliseconds. Zero means no explicit timeout."
+ default: 0
+ minimum: 0
+ maximum: 300000
+ examples:
+ - 0
+ required:
+ - database
+ - sql
+ - explain_sql
+ additionalProperties: false
+ outputSchema:
+ type: object
+ properties:
+ response_mode:
+ type: string
+ description: "Stable response mode marker for read-only SQL results."
+ enum:
+ - query
+ summary:
+ type: string
+ description: "Short model-facing SQL execution result summary."
+ result_kind:
+ type: string
+ description: "Result kind. database_gateway_execute_explain_query returns result_set for plan rows."
+ enum:
+ - result_set
+ statement_class:
+ type: string
+ description: "Statement class: explain."
+ enum:
+ - explain
+ statement_type:
+ type: string
+ description: "Database statement type reported by the execution classifier."
+ status:
+ type: string
+ description: "Execution status returned by the server."
+ normalized_sql:
+ type: string
+ description: "Classifier-normalized EXPLAIN SQL executed by the server."
+ columns:
+ type: array
+ description: "Returned column definitions."
+ rows:
+ type: array
+ description: "Returned plan rows."
+ row_objects:
+ type: array
+ description: "Object-shaped row view when column labels are unique."
+ row_object_status:
+ type: string
+ description: "Whether row_objects is available or why positional rows must be used, such as unnamed_columns, duplicate_column_labels, or omitted_large_result."
+ returned_row_count:
+ type: integer
+ description: "Number of result rows returned in this payload."
+ applied_max_rows:
+ type: integer
+ description: "Applied max_rows argument after defaulting and bounds enforcement."
+ applied_timeout_ms:
+ type: integer
+ description: "Applied timeout_ms argument; zero means the request used the server default."
+ truncated:
+ type: boolean
+ description: "Whether rows were truncated by max_rows or server limits."
+ next_actions:
+ type: array
+ description: "Ordered structured model-facing recommendations after SQL execution."
+ items:
+ type: object
+ properties:
+ order:
+ type: integer
+ description: "1-based action order."
+ type:
+ type: string
+ description: "Canonical action type such as tool_call, resource_read, ask_user, completion, or terminal."
+ title:
+ type: string
+ description: "Short model-facing action title."
+ tool_name:
+ type: string
+ description: "Canonical tool name when type is tool_call."
+ resource_uri:
+ type: string
+ description: "Canonical resource URI when type is resource_read."
+ arguments:
+ type: object
+ description: "Canonical arguments to provide to the target tool."
+ reason:
+ type: string
+ description: "Why the model should take this action."
+ required_inputs:
+ type: array
+ description: "User inputs needed before continuing."
+ examples:
+ - response_mode: query
+ summary: Executed EXPLAIN statement and returned 0 row(s).
+ result_kind: result_set
+ statement_class: explain
+ statement_type: EXPLAIN
+ status: OK
+ columns: []
+ rows: []
+ row_objects: []
+ row_object_status: available
+ returned_row_count: 0
+ applied_max_rows: 10
+ applied_timeout_ms: 0
+ truncated: false
+ next_actions:
+ - order: 1
+ type: terminal
+ title: Stop
+ reason: Return the execution plan rows to the user.
+ annotations:
+ title: Execute Explain Query SQL
+ readOnlyHint: true
+ destructiveHint: false
+ idempotentHint: true
+ openWorldHint: true
+ meta:
+ org.apache.shardingsphere/related-resource-uris:
+ - "shardingsphere://databases/{database}/capabilities"
+ - "shardingsphere://databases/{database}/schemas"
+ org.apache.shardingsphere/rejected-statement-classes:
+ - DML
+ - DDL
+ - DCL
+ - TRANSACTION_CONTROL
+ - SAVEPOINT
- name: database_gateway_execute_update
title: Preview or Execute Side-Effecting SQL
description: >-
@@ -1449,9 +1611,8 @@ tools:
- statement_ack
statement_class:
type: string
- description: "Statement class such as dml, ddl, dcl, transaction_control, savepoint, or explain_analyze."
+ description: "Statement class such as dml, ddl, dcl, transaction_control, or savepoint."
enum:
- - explain_analyze
- dml
- ddl
- dcl
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/MCPCompletionServiceTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/MCPCompletionServiceTest.java
index c33c073ded2..a92d116858c 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/MCPCompletionServiceTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/MCPCompletionServiceTest.java
@@ -144,8 +144,8 @@ class MCPCompletionServiceTest {
private MCPRuntimeContext createRuntimeContext(final WorkflowSessionContext workflowSessionContext) {
MCPDatabaseCapabilityProvider databaseCapabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
when(databaseCapabilityProvider.getDatabaseProfiles()).thenReturn(List.of(
- new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0"),
- new RuntimeDatabaseProfile("warehouse", "FixtureWarehouseDB", "2.0")));
+ new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true),
+ new RuntimeDatabaseProfile("warehouse", "FixtureWarehouseDB", "2.0", true, true)));
MCPRuntimeContext result = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
when(result.getDatabaseCapabilityProvider()).thenReturn(databaseCapabilityProvider);
when(result.getWorkflowSessionContext()).thenReturn(workflowSessionContext);
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/provider/MetadataCompletionProviderTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/provider/MetadataCompletionProviderTest.java
index b69cf8938a7..94de6294c26 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/provider/MetadataCompletionProviderTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/completion/provider/MetadataCompletionProviderTest.java
@@ -260,11 +260,11 @@ class MetadataCompletionProviderTest {
}
private RuntimeDatabaseProfile createDatabaseProfile(final String database) {
- return new RuntimeDatabaseProfile(database, "FixtureDB", "1.0");
+ return new RuntimeDatabaseProfile(database, "FixtureDB", "1.0", true, true);
}
private RuntimeDatabaseProfile createDatabaseMetadata() {
- return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0");
+ return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true);
}
private ShardingSphereSchema createSchemaMetadata() {
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
index f2657a987a7..0674eb4392e 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
@@ -34,8 +34,8 @@ class CoreHandlerProviderTest {
void assertGetToolHandlers() {
Collection<MCPToolHandler<?>> actual = new CoreHandlerProvider().getToolHandlers();
assertThat(actual.stream().map(MCPToolHandler::getToolName).toList(),
- is(List.of("database_gateway_search_metadata", "database_gateway_validate_runtime_database", "database_gateway_execute_query", "database_gateway_execute_update",
- "database_gateway_apply_workflow", "database_gateway_validate_workflow")));
+ is(List.of("database_gateway_search_metadata", "database_gateway_validate_runtime_database", "database_gateway_execute_query", "database_gateway_execute_explain_query",
+ "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow")));
}
@Test
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
index 83564cbb27a..8401de5d509 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
@@ -40,6 +40,7 @@ import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolExce
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.execute.ClassificationResult;
+import org.apache.shardingsphere.mcp.core.tool.handler.execute.ExplainSQLSyntaxException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospectionSQLStatementException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
@@ -265,7 +266,7 @@ class MCPErrorConverterTest {
@Test
void assertConvertUnsupportedMessageWithoutRecovery() {
Map<String, Object> actual = MCPErrorConverter.convert(new MCPUnsupportedException(
- "database_gateway_execute_query only supports classifier-approved QUERY and EXPLAIN_ANALYZE statements. Use database_gateway_execute_update for side-effecting SQL.")).toPayload();
+ "database_gateway_execute_query only supports classifier-approved QUERY statements.")).toPayload();
assertFalse(actual.containsKey("recovery"));
}
@@ -274,7 +275,8 @@ class MCPErrorConverterTest {
ClassificationResult classificationResult = new ClassificationResult(SupportedMCPStatement.DML, "UPDATE", "UPDATE orders SET status = 'PAID'", "orders", "");
Map<String, Object> suggestedArguments = Map.of("database", "logic_db", "schema", "public", "sql", "UPDATE orders SET status = 'PAID'", "execution_mode", "preview");
Map<String, Object> actual = MCPErrorConverter.convert(new SQLToolMismatchException(
- "database_gateway_execute_query only supports classifier-approved QUERY and EXPLAIN_ANALYZE statements. Use database_gateway_execute_update for side-effecting SQL.",
+ "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.",
"database_gateway_execute_query", "database_gateway_execute_update", classificationResult, suggestedArguments)).toPayload();
Map<?, ?> actualRecovery = (Map<?, ?>) actual.get("recovery");
assertThat(actualRecovery.get("category"), is("unsafe_sql_attempted"));
@@ -285,6 +287,20 @@ class MCPErrorConverterTest {
assertThat(((Map<?, ?>) ((List<?>) actualRecovery.get("next_actions")).getFirst()).get("arguments"), is(suggestedArguments));
}
+ @Test
+ void assertConvertExplainSQLSyntaxWithRecovery() {
+ Map<String, Object> actual = MCPErrorConverter.convert(new ExplainSQLSyntaxException("logic_db", "public", "SELECT * FROM orders", "EXPLAIN BROKEN SELECT * FROM orders",
+ new MCPInvalidRequestException("bad explain", new SQLSyntaxErrorException("bad explain")))).toPayload();
+ assertThat(actual.get("message"), is("Generated explain_sql is not valid for the target database."));
+ Map<?, ?> actualRecovery = (Map<?, ?>) actual.get("recovery");
+ assertThat(actualRecovery.get("category"), is("invalid_explain_sql"));
+ assertThat(actualRecovery.get("rejected_explain_sql"), is("EXPLAIN BROKEN SELECT * FROM orders"));
+ assertThat(actualRecovery.get("suggested_arguments"), is(Map.of("database", "logic_db", "schema", "public", "sql", "SELECT * FROM orders")));
+ Map<?, ?> actualRetryAction = (Map<?, ?>) ((List<?>) actualRecovery.get("next_actions")).get(1);
+ assertThat(actualRetryAction.get("tool_name"), is("database_gateway_execute_explain_query"));
+ assertFalse((Boolean) actualRecovery.get("ask_user_when_uncertain"));
+ }
+
@Test
void assertConvertReadOnlySQLToolMismatchWithRecovery() {
ClassificationResult classificationResult = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM orders", "orders", "");
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/ResourceTestDataFactory.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/ResourceTestDataFactory.java
index 7c74e799ef7..fd49a76297e 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/ResourceTestDataFactory.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/ResourceTestDataFactory.java
@@ -21,6 +21,8 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.system.DialectSystemDatabase;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -226,6 +228,7 @@ public final class ResourceTestDataFactory {
typedSPILoader.when(() -> TypedSPILoader.findService(DatabaseType.class, databaseType)).thenReturn(Optional.of(databaseTypeFromSPI));
typedSPILoader.when(() -> TypedSPILoader.getService(DatabaseType.class, databaseType)).thenReturn(databaseTypeFromSPI);
DialectDatabaseMetaData dialectDatabaseMetaData = mock(DialectDatabaseMetaData.class);
+ when(dialectDatabaseMetaData.getSchemaOption()).thenReturn(new DefaultSchemaOption(true, null, DialectSchemaSemantics.NATIVE_SCHEMA));
when(dialectDatabaseMetaData.getSequenceOption()).thenReturn(
sequenceSupported ? Optional.of(new DialectSequenceOption("SELECT SEQUENCE_SCHEMA, SEQUENCE_NAME FROM TEST_SEQUENCES")) : Optional.empty());
databaseTypedSPILoader.when(() -> DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class, databaseTypeFromSPI)).thenReturn(Optional.of(dialectDatabaseMetaData));
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/CoreResourceHandlerSurfaceTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/CoreResourceHandlerSurfaceTest.java
index e431ac7d9b7..50bba8a99f7 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/CoreResourceHandlerSurfaceTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/CoreResourceHandlerSurfaceTest.java
@@ -101,16 +101,19 @@ class CoreResourceHandlerSurfaceTest {
}
@Test
- void assertHandleWithUnsupportedIndexResource() {
+ void assertHandleWithoutIndexMetadata() {
try (MCPRequestScope requestContext = new MCPRequestScope(runtimeContext)) {
- MCPUnsupportedException actual = assertThrows(MCPUnsupportedException.class, () -> new MetadataResourceHandler(
+ MCPResponse actual = new MetadataResourceHandler(
"shardingsphere://databases/{database}/schemas/{schema}/tables/{table}/indexes",
(featureContext, uriVariables) -> featureContext.getMetadataQueryFacade().queryIndexes(
uriVariables.getValue("database"), uriVariables.getValue("schema"), uriVariables.getValue("table")))
.handle(requestContext,
parseUriVariables("shardingsphere://databases/{database}/schemas/{schema}/tables/{table}/indexes",
- "shardingsphere://databases/warehouse/schemas/warehouse/tables/facts/indexes")));
- assertThat(actual.getMessage(), is("Index resources are not supported for the current database."));
+ "shardingsphere://databases/warehouse/schemas/warehouse/tables/facts/indexes"));
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actual, isA(MCPItemsResponse.class));
+ assertThat(actualPayload.get("count"), is(0));
+ assertThat(actualPayload.get("self_uri"), is("shardingsphere://databases/warehouse/schemas/warehouse/tables/facts/indexes"));
}
}
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 6844bcce2f9..ea1e6d51731 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
@@ -43,7 +43,7 @@ class ServerCapabilitiesHandlerTest {
assertTrue(((Collection<?>) actual.get("supportedResources")).contains("shardingsphere://guidance"));
assertTrue(
((Collection<?>) actual.get("supportedTools")).containsAll(List.of("database_gateway_search_metadata", "database_gateway_validate_runtime_database", "database_gateway_execute_query",
- "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow")));
+ "database_gateway_execute_explain_query", "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow")));
assertThat(actual.get("guidanceResource"), is("shardingsphere://guidance"));
assertFalse(((List<?>) actual.get("resources")).isEmpty());
assertFalse(((List<?>) actual.get("resourceTemplates")).isEmpty());
@@ -134,6 +134,7 @@ class ServerCapabilitiesHandlerTest {
assertThat(((Map<?, ?>) actual.get("preflight_rule")).get("tool"), is("database_gateway_validate_runtime_database"));
Map<?, ?> sqlToolSelection = (Map<?, ?>) actual.get("sql_tool_selection");
assertThat(((Map<?, ?>) sqlToolSelection.get("read_only")).get("tool"), is("database_gateway_execute_query"));
+ assertThat(((Map<?, ?>) sqlToolSelection.get("explain")).get("tool"), is("database_gateway_execute_explain_query"));
assertThat(((Map<?, ?>) sqlToolSelection.get("side_effecting")).get("first_mode"), is("preview"));
assertThat(((Map<?, ?>) sqlToolSelection.get("side_effecting")).get("rule_change_preference"),
is("For natural-language rule changes, use the matching database_gateway_plan_* workflow tool before raw SQL execution; "
@@ -162,6 +163,8 @@ class ServerCapabilitiesHandlerTest {
assertThat(actual.get("metadata_first_resource"), is("shardingsphere://databases"));
assertTrue(String.valueOf(actual.get("preflight_rule")).contains("database_gateway_validate_runtime_database"));
Map<?, ?> sqlToolSelection = (Map<?, ?>) actual.get("sql_tool_selection");
+ assertTrue(sqlToolSelection.containsKey("read_only"));
+ assertTrue(sqlToolSelection.containsKey("explain"));
assertTrue(sqlToolSelection.containsKey("side_effecting"));
assertTrue(actual.containsKey("workflow_session_rule"));
assertTrue(actual.containsKey("next_action_rule"));
@@ -175,6 +178,8 @@ class ServerCapabilitiesHandlerTest {
assertThat(actual.get("guidance_resource"), is("shardingsphere://guidance"));
assertThat(actual.get("metadata_search_tool"), is("database_gateway_search_metadata"));
assertThat(actual.get("preflight_validation_tool"), is("database_gateway_validate_runtime_database"));
+ assertThat(actual.get("read_only_sql_tool"), is("database_gateway_execute_query"));
+ assertThat(actual.get("explain_sql_tool"), is("database_gateway_execute_explain_query"));
assertThat(actual.get("side_effect_sql_tool"), is("database_gateway_execute_update"));
}
@@ -205,6 +210,8 @@ class ServerCapabilitiesHandlerTest {
assertTrue(((List<?>) inspectMetadata.get("steps")).containsAll(List.of("resources/list", "resources/templates/list", "call_tool database_gateway_search_metadata")));
Map<?, ?> validateRuntimeDatabase = findByKey((List<?>) capabilities.get("common_flows"), "flow_id", "validate_runtime_database");
assertTrue(((List<?>) validateRuntimeDatabase.get("steps")).contains("call_tool database_gateway_validate_runtime_database"));
+ Map<?, ?> explainQuery = findByKey((List<?>) capabilities.get("common_flows"), "flow_id", "explain_query");
+ assertTrue(((List<?>) explainQuery.get("steps")).contains("call_tool database_gateway_execute_explain_query"));
Map<?, ?> sideEffectingSql = findByKey((List<?>) capabilities.get("common_flows"), "flow_id", "side_effecting_sql");
assertTrue(((List<?>) sideEffectingSql.get("steps")).contains("call_tool database_gateway_execute_update execution_mode=preview"));
assertTrue(((List<?>) sideEffectingSql.get("steps")).contains("call_tool database_gateway_execute_update execution_mode=execute"));
@@ -237,9 +244,11 @@ class ServerCapabilitiesHandlerTest {
Map<?, ?> databaseDetail = findResource(capabilities, "shardingsphere://databases/{database}");
assertThat(getResourceMeta(databaseDetail).get(MCPShardingSphereMetadataKeys.RESOURCE_KIND), is("detail"));
Map<?, ?> databaseCapabilities = findResource(capabilities, "shardingsphere://databases/{database}/capabilities");
- assertThat(getResourceMeta(databaseCapabilities).get(MCPShardingSphereMetadataKeys.RELATED_TOOLS), is(List.of("database_gateway_execute_query", "database_gateway_execute_update")));
+ assertThat(getResourceMeta(databaseCapabilities).get(MCPShardingSphereMetadataKeys.RELATED_TOOLS),
+ is(List.of("database_gateway_execute_query", "database_gateway_execute_explain_query", "database_gateway_execute_update")));
Map<?, ?> databases = findResource(capabilities, "shardingsphere://databases");
- assertThat(getResourceMeta(databases).get(MCPShardingSphereMetadataKeys.RELATED_TOOLS), is(List.of("database_gateway_search_metadata", "database_gateway_execute_query")));
+ assertThat(getResourceMeta(databases).get(MCPShardingSphereMetadataKeys.RELATED_TOOLS),
+ is(List.of("database_gateway_search_metadata", "database_gateway_execute_query", "database_gateway_execute_explain_query")));
Map<?, ?> databaseVariable = findUriVariable(databaseDetail, "database");
assertTrue((Boolean) databaseVariable.get("required"));
Map<?, ?> navigation = findByKey((List<?>) capabilities.get("resourceNavigation"), "to", "shardingsphere://guidance");
@@ -277,11 +286,20 @@ class ServerCapabilitiesHandlerTest {
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("category"));
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("recovery"));
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("next_actions"));
+ Map<?, ?> executeExplainTool = findTool(capabilities, "database_gateway_execute_explain_query");
+ Map<?, ?> executeExplainOutputProperties = (Map<?, ?>) ((Map<?, ?>) executeExplainTool.get("outputSchema")).get("properties");
+ assertThat(getInputFieldNames(executeExplainTool), is(List.of("database", "schema", "sql", "explain_sql", "max_rows", "timeout_ms")));
+ assertTrue(executeExplainOutputProperties.containsKey("response_mode"));
+ assertTrue(executeExplainOutputProperties.containsKey("result_kind"));
+ assertTrue(executeExplainOutputProperties.containsKey("statement_class"));
Map<?, ?> executeUpdateTool = findTool(capabilities, "database_gateway_execute_update");
Map<?, ?> executeUpdateOutputProperties = (Map<?, ?>) ((Map<?, ?>) executeUpdateTool.get("outputSchema")).get("properties");
assertTrue(executeUpdateOutputProperties.containsKey("response_mode"));
assertTrue(executeUpdateOutputProperties.containsKey("summary"));
assertTrue(((List<?>) ((Map<?, ?>) executeUpdateOutputProperties.get("result_kind")).get("enum")).containsAll(List.of("preview", "result_set", "update_count", "statement_ack")));
+ Map<?, ?> executeUpdateStatementClass = (Map<?, ?>) executeUpdateOutputProperties.get("statement_class");
+ assertThat(executeUpdateStatementClass.get("enum"), is(List.of("dml", "ddl", "dcl", "transaction_control", "savepoint")));
+ assertFalse(((String) executeUpdateStatementClass.get("description")).contains("explain_analyze"));
assertTrue(executeUpdateOutputProperties.containsKey("preview_semantics"));
assertTrue(executeUpdateOutputProperties.containsKey("review_summary"));
assertFalse(executeUpdateOutputProperties.containsKey("approval_summary"));
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
index ec1a9b9b17f..df4ff8adb29 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
@@ -113,7 +113,7 @@ class MetadataResourceHandlerTest {
@Test
void assertHandleListResourceWithEmptyScope() {
MetadataResourceHandler handler = new MetadataResourceHandler("shardingsphere://databases/{database}/schemas", (requestContext, uriVariables) -> List.of());
- MCPResponse actual = handler.handle(createDatabaseContext(Optional.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0"))),
+ MCPResponse actual = handler.handle(createDatabaseContext(Optional.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true))),
new MCPUriVariables(Map.of("database", "logic_db")));
Map<?, ?> actualEmptyState = (Map<?, ?>) actual.toPayload().get("empty_state");
assertThat(actualEmptyState.get("category"), is("empty_scope"));
@@ -124,7 +124,7 @@ class MetadataResourceHandlerTest {
@Test
void assertHandleSchemaDetailResourceNotVisible() {
MetadataResourceHandler handler = new MetadataResourceHandler("shardingsphere://databases/{database}/schemas/{schema}", (requestContext, uriVariables) -> List.of());
- MCPResponse actual = handler.handle(createDatabaseContext(Optional.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0"))),
+ MCPResponse actual = handler.handle(createDatabaseContext(Optional.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true))),
new MCPUriVariables(Map.of("database", "logic_db", "schema", "missing_schema")));
Map<?, ?> actualEmptyState = (Map<?, ?>) actual.toPayload().get("empty_state");
assertThat(actualEmptyState.get("category"), is("schema_not_visible"));
@@ -136,7 +136,7 @@ class MetadataResourceHandlerTest {
@Test
void assertHandleObjectDetailResourceNotVisible() {
MetadataResourceHandler handler = new MetadataResourceHandler("shardingsphere://databases/{database}/schemas/{schema}/tables/{table}", (requestContext, uriVariables) -> List.of());
- MCPResponse actual = handler.handle(createDatabaseContext(Optional.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0"))),
+ MCPResponse actual = handler.handle(createDatabaseContext(Optional.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true))),
new MCPUriVariables(Map.of("database", "logic_db", "schema", "public", "table", "missing_table")));
Map<?, ?> actualEmptyState = (Map<?, ?>) actual.toPayload().get("empty_state");
assertThat(actualEmptyState.get("category"), is("object_not_visible"));
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourcePayloadMapperTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourcePayloadMapperTest.java
index 9ffd841b2e4..0c2ee4ba367 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourcePayloadMapperTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourcePayloadMapperTest.java
@@ -46,7 +46,7 @@ class MetadataResourcePayloadMapperTest {
MCPMetadataQueryFacade metadataQueryFacade = mock(MCPMetadataQueryFacade.class);
when(metadataQueryFacade.querySchemas("logic_db")).thenReturn(List.of(createSchemaMetadata()));
List<?> actual = new MetadataResourcePayloadMapper(metadataQueryFacade, new MCPUriVariables(Map.of()), true)
- .map(createMetadata("logical-database"), List.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0")));
+ .map(createMetadata("logical-database"), List.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true)));
Map<?, ?> actualDatabase = (Map<?, ?>) actual.getFirst();
assertThat(actualDatabase.get("database"), is("logic_db"));
assertThat(actualDatabase.get("databaseType"), is("FixtureDB"));
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/session/MCPJdbcTransactionStatementExecutorTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/session/MCPJdbcTransactionStatementExecutorTest.java
index 642a62b126a..fd473e8101f 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/session/MCPJdbcTransactionStatementExecutorTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/session/MCPJdbcTransactionStatementExecutorTest.java
@@ -24,9 +24,6 @@ import org.apache.shardingsphere.mcp.core.tool.handler.execute.ClassificationRes
import org.apache.shardingsphere.mcp.core.tool.handler.execute.MCPJdbcTransactionStatementExecutor;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.StatementClassifier;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
-import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityOption;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
@@ -134,20 +131,18 @@ class MCPJdbcTransactionStatementExecutorTest {
}
private MCPDatabaseCapability createCapability() {
- return createCapability("logic_db", TransactionCapability.LOCAL_WITH_SAVEPOINT);
+ return createCapability(TransactionCapability.LOCAL_WITH_SAVEPOINT);
}
- private MCPDatabaseCapability createCapability(final String databaseName, final TransactionCapability transactionCapability) {
- MCPDatabaseCapabilityOption option = mock(MCPDatabaseCapabilityOption.class);
- when(option.getType()).thenReturn("FixtureDB");
- when(option.getTransactionCapability()).thenReturn(transactionCapability);
- when(option.getDefaultSchemaSemantics()).thenReturn(SchemaSemantics.NATIVE_SCHEMA);
- when(option.getSchemaExecutionSemantics()).thenReturn(SchemaExecutionSemantics.FIXED_TO_DATABASE);
- return new MCPDatabaseCapability(databaseName, "", option);
+ private MCPDatabaseCapability createCapability(final TransactionCapability transactionCapability) {
+ MCPDatabaseCapability result = mock(MCPDatabaseCapability.class);
+ when(result.isSupportsTransactionControl()).thenReturn(TransactionCapability.NONE != transactionCapability);
+ when(result.isSupportsSavepoint()).thenReturn(TransactionCapability.LOCAL_WITH_SAVEPOINT == transactionCapability);
+ return result;
}
private MCPDatabaseCapability createCapabilityWithoutSavepoint() {
- return createCapability("warehouse", TransactionCapability.LOCAL);
+ return createCapability(TransactionCapability.LOCAL);
}
private void prepareTransactionState(final String sql, final MCPSessionManager sessionManager, final Connection connection, final Savepoint savepoint) throws SQLException {
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
index ec8fd4649d2..8bd025cd87d 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
@@ -57,29 +57,31 @@ class ToolDefinitionRegistryTest {
@Test
void assertGetSupportedTools() {
assertThat(ToolDefinitionRegistry.getSupportedTools(), contains("database_gateway_search_metadata", "database_gateway_validate_runtime_database", "database_gateway_execute_query",
- "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow"));
+ "database_gateway_execute_explain_query", "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow"));
}
@Test
void assertGetSupportedToolDescriptors() {
List<MCPToolDescriptor> actual = ToolDefinitionRegistry.getSupportedToolDescriptors();
assertThat(actual.stream().map(MCPToolDescriptor::getName).toList(),
- is(List.of("database_gateway_search_metadata", "database_gateway_validate_runtime_database", "database_gateway_execute_query", "database_gateway_execute_update",
- "database_gateway_apply_workflow", "database_gateway_validate_workflow")));
+ is(List.of("database_gateway_search_metadata", "database_gateway_validate_runtime_database", "database_gateway_execute_query", "database_gateway_execute_explain_query",
+ "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow")));
assertToolFields(actual.get(0), List.of("database", "schema", "query", "object_types"));
assertToolFields(actual.get(1), List.of("database"));
assertRequiredFields(actual.get(1), List.of("database"));
assertToolFields(actual.get(2), List.of("database", "schema", "sql", "max_rows", "timeout_ms"));
assertRequiredFields(actual.get(2), List.of("database", "sql"));
- assertToolFields(actual.get(3), List.of("database", "schema", "sql", "execution_mode", "max_rows", "timeout_ms"));
- assertRequiredFields(actual.get(3), List.of("database", "sql", "execution_mode"));
- assertField(actual.get(3), "execution_mode", "string", List.of("execute", "preview"), true);
- assertField(actual.get(3), "max_rows", "integer", List.of(), false);
- assertField(actual.get(3), "timeout_ms", "integer", List.of(), false);
- assertToolFields(actual.get(4), List.of("plan_id", "execution_mode", "approved_steps"));
- assertRequiredFields(actual.get(4), List.of("plan_id", "execution_mode"));
- assertField(actual.get(4), "execution_mode", "string", List.of("preview", "review-then-execute", "manual-only"), true);
- assertField(actual.get(4), "approved_steps", "array", List.of(), false);
+ assertToolFields(actual.get(3), List.of("database", "schema", "sql", "explain_sql", "max_rows", "timeout_ms"));
+ assertRequiredFields(actual.get(3), List.of("database", "sql", "explain_sql"));
+ assertToolFields(actual.get(4), List.of("database", "schema", "sql", "execution_mode", "max_rows", "timeout_ms"));
+ assertRequiredFields(actual.get(4), List.of("database", "sql", "execution_mode"));
+ assertField(actual.get(4), "execution_mode", "string", List.of("execute", "preview"), true);
+ assertField(actual.get(4), "max_rows", "integer", List.of(), false);
+ assertField(actual.get(4), "timeout_ms", "integer", List.of(), false);
+ assertToolFields(actual.get(5), List.of("plan_id", "execution_mode", "approved_steps"));
+ assertRequiredFields(actual.get(5), List.of("plan_id", "execution_mode"));
+ assertField(actual.get(5), "execution_mode", "string", List.of("preview", "review-then-execute", "manual-only"), true);
+ assertField(actual.get(5), "approved_steps", "array", List.of(), false);
}
@Test
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
new file mode 100644
index 00000000000..1f7fd5d674e
--- /dev/null
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteExplainToolHandlerTest.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.core.tool.handler.execute;
+
+import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
+import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereSchema;
+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.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+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.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+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));
+ MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
+ when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
+ MCPResponse actual = new ExecuteExplainToolHandler().handle(databaseContext, new MCPToolCall("session-1",
+ Map.of("database", "logic_db", "schema", "public", "sql", "SELECT * FROM orders", "explain_sql", "EXPLAIN SELECT * FROM orders")));
+ assertThat(actual.toPayload().get("statement_class"), is("explain"));
+ ArgumentCaptor<SQLExecutionRequest> requestCaptor = ArgumentCaptor.forClass(SQLExecutionRequest.class);
+ verify(executionFacade).executeExplain(requestCaptor.capture(), eq("SELECT * FROM orders"));
+ assertThat(requestCaptor.getValue().getDatabase(), is("logic_db"));
+ assertThat(requestCaptor.getValue().getSchema(), is("public"));
+ assertThat(requestCaptor.getValue().getSql(), is("EXPLAIN SELECT * FROM orders"));
+ assertTrue(requestCaptor.getValue().isReadOnlyExecution());
+ }
+
+ @Test
+ void assertHandleExplainQueryWithSingleSchemaDefault() {
+ MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
+ when(executionFacade.executeExplain(any(), any())).thenReturn(SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN, "EXPLAIN", List.of(), List.of(), false));
+ MCPMetadataQueryFacade metadataQueryFacade = mock(MCPMetadataQueryFacade.class);
+ when(metadataQueryFacade.querySchemas("logic_db")).thenReturn(List.of(new ShardingSphereSchema("public", mock(DatabaseType.class))));
+ MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
+ when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
+ when(databaseContext.getMetadataQueryFacade()).thenReturn(metadataQueryFacade);
+ new ExecuteExplainToolHandler().handle(databaseContext, new MCPToolCall("session-1",
+ Map.of("database", "logic_db", "sql", "SELECT * FROM orders", "explain_sql", "EXPLAIN SELECT * FROM orders")));
+ ArgumentCaptor<SQLExecutionRequest> requestCaptor = ArgumentCaptor.forClass(SQLExecutionRequest.class);
+ verify(executionFacade).executeExplain(requestCaptor.capture(), eq("SELECT * FROM orders"));
+ assertThat(requestCaptor.getValue().getSchema(), is("public"));
+ }
+}
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 bd03fe86354..5fd32c7f831 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,7 +24,6 @@ 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;
@@ -82,27 +81,12 @@ class ExecuteQueryToolHandlerTest {
}
@Test
- void assertHandleExplainAnalyze() {
- MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE", List.of(), List.of(), false));
+ void assertRejectExplain() {
MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
- when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
- MCPResponse actual = new ExecuteQueryToolHandler().handle(databaseContext, new MCPToolCall("session-1",
- Map.of("database", "logic_db", "schema", "public", "sql", "EXPLAIN ANALYZE SELECT * FROM orders")));
- assertThat(actual.toPayload().get("statement_class"), is("explain_analyze"));
- verify(executionFacade).execute(any());
- }
-
- @Test
- void assertRejectSideEffectingExplainAnalyze() {
- MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
- when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
MCPUnsupportedException actual = assertThrows(MCPUnsupportedException.class, () -> new ExecuteQueryToolHandler().handle(databaseContext, new MCPToolCall("session-1",
- Map.of("database", "logic_db", "sql", "EXPLAIN ANALYZE UPDATE orders SET status = 'PAID'"))));
- assertThat(actual.getMessage(),
- is("database_gateway_execute_query only supports classifier-approved QUERY and EXPLAIN_ANALYZE statements. Use database_gateway_execute_update for side-effecting SQL."));
- verifyNoInteractions(executionFacade);
+ Map.of("database", "logic_db", "schema", "public", "sql", "EXPLAIN SELECT * FROM orders"))));
+ assertThat(actual.getMessage(), is("Statement is not supported by the MCP contract."));
+ verifyNoInteractions(databaseContext);
}
@Test
@@ -124,7 +108,8 @@ class ExecuteQueryToolHandlerTest {
MCPUnsupportedException actual = assertThrows(MCPUnsupportedException.class, () -> new ExecuteQueryToolHandler().handle(databaseContext, new MCPToolCall("session-1",
Map.of("database", "logic_db", "sql", "update orders set status = 'PAID'"))));
assertThat(actual.getMessage(),
- is("database_gateway_execute_query only supports classifier-approved QUERY and EXPLAIN_ANALYZE statements. Use database_gateway_execute_update for side-effecting SQL."));
+ is("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."));
verifyNoInteractions(executionFacade);
}
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 9de02cb18d3..8907b14fd5a 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,7 +23,6 @@ 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;
@@ -185,28 +184,4 @@ class ExecuteUpdateToolHandlerTest {
verifyNoInteractions(executionFacade);
}
- @Test
- void assertRejectExplainAnalyze() {
- MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
- when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
- MCPUnsupportedException actual = assertThrows(MCPUnsupportedException.class, () -> new ExecuteUpdateToolHandler().handle(databaseContext, new MCPToolCall("session-1",
- Map.of("database", "logic_db", "sql", "EXPLAIN ANALYZE SELECT * FROM orders", "execution_mode", "execute"))));
- assertThat(actual.getMessage(), is("database_gateway_execute_update does not accept read-only SQL. Use database_gateway_execute_query for read-only SQL."));
- verifyNoInteractions(executionFacade);
- }
-
- @Test
- void assertHandleSideEffectingExplainAnalyze() {
- MCPFeatureExecutionFacade executionFacade = mock(MCPFeatureExecutionFacade.class);
- when(executionFacade.execute(any())).thenReturn(SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN_ANALYZE,
- "EXPLAIN ANALYZE", List.of(), List.of(), false));
- MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
- when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
- MCPResponse actual = new ExecuteUpdateToolHandler().handle(databaseContext, new MCPToolCall("session-1",
- Map.of("database", "logic_db", "sql", "EXPLAIN ANALYZE UPDATE orders SET status = 'PAID'", "execution_mode", "execute")));
- assertThat(actual.toPayload().get("execution_mode"), is("execute"));
- assertThat(actual.toPayload().get("statement_class"), is("explain_analyze"));
- verify(executionFacade).execute(any());
- }
}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLCandidateValidatorTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLCandidateValidatorTest.java
new file mode 100644
index 00000000000..cec31a884ce
--- /dev/null
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExplainSQLCandidateValidatorTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.handler.execute;
+
+import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+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.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ExplainSQLCandidateValidatorTest {
+
+ private final ExplainSQLCandidateValidator validator = new ExplainSQLCandidateValidator();
+
+ @Test
+ void assertValidate() {
+ ClassificationResult actual = validator.validate("SELECT * FROM logic_db.foo_orders", "EXPLAIN FORMAT=JSON SELECT * FROM logic_db.foo_orders");
+ assertThat(actual.getStatementClass(), is(SupportedMCPStatement.EXPLAIN));
+ assertThat(actual.getStatementType(), is("EXPLAIN"));
+ assertThat(actual.getNormalizedSql(), is("EXPLAIN FORMAT=JSON SELECT * FROM logic_db.foo_orders"));
+ assertThat(actual.getReferencedObjectNames(), contains("logic_db.foo_orders"));
+ }
+
+ @Test
+ void assertValidateWithSignificantWhitespace() {
+ ClassificationResult actual = validator.validate("SELECT * FROM foo_orders WHERE status = 'READY TO SHIP'",
+ "EXPLAIN QUERY TREE SELECT * FROM foo_orders WHERE status = 'READY TO SHIP'");
+ assertThat(actual.getNormalizedSql(), is("EXPLAIN QUERY TREE SELECT * FROM foo_orders WHERE status = 'READY TO SHIP'"));
+ }
+
+ @Test
+ void assertValidateWithNonExecutableComments() {
+ ClassificationResult actual = validator.validate("SELECT '/*!80018 ANALYZE */' FROM foo_orders",
+ "EXPLAIN /* plan only */ SELECT '/*!80018 ANALYZE */' FROM foo_orders");
+ assertThat(actual.getNormalizedSql(), is("EXPLAIN /* plan only */ SELECT '/*!80018 ANALYZE */' FROM foo_orders"));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("assertValidateWithInvalidCandidateCases")
+ void assertValidateWithInvalidCandidate(final String name, final String sql, final String explainSql, final String expectedMessage) {
+ MCPInvalidRequestException actual = assertThrows(MCPInvalidRequestException.class, () -> validator.validate(sql, explainSql));
+ assertThat(actual.getMessage(), is(expectedMessage));
+ }
+
+ private static Stream<Arguments> assertValidateWithInvalidCandidateCases() {
+ return Stream.of(
+ Arguments.of("mutating explained sql", "UPDATE foo_orders SET status = 'DONE'", "EXPLAIN UPDATE foo_orders SET status = 'DONE'",
+ "database_gateway_execute_explain_query only supports QUERY statements as the explained SQL."),
+ Arguments.of("missing explain prefix", "SELECT * FROM foo_orders", "SELECT * FROM foo_orders", "explain_sql must start with EXPLAIN."),
+ Arguments.of("explain analyze", "SELECT * FROM foo_orders", "EXPLAIN ANALYZE SELECT * FROM foo_orders",
+ "EXPLAIN ANALYZE is not supported by the MCP explain query tool."),
+ Arguments.of("explain analyse", "SELECT * FROM foo_orders", "EXPLAIN ANALYSE SELECT * FROM foo_orders",
+ "EXPLAIN ANALYZE is not supported by the MCP explain query tool."),
+ Arguments.of("mysql executable comment", "SELECT * FROM foo_orders", "EXPLAIN /*!80018 ANALYZE */ SELECT * FROM foo_orders",
+ "Executable comments are not supported by the MCP explain query tool."),
+ Arguments.of("mariadb executable comment", "SELECT * FROM foo_orders", "EXPLAIN /*M!100000 ANALYZE */ SELECT * FROM foo_orders",
+ "Executable comments are not supported by the MCP explain query tool."),
+ Arguments.of("explain plan for", "SELECT * FROM foo_orders", "EXPLAIN PLAN FOR SELECT * FROM foo_orders",
+ "EXPLAIN PLAN FOR workflows are not supported by the MCP explain query tool."),
+ Arguments.of("rewritten sql", "SELECT * FROM foo_orders", "EXPLAIN SELECT order_id FROM foo_orders",
+ "explain_sql must include the original sql argument without rewriting it."),
+ Arguments.of("quoted identifier case rewrite", "SELECT * FROM \"Foo_Orders\"", "EXPLAIN SELECT * FROM \"foo_orders\"",
+ "explain_sql must include the original sql argument without rewriting it."),
+ Arguments.of("literal whitespace rewrite", "SELECT * FROM foo_orders WHERE status = 'READY TO SHIP'",
+ "EXPLAIN SELECT * FROM foo_orders WHERE status = 'READY TO SHIP'",
+ "explain_sql must include the original sql argument without rewriting it."),
+ Arguments.of("quoted identifier whitespace rewrite", "SELECT * FROM \"foo orders\"", "EXPLAIN SELECT * FROM \"foo orders\"",
+ "explain_sql must include the original sql argument without rewriting it."),
+ Arguments.of("create table wrapper", "SELECT * FROM foo_orders", "EXPLAIN CREATE TABLE archived_orders AS SELECT * FROM foo_orders",
+ "explain_sql must not wrap the original sql argument in another statement."),
+ Arguments.of("nested explain wrapper", "SELECT * FROM foo_orders", "EXPLAIN EXPLAIN SELECT * FROM foo_orders",
+ "explain_sql must not wrap the original sql argument in another statement."),
+ Arguments.of("prepare wrapper", "SELECT * FROM foo_orders", "EXPLAIN PREPARE archived_orders FROM SELECT * FROM foo_orders",
+ "explain_sql must not wrap the original sql argument in another statement."),
+ Arguments.of("output redirection", "SELECT * FROM foo_orders", "EXPLAIN INTO OUTFILE SELECT * FROM foo_orders",
+ "EXPLAIN output redirection is not supported by the MCP explain query tool."));
+ }
+}
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 c037d0740a3..032dcf401a1 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
@@ -19,10 +19,7 @@ package org.apache.shardingsphere.mcp.core.tool.handler.execute;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
-import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityOption;
import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
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;
@@ -407,12 +404,9 @@ class MCPJdbcStatementExecutorTest {
}
private static MCPDatabaseCapability createDatabaseCapability(final SchemaExecutionSemantics schemaExecutionSemantics) {
- MCPDatabaseCapabilityOption option = mock(MCPDatabaseCapabilityOption.class);
- when(option.getType()).thenReturn("FixtureDB");
- when(option.getTransactionCapability()).thenReturn(TransactionCapability.NONE);
- when(option.getDefaultSchemaSemantics()).thenReturn(SchemaSemantics.NATIVE_SCHEMA);
- when(option.getSchemaExecutionSemantics()).thenReturn(schemaExecutionSemantics);
- return new MCPDatabaseCapability("logic_db", "", option);
+ MCPDatabaseCapability result = mock(MCPDatabaseCapability.class);
+ when(result.getSchemaExecutionSemantics()).thenReturn(schemaExecutionSemantics);
+ return result;
}
private static Connection createStatementConnection(final boolean hasResultSet, final int updateCount, final ResultSet resultSet) throws SQLException {
@@ -456,10 +450,10 @@ class MCPJdbcStatementExecutorTest {
new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT status FROM orders ORDER BY order_id", "", ""),
createStatementConnection(true, 0, createResultSet(createColumns(), List.of(List.of(1, "NEW"), List.of(2, "DONE")))),
ExecuteQueryResultKind.RESULT_SET, "SELECT", 1, 0, "", true),
- Arguments.of("explain analyze result set", new SQLExecutionRequest("session-1", "logic_db", "public", "EXPLAIN ANALYZE SELECT * FROM orders", 10, 1000),
- new ClassificationResult(SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE", "EXPLAIN ANALYZE SELECT * FROM orders", "", ""),
+ 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", "", ""),
createStatementConnection(true, 0, createResultSet(List.of(new ExecuteQueryColumnDefinition("plan", "VARCHAR", "VARCHAR", true)), List.of(List.of("plan")))),
- ExecuteQueryResultKind.RESULT_SET, "EXPLAIN ANALYZE", 1, 0, "", false),
+ ExecuteQueryResultKind.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'", "", ""), createStatementConnection(false, 3, null),
ExecuteQueryResultKind.UPDATE_COUNT, "UPDATE", 0, 3, "", false),
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeConcurrencyTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeConcurrencyTest.java
index 735dc1099dd..117ee66aa20 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeConcurrencyTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPSQLExecutionFacadeConcurrencyTest.java
@@ -20,11 +20,9 @@ package org.apache.shardingsphere.mcp.core.tool.handler.execute;
import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPTransactionStateException;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
-import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityOption;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.tool.response.SQLExecutionResponse;
import org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
import org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
@@ -37,6 +35,7 @@ import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@@ -184,12 +183,11 @@ class MCPSQLExecutionFacadeConcurrencyTest {
}
private MCPDatabaseCapability createDatabaseCapability() {
- MCPDatabaseCapabilityOption option = mock(MCPDatabaseCapabilityOption.class);
- when(option.getType()).thenReturn("FixtureDB");
- when(option.getTransactionCapability()).thenReturn(TransactionCapability.LOCAL_WITH_SAVEPOINT);
- when(option.getDefaultSchemaSemantics()).thenReturn(SchemaSemantics.NATIVE_SCHEMA);
- when(option.getSchemaExecutionSemantics()).thenReturn(SchemaExecutionSemantics.FIXED_TO_DATABASE);
- return new MCPDatabaseCapability("logic_db", "", option);
+ MCPDatabaseCapability result = mock(MCPDatabaseCapability.class);
+ when(result.getSupportedStatementClasses()).thenReturn(EnumSet.of(SupportedMCPStatement.QUERY, SupportedMCPStatement.TRANSACTION_CONTROL));
+ when(result.getSchemaExecutionSemantics()).thenReturn(SchemaExecutionSemantics.FIXED_TO_DATABASE);
+ when(result.isSupportsTransactionControl()).thenReturn(true);
+ return result;
}
private SQLExecutionRequest createExecutionRequest(final String sessionId, final String sql) {
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 6583ce66221..4856eaf83f2 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
@@ -18,13 +18,14 @@
package org.apache.shardingsphere.mcp.core.tool.handler.execute;
import org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
-import org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedException;
+import org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
import org.apache.shardingsphere.mcp.core.protocol.exception.MCPBannedSQLStatementException;
import org.apache.shardingsphere.mcp.core.session.MCPSessionExecutionCoordinator;
import org.apache.shardingsphere.mcp.core.session.MCPSessionNotExistedException;
import org.apache.shardingsphere.mcp.core.tool.handler.execute.trace.SQLExecutionTraceFactory;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
+import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
import org.apache.shardingsphere.mcp.support.database.exception.StatementClassNotSupportedException;
@@ -34,7 +35,10 @@ 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 org.mockito.ArgumentCaptor;
+import java.sql.SQLException;
+import java.sql.SQLSyntaxErrorException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -43,6 +47,7 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
@@ -93,7 +98,7 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
StatementClassifier statementClassifier = mock(StatementClassifier.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
when(statementClassifier.classify(anyString())).thenThrow(new MCPBannedSQLStatementException());
@@ -118,7 +123,7 @@ class MCPSQLExecutionFacadeTest {
SQLExecutionRequest request = createExecutionRequest("SELECT 1");
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT 1", "orders", "");
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.DML), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.DML));
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
StatementClassNotSupportedException actual = assertThrows(StatementClassNotSupportedException.class, () -> facade.execute(request));
@@ -132,7 +137,7 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.TRANSACTION_CONTROL), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.TRANSACTION_CONTROL));
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.TRANSACTION_CONTROL, "BEGIN", "BEGIN", "", "");
SQLExecutionResponse response = SQLExecutionResponse.statementAck(SupportedMCPStatement.TRANSACTION_CONTROL, "BEGIN", "Transaction started.");
mockSessionLock(coordinator);
@@ -154,7 +159,7 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT 1", "orders", "");
SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
mockSessionLock(coordinator);
@@ -178,9 +183,9 @@ class MCPSQLExecutionFacadeTest {
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
SQLExecutionRequest request = createExecutionRequest("SELECT * FROM other_db.orders");
- ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM other_db.orders", "other_db.orders", "", null,
+ ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM other_db.orders", "other_db.orders", "",
List.of("other_db.orders"));
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
mockSessionLock(coordinator);
@@ -198,7 +203,7 @@ class MCPSQLExecutionFacadeTest {
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
SQLExecutionRequest request = createExecutionRequest("SELECT * FROM logic_db.orders WHERE EXISTS (SELECT 1 FROM other_db.items)");
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory);
mockSessionLock(coordinator);
@@ -217,7 +222,7 @@ class MCPSQLExecutionFacadeTest {
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY, SupportedMCPStatement.DML, SupportedMCPStatement.DDL, SupportedMCPStatement.DCL), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY, SupportedMCPStatement.DML, SupportedMCPStatement.DDL, SupportedMCPStatement.DCL));
SQLExecutionRequest request = createExecutionRequest(sql);
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory);
mockSessionLock(coordinator);
@@ -235,10 +240,10 @@ class MCPSQLExecutionFacadeTest {
MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.DML), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.DML));
SQLExecutionRequest request = createExecutionRequest("UPDATE other_db.orders SET status = 'DONE'");
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.DML, "UPDATE", "UPDATE other_db.orders SET status = 'DONE'", "other_db.orders", "",
- null, List.of("other_db.orders"));
+ List.of("other_db.orders"));
MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
@@ -253,8 +258,8 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false);
- ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM logic_db.orders", "logic_db.orders", "", null,
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
+ ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM logic_db.orders", "logic_db.orders", "",
List.of("logic_db.orders"));
SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
mockSessionLock(coordinator);
@@ -276,8 +281,8 @@ class MCPSQLExecutionFacadeTest {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false, true);
- ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM other_db.orders", "other_db.orders", "", null,
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), SchemaExecutionSemantics.BEST_EFFORT);
+ ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT * FROM other_db.orders", "other_db.orders", "",
List.of("other_db.orders"));
SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.QUERY, "SELECT", Collections.emptyList(), Collections.emptyList(), false);
mockSessionLock(coordinator);
@@ -294,30 +299,11 @@ class MCPSQLExecutionFacadeTest {
verifyNoInteractions(transactionExecutor);
}
- @Test
- void assertExecuteWithExplainAnalyzeUnsupported() {
- MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
- MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
- MCPJdbcTransactionStatementExecutor transactionExecutor = mock(MCPJdbcTransactionStatementExecutor.class);
- MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
- SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
- SQLExecutionRequest request = createExecutionRequest("EXPLAIN ANALYZE SELECT 1");
- ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE", "EXPLAIN ANALYZE SELECT 1", "orders", "");
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.EXPLAIN_ANALYZE), false);
- MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory, createStatementClassifier(classification));
- mockSessionLock(coordinator);
- when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
- MCPUnsupportedException actual = assertThrows(MCPUnsupportedException.class, () -> facade.execute(request));
- assertThat(actual.getMessage(), is("EXPLAIN ANALYZE is not supported."));
- verify(traceFactory).create("session-1", "logic_db", "EXPLAIN ANALYZE SELECT 1", false, "EXPLAIN_ANALYZE");
- verifyNoInteractions(transactionExecutor, statementExecutor);
- }
-
@Test
void assertExecuteWithExecutorFailure() {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY), false);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.QUERY));
SQLExecutionRequest request = createExecutionRequest("SELECT 1");
ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.QUERY, "SELECT", "SELECT 1", "orders", "");
mockSessionLock(coordinator);
@@ -334,27 +320,66 @@ class MCPSQLExecutionFacadeTest {
}
@Test
- void assertExecuteExplainAnalyze() {
+ void assertExecuteExplain() {
MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
- MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.EXPLAIN_ANALYZE), true);
- SQLExecutionRequest request = createExecutionRequest("EXPLAIN ANALYZE SELECT 1");
- ClassificationResult classification = new ClassificationResult(SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE", "EXPLAIN ANALYZE SELECT 1", "orders", "");
- SQLExecutionResponse response = SQLExecutionResponse.resultSet(SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE", Collections.emptyList(), Collections.emptyList(), false);
+ 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);
mockSessionLock(coordinator);
when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
- when(statementExecutor.execute(request, classification, capability)).thenReturn(response);
+ when(statementExecutor.execute(eq(request), any(), eq(capability))).thenReturn(response);
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);
+ MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, transactionExecutor, statementExecutor, traceFactory);
+ SQLExecutionResponse actual = facade.executeExplain(request, "SELECT * FROM orders");
assertThat(actual, is(response));
- verify(statementExecutor).execute(request, classification, capability);
- verify(traceFactory).create("session-1", "logic_db", "EXPLAIN ANALYZE SELECT 1", true, "EXPLAIN_ANALYZE");
+ ArgumentCaptor<ClassificationResult> classificationCaptor = ArgumentCaptor.forClass(ClassificationResult.class);
+ verify(statementExecutor).execute(eq(request), classificationCaptor.capture(), eq(capability));
+ assertThat(classificationCaptor.getValue().getStatementClass(), is(SupportedMCPStatement.EXPLAIN));
+ assertThat(classificationCaptor.getValue().getReferencedObjectNames(), contains("orders"));
+ verify(traceFactory).create("session-1", "logic_db", "EXPLAIN SELECT * FROM orders", true, "EXPLAIN");
verifyNoInteractions(transactionExecutor);
}
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("assertExecuteExplainWithSyntaxFailureCases")
+ void assertExecuteExplainWithSyntaxFailure(final String name, final RuntimeException executionFailure) {
+ MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
+ MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.EXPLAIN));
+ SQLExecutionRequest request = createExecutionRequest("EXPLAIN BROKEN SELECT * FROM orders");
+ mockSessionLock(coordinator);
+ when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
+ MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
+ when(statementExecutor.execute(eq(request), any(), eq(capability))).thenThrow(executionFailure);
+ SQLExecutionTraceFactory traceFactory = mock(SQLExecutionTraceFactory.class);
+ MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, mock(MCPJdbcTransactionStatementExecutor.class), statementExecutor, traceFactory);
+ ExplainSQLSyntaxException actual = assertThrows(ExplainSQLSyntaxException.class, () -> facade.executeExplain(request, "SELECT * FROM orders"));
+ assertThat(actual.getDatabase(), is("logic_db"));
+ assertThat(actual.getSchema(), is("public"));
+ assertThat(actual.getSql(), is("SELECT * FROM orders"));
+ assertThat(actual.getExplainSql(), is("EXPLAIN BROKEN SELECT * FROM orders"));
+ verify(traceFactory).create("session-1", "logic_db", "EXPLAIN BROKEN SELECT * FROM orders", false, "EXPLAIN");
+ }
+
+ @Test
+ void assertExecuteExplainWithNonSyntaxFailure() {
+ MCPDatabaseCapabilityProvider capabilityProvider = mock(MCPDatabaseCapabilityProvider.class);
+ MCPSessionExecutionCoordinator coordinator = mock(MCPSessionExecutionCoordinator.class);
+ MCPDatabaseCapability capability = createCapability(Set.of(SupportedMCPStatement.EXPLAIN));
+ SQLExecutionRequest request = createExecutionRequest("EXPLAIN SELECT * FROM missing_orders");
+ mockSessionLock(coordinator);
+ when(capabilityProvider.provide("logic_db")).thenReturn(Optional.of(capability));
+ MCPJdbcStatementExecutor statementExecutor = mock(MCPJdbcStatementExecutor.class);
+ MCPQueryFailedException executionFailure = new MCPQueryFailedException("missing table", new SQLException("missing table", "42P01"));
+ when(statementExecutor.execute(eq(request), any(), eq(capability))).thenThrow(executionFailure);
+ MCPSQLExecutionFacade facade = createFacade(capabilityProvider, coordinator, mock(MCPJdbcTransactionStatementExecutor.class), statementExecutor,
+ mock(SQLExecutionTraceFactory.class));
+ assertThat(assertThrows(MCPQueryFailedException.class, () -> facade.executeExplain(request, "SELECT * FROM missing_orders")), is(executionFailure));
+ }
+
private MCPSQLExecutionFacade createFacade(final MCPDatabaseCapabilityProvider capabilityProvider, final MCPSessionExecutionCoordinator coordinator,
final MCPJdbcTransactionStatementExecutor transactionExecutor, final MCPJdbcStatementExecutor statementExecutor,
final SQLExecutionTraceFactory traceFactory) {
@@ -372,15 +397,14 @@ class MCPSQLExecutionFacadeTest {
when(coordinator.executeWithSessionLock(eq("session-1"), any())).thenAnswer(invocation -> ((Supplier<SQLExecutionResponse>) invocation.getArgument(1, Supplier.class)).get());
}
- private MCPDatabaseCapability createCapability(final Set<SupportedMCPStatement> supportedStatementClasses, final boolean supportsExplainAnalyze) {
- return createCapability(supportedStatementClasses, supportsExplainAnalyze, false);
+ private MCPDatabaseCapability createCapability(final Set<SupportedMCPStatement> supportedStatementClasses) {
+ return createCapability(supportedStatementClasses, SchemaExecutionSemantics.FIXED_TO_DATABASE);
}
- private MCPDatabaseCapability createCapability(final Set<SupportedMCPStatement> supportedStatementClasses, final boolean supportsExplainAnalyze, final boolean supportsCrossSchemaSql) {
+ private MCPDatabaseCapability createCapability(final Set<SupportedMCPStatement> supportedStatementClasses, final SchemaExecutionSemantics schemaExecutionSemantics) {
MCPDatabaseCapability result = mock(MCPDatabaseCapability.class);
when(result.getSupportedStatementClasses()).thenReturn(supportedStatementClasses);
- when(result.isSupportsExplainAnalyze()).thenReturn(supportsExplainAnalyze);
- when(result.isSupportsCrossSchemaSql()).thenReturn(supportsCrossSchemaSql);
+ when(result.getSchemaExecutionSemantics()).thenReturn(schemaExecutionSemantics);
return result;
}
@@ -426,6 +450,12 @@ class MCPSQLExecutionFacadeTest {
Arguments.of("qualified function", "SELECT other_db.refresh_orders()", "other_db.refresh_orders", "QUERY"));
}
+ private static Stream<Arguments> assertExecuteExplainWithSyntaxFailureCases() {
+ return Stream.of(
+ Arguments.of("JDBC syntax exception", new MCPInvalidRequestException("bad explain", new SQLSyntaxErrorException("bad explain"))),
+ Arguments.of("SQLState syntax exception", new MCPQueryFailedException("bad explain", new SQLException("bad explain", "42601"))));
+ }
+
private SQLExecutionRequest createExecutionRequest(final String sql) {
return new SQLExecutionRequest("session-1", "logic_db", "public", sql, 1, 1000);
}
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
index 5c204c6a292..cd323d53a28 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
@@ -66,14 +66,6 @@ class StatementClassifierTest {
assertThat(actual.getStatementType(), is(expectedStatementType));
}
- @Test
- void assertClassifyExplainAnalyzeInnerStatementClass() {
- ClassificationResult actualResult = statementClassifier.classify("EXPLAIN ANALYZE UPDATE foo_orders SET status = 'DONE'");
- assertThat(actualResult.getStatementClass(), is(SupportedMCPStatement.EXPLAIN_ANALYZE));
- assertThat(actualResult.getAnalyzedStatementClass().orElseThrow(), is(SupportedMCPStatement.DML));
- assertThat(actualResult.getTargetObjectName().orElse(""), is("foo_orders"));
- }
-
@Test
void assertClassifyReferencedObjectNames() {
ClassificationResult actualResult = statementClassifier.classify(
@@ -199,9 +191,7 @@ class StatementClassifierTest {
Arguments.of("rollback to savepoint", "ROLLBACK TO SAVEPOINT foo_sp_1", SupportedMCPStatement.SAVEPOINT, "ROLLBACK TO SAVEPOINT",
"ROLLBACK TO SAVEPOINT foo_sp_1", "", "foo_sp_1"),
Arguments.of("rollback to savepoint name without optional keyword", "ROLLBACK TO foo_sp_1", SupportedMCPStatement.SAVEPOINT, "ROLLBACK TO",
- "ROLLBACK TO foo_sp_1", "", "foo_sp_1"),
- Arguments.of("explain analyze", "EXPLAIN ANALYZE SELECT * FROM foo_orders", SupportedMCPStatement.EXPLAIN_ANALYZE, "EXPLAIN ANALYZE",
- "EXPLAIN ANALYZE SELECT * FROM foo_orders", "foo_orders", ""));
+ "ROLLBACK TO foo_sp_1", "", "foo_sp_1"));
}
private static Stream<Arguments> assertClassifyReferencedObjectNamesWithObjectListsCases() {
@@ -300,6 +290,8 @@ class StatementClassifierTest {
MCPBannedSQLStatementException.class, "Statement is banned by the MCP contract."),
Arguments.of("banned mysql executable comment", "SELECT * FROM logic_db.foo_orders /*!50000 JOIN other_db.foo_order_items ON 1 = 1 */",
MCPBannedSQLStatementException.class, "Statement is banned by the MCP contract."),
+ Arguments.of("banned mariadb executable comment", "SELECT * FROM logic_db.foo_orders /*M!100000 JOIN other_db.foo_order_items ON 1 = 1 */",
+ MCPBannedSQLStatementException.class, "Statement is banned by the MCP contract."),
Arguments.of("banned nextval function", "SELECT nextval('foo_seq')", MCPBannedSQLStatementException.class, "Statement is banned by the MCP contract."),
Arguments.of("banned setval function", "SELECT pg_catalog.setval('foo_seq', 1)", MCPBannedSQLStatementException.class, "Statement is banned by the MCP contract."),
Arguments.of("banned next value for sequence", "SELECT NEXT VALUE FOR foo_seq", MCPBannedSQLStatementException.class, "Statement is banned by the MCP contract."),
@@ -334,8 +326,9 @@ class StatementClassifierTest {
"Locking read statements such as SELECT ... FOR UPDATE are not supported by the MCP read-only contract."),
Arguments.of("locking read lock in share mode", "SELECT * FROM foo_orders LOCK IN SHARE MODE", MCPLockingReadStatementException.class,
"Locking read statements such as SELECT ... FOR UPDATE are not supported by the MCP read-only contract."),
- Arguments.of("explain analyze locking read", "EXPLAIN ANALYZE SELECT * FROM foo_orders FOR SHARE", MCPLockingReadStatementException.class,
- "Locking read statements such as SELECT ... FOR UPDATE are not supported by the MCP read-only contract."),
+ Arguments.of("explain query", "EXPLAIN SELECT * FROM foo_orders", MCPUnsupportedSQLStatementException.class, "Statement is not supported by the MCP contract."),
+ Arguments.of("explain locking read", "EXPLAIN SELECT * FROM foo_orders FOR SHARE", MCPUnsupportedSQLStatementException.class, "Statement is not supported by the MCP contract."),
+ Arguments.of("explain analyze", "EXPLAIN ANALYZE SELECT * FROM foo_orders", MCPUnsupportedSQLStatementException.class, "Statement is not supported by the MCP contract."),
Arguments.of("metadata show", "SHOW", MetadataIntrospectionSQLStatementException.class, "Metadata introspection SQL should use MCP metadata resources."),
Arguments.of("metadata show tables", "SHOW TABLES", MetadataIntrospectionSQLStatementException.class, "Metadata introspection SQL should use MCP metadata resources."),
Arguments.of("metadata describe", "DESCRIBE", MetadataIntrospectionSQLStatementException.class, "Metadata introspection SQL should use MCP metadata resources."),
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataPayloadBuilderTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataPayloadBuilderTest.java
index c2ec2f5055d..3e2c8b01ea9 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataPayloadBuilderTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataPayloadBuilderTest.java
@@ -101,7 +101,7 @@ class SearchMetadataPayloadBuilderTest {
MCPFeatureCapabilityFacade capabilityFacade = mock(MCPFeatureCapabilityFacade.class);
when(result.getMetadataQueryFacade()).thenReturn(metadataQueryFacade);
when(result.getCapabilityFacade()).thenReturn(capabilityFacade);
- when(metadataQueryFacade.queryDatabases()).thenReturn(List.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0")));
+ when(metadataQueryFacade.queryDatabases()).thenReturn(List.of(new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true)));
when(metadataQueryFacade.querySchema("logic_db", "public")).thenReturn(Optional.of(mock(ShardingSphereSchema.class)));
when(capabilityFacade.findDatabaseProfile("logic_db")).thenReturn(Optional.of(mock(RuntimeDatabaseProfile.class)));
return result;
diff --git a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
index 2741e3b43ae..e90d84b3d28 100644
--- a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
+++ b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/workflow/WorkflowProxyQueryServiceTest.java
@@ -19,6 +19,8 @@ package org.apache.shardingsphere.mcp.core.workflow;
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
import org.apache.shardingsphere.database.connector.core.type.DatabaseTypeFactory;
@@ -167,7 +169,7 @@ class WorkflowProxyQueryServiceTest {
try (
MockedStatic<TypedSPILoader> typedSPILoader = mockStatic(TypedSPILoader.class, CALLS_REAL_METHODS);
MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader = mockStatic(DatabaseTypedSPILoader.class)) {
- mockQuoteCharacter("MySQL", QuoteCharacter.BACK_QUOTE, typedSPILoader, databaseTypedSPILoader);
+ mockDialectDatabaseMetaData("MySQL", QuoteCharacter.BACK_QUOTE, DialectSchemaSemantics.DATABASE_AS_SCHEMA, typedSPILoader, databaseTypedSPILoader);
WorkflowProxyQueryService service = createService(Map.of("logic_db", runtimeDatabaseConfig));
String actual = service.queryColumnDefinition("`logic_db`", "`public`", "order detail", "amount due");
assertThat(actual, is("VARCHAR(4000)"));
@@ -190,7 +192,7 @@ class WorkflowProxyQueryServiceTest {
try (
MockedStatic<TypedSPILoader> typedSPILoader = mockStatic(TypedSPILoader.class, CALLS_REAL_METHODS);
MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader = mockStatic(DatabaseTypedSPILoader.class)) {
- mockQuoteCharacter("PostgreSQL", QuoteCharacter.QUOTE, typedSPILoader, databaseTypedSPILoader);
+ mockDialectDatabaseMetaData("PostgreSQL", QuoteCharacter.QUOTE, DialectSchemaSemantics.NATIVE_SCHEMA, typedSPILoader, databaseTypedSPILoader);
WorkflowProxyQueryService service = createService(Map.of("logic_db", runtimeDatabaseConfig), "PostgreSQL");
String actual = service.queryColumnDefinition("\"logic_db\"", "\"public\"", "order detail", "amount due");
assertThat(actual, is("VARCHAR(4000)"));
@@ -220,13 +222,15 @@ class WorkflowProxyQueryServiceTest {
return result;
}
- private void mockQuoteCharacter(final String databaseType, final QuoteCharacter quoteCharacter,
- final MockedStatic<TypedSPILoader> typedSPILoader, final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
+ private void mockDialectDatabaseMetaData(final String databaseType, final QuoteCharacter quoteCharacter, final DialectSchemaSemantics schemaSemantics,
+ final MockedStatic<TypedSPILoader> typedSPILoader, final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
DatabaseType databaseTypeFromSPI = mock(DatabaseType.class);
when(databaseTypeFromSPI.getTrunkDatabaseType()).thenReturn(Optional.empty());
typedSPILoader.when(() -> TypedSPILoader.findService(DatabaseType.class, databaseType)).thenReturn(Optional.of(databaseTypeFromSPI));
DialectDatabaseMetaData dialectDatabaseMetaData = mock(DialectDatabaseMetaData.class);
when(dialectDatabaseMetaData.getQuoteCharacter()).thenReturn(quoteCharacter);
+ when(dialectDatabaseMetaData.getSchemaOption()).thenReturn(new DefaultSchemaOption(false, null, schemaSemantics));
+ when(dialectDatabaseMetaData.getSequenceOption()).thenReturn(Optional.empty());
databaseTypedSPILoader.when(() -> DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class, databaseTypeFromSPI)).thenReturn(Optional.of(dialectDatabaseMetaData));
}
diff --git a/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowPlanningServiceTest.java b/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowPlanningServiceTest.java
index 16a9f2f3b58..a3cd988dd37 100644
--- a/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowPlanningServiceTest.java
+++ b/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowPlanningServiceTest.java
@@ -342,7 +342,7 @@ class EncryptWorkflowPlanningServiceTest {
}
private RuntimeDatabaseProfile createDatabaseMetadata() {
- return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0");
+ return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true);
}
private ShardingSphereSchema createSchemaMetadata() {
diff --git a/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowPlanningServiceTest.java b/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowPlanningServiceTest.java
index 45b085c9499..d71bbc24789 100644
--- a/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowPlanningServiceTest.java
+++ b/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowPlanningServiceTest.java
@@ -272,7 +272,7 @@ class MaskWorkflowPlanningServiceTest {
}
private RuntimeDatabaseProfile createDatabaseMetadata() {
- return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0");
+ return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true);
}
private ShardingSphereSchema createSchemaMetadata() {
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/DatabaseVersionUtil.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/DatabaseVersionUtil.java
deleted file mode 100644
index 9d26cb2fdae..00000000000
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/DatabaseVersionUtil.java
+++ /dev/null
@@ -1,60 +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.capability;
-
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
-
-import java.util.Objects;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Database version util.
- */
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
-public final class DatabaseVersionUtil {
-
- private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?.*");
-
- /**
- * Judge whether database version is at least target version.
- *
- * @param databaseVersion database version
- * @param major target major version
- * @param minor target minor version
- * @param patch target patch version
- * @return whether the actual version is at least the target version
- */
- public static boolean isVersionAtLeast(final String databaseVersion, final int major, final int minor, final int patch) {
- Matcher matcher = VERSION_PATTERN.matcher(Objects.toString(databaseVersion, "").trim());
- if (!matcher.matches()) {
- return false;
- }
- int actualMajor = Integer.parseInt(matcher.group(1));
- int actualMinor = null == matcher.group(2) ? 0 : Integer.parseInt(matcher.group(2));
- int actualPatch = null == matcher.group(3) ? 0 : Integer.parseInt(matcher.group(3));
- if (actualMajor != major) {
- return actualMajor > major;
- }
- if (actualMinor != minor) {
- return actualMinor > minor;
- }
- return actualPatch >= patch;
- }
-}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapability.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapability.java
index e4ba73678a7..5d4f8159b0b 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapability.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapability.java
@@ -18,6 +18,7 @@
package org.apache.shardingsphere.mcp.support.database.capability;
import lombok.Getter;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -38,55 +39,52 @@ public final class MCPDatabaseCapability {
private final TransactionCapability transactionCapability;
- private final SchemaSemantics defaultSchemaSemantics;
+ private final DialectSchemaSemantics defaultSchemaSemantics;
private final SchemaExecutionSemantics schemaExecutionSemantics;
- private final boolean supportsCrossSchemaSql;
-
- private final boolean supportsExplainAnalyze;
-
- public MCPDatabaseCapability(final String databaseName, final String databaseVersion, final MCPDatabaseCapabilityOption option) {
+ public MCPDatabaseCapability(final String databaseName, final boolean supportsTransaction, final boolean supportsSavepoint, final MCPDatabaseCapabilityOption option) {
this.databaseName = databaseName;
databaseType = option.getType();
- supportedMetadataObjectTypes = createSupportedMetadataObjectTypes(option);
- supportedStatementClasses = createSupportedStatementClasses(databaseVersion, option);
- transactionCapability = option.getTransactionCapability();
- defaultSchemaSemantics = option.getDefaultSchemaSemantics();
- schemaExecutionSemantics = option.getSchemaExecutionSemantics();
- supportsCrossSchemaSql = option.isCrossSchemaQuerySupported();
- supportsExplainAnalyze = option.isExplainAnalyzeSupported(databaseVersion);
+ MCPDatabaseDialect databaseDialect = MCPDatabaseDialect.of(option.getType());
+ supportedMetadataObjectTypes = createSupportedMetadataObjectTypes(databaseDialect);
+ transactionCapability = databaseDialect.getTransactionCapability(supportsTransaction, supportsSavepoint);
+ supportedStatementClasses = createSupportedStatementClasses(transactionCapability, option.isExplainSupported());
+ defaultSchemaSemantics = databaseDialect.getDefaultSchemaSemantics();
+ schemaExecutionSemantics = createSchemaExecutionSemantics(defaultSchemaSemantics);
+ }
+
+ private static SchemaExecutionSemantics createSchemaExecutionSemantics(final DialectSchemaSemantics defaultSchemaSemantics) {
+ return DialectSchemaSemantics.DATABASE_AS_SCHEMA == defaultSchemaSemantics ? SchemaExecutionSemantics.FIXED_TO_DATABASE : SchemaExecutionSemantics.BEST_EFFORT;
}
- private Set<SupportedMCPMetadataObjectType> createSupportedMetadataObjectTypes(final MCPDatabaseCapabilityOption option) {
+ private Set<SupportedMCPMetadataObjectType> createSupportedMetadataObjectTypes(final MCPDatabaseDialect databaseDialect) {
Set<SupportedMCPMetadataObjectType> result = new LinkedHashSet<>(16, 1F);
result.add(SupportedMCPMetadataObjectType.SCHEMA);
result.add(SupportedMCPMetadataObjectType.TABLE);
result.add(SupportedMCPMetadataObjectType.VIEW);
result.add(SupportedMCPMetadataObjectType.COLUMN);
- if (option.isIndexSupported()) {
- result.add(SupportedMCPMetadataObjectType.INDEX);
- }
- if (MCPDatabaseDialect.of(option.getType()).isSequenceSupported()) {
+ result.add(SupportedMCPMetadataObjectType.INDEX);
+ if (databaseDialect.isSequenceSupported()) {
result.add(SupportedMCPMetadataObjectType.SEQUENCE);
}
return result;
}
- private Set<SupportedMCPStatement> createSupportedStatementClasses(final String databaseVersion, final MCPDatabaseCapabilityOption option) {
+ private Set<SupportedMCPStatement> createSupportedStatementClasses(final TransactionCapability transactionCapability, final boolean explainSupported) {
Set<SupportedMCPStatement> result = new LinkedHashSet<>(16, 1F);
result.add(SupportedMCPStatement.QUERY);
result.add(SupportedMCPStatement.DML);
result.add(SupportedMCPStatement.DDL);
result.add(SupportedMCPStatement.DCL);
- if (TransactionCapability.NONE != option.getTransactionCapability()) {
+ if (TransactionCapability.NONE != transactionCapability) {
result.add(SupportedMCPStatement.TRANSACTION_CONTROL);
}
- if (TransactionCapability.LOCAL_WITH_SAVEPOINT == option.getTransactionCapability()) {
+ if (TransactionCapability.LOCAL_WITH_SAVEPOINT == transactionCapability) {
result.add(SupportedMCPStatement.SAVEPOINT);
}
- if (option.isExplainAnalyzeSupported(databaseVersion)) {
- result.add(SupportedMCPStatement.EXPLAIN_ANALYZE);
+ if (explainSupported) {
+ result.add(SupportedMCPStatement.EXPLAIN);
}
return result;
}
@@ -108,4 +106,22 @@ public final class MCPDatabaseCapability {
public boolean isSupportsSavepoint() {
return TransactionCapability.LOCAL_WITH_SAVEPOINT == transactionCapability;
}
+
+ /**
+ * Judge whether EXPLAIN query is supported.
+ *
+ * @return whether EXPLAIN query is supported
+ */
+ public boolean isSupportsExplain() {
+ return supportedStatementClasses.contains(SupportedMCPStatement.EXPLAIN);
+ }
+
+ /**
+ * Judge whether cross-schema SQL is supported.
+ *
+ * @return whether cross-schema SQL is supported
+ */
+ public boolean isSupportsCrossSchemaSql() {
+ return SchemaExecutionSemantics.BEST_EFFORT == schemaExecutionSemantics;
+ }
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityOption.java
index 61026de18a1..629ba1c29e9 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityOption.java
@@ -29,47 +29,11 @@ import org.apache.shardingsphere.infra.spi.type.typed.TypedSPI;
public interface MCPDatabaseCapabilityOption extends TypedSPI {
/**
- * Get transaction capability.
+ * Judge whether MCP can execute a single database-native EXPLAIN statement and read its result set.
*
- * @return transaction capability
+ * @return whether MCP EXPLAIN execution is supported
*/
- TransactionCapability getTransactionCapability();
-
- /**
- * Judge whether index metadata is supported.
- *
- * @return whether index metadata is supported
- */
- boolean isIndexSupported();
-
- /**
- * Get default schema semantics.
- *
- * @return default schema semantics
- */
- SchemaSemantics getDefaultSchemaSemantics();
-
- /**
- * Get execution-time schema semantics.
- *
- * @return execution-time schema semantics
- */
- SchemaExecutionSemantics getSchemaExecutionSemantics();
-
- /**
- * Judge whether cross-schema query is supported.
- *
- * @return whether cross-schema query is supported
- */
- boolean isCrossSchemaQuerySupported();
-
- /**
- * Judge whether explain analyze is supported for database version.
- *
- * @param databaseVersion database version
- * @return whether explain analyze is supported
- */
- boolean isExplainAnalyzeSupported(String databaseVersion);
+ boolean isExplainSupported();
/**
* Get identifier case policy set.
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProvider.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProvider.java
index 65f4bb49343..f5fc8c3f44b 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProvider.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProvider.java
@@ -62,7 +62,7 @@ public final class MCPDatabaseCapabilityProvider implements MCPFeatureCapability
Map<String, MCPDatabaseCapability> result = new LinkedHashMap<>(databaseProfiles.size(), 1F);
for (RuntimeDatabaseProfile each : databaseProfiles.values()) {
TypedSPILoader.findService(MCPDatabaseCapabilityOption.class, each.getDatabaseType())
- .ifPresent(optional -> result.put(each.getDatabase(), new MCPDatabaseCapability(each.getDatabase(), each.getDatabaseVersion(), optional)));
+ .ifPresent(optional -> result.put(each.getDatabase(), new MCPDatabaseCapability(each.getDatabase(), each.isSupportsTransaction(), each.isSupportsSavepoint(), optional)));
}
return result;
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialect.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialect.java
index 3e52558a016..94d1bba8100 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialect.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialect.java
@@ -20,6 +20,7 @@ package org.apache.shardingsphere.mcp.support.database.capability;
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.system.SystemDatabase;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicy;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicyFactory;
@@ -119,8 +120,22 @@ public final class MCPDatabaseDialect {
*
* @return default schema semantics
*/
- public SchemaSemantics getDefaultSchemaSemantics() {
- return option.map(MCPDatabaseCapabilityOption::getDefaultSchemaSemantics).orElse(SchemaSemantics.NATIVE_SCHEMA);
+ public DialectSchemaSemantics getDefaultSchemaSemantics() {
+ return dialectDatabaseMetaData.map(each -> each.getSchemaOption().getSchemaSemantics()).orElse(DialectSchemaSemantics.NATIVE_SCHEMA);
+ }
+
+ /**
+ * Get transaction capability.
+ *
+ * @param supportsTransaction whether transaction is supported
+ * @param supportsSavepoint whether savepoint is supported
+ * @return transaction capability
+ */
+ public TransactionCapability getTransactionCapability(final boolean supportsTransaction, final boolean supportsSavepoint) {
+ if (!supportsTransaction) {
+ return TransactionCapability.NONE;
+ }
+ return supportsSavepoint ? TransactionCapability.LOCAL_WITH_SAVEPOINT : TransactionCapability.LOCAL;
}
/**
@@ -167,8 +182,8 @@ public final class MCPDatabaseDialect {
* @param defaultSchemaSemantics default schema semantics
* @return whether schema or catalog is a system schema
*/
- public boolean isSystemSchema(final String schemaName, final String catalogName, final SchemaSemantics defaultSchemaSemantics) {
- return isSystemSchema(schemaName) || SchemaSemantics.DATABASE_AS_SCHEMA == defaultSchemaSemantics && isSystemSchema(catalogName);
+ public boolean isSystemSchema(final String schemaName, final String catalogName, final DialectSchemaSemantics defaultSchemaSemantics) {
+ return isSystemSchema(schemaName) || DialectSchemaSemantics.DATABASE_AS_SCHEMA == defaultSchemaSemantics && isSystemSchema(catalogName);
}
private Collection<String> getSystemSchemas() {
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SupportedMCPStatement.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SupportedMCPStatement.java
index a4b8d78b353..1b85ada4736 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SupportedMCPStatement.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/SupportedMCPStatement.java
@@ -22,5 +22,5 @@ package org.apache.shardingsphere.mcp.support.database.capability;
*/
public enum SupportedMCPStatement {
- QUERY, DML, DDL, DCL, TRANSACTION_CONTROL, SAVEPOINT, EXPLAIN_ANALYZE
+ QUERY, DML, DDL, DCL, TRANSACTION_CONTROL, SAVEPOINT, EXPLAIN
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/AbstractMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/AbstractMCPDatabaseCapabilityOption.java
index 8e562083379..bbf286abf0d 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/AbstractMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/AbstractMCPDatabaseCapabilityOption.java
@@ -17,81 +17,19 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityOption;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
-import java.util.Collection;
-import java.util.Collections;
/**
* Common base implementation for MCP database capability options.
*/
+@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
abstract class AbstractMCPDatabaseCapabilityOption implements MCPDatabaseCapabilityOption {
private final String type;
- private final Collection<Object> typeAliases;
-
- private final TransactionCapability transactionCapability;
-
- private final boolean indexSupported;
-
- private final SchemaSemantics defaultSchemaSemantics;
-
- private final SchemaExecutionSemantics schemaExecutionSemantics;
-
- private final boolean crossSchemaQuerySupported;
-
- protected AbstractMCPDatabaseCapabilityOption(final String type, final TransactionCapability transactionCapability, final boolean indexSupported,
- final SchemaSemantics defaultSchemaSemantics, final SchemaExecutionSemantics schemaExecutionSemantics,
- final boolean crossSchemaQuerySupported) {
- this(type, Collections.emptyList(), transactionCapability, indexSupported, defaultSchemaSemantics, schemaExecutionSemantics,
- crossSchemaQuerySupported);
- }
-
- protected AbstractMCPDatabaseCapabilityOption(final String type, final Collection<String> typeAliases, final TransactionCapability transactionCapability,
- final boolean indexSupported, final SchemaSemantics defaultSchemaSemantics,
- final SchemaExecutionSemantics schemaExecutionSemantics, final boolean crossSchemaQuerySupported) {
- this.type = type;
- this.typeAliases = typeAliases.stream().map(each -> (Object) each).toList();
- this.transactionCapability = transactionCapability;
- this.indexSupported = indexSupported;
- this.defaultSchemaSemantics = defaultSchemaSemantics;
- this.schemaExecutionSemantics = schemaExecutionSemantics;
- this.crossSchemaQuerySupported = crossSchemaQuerySupported;
- }
-
- @Override
- public final TransactionCapability getTransactionCapability() {
- return transactionCapability;
- }
-
- @Override
- public final boolean isIndexSupported() {
- return indexSupported;
- }
-
- @Override
- public final SchemaSemantics getDefaultSchemaSemantics() {
- return defaultSchemaSemantics;
- }
-
- @Override
- public final SchemaExecutionSemantics getSchemaExecutionSemantics() {
- return schemaExecutionSemantics;
- }
-
- @Override
- public final boolean isCrossSchemaQuerySupported() {
- return crossSchemaQuerySupported;
- }
-
- @Override
- public boolean isExplainAnalyzeSupported(final String databaseVersion) {
- return false;
- }
+ private final boolean explainSupported;
@Override
public final String getType() {
@@ -99,7 +37,7 @@ abstract class AbstractMCPDatabaseCapabilityOption implements MCPDatabaseCapabil
}
@Override
- public final Collection<Object> getTypeAliases() {
- return typeAliases;
+ public final boolean isExplainSupported() {
+ return explainSupported;
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/ClickHouseMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/ClickHouseMCPDatabaseCapabilityOption.java
index dceb9d0d928..11619244bf5 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/ClickHouseMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/ClickHouseMCPDatabaseCapabilityOption.java
@@ -17,16 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for ClickHouse.
*/
public final class ClickHouseMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public ClickHouseMCPDatabaseCapabilityOption() {
- super("ClickHouse", TransactionCapability.NONE, false, SchemaSemantics.DATABASE_AS_SCHEMA, SchemaExecutionSemantics.FIXED_TO_DATABASE, false);
+ super("ClickHouse", false);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/FirebirdMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/FirebirdMCPDatabaseCapabilityOption.java
index d9fe991a1c3..e4a763904e6 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/FirebirdMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/FirebirdMCPDatabaseCapabilityOption.java
@@ -17,17 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for Firebird.
*/
public final class FirebirdMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public FirebirdMCPDatabaseCapabilityOption() {
- super("Firebird", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.NATIVE_SCHEMA, SchemaExecutionSemantics.BEST_EFFORT, true);
+ super("Firebird", false);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/HiveMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/HiveMCPDatabaseCapabilityOption.java
index 36aaff14419..2665add48fe 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/HiveMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/HiveMCPDatabaseCapabilityOption.java
@@ -17,16 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for Hive.
*/
public final class HiveMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public HiveMCPDatabaseCapabilityOption() {
- super("Hive", TransactionCapability.NONE, false, SchemaSemantics.DATABASE_AS_SCHEMA, SchemaExecutionSemantics.FIXED_TO_DATABASE, false);
+ super("Hive", false);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MariaDBMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MariaDBMCPDatabaseCapabilityOption.java
index 0a93ec7e592..f8d4526639b 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MariaDBMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MariaDBMCPDatabaseCapabilityOption.java
@@ -19,9 +19,6 @@ package org.apache.shardingsphere.mcp.support.database.capability.dialect;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicyFactory;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicySet;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
/**
* MCP database capability option for MariaDB.
@@ -29,8 +26,7 @@ import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapa
public final class MariaDBMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public MariaDBMCPDatabaseCapabilityOption() {
- super("MariaDB", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.DATABASE_AS_SCHEMA, SchemaExecutionSemantics.FIXED_TO_DATABASE, false);
+ super("MariaDB", true);
}
@Override
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MySQLMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MySQLMCPDatabaseCapabilityOption.java
index f464c8bbeb4..d42b9ed550a 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MySQLMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/MySQLMCPDatabaseCapabilityOption.java
@@ -19,10 +19,6 @@ package org.apache.shardingsphere.mcp.support.database.capability.dialect;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicyFactory;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicySet;
-import org.apache.shardingsphere.mcp.support.database.capability.DatabaseVersionUtil;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
/**
* MCP database capability option for MySQL.
@@ -30,17 +26,11 @@ import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapa
public final class MySQLMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public MySQLMCPDatabaseCapabilityOption() {
- super("MySQL", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.DATABASE_AS_SCHEMA, SchemaExecutionSemantics.FIXED_TO_DATABASE, false);
+ super("MySQL", true);
}
@Override
public IdentifierCasePolicySet getIdentifierCasePolicySet() {
return IdentifierCasePolicyFactory.newMySQLInsensitivePolicySet();
}
-
- @Override
- public boolean isExplainAnalyzeSupported(final String databaseVersion) {
- return DatabaseVersionUtil.isVersionAtLeast(databaseVersion, 8, 0, 18);
- }
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OpenGaussMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OpenGaussMCPDatabaseCapabilityOption.java
index f0b41dc3a85..82a03c34b25 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OpenGaussMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OpenGaussMCPDatabaseCapabilityOption.java
@@ -17,22 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for openGauss.
*/
public final class OpenGaussMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public OpenGaussMCPDatabaseCapabilityOption() {
- super("openGauss", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.NATIVE_SCHEMA, SchemaExecutionSemantics.BEST_EFFORT, true);
- }
-
- @Override
- public boolean isExplainAnalyzeSupported(final String databaseVersion) {
- return true;
+ super("openGauss", true);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OracleMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OracleMCPDatabaseCapabilityOption.java
index e081f0bbdcb..3bb5e65ee8e 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OracleMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/OracleMCPDatabaseCapabilityOption.java
@@ -17,17 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for Oracle.
*/
public final class OracleMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public OracleMCPDatabaseCapabilityOption() {
- super("Oracle", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.NATIVE_SCHEMA, SchemaExecutionSemantics.BEST_EFFORT, true);
+ super("Oracle", false);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PostgreSQLMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PostgreSQLMCPDatabaseCapabilityOption.java
index 0f68ea7a0a2..f6a6aebdd24 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PostgreSQLMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PostgreSQLMCPDatabaseCapabilityOption.java
@@ -17,22 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for PostgreSQL.
*/
public final class PostgreSQLMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public PostgreSQLMCPDatabaseCapabilityOption() {
- super("PostgreSQL", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.NATIVE_SCHEMA, SchemaExecutionSemantics.BEST_EFFORT, true);
- }
-
- @Override
- public boolean isExplainAnalyzeSupported(final String databaseVersion) {
- return true;
+ super("PostgreSQL", true);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PrestoMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PrestoMCPDatabaseCapabilityOption.java
index 541902f6dd2..cb9a6ea48fd 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PrestoMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/PrestoMCPDatabaseCapabilityOption.java
@@ -17,21 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for Presto.
*/
public final class PrestoMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public PrestoMCPDatabaseCapabilityOption() {
- super("Presto", TransactionCapability.LOCAL, false, SchemaSemantics.NATIVE_SCHEMA, SchemaExecutionSemantics.BEST_EFFORT, true);
- }
-
- @Override
- public boolean isExplainAnalyzeSupported(final String databaseVersion) {
- return true;
+ super("Presto", true);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/SQLServerMCPDatabaseCapabilityOption.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/SQLServerMCPDatabaseCapabilityOption.java
index 21704d07823..a39aff11ea6 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/SQLServerMCPDatabaseCapabilityOption.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/capability/dialect/SQLServerMCPDatabaseCapabilityOption.java
@@ -17,17 +17,12 @@
package org.apache.shardingsphere.mcp.support.database.capability.dialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-
/**
* MCP database capability option for SQLServer.
*/
public final class SQLServerMCPDatabaseCapabilityOption extends AbstractMCPDatabaseCapabilityOption {
public SQLServerMCPDatabaseCapabilityOption() {
- super("SQLServer", TransactionCapability.LOCAL_WITH_SAVEPOINT, true,
- SchemaSemantics.NATIVE_SCHEMA, SchemaExecutionSemantics.BEST_EFFORT, true);
+ super("SQLServer", false);
}
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
index fe4183375d4..df1476994a0 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
@@ -62,7 +62,8 @@ public final class MCPJdbcDatabaseProfileLoader {
DatabaseMetaData databaseMetaData = connection.getMetaData();
DatabaseType databaseType = loadDatabaseType(databaseName, databaseMetaData);
String databaseVersion = Objects.toString(databaseMetaData.getDatabaseProductVersion(), "").trim();
- return new RuntimeDatabaseProfile(databaseName, databaseType.getType(), databaseVersion);
+ boolean supportsTransaction = databaseMetaData.supportsTransactions();
+ return new RuntimeDatabaseProfile(databaseName, databaseType.getType(), databaseVersion, supportsTransaction, supportsTransaction && databaseMetaData.supportsSavepoints());
} catch (final SQLException ex) {
throw RuntimeDatabaseConnectionException.connectionFailed(databaseName, ex);
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
index 3b5dc8ac9f3..cbb036828de 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
@@ -21,6 +21,7 @@ import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.database.connector.core.metadata.data.loader.type.SequenceMetaDataLoader;
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.TableType;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereColumn;
import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereIndex;
@@ -29,12 +30,12 @@ import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSp
import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereTable;
import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseDialect;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
import java.sql.Types;
import java.util.Collection;
import java.util.Collections;
@@ -73,7 +74,7 @@ public final class MCPJdbcMetadataLoader {
final Connection connection, final DatabaseMetaData databaseMetaData) throws SQLException {
DatabaseType protocolType = TypedSPILoader.getService(DatabaseType.class, databaseProfile.getDatabaseType());
MCPDatabaseDialect databaseDialect = MCPDatabaseDialect.of(databaseProfile.getDatabaseType());
- SchemaSemantics defaultSchemaSemantics = databaseDialect.getDefaultSchemaSemantics();
+ DialectSchemaSemantics defaultSchemaSemantics = databaseDialect.getDefaultSchemaSemantics();
DatabaseMetadataAccumulator accumulator = new DatabaseMetadataAccumulator(protocolType);
loadTables(databaseName, defaultSchemaSemantics, databaseDialect, accumulator, databaseMetaData);
loadViews(databaseName, defaultSchemaSemantics, databaseDialect, accumulator, databaseMetaData);
@@ -81,7 +82,7 @@ public final class MCPJdbcMetadataLoader {
return accumulator.build();
}
- private void loadTables(final String databaseName, final SchemaSemantics defaultSchemaSemantics, final MCPDatabaseDialect databaseDialect,
+ private void loadTables(final String databaseName, final DialectSchemaSemantics defaultSchemaSemantics, final MCPDatabaseDialect databaseDialect,
final DatabaseMetadataAccumulator accumulator, final DatabaseMetaData databaseMetaData) throws SQLException {
try (ResultSet tables = databaseMetaData.getTables(null, null, "%", new String[]{"TABLE"})) {
while (tables.next()) {
@@ -106,7 +107,7 @@ public final class MCPJdbcMetadataLoader {
}
}
- private void loadViews(final String databaseName, final SchemaSemantics defaultSchemaSemantics, final MCPDatabaseDialect databaseDialect,
+ private void loadViews(final String databaseName, final DialectSchemaSemantics defaultSchemaSemantics, final MCPDatabaseDialect databaseDialect,
final DatabaseMetadataAccumulator accumulator, final DatabaseMetaData databaseMetaData) throws SQLException {
try (ResultSet views = databaseMetaData.getTables(null, null, "%", new String[]{"VIEW"})) {
while (views.next()) {
@@ -128,7 +129,7 @@ public final class MCPJdbcMetadataLoader {
}
}
- private void loadSequences(final String databaseName, final SchemaSemantics defaultSchemaSemantics, final DatabaseType protocolType,
+ private void loadSequences(final String databaseName, final DialectSchemaSemantics defaultSchemaSemantics, final DatabaseType protocolType,
final DatabaseMetadataAccumulator accumulator, final Connection connection) throws SQLException {
for (Entry<String, Collection<String>> entry : new SequenceMetaDataLoader(protocolType).load(connection).entrySet()) {
SchemaMetadataAccumulator schema = accumulator.getSchemaAccumulator(normalizeSchemaName(databaseName, defaultSchemaSemantics, entry.getKey()));
@@ -161,6 +162,8 @@ public final class MCPJdbcMetadataLoader {
result.add(indexName);
}
}
+ } catch (final SQLFeatureNotSupportedException ignored) {
+ return result;
}
return result;
}
@@ -170,12 +173,12 @@ public final class MCPJdbcMetadataLoader {
return result.isEmpty() ? null : result;
}
- private String normalizeSchemaName(final String databaseName, final SchemaSemantics defaultSchemaSemantics, final String schemaName) {
+ private String normalizeSchemaName(final String databaseName, final DialectSchemaSemantics defaultSchemaSemantics, final String schemaName) {
String result = Objects.toString(schemaName, "").trim();
if (!result.isEmpty()) {
return result;
}
- return SchemaSemantics.DATABASE_AS_SCHEMA == defaultSchemaSemantics ? databaseName : result;
+ return DialectSchemaSemantics.DATABASE_AS_SCHEMA == defaultSchemaSemantics ? databaseName : result;
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java
index 20000fee87a..c7d041c2544 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseProfile.java
@@ -32,4 +32,8 @@ public final class RuntimeDatabaseProfile {
private final String databaseType;
private final String databaseVersion;
+
+ private final boolean supportsTransaction;
+
+ private final boolean supportsSavepoint;
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponse.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponse.java
index e9946024d91..45a8e580b2c 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponse.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponse.java
@@ -46,10 +46,11 @@ public final class MCPDatabaseCapabilityResponse implements MCPResponse {
result.put("defaultSchemaSemantics", databaseCapability.getDefaultSchemaSemantics());
result.put("schemaExecutionSemantics", databaseCapability.getSchemaExecutionSemantics());
result.put("supportsCrossSchemaSql", databaseCapability.isSupportsCrossSchemaSql());
- result.put("supportsExplainAnalyze", databaseCapability.isSupportsExplainAnalyze());
- result.put("explainAnalyzeExecutionRisk", databaseCapability.isSupportsExplainAnalyze()
- ? "EXPLAIN ANALYZE may execute the underlying SELECT on this engine; use it only when the user accepts runtime execution cost."
- : "EXPLAIN ANALYZE is not supported for this logical database.");
+ boolean supportsExplain = databaseCapability.isSupportsExplain();
+ result.put("supportsExplain", supportsExplain);
+ result.put("explainExecutionSemantics", supportsExplain
+ ? "database_gateway_execute_explain_query executes model-generated database-native EXPLAIN SQL for one classifier-approved SELECT. EXPLAIN ANALYZE is not supported."
+ : "database_gateway_execute_explain_query is not supported for this database type.");
return result;
}
}
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 0ccd1e5b2a4..3780f7f4d43 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
@@ -32,4 +32,13 @@ public interface MCPFeatureExecutionFacade {
* @return execution response
*/
SQLExecutionResponse 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
+ */
+ SQLExecutionResponse executeExplain(SQLExecutionRequest executionRequest, String sql);
}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
index 36a541bd326..9009a2b0a08 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
@@ -90,6 +90,8 @@ final class MCPGuidancePayloadBuilder {
"Follow top-level next_actions when validation fails."),
createFirstCallRoute("read_only_sql", "read_resource shardingsphere://databases/{database}/capabilities", "call_tool database_gateway_execute_query",
"Stop after reporting the result rows."),
+ createFirstCallRoute("explain_query", "read_resource shardingsphere://databases/{database}/capabilities",
+ "generate database-native EXPLAIN SQL, then call_tool database_gateway_execute_explain_query", "Stop after reporting the execution plan rows."),
createFirstCallRoute("rule_workflow", "call_tool matching database_gateway_plan_* workflow tool without plan_id for a new plan",
"call_tool database_gateway_apply_workflow execution_mode=preview with the returned plan_id",
"Use workflow apply and validation for natural-language rule changes before considering raw side-effect SQL."),
@@ -118,8 +120,9 @@ final class MCPGuidancePayloadBuilder {
result.put("guidance_resource", GUIDANCE_RESOURCE_URI);
result.put("metadata_first_resource", "shardingsphere://databases");
result.put("preflight_rule", "Use database_gateway_validate_runtime_database with a configured database name before onboarding or troubleshooting runtime connectivity.");
- Map<String, Object> sqlToolSelection = new LinkedHashMap<>(2, 1F);
- sqlToolSelection.put("read_only", "Use database_gateway_execute_query for one classifier-approved SELECT or EXPLAIN ANALYZE statement.");
+ Map<String, Object> sqlToolSelection = new LinkedHashMap<>(3, 1F);
+ sqlToolSelection.put("read_only", "Use database_gateway_execute_query for one classifier-approved SELECT statement.");
+ sqlToolSelection.put("explain", "Use database_gateway_execute_explain_query with the original SELECT and a model-generated database-native EXPLAIN SQL; do not use EXPLAIN ANALYZE.");
sqlToolSelection.put("side_effecting", "Use database_gateway_execute_update with execution_mode=preview before execution.");
result.put("sql_tool_selection", sqlToolSelection);
result.put("workflow_session_rule", "Reuse the current-session plan_id returned by a planning tool; re-plan when the plan is unavailable.");
@@ -141,6 +144,7 @@ final class MCPGuidancePayloadBuilder {
result.put("metadata_search_tool", "database_gateway_search_metadata");
result.put("preflight_validation_tool", PREFLIGHT_TOOL_NAME);
result.put("read_only_sql_tool", "database_gateway_execute_query");
+ result.put("explain_sql_tool", "database_gateway_execute_explain_query");
result.put("side_effect_sql_tool", "database_gateway_execute_update");
result.put("workflow_tools", List.of(WorkflowToolDescriptors.APPLY_TOOL_NAME, WorkflowToolDescriptors.VALIDATE_TOOL_NAME));
result.put("side_effect_rule", "Preview first and continue only when the requested side effect is still intended.");
@@ -185,8 +189,12 @@ final class MCPGuidancePayloadBuilder {
"Stop after the configured runtime database reports ready or returns structured recovery guidance.",
List.of(PREFLIGHT_TOOL_NAME), List.of("shardingsphere://databases")),
createCommonFlow("read_only_sql", List.of("read_resource shardingsphere://databases/{database}/capabilities", "call_tool database_gateway_execute_query"),
- "Use one SELECT or EXPLAIN ANALYZE statement and stop after the result is reported.",
+ "Use one SELECT statement and stop after the result is reported.",
List.of("database_gateway_execute_query"), List.of("shardingsphere://databases/{database}/capabilities")),
+ createCommonFlow("explain_query", List.of("read_resource shardingsphere://databases/{database}/capabilities",
+ "generate database-native EXPLAIN SQL without ANALYZE", "call_tool database_gateway_execute_explain_query"),
+ "Use the original SELECT as sql, the generated EXPLAIN as explain_sql, and stop after the plan rows are reported.",
+ List.of("database_gateway_execute_explain_query"), List.of("shardingsphere://databases/{database}/capabilities")),
createCommonFlow("side_effecting_sql", List.of("call_tool database_gateway_execute_update execution_mode=preview",
"call_tool database_gateway_execute_update execution_mode=execute"),
"Execute only after reviewing the previewed SQL and side-effect scope.", List.of("database_gateway_execute_update"), List.of()),
@@ -247,11 +255,16 @@ final class MCPGuidancePayloadBuilder {
}
private Map<String, Object> createSqlToolSelection() {
- Map<String, Object> result = new LinkedHashMap<>(2, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(3, 1F);
Map<String, Object> readOnly = new LinkedHashMap<>(2, 1F);
readOnly.put("tool", "database_gateway_execute_query");
- readOnly.put("statement_rule", "Use for one SELECT or EXPLAIN ANALYZE statement.");
+ readOnly.put("statement_rule", "Use for one SELECT statement.");
result.put("read_only", readOnly);
+ Map<String, Object> explain = new LinkedHashMap<>(3, 1F);
+ explain.put("tool", "database_gateway_execute_explain_query");
+ explain.put("statement_rule", "Use for an execution plan of one SELECT statement.");
+ explain.put("candidate_rule", "Generate database-native EXPLAIN SQL in explain_sql and never use EXPLAIN ANALYZE.");
+ result.put("explain", explain);
Map<String, Object> sideEffecting = new LinkedHashMap<>(4, 1F);
sideEffecting.put("tool", "database_gateway_execute_update");
sideEffecting.put("first_mode", "preview");
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 1b66a9cefdb..ba3f6df74b2 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
@@ -51,6 +51,8 @@ public final class MCPToolDescriptorCatalogValidator {
private static final String EXECUTE_QUERY = "database_gateway_execute_query";
+ private static final String EXECUTE_EXPLAIN = "database_gateway_execute_explain_query";
+
private static final String EXECUTE_UPDATE = "database_gateway_execute_update";
private static final String VALIDATE_RUNTIME_DATABASE = "database_gateway_validate_runtime_database";
@@ -138,6 +140,10 @@ public final class MCPToolDescriptorCatalogValidator {
validateExecuteQueryDescriptor(descriptor);
return;
}
+ if (EXECUTE_EXPLAIN.equals(descriptor.getName())) {
+ validateExecuteExplainDescriptor(descriptor);
+ return;
+ }
if (EXECUTE_UPDATE.equals(descriptor.getName())) {
validateExecuteUpdateDescriptor(descriptor);
return;
@@ -195,6 +201,20 @@ public final class MCPToolDescriptorCatalogValidator {
"applied_max_rows", "applied_timeout_ms", "truncated", MCPPayloadFieldNames.NEXT_ACTIONS));
}
+ private static void validateExecuteExplainDescriptor(final MCPToolDescriptor descriptor) {
+ validateRequiredInput(descriptor, "database");
+ validateRequiredInput(descriptor, "sql");
+ validateRequiredInput(descriptor, "explain_sql");
+ validateExecuteQueryDescriptor(descriptor);
+ }
+
+ private static void validateRequiredInput(final MCPToolDescriptor descriptor, final String fieldName) {
+ ShardingSpherePreconditions.checkState(MCPToolDescriptorValidationUtils.findToolInputProperty(descriptor, fieldName).isPresent(),
+ () -> new IllegalStateException(String.format("Tool `%s` must declare `%s`.", descriptor.getName(), fieldName)));
+ ShardingSpherePreconditions.checkState(MCPToolDescriptorValidationUtils.isRequiredToolInput(descriptor, fieldName),
+ () -> new IllegalStateException(String.format("Tool `%s` `%s` must be required.", descriptor.getName(), fieldName)));
+ }
+
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));
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
index 59244c99408..251cf3ad9a9 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
@@ -39,7 +39,7 @@ public final class MCPToolOutputSchemaValidator {
private static final Collection<String> CONTINUATION_MODES = List.of("none", "pagination", "metadata_search");
private static final Collection<String> RECOVERY_CATEGORIES = List.of("not_found", "ambiguous", "empty_scope", "missing_context", "validation", "terminal",
- "unsupported_target", "invalid_enum", "unsafe_sql", "stale_workflow", "unavailable_runtime", "terminal_operator_action");
+ "unsupported_target", "invalid_enum", "unsafe_sql", "invalid_explain_sql", "stale_workflow", "unavailable_runtime", "terminal_operator_action");
/**
* Validate one tool output schema.
diff --git a/mcp/support/src/main/resources/META-INF/shardingsphere-mcp/prompts/safe-sql-execution.md b/mcp/support/src/main/resources/META-INF/shardingsphere-mcp/prompts/safe-sql-execution.md
index 0c5a330c965..08e4f6b6381 100644
--- a/mcp/support/src/main/resources/META-INF/shardingsphere-mcp/prompts/safe-sql-execution.md
+++ b/mcp/support/src/main/resources/META-INF/shardingsphere-mcp/prompts/safe-sql-execution.md
@@ -24,12 +24,13 @@ User context:
Model path:
1. Read shardingsphere://databases/{{database}}/capabilities before execution when statement support is uncertain.
-2. Use database_gateway_execute_query only for one classifier-approved SELECT or EXPLAIN ANALYZE statement.
-3. Use database_gateway_execute_update with execution_mode=preview for DML, DDL, DCL, transaction control, savepoint, or side-effecting EXPLAIN ANALYZE statements before execution.
+2. Use database_gateway_execute_query only for one classifier-approved SELECT statement.
+3. Use database_gateway_execute_explain_query for execution-plan diagnostics; pass the original SELECT as sql and a database-native EXPLAIN as explain_sql. Do not use EXPLAIN ANALYZE.
+4. Use database_gateway_execute_update with execution_mode=preview for DML, DDL, DCL, transaction control, or savepoint statements before execution.
Treat preview as classification-only, not as a database dry run.
-4. After reviewing the preview, call database_gateway_execute_update with execution_mode=execute and the reviewed SQL only when execution is still intended.
-5. Never split or batch multiple SQL statements into one MCP call.
-6. Before choosing uncertain database, schema, table, or column names, use completion/complete or read the nearest MCP resource; do not guess identifiers.
+5. After reviewing the preview, call database_gateway_execute_update with execution_mode=execute and the reviewed SQL only when execution is still intended.
+6. Never split or batch multiple SQL statements into one MCP call.
+7. Before choosing uncertain database, schema, table, or column names, use completion/complete or read the nearest MCP resource; do not guess identifiers.
Ask-user conditions:
- Ask before database_gateway_execute_update execution when the previewed side effects are ambiguous.
@@ -37,6 +38,7 @@ Ask-user conditions:
Stop conditions:
- Stop after database_gateway_execute_query returns the requested query result.
+- Stop after database_gateway_execute_explain_query returns the requested execution plan.
- Stop after database_gateway_execute_update preview unless execution is still intended.
Final answer rule:
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/DatabaseVersionUtilTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/DatabaseVersionUtilTest.java
deleted file mode 100644
index f24f5e6e268..00000000000
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/DatabaseVersionUtilTest.java
+++ /dev/null
@@ -1,48 +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.capability;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import java.util.stream.Stream;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-class DatabaseVersionUtilTest {
-
- @ParameterizedTest(name = "{0}")
- @MethodSource("isVersionAtLeastArguments")
- void assertIsVersionAtLeast(final String name, final String databaseVersion, final int major, final int minor, final int patch, final boolean expected) {
- assertThat(DatabaseVersionUtil.isVersionAtLeast(databaseVersion, major, minor, patch), is(expected));
- }
-
- private static Stream<Arguments> isVersionAtLeastArguments() {
- return Stream.of(
- Arguments.of("null version", null, 1, 0, 0, false),
- Arguments.of("major is less than target", "4.0.0", 5, 0, 0, false),
- Arguments.of("major is greater than target", "6.0.0", 5, 0, 0, true),
- Arguments.of("minor is less than target", "5.1.0", 5, 2, 0, false),
- Arguments.of("minor is greater than target", "5.3.0", 5, 2, 0, true),
- Arguments.of("missing patch is less than target", "5.2", 5, 2, 1, false),
- Arguments.of("patch equals target with suffix", " 5.2.1-log ", 5, 2, 1, true),
- Arguments.of("missing minor and patch default to zero", "5", 5, 0, 0, true));
- }
-}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProviderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProviderTest.java
index 554d305d239..c8262a9dd1a 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProviderTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseCapabilityProviderTest.java
@@ -18,6 +18,8 @@
package org.apache.shardingsphere.mcp.support.database.capability;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
@@ -67,96 +69,95 @@ class MCPDatabaseCapabilityProviderTest {
assertThat(actual.get().getTransactionCapability(), is(TransactionCapability.LOCAL_WITH_SAVEPOINT));
assertTrue(actual.get().isSupportsTransactionControl());
assertTrue(actual.get().isSupportsSavepoint());
- assertThat(actual.get().getDefaultSchemaSemantics(), is(SchemaSemantics.DATABASE_AS_SCHEMA));
+ assertThat(actual.get().getDefaultSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
assertThat(actual.get().getSchemaExecutionSemantics(), is(SchemaExecutionSemantics.FIXED_TO_DATABASE));
assertFalse(actual.get().isSupportsCrossSchemaSql());
- assertFalse(actual.get().isSupportsExplainAnalyze());
+ assertTrue(actual.get().isSupportsExplain());
}
@Test
- void assertProvideWithoutIndex() {
+ void assertProvideWithoutSequence() {
Optional<MCPDatabaseCapability> actual = createCapabilityProvider().provide("warehouse");
assertTrue(actual.isPresent());
- assertFalse(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.INDEX));
+ assertTrue(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.INDEX));
assertFalse(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.SEQUENCE));
assertThat(actual.get().getTransactionCapability(), is(TransactionCapability.NONE));
assertFalse(actual.get().isSupportsTransactionControl());
assertFalse(actual.get().isSupportsSavepoint());
- assertThat(actual.get().getDefaultSchemaSemantics(), is(SchemaSemantics.DATABASE_AS_SCHEMA));
+ assertThat(actual.get().getDefaultSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
assertThat(actual.get().getSchemaExecutionSemantics(), is(SchemaExecutionSemantics.FIXED_TO_DATABASE));
}
@ParameterizedTest(name = "{0}")
@MethodSource("provideCapabilityMatrixArguments")
void assertProvideWithCapabilityMatrix(final String name, final String databaseType, final boolean expectedTransactionControl,
- final boolean expectedSavepoint, final boolean expectedIndexSupport, final boolean expectedSequenceSupport,
- final SchemaExecutionSemantics expectedSchemaExecutionSemantics) {
- Optional<MCPDatabaseCapability> actual = createCapabilityProvider(databaseType, "", expectedSequenceSupport).provide("logic_db");
+ final boolean expectedSavepoint, final boolean expectedSequenceSupport,
+ final SchemaExecutionSemantics expectedSchemaExecutionSemantics, final boolean expectedExplainSupport) {
+ CapabilityFixture capabilityFixture = new CapabilityFixture(expectedTransactionControl, expectedSavepoint, expectedSequenceSupport,
+ SchemaExecutionSemantics.FIXED_TO_DATABASE == expectedSchemaExecutionSemantics ? DialectSchemaSemantics.DATABASE_AS_SCHEMA : DialectSchemaSemantics.NATIVE_SCHEMA);
+ Optional<MCPDatabaseCapability> actual = createCapabilityProvider(databaseType, "", capabilityFixture).provide("logic_db");
assertTrue(actual.isPresent());
assertThat(actual.get().isSupportsTransactionControl(), is(expectedTransactionControl));
assertThat(actual.get().isSupportsSavepoint(), is(expectedSavepoint));
- assertThat(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.INDEX), is(expectedIndexSupport));
+ assertTrue(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.INDEX));
assertThat(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.SEQUENCE), is(expectedSequenceSupport));
assertThat(actual.get().getSchemaExecutionSemantics(), is(expectedSchemaExecutionSemantics));
- }
-
- @Test
- void assertProvideWithRuntimeOverlay() {
- Optional<MCPDatabaseCapability> actual = createCapabilityProvider("MySQL", "8.0.32").provide("logic_db");
- assertTrue(actual.isPresent());
- assertTrue(actual.get().getSupportedMetadataObjectTypes().contains(SupportedMCPMetadataObjectType.INDEX));
- assertThat(actual.get().getSchemaExecutionSemantics(), is(SchemaExecutionSemantics.FIXED_TO_DATABASE));
- assertFalse(actual.get().isSupportsCrossSchemaSql());
- assertTrue(actual.get().isSupportsExplainAnalyze());
+ assertThat(actual.get().isSupportsCrossSchemaSql(), is(SchemaExecutionSemantics.BEST_EFFORT == expectedSchemaExecutionSemantics));
+ assertThat(actual.get().isSupportsExplain(), is(expectedExplainSupport));
}
private MCPDatabaseCapabilityProvider createCapabilityProvider() {
- return createCapabilityProvider(createRuntimeDatabases(Map.of("logic_db", "MySQL", "warehouse", "Hive")), Map.of("MySQL", false, "Hive", false));
- }
-
- private MCPDatabaseCapabilityProvider createCapabilityProvider(final String databaseType, final String databaseVersion) {
- return createCapabilityProvider(databaseType, databaseVersion, false);
+ Map<String, CapabilityFixture> capabilityFixtures = Map.of(
+ "MySQL", new CapabilityFixture(true, true, false, DialectSchemaSemantics.DATABASE_AS_SCHEMA),
+ "Hive", new CapabilityFixture(false, false, false, DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ return createCapabilityProvider(createRuntimeDatabases(Map.of("logic_db", "MySQL", "warehouse", "Hive"), capabilityFixtures), capabilityFixtures);
}
- private MCPDatabaseCapabilityProvider createCapabilityProvider(final String databaseType, final String databaseVersion, final boolean sequenceSupported) {
- return createCapabilityProvider(Map.of("logic_db", createRuntimeDatabaseConfiguration("logic_db", databaseType, databaseVersion)),
- Map.of(databaseType, sequenceSupported));
+ private MCPDatabaseCapabilityProvider createCapabilityProvider(final String databaseType, final String databaseVersion, final CapabilityFixture capabilityFixture) {
+ return createCapabilityProvider(Map.of("logic_db", createRuntimeDatabaseConfiguration("logic_db", databaseType, databaseVersion,
+ capabilityFixture.transactionSupported, capabilityFixture.savepointSupported)),
+ Map.of(databaseType, capabilityFixture));
}
private MCPDatabaseCapabilityProvider createCapabilityProvider(final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases,
- final Map<String, Boolean> sequenceSupportByDatabaseType) {
+ final Map<String, CapabilityFixture> capabilityFixtures) {
try (
MockedStatic<DatabaseTypeFactory> ignored = SupportDatabaseTypeFactoryMocker.mockByConnectionMetadata();
MockedStatic<TypedSPILoader> typedSPILoader = mockStatic(TypedSPILoader.class, CALLS_REAL_METHODS);
MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader = mockStatic(DatabaseTypedSPILoader.class)) {
- for (Entry<String, Boolean> entry : sequenceSupportByDatabaseType.entrySet()) {
+ for (Entry<String, CapabilityFixture> entry : capabilityFixtures.entrySet()) {
mockDatabaseType(entry.getKey(), entry.getValue(), typedSPILoader, databaseTypedSPILoader);
}
return new MCPDatabaseCapabilityProvider(runtimeDatabases);
}
}
- private void mockDatabaseType(final String databaseType, final boolean sequenceSupported, final MockedStatic<TypedSPILoader> typedSPILoader,
+ private void mockDatabaseType(final String databaseType, final CapabilityFixture capabilityFixture, final MockedStatic<TypedSPILoader> typedSPILoader,
final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
DatabaseType databaseTypeFromSPI = mock(DatabaseType.class);
when(databaseTypeFromSPI.getType()).thenReturn(databaseType);
when(databaseTypeFromSPI.getTrunkDatabaseType()).thenReturn(Optional.empty());
typedSPILoader.when(() -> TypedSPILoader.findService(DatabaseType.class, databaseType)).thenReturn(Optional.of(databaseTypeFromSPI));
DialectDatabaseMetaData dialectDatabaseMetaData = mock(DialectDatabaseMetaData.class);
+ when(dialectDatabaseMetaData.getSchemaOption()).thenReturn(
+ new DefaultSchemaOption(false, null, capabilityFixture.schemaSemantics));
when(dialectDatabaseMetaData.getSequenceOption()).thenReturn(
- sequenceSupported ? Optional.of(new DialectSequenceOption("SELECT SEQUENCE_SCHEMA, SEQUENCE_NAME FROM TEST_SEQUENCES")) : Optional.empty());
+ capabilityFixture.sequenceSupported ? Optional.of(new DialectSequenceOption("SELECT SEQUENCE_SCHEMA, SEQUENCE_NAME FROM TEST_SEQUENCES")) : Optional.empty());
databaseTypedSPILoader.when(() -> DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class, databaseTypeFromSPI)).thenReturn(Optional.of(dialectDatabaseMetaData));
}
- private Map<String, RuntimeDatabaseConfiguration> createRuntimeDatabases(final Map<String, String> databaseTypes) {
+ private Map<String, RuntimeDatabaseConfiguration> createRuntimeDatabases(final Map<String, String> databaseTypes, final Map<String, CapabilityFixture> capabilityFixtures) {
Map<String, RuntimeDatabaseConfiguration> result = new LinkedHashMap<>(databaseTypes.size(), 1F);
for (Entry<String, String> entry : databaseTypes.entrySet()) {
- result.put(entry.getKey(), createRuntimeDatabaseConfiguration(entry.getKey(), entry.getValue(), ""));
+ CapabilityFixture capabilityFixture = capabilityFixtures.get(entry.getValue());
+ result.put(entry.getKey(), createRuntimeDatabaseConfiguration(entry.getKey(), entry.getValue(), "",
+ capabilityFixture.transactionSupported, capabilityFixture.savepointSupported));
}
return result;
}
- private RuntimeDatabaseConfiguration createRuntimeDatabaseConfiguration(final String databaseName, final String databaseType, final String databaseVersion) {
+ private RuntimeDatabaseConfiguration createRuntimeDatabaseConfiguration(final String databaseName, final String databaseType, final String databaseVersion,
+ final boolean transactionSupported, final boolean savepointSupported) {
RuntimeDatabaseConfiguration result = mock(RuntimeDatabaseConfiguration.class);
try {
Connection connection = mock(Connection.class);
@@ -169,6 +170,8 @@ class MCPDatabaseCapabilityProviderTest {
when(statement.executeQuery(anyString())).thenReturn(scalarResultSet);
when(scalarResultSet.next()).thenReturn(false);
when(databaseMetaData.getDatabaseProductVersion()).thenReturn(databaseVersion);
+ when(databaseMetaData.supportsTransactions()).thenReturn(transactionSupported);
+ when(databaseMetaData.supportsSavepoints()).thenReturn(savepointSupported);
when(databaseMetaData.getURL()).thenReturn(SupportDatabaseTypeFactoryMocker.createJdbcUrl(databaseType));
} catch (final SQLException ex) {
throw new IllegalStateException(ex);
@@ -178,15 +181,34 @@ class MCPDatabaseCapabilityProviderTest {
private static Stream<Arguments> provideCapabilityMatrixArguments() {
return Stream.of(
- Arguments.of("mysql", "MySQL", true, true, true, false, SchemaExecutionSemantics.FIXED_TO_DATABASE),
- Arguments.of("postgresql", "PostgreSQL", true, true, true, true, SchemaExecutionSemantics.BEST_EFFORT),
- Arguments.of("open gauss", "openGauss", true, true, true, true, SchemaExecutionSemantics.BEST_EFFORT),
- Arguments.of("sql server", "SQLServer", true, true, true, true, SchemaExecutionSemantics.BEST_EFFORT),
- Arguments.of("mariadb", "MariaDB", true, true, true, true, SchemaExecutionSemantics.FIXED_TO_DATABASE),
- Arguments.of("oracle", "Oracle", true, true, true, true, SchemaExecutionSemantics.BEST_EFFORT),
- Arguments.of("clickhouse", "ClickHouse", false, false, false, false, SchemaExecutionSemantics.FIXED_TO_DATABASE),
- Arguments.of("hive", "Hive", false, false, false, false, SchemaExecutionSemantics.FIXED_TO_DATABASE),
- Arguments.of("presto", "Presto", true, false, false, false, SchemaExecutionSemantics.BEST_EFFORT),
- Arguments.of("firebird", "Firebird", true, true, true, true, SchemaExecutionSemantics.BEST_EFFORT));
+ Arguments.of("mysql", "MySQL", true, true, false, SchemaExecutionSemantics.FIXED_TO_DATABASE, true),
+ Arguments.of("postgresql", "PostgreSQL", true, true, true, SchemaExecutionSemantics.BEST_EFFORT, true),
+ Arguments.of("open gauss", "openGauss", true, true, true, SchemaExecutionSemantics.BEST_EFFORT, true),
+ Arguments.of("sql server", "SQLServer", true, true, true, SchemaExecutionSemantics.BEST_EFFORT, false),
+ Arguments.of("mariadb", "MariaDB", true, true, true, SchemaExecutionSemantics.FIXED_TO_DATABASE, true),
+ Arguments.of("oracle", "Oracle", true, true, true, SchemaExecutionSemantics.BEST_EFFORT, false),
+ Arguments.of("clickhouse", "ClickHouse", false, false, false, SchemaExecutionSemantics.FIXED_TO_DATABASE, false),
+ Arguments.of("hive", "Hive", false, false, false, SchemaExecutionSemantics.FIXED_TO_DATABASE, false),
+ Arguments.of("presto", "Presto", true, false, false, SchemaExecutionSemantics.BEST_EFFORT, true),
+ Arguments.of("firebird", "Firebird", true, true, true, SchemaExecutionSemantics.BEST_EFFORT, false));
+ }
+
+ private static final class CapabilityFixture {
+
+ private final boolean transactionSupported;
+
+ private final boolean savepointSupported;
+
+ private final boolean sequenceSupported;
+
+ private final DialectSchemaSemantics schemaSemantics;
+
+ private CapabilityFixture(final boolean transactionSupported, final boolean savepointSupported, final boolean sequenceSupported,
+ final DialectSchemaSemantics schemaSemantics) {
+ this.transactionSupported = transactionSupported;
+ this.savepointSupported = savepointSupported;
+ this.sequenceSupported = sequenceSupported;
+ this.schemaSemantics = schemaSemantics;
+ }
}
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialectTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialectTest.java
index 6681745177c..6fdf31fae86 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialectTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/capability/MCPDatabaseDialectTest.java
@@ -20,6 +20,8 @@ package org.apache.shardingsphere.mcp.support.database.capability;
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.system.DialectSystemDatabase;
import org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicyFactory;
@@ -110,11 +112,27 @@ class MCPDatabaseDialectTest {
}
}
+ @Test
+ void assertGetDefaultSchemaSemantics() {
+ try (
+ MockedStatic<TypedSPILoader> typedSPILoader = mockStatic(TypedSPILoader.class);
+ MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader = mockStatic(DatabaseTypedSPILoader.class)) {
+ DatabaseType databaseType = mockDatabaseType("Fixture", typedSPILoader);
+ DialectDatabaseMetaData dialectDatabaseMetaData = mockDialectDatabaseMetaData(databaseType, databaseTypedSPILoader);
+ when(dialectDatabaseMetaData.getSchemaOption()).thenReturn(new DefaultSchemaOption(false, null, DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ assertThat(MCPDatabaseDialect.of("Fixture").getDefaultSchemaSemantics(), is(DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ }
+ }
+
+ @Test
+ void assertGetDefaultSchemaSemanticsWithUnknownDatabaseType() {
+ assertThat(MCPDatabaseDialect.of("FixtureDB").getDefaultSchemaSemantics(), is(DialectSchemaSemantics.NATIVE_SCHEMA));
+ }
+
@ParameterizedTest(name = "{0}")
- @MethodSource("getDefaultSchemaSemanticsArguments")
- void assertGetDefaultSchemaSemantics(final String name, final String databaseType, final SchemaSemantics expected) {
- SchemaSemantics actual = MCPDatabaseDialect.of(databaseType).getDefaultSchemaSemantics();
- assertThat(actual, is(expected));
+ @MethodSource("getTransactionCapabilityArguments")
+ void assertGetTransactionCapability(final String name, final boolean transactionSupported, final boolean savepointSupported, final TransactionCapability expected) {
+ assertThat(MCPDatabaseDialect.of("Fixture").getTransactionCapability(transactionSupported, savepointSupported), is(expected));
}
@ParameterizedTest(name = "{0}")
@@ -184,7 +202,7 @@ class MCPDatabaseDialectTest {
mockDialectDatabaseMetaDataAbsent(databaseType, databaseTypedSPILoader);
DialectSystemDatabase dialectSystemDatabase = mockDialectSystemDatabase(databaseType, databaseTypedSPILoader);
when(dialectSystemDatabase.getSystemSchemas()).thenReturn(List.of("fixture_system"));
- boolean actual = MCPDatabaseDialect.of("Fixture").isSystemSchema("", "fixture_system", SchemaSemantics.DATABASE_AS_SCHEMA);
+ boolean actual = MCPDatabaseDialect.of("Fixture").isSystemSchema("", "fixture_system", DialectSchemaSemantics.DATABASE_AS_SCHEMA);
assertTrue(actual);
}
}
@@ -250,11 +268,11 @@ class MCPDatabaseDialectTest {
return result;
}
- private static Stream<Arguments> getDefaultSchemaSemanticsArguments() {
+ private static Stream<Arguments> getTransactionCapabilityArguments() {
return Stream.of(
- Arguments.of("mysql", "MySQL", SchemaSemantics.DATABASE_AS_SCHEMA),
- Arguments.of("postgresql", "PostgreSQL", SchemaSemantics.NATIVE_SCHEMA),
- Arguments.of("unknown", "FixtureDB", SchemaSemantics.NATIVE_SCHEMA));
+ Arguments.of("not supported", false, false, TransactionCapability.NONE),
+ Arguments.of("local only", true, false, TransactionCapability.LOCAL),
+ Arguments.of("local with savepoint", true, true, TransactionCapability.LOCAL_WITH_SAVEPOINT));
}
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java
index 2e0fc7c7257..85d3a924921 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java
@@ -22,6 +22,8 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.system.DialectSystemDatabase;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -84,6 +86,11 @@ abstract class AbstractMCPJdbcMetadataLoaderTest {
protected LoadedMetadataCatalog load(final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases, final Collection<String> sequenceSupportedDatabaseTypes,
final Map<String, Collection<String>> systemSchemas) {
+ return load(runtimeDatabases, sequenceSupportedDatabaseTypes, systemSchemas, Collections.emptyMap());
+ }
+
+ protected LoadedMetadataCatalog load(final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases, final Collection<String> sequenceSupportedDatabaseTypes,
+ final Map<String, Collection<String>> systemSchemas, final Map<String, DialectSchemaSemantics> schemaSemantics) {
try (
MockedStatic<DatabaseTypeFactory> ignored = SupportDatabaseTypeFactoryMocker.mockByConnectionMetadata();
MockedStatic<TypedSPILoader> typedSPILoader = mockStatic(TypedSPILoader.class, CALLS_REAL_METHODS);
@@ -91,7 +98,7 @@ abstract class AbstractMCPJdbcMetadataLoaderTest {
MCPJdbcDatabaseProfileLoader databaseProfileLoader = new MCPJdbcDatabaseProfileLoader();
MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
Map<String, RuntimeDatabaseProfile> databaseProfiles = databaseProfileLoader.load(runtimeDatabases);
- mockDatabaseTypedSPI(databaseProfiles.values(), sequenceSupportedDatabaseTypes, systemSchemas, typedSPILoader, databaseTypedSPILoader);
+ mockDatabaseTypedSPI(databaseProfiles.values(), sequenceSupportedDatabaseTypes, systemSchemas, schemaSemantics, typedSPILoader, databaseTypedSPILoader);
Map<String, Collection<ShardingSphereSchema>> result = new LinkedHashMap<>(runtimeDatabases.size(), 1F);
for (Entry<String, RuntimeDatabaseConfiguration> entry : runtimeDatabases.entrySet()) {
result.put(entry.getKey(), metadataLoader.load(entry.getKey(), entry.getValue(), databaseProfiles.get(entry.getKey())));
@@ -101,28 +108,30 @@ abstract class AbstractMCPJdbcMetadataLoaderTest {
}
private void mockDatabaseTypedSPI(final Collection<RuntimeDatabaseProfile> databaseProfiles, final Collection<String> sequenceSupportedDatabaseTypes,
- final Map<String, Collection<String>> systemSchemas, final MockedStatic<TypedSPILoader> typedSPILoader,
- final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
+ final Map<String, Collection<String>> systemSchemas, final Map<String, DialectSchemaSemantics> schemaSemantics,
+ final MockedStatic<TypedSPILoader> typedSPILoader, final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
for (RuntimeDatabaseProfile each : databaseProfiles) {
- mockDatabaseTypedSPI(each.getDatabaseType(), sequenceSupportedDatabaseTypes, systemSchemas, typedSPILoader, databaseTypedSPILoader);
+ mockDatabaseTypedSPI(each.getDatabaseType(), sequenceSupportedDatabaseTypes, systemSchemas, schemaSemantics, typedSPILoader, databaseTypedSPILoader);
}
}
private void mockDatabaseTypedSPI(final String databaseType, final Collection<String> sequenceSupportedDatabaseTypes,
- final Map<String, Collection<String>> systemSchemas, final MockedStatic<TypedSPILoader> typedSPILoader,
- final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
+ final Map<String, Collection<String>> systemSchemas, final Map<String, DialectSchemaSemantics> schemaSemantics,
+ final MockedStatic<TypedSPILoader> typedSPILoader, final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
DatabaseType databaseTypeFromSPI = mock(DatabaseType.class);
when(databaseTypeFromSPI.getType()).thenReturn(databaseType);
when(databaseTypeFromSPI.getTrunkDatabaseType()).thenReturn(Optional.empty());
typedSPILoader.when(() -> TypedSPILoader.findService(DatabaseType.class, databaseType)).thenReturn(Optional.of(databaseTypeFromSPI));
typedSPILoader.when(() -> TypedSPILoader.getService(DatabaseType.class, databaseType)).thenReturn(databaseTypeFromSPI);
- mockDialectDatabaseMetaData(databaseTypeFromSPI, sequenceSupportedDatabaseTypes.contains(databaseType), databaseTypedSPILoader);
+ mockDialectDatabaseMetaData(databaseTypeFromSPI, sequenceSupportedDatabaseTypes.contains(databaseType),
+ schemaSemantics.getOrDefault(databaseType, DialectSchemaSemantics.NATIVE_SCHEMA), databaseTypedSPILoader);
mockDialectSystemDatabase(databaseTypeFromSPI, systemSchemas.getOrDefault(databaseType, List.of()), databaseTypedSPILoader);
}
- private void mockDialectDatabaseMetaData(final DatabaseType databaseType, final boolean sequenceSupported,
+ private void mockDialectDatabaseMetaData(final DatabaseType databaseType, final boolean sequenceSupported, final DialectSchemaSemantics schemaSemantics,
final MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader) {
DialectDatabaseMetaData result = mock(DialectDatabaseMetaData.class);
+ when(result.getSchemaOption()).thenReturn(new DefaultSchemaOption(false, null, schemaSemantics));
when(result.getSequenceOption()).thenReturn(sequenceSupported ? Optional.of(new DialectSequenceOption(SEQUENCE_METADATA_QUERY)) : Optional.empty());
databaseTypedSPILoader.when(() -> DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class, databaseType)).thenReturn(Optional.of(result));
databaseTypedSPILoader.when(() -> DatabaseTypedSPILoader.getService(DialectDatabaseMetaData.class, databaseType)).thenReturn(result);
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoaderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoaderTest.java
index df9f6271b56..169fda8e66a 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoaderTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoaderTest.java
@@ -29,7 +29,9 @@ import java.sql.SQLException;
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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -41,10 +43,23 @@ class MCPJdbcDatabaseProfileLoaderTest {
@Test
void assertLoad() throws SQLException {
try (MockedStatic<DatabaseTypeFactory> ignored = SupportDatabaseTypeFactoryMocker.mockByConnectionMetadata()) {
- RuntimeDatabaseProfile actual = new MCPJdbcDatabaseProfileLoader().load("logic_db", createRuntimeDatabaseConfiguration(SupportDatabaseTypeFactoryMocker.createJdbcUrl("FixtureDB"), "1.0"));
+ RuntimeDatabaseProfile actual =
+ new MCPJdbcDatabaseProfileLoader().load("logic_db", createRuntimeDatabaseConfiguration(SupportDatabaseTypeFactoryMocker.createJdbcUrl("FixtureDB"), "1.0", true, true));
assertThat(actual.getDatabase(), is("logic_db"));
assertThat(actual.getDatabaseType(), is("FixtureDB"));
assertThat(actual.getDatabaseVersion(), is("1.0"));
+ assertTrue(actual.isSupportsTransaction());
+ assertTrue(actual.isSupportsSavepoint());
+ }
+ }
+
+ @Test
+ void assertLoadWithoutTransaction() throws SQLException {
+ try (MockedStatic<DatabaseTypeFactory> ignored = SupportDatabaseTypeFactoryMocker.mockByConnectionMetadata()) {
+ RuntimeDatabaseProfile actual =
+ new MCPJdbcDatabaseProfileLoader().load("logic_db", createRuntimeDatabaseConfiguration(SupportDatabaseTypeFactoryMocker.createJdbcUrl("FixtureDB"), "1.0", false, true));
+ assertFalse(actual.isSupportsTransaction());
+ assertFalse(actual.isSupportsSavepoint());
}
}
@@ -54,13 +69,14 @@ class MCPJdbcDatabaseProfileLoaderTest {
ShardingSphereExternalException expectedCause = mock(ShardingSphereExternalException.class);
mocked.when(() -> DatabaseTypeFactory.get(any(DatabaseMetaData.class))).thenThrow(expectedCause);
RuntimeDatabaseConnectionException actual = assertThrows(RuntimeDatabaseConnectionException.class,
- () -> new MCPJdbcDatabaseProfileLoader().load("logic_db", createRuntimeDatabaseConfiguration("jdbc:unknown:test", "8.0.32")));
+ () -> new MCPJdbcDatabaseProfileLoader().load("logic_db", createRuntimeDatabaseConfiguration("jdbc:unknown:test", "8.0.32", true, true)));
assertThat(actual.getCategory(), is(RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION));
assertThat(actual.getCause(), is(expectedCause));
}
}
- private RuntimeDatabaseConfiguration createRuntimeDatabaseConfiguration(final String jdbcUrl, final String databaseVersion) throws SQLException {
+ private RuntimeDatabaseConfiguration createRuntimeDatabaseConfiguration(final String jdbcUrl, final String databaseVersion,
+ final boolean supportsTransaction, final boolean supportsSavepoint) throws SQLException {
RuntimeDatabaseConfiguration result = mock(RuntimeDatabaseConfiguration.class);
Connection connection = mock(Connection.class);
DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
@@ -68,6 +84,8 @@ class MCPJdbcDatabaseProfileLoaderTest {
when(connection.getMetaData()).thenReturn(databaseMetaData);
when(databaseMetaData.getURL()).thenReturn(jdbcUrl);
when(databaseMetaData.getDatabaseProductVersion()).thenReturn(databaseVersion);
+ when(databaseMetaData.supportsTransactions()).thenReturn(supportsTransaction);
+ when(databaseMetaData.supportsSavepoints()).thenReturn(supportsSavepoint);
return result;
}
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java
index 8c436bd1196..6b0cd6b7f6c 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java
@@ -36,7 +36,7 @@ class MCPJdbcMetadataLoaderFailureTest {
when(runtimeDatabaseConfiguration.openConnection("logic_db")).thenThrow(expected);
MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
RuntimeDatabaseConnectionException actual = assertThrows(RuntimeDatabaseConnectionException.class,
- () -> metadataLoader.load("logic_db", runtimeDatabaseConfiguration, new RuntimeDatabaseProfile("logic_db", "FixtureDB", "")));
+ () -> metadataLoader.load("logic_db", runtimeDatabaseConfiguration, new RuntimeDatabaseProfile("logic_db", "FixtureDB", "", true, true)));
assertThat(actual.getMessage(), is("Runtime database `logic_db` connection failed: connection_failed."));
assertThat(actual.getCause(), is(expected));
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java
index 573239abef0..22ff5c8b8a0 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java
@@ -17,6 +17,7 @@
package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereSchema;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPMetadataObjectType;
import org.junit.jupiter.api.Test;
@@ -46,7 +47,7 @@ class MCPJdbcMetadataLoaderSchemaFilteringTest extends AbstractMCPJdbcMetadataLo
void assertLoadWithoutSchemaObjects() throws SQLException {
Driver mockDriver = new MockDriver("jdbc:mock:no-schema", createConnectionWithoutSchema("MySQL"));
try (MockDriverRegistration ignored = MockDriverRegistration.register(mockDriver)) {
- LoadedMetadataCatalog actual = load(Map.of("logic_db", new RuntimeDatabaseConfiguration("jdbc:mock:no-schema", "", "", MockDriver.class.getName())));
+ LoadedMetadataCatalog actual = loadWithDatabaseAsSchema(Map.of("logic_db", new RuntimeDatabaseConfiguration("jdbc:mock:no-schema", "", "", MockDriver.class.getName())));
Collection<ShardingSphereSchema> schemas = actual.findMetadata("logic_db").orElseThrow();
assertThat(schemas.size(), is(1));
assertThat(schemas.iterator().next().getName(), is("logic_db"));
@@ -79,7 +80,7 @@ class MCPJdbcMetadataLoaderSchemaFilteringTest extends AbstractMCPJdbcMetadataLo
.thenThrow(new SQLException("system catalog should be skipped"));
RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
Collection<ShardingSphereSchema> actual = load(Map.of("logic_db", runtimeDatabaseConfiguration), List.of(),
- Map.of("MySQL", List.of("information_schema"))).findMetadata("logic_db").orElseThrow();
+ Map.of("MySQL", List.of("information_schema")), Map.of("MySQL", DialectSchemaSemantics.DATABASE_AS_SCHEMA)).findMetadata("logic_db").orElseThrow();
assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "GLOBAL_STATUS"));
assertThat(actual.iterator().next().getName(), is("logic_db"));
@@ -104,7 +105,7 @@ class MCPJdbcMetadataLoaderSchemaFilteringTest extends AbstractMCPJdbcMetadataLo
ResultSet orderIndexes = mockResultSet("INDEX_NAME", "idx_orders_status");
when(databaseMetaData.getIndexInfo(eq("orders"), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(orderIndexes);
RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
- Collection<ShardingSphereSchema> actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+ Collection<ShardingSphereSchema> actual = loadWithDatabaseAsSchema(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
assertThat(actual.iterator().next().getName(), is("logic_db"));
assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"));
@@ -172,4 +173,8 @@ class MCPJdbcMetadataLoaderSchemaFilteringTest extends AbstractMCPJdbcMetadataLo
Collection<ShardingSphereSchema> actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.VIEW, "active_orders"), is(1));
}
+
+ private LoadedMetadataCatalog loadWithDatabaseAsSchema(final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases) {
+ return load(runtimeDatabases, List.of(), Map.of(), Map.of("MySQL", DialectSchemaSemantics.DATABASE_AS_SCHEMA));
+ }
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
index 928f56de0f2..9a89e1f5f20 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
@@ -23,13 +23,22 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
import java.util.Collection;
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;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.when;
class MCPJdbcMetadataLoaderTest extends AbstractMCPJdbcMetadataLoaderTest {
@@ -66,4 +75,16 @@ class MCPJdbcMetadataLoaderTest extends AbstractMCPJdbcMetadataLoaderTest {
assertTrue(containsMetadata(schemas, SupportedMCPMetadataObjectType.VIEW, "active_orders"));
assertThat(schemas.size(), is(1));
}
+
+ @Test
+ void assertLoadWithoutIndexMetadataSupport() throws SQLException {
+ Connection connection = createStandardPostgreSQLMetadataConnection();
+ DatabaseMetaData databaseMetaData = connection.getMetaData();
+ when(databaseMetaData.getIndexInfo(isNull(), nullable(String.class), anyString(), eq(false), eq(false)))
+ .thenThrow(new SQLFeatureNotSupportedException("unsupported"));
+ LoadedMetadataCatalog actual = load(Map.of("logic_db", createMockRuntimeDatabaseConfiguration(connection)));
+ Collection<ShardingSphereSchema> schemas = actual.findMetadata("logic_db").orElseThrow();
+ assertTrue(containsMetadata(schemas, SupportedMCPMetadataObjectType.TABLE, "orders"));
+ assertFalse(containsMetadata(schemas, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"));
+ }
}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/query/MetadataQueryServiceTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/query/MetadataQueryServiceTest.java
index 0a7c037057b..123abad14f3 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/query/MetadataQueryServiceTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/query/MetadataQueryServiceTest.java
@@ -19,6 +19,8 @@ package org.apache.shardingsphere.mcp.support.database.metadata.query;
import org.apache.shardingsphere.database.connector.core.metadata.database.enums.TableType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.sequence.DialectSequenceOption;
import org.apache.shardingsphere.database.connector.core.metadata.database.system.DialectSystemDatabase;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
@@ -108,6 +110,7 @@ class MetadataQueryServiceTest {
private void mockDialectDatabaseMetaData(final DatabaseType databaseType, final boolean sequenceSupported) {
DialectDatabaseMetaData result = mock(DialectDatabaseMetaData.class);
+ when(result.getSchemaOption()).thenReturn(new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA));
when(result.getSequenceOption()).thenReturn(
sequenceSupported ? Optional.of(new DialectSequenceOption("SELECT SEQUENCE_SCHEMA, SEQUENCE_NAME FROM TEST_SEQUENCES")) : Optional.empty());
databaseTypedSPILoader.when(() -> DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class, databaseType)).thenReturn(Optional.of(result));
@@ -268,9 +271,8 @@ class MetadataQueryServiceTest {
}
@Test
- void assertQueryIndexesWithUnsupportedIndexType() {
- assertThat(assertThrows(MCPUnsupportedException.class, () -> metadataQueryService.queryIndexes("warehouse", "warehouse", "facts")).getMessage(),
- is("Index resources are not supported for the current database."));
+ void assertQueryIndexesWithoutIndexMetadata() {
+ assertTrue(metadataQueryService.queryIndexes("warehouse", "warehouse", "facts").isEmpty());
}
@Test
@@ -281,9 +283,8 @@ class MetadataQueryServiceTest {
}
@Test
- void assertQueryIndexWithUnsupportedIndexType() {
- assertThat(assertThrows(MCPUnsupportedException.class, () -> metadataQueryService.queryIndex("warehouse", "warehouse", "facts", "facts_idx")).getMessage(),
- is("Index resources are not supported for the current database."));
+ void assertQueryIndexWithoutIndexMetadata() {
+ assertFalse(metadataQueryService.queryIndex("warehouse", "warehouse", "facts", "facts_idx").isPresent());
}
@Test
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponseTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponseTest.java
index 8b5f6087998..e281aa5a2e5 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponseTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/response/MCPDatabaseCapabilityResponseTest.java
@@ -17,17 +17,19 @@
package org.apache.shardingsphere.mcp.support.database.response;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPMetadataObjectType;
import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapability;
-import org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityOption;
import org.apache.shardingsphere.mcp.support.database.capability.SchemaExecutionSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.SchemaSemantics;
-import org.apache.shardingsphere.mcp.support.database.capability.TransactionCapability;
-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.EnumSet;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@@ -36,16 +38,25 @@ import static org.mockito.Mockito.when;
class MCPDatabaseCapabilityResponseTest {
- @Test
- void assertToPayload() {
- MCPDatabaseCapabilityOption option = mock(MCPDatabaseCapabilityOption.class);
- when(option.getType()).thenReturn("FixtureDB");
- when(option.isIndexSupported()).thenReturn(true);
- when(option.getTransactionCapability()).thenReturn(TransactionCapability.LOCAL_WITH_SAVEPOINT);
- when(option.getDefaultSchemaSemantics()).thenReturn(SchemaSemantics.DATABASE_AS_SCHEMA);
- when(option.getSchemaExecutionSemantics()).thenReturn(SchemaExecutionSemantics.FIXED_TO_DATABASE);
- when(option.isExplainAnalyzeSupported("1.0")).thenReturn(true);
- MCPDatabaseCapability actualCapability = new MCPDatabaseCapability("logic_db", "1.0", option);
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("assertToPayloadArguments")
+ void assertToPayload(final String name, final boolean supportsExplain, final String explainExecutionSemantics) {
+ MCPDatabaseCapability actualCapability = mock(MCPDatabaseCapability.class);
+ when(actualCapability.getDatabaseName()).thenReturn("logic_db");
+ when(actualCapability.getDatabaseType()).thenReturn("FixtureDB");
+ when(actualCapability.getSupportedMetadataObjectTypes()).thenReturn(EnumSet.of(SupportedMCPMetadataObjectType.SCHEMA, SupportedMCPMetadataObjectType.TABLE,
+ SupportedMCPMetadataObjectType.VIEW, SupportedMCPMetadataObjectType.COLUMN, SupportedMCPMetadataObjectType.INDEX));
+ Set<SupportedMCPStatement> supportedStatementClasses = EnumSet.of(SupportedMCPStatement.QUERY, SupportedMCPStatement.DML, SupportedMCPStatement.DDL,
+ SupportedMCPStatement.DCL, SupportedMCPStatement.TRANSACTION_CONTROL, SupportedMCPStatement.SAVEPOINT);
+ if (supportsExplain) {
+ supportedStatementClasses.add(SupportedMCPStatement.EXPLAIN);
+ }
+ when(actualCapability.getSupportedStatementClasses()).thenReturn(supportedStatementClasses);
+ when(actualCapability.isSupportsTransactionControl()).thenReturn(true);
+ when(actualCapability.isSupportsSavepoint()).thenReturn(true);
+ when(actualCapability.getDefaultSchemaSemantics()).thenReturn(DialectSchemaSemantics.DATABASE_AS_SCHEMA);
+ when(actualCapability.getSchemaExecutionSemantics()).thenReturn(SchemaExecutionSemantics.FIXED_TO_DATABASE);
+ when(actualCapability.isSupportsExplain()).thenReturn(supportsExplain);
Map<String, Object> actual = new MCPDatabaseCapabilityResponse(actualCapability).toPayload();
assertThat(actual, is(Map.ofEntries(
Map.entry("response_mode", "detail"),
@@ -53,14 +64,20 @@ class MCPDatabaseCapabilityResponseTest {
Map.entry("databaseType", "FixtureDB"),
Map.entry("supportedObjectTypes", EnumSet.of(SupportedMCPMetadataObjectType.SCHEMA, SupportedMCPMetadataObjectType.TABLE,
SupportedMCPMetadataObjectType.VIEW, SupportedMCPMetadataObjectType.COLUMN, SupportedMCPMetadataObjectType.INDEX)),
- Map.entry("supportedStatementClasses", EnumSet.of(SupportedMCPStatement.QUERY, SupportedMCPStatement.DML, SupportedMCPStatement.DDL,
- SupportedMCPStatement.DCL, SupportedMCPStatement.TRANSACTION_CONTROL, SupportedMCPStatement.SAVEPOINT, SupportedMCPStatement.EXPLAIN_ANALYZE)),
+ Map.entry("supportedStatementClasses", supportedStatementClasses),
Map.entry("supportsTransactionControl", true),
Map.entry("supportsSavepoint", true),
- Map.entry("defaultSchemaSemantics", SchemaSemantics.DATABASE_AS_SCHEMA),
+ Map.entry("defaultSchemaSemantics", DialectSchemaSemantics.DATABASE_AS_SCHEMA),
Map.entry("schemaExecutionSemantics", SchemaExecutionSemantics.FIXED_TO_DATABASE),
Map.entry("supportsCrossSchemaSql", false),
- Map.entry("supportsExplainAnalyze", true),
- Map.entry("explainAnalyzeExecutionRisk", "EXPLAIN ANALYZE may execute the underlying SELECT on this engine; use it only when the user accepts runtime execution cost."))));
+ Map.entry("supportsExplain", supportsExplain),
+ Map.entry("explainExecutionSemantics", explainExecutionSemantics))));
+ }
+
+ private static Stream<Arguments> assertToPayloadArguments() {
+ return Stream.of(
+ Arguments.of("supported", true,
+ "database_gateway_execute_explain_query executes model-generated database-native EXPLAIN SQL for one classifier-approved SELECT. EXPLAIN ANALYZE is not supported."),
+ Arguments.of("unsupported", false, "database_gateway_execute_explain_query is not supported for this database type."));
}
}
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 853a4c3dbd3..a1755161737 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
@@ -199,7 +199,7 @@ class RuntimeDatabaseValidationServiceTest {
}
private static RuntimeDatabaseProfile createProfile() {
- return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0");
+ return new RuntimeDatabaseProfile("logic_db", "FixtureDB", "1.0", true, true);
}
private static RuntimeDatabaseConfiguration createRuntimeDatabaseConfiguration() {
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
index 51eb890c596..7b32e59de43 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
@@ -86,7 +86,7 @@ class MCPGuidancePayloadBuilderTest {
assertThat(actual.get("metadata_first_resource"), is("shardingsphere://databases"));
assertTrue(String.valueOf(actual.get("preflight_rule")).contains("database_gateway_validate_runtime_database"));
Map<?, ?> actualSqlToolSelection = castToMap(actual.get("sql_tool_selection"));
- assertThat(actualSqlToolSelection.keySet().stream().toList(), is(List.of("read_only", "side_effecting")));
+ assertThat(actualSqlToolSelection.keySet().stream().toList(), is(List.of("read_only", "explain", "side_effecting")));
assertThat(actual.get("side_effect_rule"), is("Preview before side effects and continue only when the requested side effect is still intended."));
assertThat(actual.get("completion_rule"),
is("Use completion/complete for one uncertain argument at a time; when completion reports missing context, follow meta.next_actions before guessing."));
@@ -116,6 +116,9 @@ class MCPGuidancePayloadBuilderTest {
Map<?, ?> actualValidateRuntimeDatabase = findByKey(actual, "flow_id", "validate_runtime_database");
assertTrue(((Collection<?>) actualValidateRuntimeDatabase.get("steps")).contains("call_tool database_gateway_validate_runtime_database"));
assertThat(actualValidateRuntimeDatabase.get("referenced_tools"), is(List.of("database_gateway_validate_runtime_database")));
+ Map<?, ?> actualExplainQuery = findByKey(actual, "flow_id", "explain_query");
+ assertThat(actualExplainQuery.get("referenced_tools"), is(List.of("database_gateway_execute_explain_query")));
+ assertTrue(((Collection<?>) actualExplainQuery.get("steps")).contains("call_tool database_gateway_execute_explain_query"));
Map<?, ?> actualCompleteArgument = findByKey(actual, "flow_id", "complete_uncertain_argument");
assertTrue(((Collection<?>) actualCompleteArgument.get("steps")).contains("call completion/complete for one argument"));
Map<?, ?> actualSideEffectingSql = findByKey(actual, "flow_id", "side_effecting_sql");
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningContextValidatorTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningContextValidatorTest.java
index c027fa81a5c..6b909bfa23e 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningContextValidatorTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningContextValidatorTest.java
@@ -95,7 +95,7 @@ class WorkflowPlanningContextValidatorTest {
}
private RuntimeDatabaseProfile createDatabaseMetadata() {
- return new RuntimeDatabaseProfile("logic_db", "MySQL", "8.0");
+ return new RuntimeDatabaseProfile("logic_db", "MySQL", "8.0", true, true);
}
private ShardingSphereSchema createSchemaMetadata() {
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
index 76b7df1b873..e8bcab2c904 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
@@ -366,7 +366,7 @@ class WorkflowPlanningSupportTest {
}
private RuntimeDatabaseProfile createDatabaseMetadata(final String databaseType) {
- return new RuntimeDatabaseProfile("logic_db", databaseType, "8.0");
+ return new RuntimeDatabaseProfile("logic_db", databaseType, "8.0", true, true);
}
private ShardingSphereSchema createSchemaMetadata(final String schemaName, final String tableName, final String columnName) {
diff --git a/mode/core/src/test/java/org/apache/shardingsphere/mode/metadata/persist/metadata/DatabaseMetaDataPersistFacadeTest.java b/mode/core/src/test/java/org/apache/shardingsphere/mode/metadata/persist/metadata/DatabaseMetaDataPersistFacadeTest.java
index cc01b82d503..41bbe0980c4 100644
--- a/mode/core/src/test/java/org/apache/shardingsphere/mode/metadata/persist/metadata/DatabaseMetaDataPersistFacadeTest.java
+++ b/mode/core/src/test/java/org/apache/shardingsphere/mode/metadata/persist/metadata/DatabaseMetaDataPersistFacadeTest.java
@@ -20,6 +20,7 @@ package org.apache.shardingsphere.mode.metadata.persist.metadata;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.IdentifierPatternType;
import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DefaultSchemaOption;
+import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.option.schema.DialectSchemaSemantics;
import org.apache.shardingsphere.database.connector.core.spi.DatabaseTypedSPILoader;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
import org.apache.shardingsphere.infra.config.props.ConfigurationProperties;
@@ -230,7 +231,7 @@ class DatabaseMetaDataPersistFacadeTest {
private DialectDatabaseMetaData createDialectDatabaseMetaData() {
DialectDatabaseMetaData result = mock(DialectDatabaseMetaData.class);
- when(result.getSchemaOption()).thenReturn(new DefaultSchemaOption(false, null));
+ when(result.getSchemaOption()).thenReturn(new DefaultSchemaOption(false, null, DialectSchemaSemantics.NATIVE_SCHEMA));
when(result.getIdentifierPatternType()).thenReturn(IdentifierPatternType.KEEP_ORIGIN);
return result;
}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidator.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidator.java
index 72bfd28e0ac..075c8de58ad 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidator.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidator.java
@@ -42,10 +42,17 @@ final class LLMMCPSafetyValidator {
if (MCPInteractionActionNames.GET_PROMPT.equals(actionName) && Objects.toString(arguments.get("name"), "").trim().isEmpty()) {
return Optional.of(new LLMMCPToolCallValidationFailure(MCPInteractionActionNames.PROMPT_GET_KIND, "invalid_tool_arguments", "Model returned an empty prompt name."));
}
- if ("database_gateway_execute_query".equals(actionName) && !isReadOnlyQuery(arguments)) {
+ if ("database_gateway_execute_query".equals(actionName) && isExplain(arguments)) {
+ return Optional.of(new LLMMCPToolCallValidationFailure("tool_call", "invalid_tool_arguments",
+ "Model routed EXPLAIN SQL to database_gateway_execute_query instead of database_gateway_execute_explain_query."));
+ }
+ if ("database_gateway_execute_query".equals(actionName) && !isQuery(arguments)) {
return Optional.of(new LLMMCPToolCallValidationFailure("tool_call", "unsafe_sql_attempted", "Model attempted a non-read-only SQL statement."));
}
- if ("database_gateway_execute_update".equals(actionName) && isReadOnlyQuery(arguments)) {
+ if ("database_gateway_execute_explain_query".equals(actionName) && !isQuery(arguments)) {
+ return Optional.of(new LLMMCPToolCallValidationFailure("tool_call", "unsafe_sql_attempted", "Model attempted to explain a non-read-only SQL statement."));
+ }
+ if ("database_gateway_execute_update".equals(actionName) && isQuery(arguments)) {
return Optional.of(new LLMMCPToolCallValidationFailure("tool_call", "invalid_tool_arguments", "Model routed a read-only SQL statement to the side-effecting SQL tool."));
}
if ("database_gateway_execute_update".equals(actionName) && !EXECUTION_MODE_PREVIEW.equals(Objects.toString(arguments.get("execution_mode"), ""))) {
@@ -59,16 +66,21 @@ final class LLMMCPSafetyValidator {
return Optional.empty();
}
- private boolean isReadOnlyQuery(final Map<String, Object> arguments) {
+ private boolean isQuery(final Map<String, Object> arguments) {
String sql = Objects.toString(arguments.get("sql"), "").trim();
try {
- SupportedMCPStatement statementClass = statementClassifier.classify(sql).getStatementClass();
- return SupportedMCPStatement.QUERY == statementClass || SupportedMCPStatement.EXPLAIN_ANALYZE == statementClass;
+ return SupportedMCPStatement.QUERY == statementClassifier.classify(sql).getStatementClass();
} catch (final MCPInvalidRequestException | MCPUnsupportedException | IllegalArgumentException ignored) {
return false;
}
}
+ private boolean isExplain(final Map<String, Object> arguments) {
+ String sql = Objects.toString(arguments.get("sql"), "").trim();
+ return "EXPLAIN".equalsIgnoreCase(sql) || 7 < sql.length() && sql.regionMatches(true, 0, "EXPLAIN", 0, 7)
+ && (Character.isWhitespace(sql.charAt(7)) || '(' == sql.charAt(7));
+ }
+
private boolean isSafeWorkflowExecutionMode(final Map<String, Object> arguments) {
String executionMode = Objects.toString(arguments.get("execution_mode"), "");
return EXECUTION_MODE_PREVIEW.equals(executionMode) || EXECUTION_MODE_MANUAL_ONLY.equals(executionMode);
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidatorTest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidatorTest.java
index 777e3757a21..8dce412dff1 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidatorTest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSafetyValidatorTest.java
@@ -39,12 +39,28 @@ class LLMMCPSafetyValidatorTest {
}
@Test
- void assertAllowExplainAnalyzeQuery() {
+ void assertRejectExplainFromQueryTool() {
Optional<LLMMCPToolCallValidationFailure> actual = validator.validate("database_gateway_execute_query",
- Map.of("sql", "EXPLAIN ANALYZE SELECT * FROM orders"));
+ Map.of("sql", "EXPLAIN SELECT * FROM orders"));
+ assertTrue(actual.isPresent());
+ assertThat(actual.get().failureType(), is("invalid_tool_arguments"));
+ }
+
+ @Test
+ void assertAllowExplainQuery() {
+ Optional<LLMMCPToolCallValidationFailure> actual = validator.validate("database_gateway_execute_explain_query",
+ Map.of("sql", "SELECT * FROM orders", "explain_sql", "EXPLAIN SELECT * FROM orders"));
assertFalse(actual.isPresent());
}
+ @Test
+ void assertRejectMutatingExplainQuery() {
+ Optional<LLMMCPToolCallValidationFailure> actual = validator.validate("database_gateway_execute_explain_query",
+ Map.of("sql", "UPDATE orders SET status = 'PAID'", "explain_sql", "EXPLAIN UPDATE orders SET status = 'PAID'"));
+ assertTrue(actual.isPresent());
+ assertThat(actual.get().failureType(), is("unsafe_sql_attempted"));
+ }
+
@Test
void assertRejectDataModifyingCommonTableExpressionQuery() {
Optional<LLMMCPToolCallValidationFailure> actual = validator.validate("database_gateway_execute_query",
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
index 9e2df128e63..95e16f10bd9 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
@@ -72,7 +72,8 @@ class PackagedDistributionE2ETest {
"shardingsphere-mcp-feature-sharding");
private static final List<String> CORE_TOOL_NAMES = List.of("database_gateway_search_metadata", "database_gateway_validate_runtime_database",
- "database_gateway_execute_query", "database_gateway_execute_update", "database_gateway_apply_workflow", "database_gateway_validate_workflow");
+ "database_gateway_execute_query", "database_gateway_execute_explain_query", "database_gateway_execute_update",
+ "database_gateway_apply_workflow", "database_gateway_validate_workflow");
private static final List<String> REMOVED_FEATURE_TOOL_NAMES = OfficialMCPToolNames.getAll().stream().filter(each -> !CORE_TOOL_NAMES.contains(each)).toList();
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLReadOnlySQLRuntimeE2ETest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLReadOnlySQLRuntimeE2ETest.java
index f84aed8d6d9..7b23614b63e 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLReadOnlySQLRuntimeE2ETest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLReadOnlySQLRuntimeE2ETest.java
@@ -70,13 +70,14 @@ class ProductionMySQLReadOnlySQLRuntimeE2ETest extends AbstractProductionMySQLRu
@ParameterizedTest(name = "{0}")
@MethodSource("semanticPrimaryTransport")
- void assertExecuteExplainAnalyzeSelectWithActualMySQLBackend(final String name, final RuntimeTransport transport) throws IOException, InterruptedException {
+ void assertExecuteExplainSelectWithActualMySQLBackend(final String name, final RuntimeTransport transport) throws IOException, InterruptedException {
useTransport(transport);
try (MCPInteractionClient interactionClient = createOpenedInteractionClient()) {
- Map<String, Object> actual = interactionClient.call("database_gateway_execute_query",
- Map.of("database", LOGICAL_DATABASE_NAME, "schema", LOGICAL_DATABASE_NAME, "sql", "EXPLAIN ANALYZE SELECT * FROM orders WHERE order_id = 1", "max_rows", 10));
+ Map<String, Object> actual = interactionClient.call("database_gateway_execute_explain_query",
+ Map.of("database", LOGICAL_DATABASE_NAME, "schema", LOGICAL_DATABASE_NAME, "sql", "SELECT * FROM orders WHERE order_id = 1",
+ "explain_sql", "EXPLAIN SELECT * FROM orders WHERE order_id = 1", "max_rows", 10));
assertThat(String.valueOf(actual.get("result_kind")), is("result_set"));
- assertThat(String.valueOf(actual.get("statement_type")), is("EXPLAIN ANALYZE"));
+ assertThat(String.valueOf(actual.get("statement_type")), is("EXPLAIN"));
assertFalse(((List<?>) actual.get("rows")).isEmpty());
}
}
@@ -105,15 +106,13 @@ class ProductionMySQLReadOnlySQLRuntimeE2ETest extends AbstractProductionMySQLRu
@ParameterizedTest(name = "{0}")
@MethodSource("semanticPrimaryTransport")
- void assertRejectExplainAnalyzeUpdateFromReadOnlyToolWithActualMySQLBackend(final String name, final RuntimeTransport transport) throws IOException, InterruptedException {
+ void assertRejectExplainUpdateWithActualMySQLBackend(final String name, final RuntimeTransport transport) throws IOException, InterruptedException {
useTransport(transport);
try (MCPInteractionClient interactionClient = createOpenedInteractionClient()) {
- Map<String, Object> actual = interactionClient.call("database_gateway_execute_query",
- Map.of("database", LOGICAL_DATABASE_NAME, "schema", LOGICAL_DATABASE_NAME, "sql",
- "EXPLAIN ANALYZE UPDATE orders SET status = 'DONE' WHERE order_id = 1"));
- assertRecoveryResponse(actual,
- "database_gateway_execute_query only supports classifier-approved QUERY and EXPLAIN_ANALYZE statements. Use database_gateway_execute_update for side-effecting SQL.",
- "unsafe_sql_attempted");
+ Map<String, Object> actual = interactionClient.call("database_gateway_execute_explain_query",
+ Map.of("database", LOGICAL_DATABASE_NAME, "schema", LOGICAL_DATABASE_NAME, "sql", "UPDATE orders SET status = 'DONE' WHERE order_id = 1",
+ "explain_sql", "EXPLAIN UPDATE orders SET status = 'DONE' WHERE order_id = 1"));
+ assertRecoveryResponse(actual, "database_gateway_execute_explain_query only supports QUERY statements as the explained SQL.");
}
}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
index 2be8a63263b..4448a142e2c 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
@@ -52,7 +52,7 @@ class ProductionMySQLRuntimeE2ETest extends AbstractProductionMySQLRuntimeE2ETes
try (MCPInteractionClient interactionClient = createOpenedInteractionClient()) {
Map<String, Object> actual = interactionClient.readResource("shardingsphere://databases/logic_db/capabilities");
assertThat(String.valueOf(actual.get("databaseType")), is("MySQL"));
- assertThat(String.valueOf(actual.get("supportsExplainAnalyze")), is("true"));
+ assertThat(String.valueOf(actual.get("supportsExplain")), is("true"));
}
}
diff --git a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
index b3c302360a9..646c7b1f123 100644
--- a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
+++ b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
@@ -33,7 +33,8 @@ resources:
name: logical-databases
_meta:
org.apache.shardingsphere/resource-kind: list
- org.apache.shardingsphere/related-tools: [database_gateway_search_metadata, database_gateway_execute_query]
+ org.apache.shardingsphere/related-tools: [database_gateway_search_metadata, database_gateway_execute_query,
+ database_gateway_execute_explain_query]
- uri: shardingsphere://features/encrypt/algorithms
name: encrypt-algorithms
_meta:
@@ -102,7 +103,8 @@ resourceTemplates:
name: database-capabilities
_meta:
org.apache.shardingsphere/resource-kind: detail
- org.apache.shardingsphere/related-tools: [database_gateway_execute_query, database_gateway_execute_update]
+ org.apache.shardingsphere/related-tools: [database_gateway_execute_query, database_gateway_execute_explain_query,
+ database_gateway_execute_update]
org.apache.shardingsphere/uri-variables:
- {name: database, required: true}
- uriTemplate: shardingsphere://databases/{database}/storage-units
@@ -574,6 +576,9 @@ tools:
- name: database_gateway_execute_query
annotations: {title: Execute Query SQL, readOnlyHint: true, destructiveHint: false,
idempotentHint: true, openWorldHint: true}
+- name: database_gateway_execute_explain_query
+ annotations: {title: Execute Explain Query SQL, readOnlyHint: true, destructiveHint: false,
+ idempotentHint: true, openWorldHint: true}
- name: database_gateway_execute_update
annotations: {title: Preview or Execute Side-Effecting SQL, readOnlyHint: false,
destructiveHint: true, idempotentHint: false, openWorldHint: true}
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 ca27d47b403..bdf630f7677 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
@@ -34,6 +34,9 @@ model_first_summary:
- {intent: read_only_sql, first_action: 'read_resource shardingsphere://databases/{database}/capabilities',
next_step: call_tool database_gateway_execute_query, stop_rule: Stop after reporting
the result rows.}
+ - {intent: explain_query, first_action: 'read_resource shardingsphere://databases/{database}/capabilities',
+ next_step: 'generate database-native EXPLAIN SQL, then call_tool database_gateway_execute_explain_query',
+ stop_rule: Stop after reporting the execution plan rows.}
- {intent: rule_workflow, first_action: call_tool matching database_gateway_plan_*
workflow tool without plan_id for a new plan, next_step: call_tool database_gateway_apply_workflow
execution_mode=preview with the returned plan_id, stop_rule: Use workflow apply
@@ -57,8 +60,11 @@ model_first_summary:
only a configured runtime database name., secret_rule: Connection details stay
in administrator runtime configuration and are not tool arguments.}
sql_tool_selection:
- read_only: {tool: database_gateway_execute_query, statement_rule: Use for one
- SELECT or EXPLAIN ANALYZE statement.}
+ read_only: {tool: database_gateway_execute_query, statement_rule: Use for one SELECT
+ statement.}
+ explain: {tool: database_gateway_execute_explain_query, statement_rule: Use for
+ an execution plan of one SELECT statement., candidate_rule: Generate database-native
+ EXPLAIN SQL in explain_sql and never use EXPLAIN ANALYZE.}
side_effecting:
tool: database_gateway_execute_update
first_mode: preview
@@ -99,8 +105,10 @@ model_contract:
preflight_rule: Use database_gateway_validate_runtime_database with a configured
database name before onboarding or troubleshooting runtime connectivity.
sql_tool_selection: {read_only: Use database_gateway_execute_query for one classifier-approved
- SELECT or EXPLAIN ANALYZE statement., side_effecting: Use database_gateway_execute_update
- with execution_mode=preview before execution.}
+ SELECT statement., explain: Use database_gateway_execute_explain_query with
+ the original SELECT and a model-generated database-native EXPLAIN SQL; do not
+ use EXPLAIN ANALYZE., side_effecting: Use database_gateway_execute_update with
+ execution_mode=preview before execution.}
workflow_session_rule: Reuse the current-session plan_id returned by a planning
tool; re-plan when the plan is unavailable.
side_effect_rule: Preview before side effects and continue only when the requested
@@ -124,6 +132,7 @@ surface_summary:
metadata_search_tool: database_gateway_search_metadata
preflight_validation_tool: database_gateway_validate_runtime_database
read_only_sql_tool: database_gateway_execute_query
+ explain_sql_tool: database_gateway_execute_explain_query
side_effect_sql_tool: database_gateway_execute_update
workflow_tools: [database_gateway_apply_workflow, database_gateway_validate_workflow]
side_effect_rule: Preview first and continue only when the requested side effect
@@ -177,10 +186,16 @@ common_flows:
- flow_id: read_only_sql
steps: ['read_resource shardingsphere://databases/{database}/capabilities', call_tool
database_gateway_execute_query]
- stop_condition: Use one SELECT or EXPLAIN ANALYZE statement and stop after the result
- is reported.
+ stop_condition: Use one SELECT statement and stop after the result is reported.
referenced_tools: [database_gateway_execute_query]
referenced_resources: ['shardingsphere://databases/{database}/capabilities']
+- flow_id: explain_query
+ steps: ['read_resource shardingsphere://databases/{database}/capabilities', generate
+ database-native EXPLAIN SQL without ANALYZE, call_tool database_gateway_execute_explain_query]
+ stop_condition: Use the original SELECT as sql, the generated EXPLAIN as explain_sql,
+ and stop after the plan rows are reported.
+ referenced_tools: [database_gateway_execute_explain_query]
+ referenced_resources: ['shardingsphere://databases/{database}/capabilities']
- flow_id: side_effecting_sql
steps: [call_tool database_gateway_execute_update execution_mode=preview, call_tool
database_gateway_execute_update execution_mode=execute]
diff --git a/test/e2e/mcp/src/test/resources/llm/evaluation/mcp-builder-evaluation.xml b/test/e2e/mcp/src/test/resources/llm/evaluation/mcp-builder-evaluation.xml
index 97ef0c2475e..28b21f507a2 100644
--- a/test/e2e/mcp/src/test/resources/llm/evaluation/mcp-builder-evaluation.xml
+++ b/test/e2e/mcp/src/test/resources/llm/evaluation/mcp-builder-evaluation.xml
@@ -78,15 +78,16 @@
An operator asks for current order counts and an execution explanation on a configured runtime database. Explain how the model should
answer using MCP while preserving a read-only SQL boundary.
</question>
- <answer>metadata_resources|database_gateway_execute_query|no_update_or_apply</answer>
+ <answer>metadata|database_gateway_execute_query|database_gateway_execute_explain_query|read_only</answer>
<expected_answer>
- The model should confirm the target database and schema through metadata resources, then call database_gateway_execute_query for SELECT
- or supported EXPLAIN style statements with a bounded max_rows value. It should report the structured result evidence instead of guessing
- counts, and it must not call database_gateway_execute_update, database_gateway_apply_workflow, or any approval-gated side-effect path for
- this read-only request.
+ The model should confirm the target database and schema through metadata resources, call database_gateway_execute_query for the SELECT
+ count, then generate database-native EXPLAIN SQL without ANALYZE and call database_gateway_execute_explain_query with the original SELECT
+ as sql and the generated statement as explain_sql. It should report both structured results instead of guessing, and it must not call
+ database_gateway_execute_update, database_gateway_apply_workflow, or any approval-gated side-effect path for this read-only request.
</expected_answer>
<verification>
- <step>Confirm the answer uses database_gateway_execute_query for SELECT or EXPLAIN style SQL.</step>
+ <step>Confirm the answer uses database_gateway_execute_query only for SELECT.</step>
+ <step>Confirm the answer uses database_gateway_execute_explain_query with separate sql and explain_sql arguments and excludes EXPLAIN ANALYZE.</step>
<step>Confirm the answer requires metadata-backed database and schema selection before the query.</step>
<step>Confirm the answer forbids update and workflow apply tools for this request.</step>
</verification>