🐛 fix(mysql): 兼容 MyCAT 场景下数据库列表解析逻辑

- 扩展数据库名字段识别,兼容 SCHEMA、database_name 等返回列名
- 按驱动返回列顺序兜底提取单列结果,避免非标准列名导致误判为空
- 补充 MyCAT 风格回归测试,覆盖 SHOW DATABASES 与当前库回退逻辑
Close #552
This commit is contained in:
Syngnat
2026-06-14 16:36:42 +08:00
parent 70b469d349
commit 3da3a3fb13
13 changed files with 111 additions and 43 deletions

View File

@@ -163,7 +163,8 @@ func (s *Service) stopMCPHTTPServer(ctx context.Context, message string) (ai.MCP
}
// Shutdown 释放 AI Service 中的运行时资源。
func (s *Service) Shutdown(ctx context.Context) {
func (s *Service) Shutdown() {
ctx := context.Background()
_, _ = s.stopMCPHTTPServer(ctx, "应用关闭GoNavi MCP HTTP 服务已停止")
}

View File

@@ -57,7 +57,7 @@ func TestMCPHTTPServerLifecycleFromAIService(t *testing.T) {
service := NewServiceWithSecretStore(secretstore.NewUnavailableStore("test"))
InitializeLifecycle(service, context.Background())
t.Cleanup(func() {
service.Shutdown(context.Background())
service.Shutdown()
})
initial := service.AIGetMCPHTTPServerStatus()
@@ -123,7 +123,7 @@ func TestMCPHTTPServerStartUsesCustomAddrAndToken(t *testing.T) {
service := NewServiceWithSecretStore(secretstore.NewUnavailableStore("test"))
InitializeLifecycle(service, context.Background())
t.Cleanup(func() {
service.Shutdown(context.Background())
service.Shutdown()
})
started, err := service.AIStartMCPHTTPServer(ai.MCPHTTPServerOptions{

View File

@@ -178,8 +178,8 @@ func (a *App) LogWindowDiagnostic(stage string, payload string) {
logger.Warnf("窗口诊断stage=%s payload=%s", stage, payload)
}
// Shutdown is called when the app terminates
func (a *App) Shutdown(ctx context.Context) {
// Shutdown is called when the app terminates.
func (a *App) Shutdown() {
logger.Infof("应用开始关闭,准备释放资源")
a.rollbackPendingSQLTransactionsOnShutdown()
a.mu.Lock()

View File

@@ -594,6 +594,18 @@ var mysqlDatabaseQueries = []string{
"SELECT DATABASE() AS `Database`",
}
var mysqlDatabaseNameKeys = []string{
"Database",
"database",
"DATABASE",
"database_name",
"DATABASE_NAME",
"schema",
"SCHEMA",
"schema_name",
"SCHEMA_NAME",
}
func collectMySQLDatabaseNames(queryFn func(string) ([]map[string]interface{}, []string, error)) ([]string, error) {
if queryFn == nil {
return nil, fmt.Errorf("查询函数为空")
@@ -603,34 +615,59 @@ func collectMySQLDatabaseNames(queryFn func(string) ([]map[string]interface{}, [
seen := make(map[string]struct{}, 8)
var lastErr error
appendNames := func(rows []map[string]interface{}) {
for _, row := range rows {
for _, key := range []string{"Database", "database"} {
val, ok := row[key]
if !ok || val == nil {
continue
}
name := strings.TrimSpace(fmt.Sprintf("%v", val))
if name == "" || strings.EqualFold(name, "<nil>") {
continue
}
if _, exists := seen[name]; exists {
continue
}
seen[name] = struct{}{}
names = append(names, name)
break
normalizeName := func(val interface{}) string {
if val == nil {
return ""
}
name := strings.TrimSpace(fmt.Sprintf("%v", val))
if name == "" || strings.EqualFold(name, "<nil>") || strings.EqualFold(name, "null") {
return ""
}
return name
}
extractName := func(row map[string]interface{}, columns []string) string {
for _, key := range mysqlDatabaseNameKeys {
if name := normalizeName(row[key]); name != "" {
return name
}
}
for _, column := range columns {
if name := normalizeName(row[column]); name != "" {
return name
}
}
if len(row) == 1 {
for _, val := range row {
if name := normalizeName(val); name != "" {
return name
}
}
}
return ""
}
appendNames := func(rows []map[string]interface{}, columns []string) {
for _, row := range rows {
name := extractName(row, columns)
if name == "" {
continue
}
if _, exists := seen[name]; exists {
continue
}
seen[name] = struct{}{}
names = append(names, name)
}
}
for _, sqlText := range mysqlDatabaseQueries {
rows, _, err := queryFn(sqlText)
rows, columns, err := queryFn(sqlText)
if err != nil {
lastErr = err
continue
}
appendNames(rows)
appendNames(rows, columns)
if len(names) > 0 {
return names, nil
}

View File

@@ -15,8 +15,8 @@ func TestCollectMySQLDatabaseNames_FallsBackToCurrentDatabase(t *testing.T) {
return nil, nil, errors.New("Error 1227 (42000): Access denied; you need (at least one of) the SHOW DATABASES privilege(s) for this operation")
case mysqlDatabaseQueries[1]:
return []map[string]interface{}{
{"Database": "biz_app"},
}, nil, nil
{"database_name": "biz_app"},
}, []string{"database_name"}, nil
default:
return nil, nil, errors.New("unexpected query")
}
@@ -31,6 +31,33 @@ func TestCollectMySQLDatabaseNames_FallsBackToCurrentDatabase(t *testing.T) {
}
}
func TestCollectMySQLDatabaseNames_AcceptsMyCATStyleSchemaColumn(t *testing.T) {
t.Parallel()
got, err := collectMySQLDatabaseNames(func(query string) ([]map[string]interface{}, []string, error) {
switch query {
case mysqlDatabaseQueries[0]:
return []map[string]interface{}{
{"SCHEMA": "analytics"},
}, []string{"SCHEMA"}, nil
case mysqlDatabaseQueries[1]:
return []map[string]interface{}{
{"Database": "should_not_be_used"},
}, []string{"Database"}, nil
default:
return nil, nil, errors.New("unexpected query")
}
})
if err != nil {
t.Fatalf("collectMySQLDatabaseNames 返回错误: %v", err)
}
want := []string{"analytics"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected database names, got=%v want=%v", got, want)
}
}
func TestCollectMySQLDatabaseNames_PrefersShowDatabasesWhenAvailable(t *testing.T) {
t.Parallel()

View File

@@ -47,10 +47,7 @@ func (b *AppBackend) Close(ctx context.Context) error {
if b == nil || b.app == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
b.app.Shutdown(ctx)
b.app.Shutdown()
return nil
}