🐛 fix(sqlserver): 修复普通查询结果被原生多结果集吃空

- 对只读 SQL 的原生多结果集空返回增加顺序回退兜底
- 避免 optional driver-agent 成功返回空结果时前端只剩日志无结果集
- 补充 SQLServer 读查询空结果回退回归测试
This commit is contained in:
Syngnat
2026-06-23 09:46:44 +08:00
parent 495a985ae1
commit e8cad189be
2 changed files with 85 additions and 0 deletions

View File

@@ -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}

View File

@@ -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() {