This is an automated email from the ASF dual-hosted git repository.
terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new 443d736dd59 Reuse database connector metadata in MCP dialect (#39041)
443d736dd59 is described below
commit 443d736dd5945cf7c0474034569fcc9a75b5e965
Author: Liang Zhang <[email protected]>
AuthorDate: Wed Jul 8 00:50:25 2026 +0800
Reuse database connector metadata in MCP dialect (#39041)
Reuse database connector SPI metadata for MCP identifier quote, lower-case
identifier handling, and equivalent system schema lookup while preserving
MCP
capability options as connector-absent and non-equivalent fallback behavior.
Cover precedence and compatibility paths with scoped SPI mocks.
---
.../database/capability/MCPDatabaseDialect.java | 80 +++++++++++--
.../capability/MCPDatabaseDialectTest.java | 131 +++++++++++++++++++++
2 files changed, 201 insertions(+), 10 deletions(-)
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 34d28c641a3..3357f634b33 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
@@ -18,9 +18,15 @@
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.system.SystemDatabase;
import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicy;
import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicyFactory;
+import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicySet;
import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierScope;
+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;
import java.util.Collection;
@@ -34,10 +40,17 @@ public final class MCPDatabaseDialect {
private final String databaseType;
+ private final Optional<DialectDatabaseMetaData> dialectDatabaseMetaData;
+
+ private final Optional<SystemDatabase> systemDatabase;
+
private final Optional<MCPDatabaseCapabilityOption> option;
- private MCPDatabaseDialect(final String databaseType, final
Optional<MCPDatabaseCapabilityOption> option) {
+ private MCPDatabaseDialect(final String databaseType, final
Optional<DialectDatabaseMetaData> dialectDatabaseMetaData, final
Optional<SystemDatabase> systemDatabase,
+ final Optional<MCPDatabaseCapabilityOption>
option) {
this.databaseType = databaseType;
+ this.dialectDatabaseMetaData = dialectDatabaseMetaData;
+ this.systemDatabase = systemDatabase;
this.option = option;
}
@@ -49,10 +62,17 @@ public final class MCPDatabaseDialect {
*/
public static MCPDatabaseDialect of(final String databaseType) {
String actualDatabaseType = trimToEmpty(databaseType);
+ Optional<DatabaseType> databaseTypeFromSPI =
actualDatabaseType.isEmpty()
+ ? Optional.empty()
+ : TypedSPILoader.findService(DatabaseType.class,
actualDatabaseType);
Optional<MCPDatabaseCapabilityOption> option =
actualDatabaseType.isEmpty()
? Optional.empty()
:
TypedSPILoader.findService(MCPDatabaseCapabilityOption.class,
actualDatabaseType);
- return new MCPDatabaseDialect(actualDatabaseType, option);
+ return new MCPDatabaseDialect(actualDatabaseType,
databaseTypeFromSPI.flatMap(MCPDatabaseDialect::findDialectDatabaseMetaData),
databaseTypeFromSPI.map(SystemDatabase::new), option);
+ }
+
+ private static Optional<DialectDatabaseMetaData>
findDialectDatabaseMetaData(final DatabaseType databaseType) {
+ return
DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class, databaseType);
}
/**
@@ -61,7 +81,12 @@ public final class MCPDatabaseDialect {
* @return identifier quote character
*/
public QuoteCharacter getIdentifierQuoteCharacter() {
- return
option.map(MCPDatabaseCapabilityOption::getIdentifierQuoteCharacter).orElseGet(this::getFallbackIdentifierQuoteCharacter);
+ return
dialectDatabaseMetaData.map(DialectDatabaseMetaData::getQuoteCharacter)
+ .orElseGet(() ->
option.map(MCPDatabaseCapabilityOption::getIdentifierQuoteCharacter).orElseGet(this::getFallbackIdentifierQuoteCharacter));
+ }
+
+ private QuoteCharacter getFallbackIdentifierQuoteCharacter() {
+ return databaseType.isEmpty() ? QuoteCharacter.BACK_QUOTE :
QuoteCharacter.QUOTE;
}
/**
@@ -71,8 +96,23 @@ public final class MCPDatabaseDialect {
* @return identifier case policy
*/
public IdentifierCasePolicy getIdentifierCasePolicy(final IdentifierScope
identifierScope) {
- return
option.map(MCPDatabaseCapabilityOption::getIdentifierCasePolicySet)
-
.orElseGet(IdentifierCasePolicyFactory::newSensitivePolicySet).getPolicy(identifierScope);
+ return getIdentifierCasePolicySet().getPolicy(identifierScope);
+ }
+
+ private IdentifierCasePolicySet getIdentifierCasePolicySet() {
+ return findLowerCaseDialectIdentifierCasePolicySet()
+ .orElseGet(() ->
option.map(MCPDatabaseCapabilityOption::getIdentifierCasePolicySet).orElseGet(IdentifierCasePolicyFactory::newSensitivePolicySet));
+ }
+
+ private Optional<IdentifierCasePolicySet>
findLowerCaseDialectIdentifierCasePolicySet() {
+ return
dialectDatabaseMetaData.flatMap(MCPDatabaseDialect::createLowerCaseDialectIdentifierCasePolicySet);
+ }
+
+ private static Optional<IdentifierCasePolicySet>
createLowerCaseDialectIdentifierCasePolicySet(final DialectDatabaseMetaData
dialectDatabaseMetaData) {
+ IdentifierPatternType identifierPatternType =
dialectDatabaseMetaData.getIdentifierPatternType();
+ return IdentifierPatternType.LOWER_CASE == identifierPatternType
+ ?
Optional.of(IdentifierCasePolicyFactory.newDialectDefaultPolicySet(identifierPatternType,
dialectDatabaseMetaData.isCaseSensitive()))
+ : Optional.empty();
}
/**
@@ -90,7 +130,11 @@ public final class MCPDatabaseDialect {
* @return whether unquoted identifiers are folded
*/
public boolean isUnquotedIdentifierCaseFolded() {
- return
option.map(MCPDatabaseCapabilityOption::isUnquotedIdentifierCaseFolded).orElse(false);
+ return isLowerCaseDialectIdentifierPattern() ||
option.map(MCPDatabaseCapabilityOption::isUnquotedIdentifierCaseFolded).orElse(false);
+ }
+
+ private boolean isLowerCaseDialectIdentifierPattern() {
+ return dialectDatabaseMetaData.map(each ->
IdentifierPatternType.LOWER_CASE ==
each.getIdentifierPatternType()).orElse(false);
}
/**
@@ -122,8 +166,7 @@ public final class MCPDatabaseDialect {
if (actualSchemaName.isEmpty()) {
return false;
}
- Collection<String> systemSchemas =
option.map(MCPDatabaseCapabilityOption::getSystemSchemas).orElseGet(List::of);
- return containsSystemSchema(systemSchemas, actualSchemaName);
+ return containsSystemSchema(getSystemSchemas(), actualSchemaName);
}
/**
@@ -138,8 +181,25 @@ public final class MCPDatabaseDialect {
return isSystemSchema(schemaName) ||
SchemaSemantics.DATABASE_AS_SCHEMA == defaultSchemaSemantics &&
isSystemSchema(catalogName);
}
- private QuoteCharacter getFallbackIdentifierQuoteCharacter() {
- return databaseType.isEmpty() ? QuoteCharacter.BACK_QUOTE :
QuoteCharacter.QUOTE;
+ private Collection<String> getSystemSchemas() {
+ Collection<String> optionSystemSchemas =
option.map(MCPDatabaseCapabilityOption::getSystemSchemas).orElseGet(List::of);
+ return findDialectSystemSchemas().filter(each ->
optionSystemSchemas.isEmpty() || containsSameSystemSchemas(each,
optionSystemSchemas)).orElse(optionSystemSchemas);
+ }
+
+ private Optional<Collection<String>> findDialectSystemSchemas() {
+ return
systemDatabase.map(SystemDatabase::getSystemSchemas).filter(each ->
!each.isEmpty());
+ }
+
+ private static boolean containsSameSystemSchemas(final Collection<String>
systemSchemas, final Collection<String> expectedSystemSchemas) {
+ if (systemSchemas.size() != expectedSystemSchemas.size()) {
+ return false;
+ }
+ for (String each : systemSchemas) {
+ if (!containsSystemSchema(expectedSystemSchemas, each)) {
+ return false;
+ }
+ }
+ return true;
}
private static boolean containsSystemSchema(final Collection<String>
systemSchemas, final String schemaName) {
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 4fa2e97136c..165d56c04cf 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
@@ -18,19 +18,32 @@
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.system.DialectSystemDatabase;
+import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicyFactory;
import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierCasePolicy;
import
org.apache.shardingsphere.database.connector.core.metadata.identifier.IdentifierScope;
+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;
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.MockedStatic;
+import java.util.List;
+import java.util.Optional;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
class MCPDatabaseDialectTest {
@@ -47,12 +60,42 @@ class MCPDatabaseDialectTest {
assertThat(actual, is(expected));
}
+ @Test
+ void assertGetIdentifierQuoteCharacterFromDialectDatabaseMetaData() {
+ try (
+ MockedStatic<TypedSPILoader> typedSPILoader =
mockStatic(TypedSPILoader.class);
+ MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader =
mockStatic(DatabaseTypedSPILoader.class)) {
+ DatabaseType databaseType = mockDatabaseType("FixtureWithOption",
typedSPILoader);
+ DialectDatabaseMetaData dialectDatabaseMetaData =
mockDialectDatabaseMetaData(databaseType, databaseTypedSPILoader);
+
when(dialectDatabaseMetaData.getQuoteCharacter()).thenReturn(QuoteCharacter.BRACKETS);
+ MCPDatabaseCapabilityOption option =
mockMCPDatabaseCapabilityOption("FixtureWithOption", typedSPILoader);
+
when(option.getIdentifierQuoteCharacter()).thenReturn(QuoteCharacter.BACK_QUOTE);
+ QuoteCharacter actual =
MCPDatabaseDialect.of("FixtureWithOption").getIdentifierQuoteCharacter();
+ assertThat(actual, is(QuoteCharacter.BRACKETS));
+ }
+ }
+
@Test
void assertGetIdentifierCasePolicyWithCaseInsensitiveIdentifier() {
IdentifierCasePolicy actual =
MCPDatabaseDialect.of("MySQL").getIdentifierCasePolicy(IdentifierScope.TABLE);
assertTrue(actual.matches("phone", "Phone", QuoteCharacter.QUOTE));
}
+ @Test
+ void assertGetIdentifierCasePolicyFromLowerCaseDialectDatabaseMetaData() {
+ try (
+ MockedStatic<TypedSPILoader> typedSPILoader =
mockStatic(TypedSPILoader.class);
+ MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader =
mockStatic(DatabaseTypedSPILoader.class)) {
+ DatabaseType databaseType = mockDatabaseType("FixtureWithOption",
typedSPILoader);
+ DialectDatabaseMetaData dialectDatabaseMetaData =
mockDialectDatabaseMetaData(databaseType, databaseTypedSPILoader);
+
when(dialectDatabaseMetaData.getIdentifierPatternType()).thenReturn(IdentifierPatternType.LOWER_CASE);
+ MCPDatabaseCapabilityOption option =
mockMCPDatabaseCapabilityOption("FixtureWithOption", typedSPILoader);
+
when(option.getIdentifierCasePolicySet()).thenReturn(IdentifierCasePolicyFactory.newSensitivePolicySet());
+ IdentifierCasePolicy actual =
MCPDatabaseDialect.of("FixtureWithOption").getIdentifierCasePolicy(IdentifierScope.TABLE);
+ assertTrue(actual.matches("fixture", "Fixture",
QuoteCharacter.NONE));
+ }
+ }
+
@ParameterizedTest(name = "{0}")
@MethodSource("getDefaultSchemaSemanticsArguments")
void assertGetDefaultSchemaSemantics(final String name, final String
databaseType, final SchemaSemantics expected) {
@@ -67,6 +110,19 @@ class MCPDatabaseDialectTest {
assertThat(actual, is(expected));
}
+ @Test
+ void assertIsUnquotedIdentifierCaseFoldedFromDialectDatabaseMetaData() {
+ 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.getIdentifierPatternType()).thenReturn(IdentifierPatternType.LOWER_CASE);
+ boolean actual =
MCPDatabaseDialect.of("Fixture").isUnquotedIdentifierCaseFolded();
+ assertTrue(actual);
+ }
+ }
+
@ParameterizedTest(name = "{0}")
@MethodSource("getSequenceQueryArguments")
void assertGetSequenceQuery(final String name, final String databaseType,
final String expected) {
@@ -93,6 +149,52 @@ class MCPDatabaseDialectTest {
assertTrue(actual);
}
+ @Test
+ void assertIsSystemSchemaFromDialectSystemDatabase() {
+ try (
+ MockedStatic<TypedSPILoader> typedSPILoader =
mockStatic(TypedSPILoader.class);
+ MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader =
mockStatic(DatabaseTypedSPILoader.class)) {
+ DatabaseType databaseType = mockDatabaseType("Fixture",
typedSPILoader);
+ mockDialectDatabaseMetaDataAbsent(databaseType,
databaseTypedSPILoader);
+ DialectSystemDatabase dialectSystemDatabase =
mockDialectSystemDatabase(databaseType, databaseTypedSPILoader);
+
when(dialectSystemDatabase.getSystemSchemas()).thenReturn(List.of("fixture_system"));
+ boolean actual =
MCPDatabaseDialect.of("Fixture").isSystemSchema("fixture_system");
+ assertTrue(actual);
+ }
+ }
+
+ @Test
+ void assertIsSystemSchemaWithOptionSystemSchema() {
+ try (
+ MockedStatic<TypedSPILoader> typedSPILoader =
mockStatic(TypedSPILoader.class);
+ MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader =
mockStatic(DatabaseTypedSPILoader.class)) {
+ DatabaseType databaseType = mockDatabaseType("FixtureWithOption",
typedSPILoader);
+ mockDialectDatabaseMetaDataAbsent(databaseType,
databaseTypedSPILoader);
+ DialectSystemDatabase dialectSystemDatabase =
mockDialectSystemDatabase(databaseType, databaseTypedSPILoader);
+
when(dialectSystemDatabase.getSystemSchemas()).thenReturn(List.of("dialect_system"));
+ MCPDatabaseCapabilityOption option =
mockMCPDatabaseCapabilityOption("FixtureWithOption", typedSPILoader);
+
when(option.getSystemSchemas()).thenReturn(List.of("option_system"));
+ boolean actual =
MCPDatabaseDialect.of("FixtureWithOption").isSystemSchema("option_system");
+ assertTrue(actual);
+ }
+ }
+
+ @Test
+ void assertIsSystemSchemaIgnoresDifferentDialectSystemSchema() {
+ try (
+ MockedStatic<TypedSPILoader> typedSPILoader =
mockStatic(TypedSPILoader.class);
+ MockedStatic<DatabaseTypedSPILoader> databaseTypedSPILoader =
mockStatic(DatabaseTypedSPILoader.class)) {
+ DatabaseType databaseType = mockDatabaseType("FixtureWithOption",
typedSPILoader);
+ mockDialectDatabaseMetaDataAbsent(databaseType,
databaseTypedSPILoader);
+ DialectSystemDatabase dialectSystemDatabase =
mockDialectSystemDatabase(databaseType, databaseTypedSPILoader);
+
when(dialectSystemDatabase.getSystemSchemas()).thenReturn(List.of("dialect_system"));
+ MCPDatabaseCapabilityOption option =
mockMCPDatabaseCapabilityOption("FixtureWithOption", typedSPILoader);
+
when(option.getSystemSchemas()).thenReturn(List.of("option_system"));
+ boolean actual =
MCPDatabaseDialect.of("FixtureWithOption").isSystemSchema("dialect_system");
+ assertFalse(actual);
+ }
+ }
+
@Test
void assertIsSystemSchemaWithUserSchema() {
boolean actual =
MCPDatabaseDialect.of("MySQL").isSystemSchema("orders");
@@ -128,6 +230,35 @@ class MCPDatabaseDialectTest {
Arguments.of("unknown", "FixtureDB", false));
}
+ private static DatabaseType mockDatabaseType(final String databaseType,
final MockedStatic<TypedSPILoader> typedSPILoader) {
+ DatabaseType result = mock(DatabaseType.class);
+ typedSPILoader.when(() ->
TypedSPILoader.findService(DatabaseType.class,
databaseType)).thenReturn(Optional.of(result));
+ typedSPILoader.when(() ->
TypedSPILoader.findService(MCPDatabaseCapabilityOption.class,
databaseType)).thenReturn(Optional.empty());
+ return result;
+ }
+
+ private static DialectDatabaseMetaData mockDialectDatabaseMetaData(final
DatabaseType databaseType, final MockedStatic<DatabaseTypedSPILoader>
databaseTypedSPILoader) {
+ DialectDatabaseMetaData result = mock(DialectDatabaseMetaData.class);
+ databaseTypedSPILoader.when(() ->
DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class,
databaseType)).thenReturn(Optional.of(result));
+ return result;
+ }
+
+ private static void mockDialectDatabaseMetaDataAbsent(final DatabaseType
databaseType, final MockedStatic<DatabaseTypedSPILoader>
databaseTypedSPILoader) {
+ databaseTypedSPILoader.when(() ->
DatabaseTypedSPILoader.findService(DialectDatabaseMetaData.class,
databaseType)).thenReturn(Optional.empty());
+ }
+
+ private static MCPDatabaseCapabilityOption
mockMCPDatabaseCapabilityOption(final String databaseType, final
MockedStatic<TypedSPILoader> typedSPILoader) {
+ MCPDatabaseCapabilityOption result =
mock(MCPDatabaseCapabilityOption.class);
+ typedSPILoader.when(() ->
TypedSPILoader.findService(MCPDatabaseCapabilityOption.class,
databaseType)).thenReturn(Optional.of(result));
+ return result;
+ }
+
+ private static DialectSystemDatabase mockDialectSystemDatabase(final
DatabaseType databaseType, final MockedStatic<DatabaseTypedSPILoader>
databaseTypedSPILoader) {
+ DialectSystemDatabase result = mock(DialectSystemDatabase.class);
+ databaseTypedSPILoader.when(() ->
DatabaseTypedSPILoader.findService(DialectSystemDatabase.class,
databaseType)).thenReturn(Optional.of(result));
+ return result;
+ }
+
private static Stream<Arguments> getDefaultSchemaSemanticsArguments() {
return Stream.of(
Arguments.of("mysql", "MySQL",
SchemaSemantics.DATABASE_AS_SCHEMA),