🐛 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

@@ -1,7 +1,7 @@
// @ts-nocheck
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {ai} from '../models';
import {context} from '../models';
export function AICallMCPTool(arg1:string,arg2:string):Promise<ai.MCPToolCallResult>;
@@ -79,4 +79,4 @@ export function AITestMCPServer(arg1:ai.MCPServerConfig):Promise<Record<string,
export function AITestProvider(arg1:ai.ProviderConfig):Promise<Record<string, any>>;
export function Shutdown(arg1:context.Context):Promise<void>;
export function Shutdown():Promise<void>;

View File

@@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
@@ -154,6 +154,6 @@ export function AITestProvider(arg1) {
return window['go']['aiservice']['Service']['AITestProvider'](arg1);
}
export function Shutdown(arg1) {
return window['go']['aiservice']['Service']['Shutdown'](arg1);
export function Shutdown() {
return window['go']['aiservice']['Service']['Shutdown']();
}

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {connection} from '../models';
@@ -5,7 +6,6 @@ import {sync} from '../models';
import {app} from '../models';
import {jvm} from '../models';
import {redis} from '../models';
import {context} from '../models';
export function ApplyChanges(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:connection.ChangeSet):Promise<connection.QueryResult>;
@@ -307,7 +307,7 @@ export function SetMacNativeWindowControls(arg1:boolean):Promise<void>;
export function SetWindowTranslucency(arg1:number,arg2:number):Promise<void>;
export function Shutdown(arg1:context.Context):Promise<void>;
export function Shutdown():Promise<void>;
export function StartSecurityUpdate(arg1:app.StartSecurityUpdateRequest):Promise<app.SecurityUpdateStatus>;

View File

@@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
@@ -602,8 +602,8 @@ export function SetWindowTranslucency(arg1, arg2) {
return window['go']['app']['App']['SetWindowTranslucency'](arg1, arg2);
}
export function Shutdown(arg1) {
return window['go']['app']['App']['Shutdown'](arg1);
export function Shutdown() {
return window['go']['app']['App']['Shutdown']();
}
export function StartSecurityUpdate(arg1) {

2
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
github.com/HuaweiCloudDeveloper/gaussdb-go v1.0.0-rc1
github.com/apache/iotdb-client-go v1.3.7
github.com/apache/rocketmq-client-go/v2 v2.1.2
github.com/caretdev/go-irisnative v0.2.1
github.com/duckdb/duckdb-go/v2 v2.5.5
github.com/eclipse/paho.mqtt.golang v1.5.1
@@ -35,7 +36,6 @@ require (
)
require (
github.com/apache/rocketmq-client-go/v2 v2.1.2 // indirect
github.com/apache/thrift v0.22.0 // indirect
github.com/elastic/elastic-transport-go/v8 v8.9.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect

6
go.sum
View File

@@ -22,6 +22,7 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
@@ -163,6 +164,7 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -186,6 +188,7 @@ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4P
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -308,7 +311,9 @@ github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYo
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -545,6 +550,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

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
}

View File

@@ -51,8 +51,8 @@ func main() {
aiservice.InitializeLifecycle(aiService, ctx)
},
OnShutdown: func(ctx context.Context) {
aiService.Shutdown(ctx)
application.Shutdown(ctx)
aiService.Shutdown()
application.Shutdown()
},
Bind: []interface{}{
application,