🐛 fix(query-editor): 修复 SQL Server 多查询冗余影响行数结果

- 将 SQL Server 成对返回的数据结果与 affectedRows 归属同一语句
- 保留 DML 影响行数与存储过程真实多结果集
- 补充后端正反用例及 V2 结果标签回归测试
This commit is contained in:
Syngnat
2026-07-12 09:37:29 +08:00
parent ef5e344c4f
commit 0b33b3a683
3 changed files with 137 additions and 0 deletions

View File

@@ -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(<QueryEditor tab={createTab({ dbName: 'master', query: 'SELECT 1;\nSELECT 2;' })} />);
});
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';

View File

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

View File

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