diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx index 18ece7e4..2db0a547 100644 --- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx +++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx @@ -1085,6 +1085,53 @@ describe('QueryEditor external SQL save', () => { expect(messageApi.success).toHaveBeenCalledWith('已执行完成,生成 1 个结果集。'); }); + it('hides redundant sqlserver affected-row status results for every statement in a batch', async () => { + storeState.appearance.uiVersion = 'v2'; + storeState.connections[0].config.type = 'sqlserver'; + storeState.connections[0].config.database = 'master'; + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [ + { statementIndex: 1, columns: ['value'], rows: [{ value: 1 }] }, + { statementIndex: 1, columns: ['affectedRows'], rows: [{ affectedRows: 1 }] }, + { statementIndex: 2, columns: ['value'], rows: [{ value: 2 }] }, + { statementIndex: 2, columns: ['affectedRows'], rows: [{ affectedRows: 1 }] }, + ], + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer!, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const rendered = textContent(renderer!.toJSON()); + expect(rendered).toContain('结果 1'); + expect(rendered).toContain('结果 2'); + expect(rendered).not.toContain('结果 3'); + expect(rendered).not.toContain('结果 4'); + expect(rendered).not.toContain('影响行数:1'); + expect(messageApi.success).toHaveBeenCalledWith('已执行完成,生成 2 个结果集。'); + + const resultTabButtons = renderer!.root.findAll((node) => + node.type === 'button' && String(node.props['data-tab-key'] || '').startsWith('result-')); + expect(resultTabButtons).toHaveLength(2); + + await act(async () => { + resultTabButtons[1].props.onClick(); + }); + + expect(dataGridState.latestProps?.columnNames).toEqual(['value']); + expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ value: 2 }); + }); + it('prefers the first displayable sqlserver procedure result when empty result sets are returned', async () => { storeState.connections[0].config.type = 'sqlserver'; storeState.connections[0].config.database = 'hydee'; diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 537e2281..a620de5e 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -1443,6 +1443,21 @@ func normalizeNativeResultStatementIndexes(dbType string, statements []string, r for idx := range results { results[idx].StatementIndex = idx + 1 } + case len(results) == len(statements)*2: + // go-mssqldb 会在每个 SELECT 数据结果后再发送一条 MsgRowsAffected。 + // 仅在整个批次严格呈现 [数据结果, affectedRows] 成对结构时补索引, + // 避免把存储过程返回的多个真实结果集错误归并到不同语句。 + for statementIdx := range statements { + resultIdx := statementIdx * 2 + if isAffectedRowsResultSet(results[resultIdx]) || !isAffectedRowsResultSet(results[resultIdx+1]) { + return + } + } + for statementIdx := range statements { + resultIdx := statementIdx * 2 + results[resultIdx].StatementIndex = statementIdx + 1 + results[resultIdx+1].StatementIndex = statementIdx + 1 + } } } diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index cd8b73df..e649df96 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -2258,6 +2258,81 @@ func TestDBQueryMultiNormalizesSingleSQLServerSelectAffectedRowsStatementIndex(t } } +func TestDBQueryMultiNormalizesSQLServerSelectAffectedRowsPairsByStatement(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + query := "SELECT 1;\nSELECT 2;" + baseDB := &fakeBatchWriteDB{ + multiResult: map[string][]connection.ResultSetData{ + query: { + { + Rows: []map[string]interface{}{{"value": int64(1)}}, + Columns: []string{"value"}, + }, + { + Rows: []map[string]interface{}{{"affectedRows": int64(1)}}, + Columns: []string{"affectedRows"}, + }, + { + Rows: []map[string]interface{}{{"value": int64(2)}}, + Columns: []string{"value"}, + }, + { + Rows: []map[string]interface{}{{"affectedRows": int64(1)}}, + Columns: []string{"affectedRows"}, + }, + }, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeNativeMultiResultDB{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-select-pairs-index-test") + if !result.Success { + t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message) + } + resultSets, ok := result.Data.([]connection.ResultSetData) + if !ok { + t.Fatalf("expected []connection.ResultSetData, got %T", result.Data) + } + if len(resultSets) != 4 { + t.Fatalf("expected four raw SQL Server result sets, got %#v", resultSets) + } + wantStatementIndexes := []int{1, 1, 2, 2} + for idx, want := range wantStatementIndexes { + if got := resultSets[idx].StatementIndex; got != want { + t.Fatalf("result set %d statementIndex = %d, want %d; all results: %#v", idx, got, want, resultSets) + } + } +} + +func TestNormalizeNativeResultStatementIndexesKeepsAmbiguousSQLServerResultsUnassigned(t *testing.T) { + statements := []string{"SELECT 1", "SELECT 2"} + results := []connection.ResultSetData{ + {Rows: []map[string]interface{}{{"first": int64(1)}}, Columns: []string{"first"}}, + {Rows: []map[string]interface{}{{"second": int64(2)}}, Columns: []string{"second"}}, + {Rows: []map[string]interface{}{{"affectedRows": int64(1)}}, Columns: []string{"affectedRows"}}, + {Rows: []map[string]interface{}{{"affectedRows": int64(1)}}, Columns: []string{"affectedRows"}}, + } + + normalizeNativeResultStatementIndexes("sqlserver", statements, results) + + for idx, result := range results { + if result.StatementIndex != 0 { + t.Fatalf("ambiguous result set %d received guessed statementIndex=%d: %#v", idx, result.StatementIndex, results) + } + } +} + func TestDBQueryMultiTreatsBareSQLServerProcedureCallAsQueryFirst(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc t.Cleanup(func() {