From e8cad189be8ed55713633a7b42dee0b6a049052d Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 23 Jun 2026 09:46:44 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sqlserver):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=99=AE=E9=80=9A=E6=9F=A5=E8=AF=A2=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E8=A2=AB=E5=8E=9F=E7=94=9F=E5=A4=9A=E7=BB=93=E6=9E=9C=E9=9B=86?= =?UTF-8?q?=E5=90=83=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 对只读 SQL 的原生多结果集空返回增加顺序回退兜底 - 避免 optional driver-agent 成功返回空结果时前端只剩日志无结果集 - 补充 SQLServer 读查询空结果回退回归测试 --- internal/app/methods_db.go | 7 +++ internal/app/methods_db_multi_test.go | 78 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 6c79970..ebb27d5 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -1000,6 +1000,13 @@ func (a *App) DBQueryMulti(config connection.ConnectionConfig, dbName string, qu return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID} } + // 某些 optional driver-agent 的原生多结果集路径会异常返回“成功但无任何结果集”。 + // 对只读查询这是不可信信号,回退到逐条执行可以避免普通 SELECT 在结果面板中被吃空。 + if useNativeMultiResult && allReadOnly && results != nil && len(results) == 0 && len(resultMessages) == 0 { + logger.Warnf("DBQueryMulti 原生多结果集返回空结果,将回退逐条执行:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query)) + results = nil + } + // 驱动支持多结果集,直接返回 if results != nil { return connection.QueryResult{Success: true, Data: results, Messages: resultMessages, QueryID: queryID} diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index 7bc13a5..0b4a382 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -34,6 +34,11 @@ type fakeNativeMultiResultDB struct { multiCalls int } +type fakeEmptyNativeMultiResultDB struct { + *fakeBatchWriteDB + multiCalls int +} + func (f *fakeNativeMultiResultDB) QueryMulti(query string) ([]connection.ResultSetData, error) { results, _, err := f.QueryMultiWithMessages(query) return results, err @@ -67,6 +72,28 @@ func (f *fakeNativeMultiResultDB) QueryMultiContextWithMessages(ctx context.Cont }}, append([]string(nil), messages...), nil } +func (f *fakeEmptyNativeMultiResultDB) QueryMulti(query string) ([]connection.ResultSetData, error) { + results, _, err := f.QueryMultiWithMessages(query) + return results, err +} + +func (f *fakeEmptyNativeMultiResultDB) QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error) { + return f.QueryMultiContextWithMessages(context.Background(), query) +} + +func (f *fakeEmptyNativeMultiResultDB) QueryMultiContext(ctx context.Context, query string) ([]connection.ResultSetData, error) { + results, _, err := f.QueryMultiContextWithMessages(ctx, query) + return results, err +} + +func (f *fakeEmptyNativeMultiResultDB) QueryMultiContextWithMessages(ctx context.Context, query string) ([]connection.ResultSetData, []string, error) { + f.multiCalls++ + if err := f.queryErr[query]; err != nil { + return nil, nil, err + } + return []connection.ResultSetData{}, nil, nil +} + func (f *fakeBatchWriteDB) Connect(config connection.ConnectionConfig) error { return nil } @@ -1332,6 +1359,57 @@ func TestDBQueryMultiRunsSQLServerStatisticsBatchNatively(t *testing.T) { } } +func TestDBQueryMultiFallsBackWhenNativeReadOnlyBatchReturnsEmptyResults(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + query := "SELECT 1 AS value" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: { + {"value": 1}, + }, + }, + fieldMap: map[string][]string{ + query: {"value"}, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeEmptyNativeMultiResultDB{fakeBatchWriteDB: baseDB} + newDatabaseFunc = func(dbType string) (db.Database, error) { + return fakeDB, nil + } + + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + config := connection.ConnectionConfig{Type: "sqlserver", Host: "127.0.0.1", Port: 1433, User: "sa"} + + result := app.DBQueryMulti(config, "master", query, "sqlserver-empty-native-read-fallback-test") + if !result.Success { + t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message) + } + if fakeDB.multiCalls != 1 { + t.Fatalf("expected one native multi-result attempt, got %d", fakeDB.multiCalls) + } + if baseDB.session == nil { + t.Fatal("expected empty native result to fall back to pinned session query") + } + if baseDB.session.queryCalls != 1 { + t.Fatalf("expected fallback to query through pinned session once, got %d", baseDB.session.queryCalls) + } + resultSets, ok := result.Data.([]connection.ResultSetData) + if !ok { + t.Fatalf("expected []connection.ResultSetData, got %T", result.Data) + } + if len(resultSets) != 1 { + t.Fatalf("expected one fallback result set, got %#v", resultSets) + } + if got := resultSets[0].Rows[0]["value"]; got != 1 { + t.Fatalf("expected fallback SELECT result value=1, got %#v", got) + } +} + func TestDBQueryMultiUsesPinnedSessionForSequentialFallback(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc t.Cleanup(func() {