diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 668ba174..49368485 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -1496,7 +1496,7 @@ func (a *App) dbQueryMulti( } else { err = runStatementQuery() } - if err == nil && usedMultiResult && nativeReadOnlyResultsMissingTabularPayload(isReadStmt, statementResults) { + if err == nil && usedMultiResult && shouldFallbackToPlainQueryAfterMultiResult(isReadStmt, statementResults, messages) { logger.Warnf("DBQueryMulti 逐条多结果集返回空结果,将回退普通查询(第 %d/%d 条):%s SQL片段=%q", idx+1, len(statements), formatConnSummary(runConfig), sqlSnippet(stmt)) usedMultiResult = false statementResults = nil @@ -1641,6 +1641,9 @@ func nativeReadOnlyResultsMissingTabularPayload(allReadOnly bool, results []conn return true } for _, result := range results { + if isAffectedRowsResultSet(result) { + continue + } if len(result.Columns) > 0 || len(result.Rows) > 0 { return false } @@ -1648,6 +1651,19 @@ func nativeReadOnlyResultsMissingTabularPayload(allReadOnly bool, results []conn return true } +func shouldFallbackToPlainQueryAfterMultiResult(readOnly bool, results []connection.ResultSetData, messages []string) bool { + if !readOnly { + return false + } + // Optional driver agents use nil results with no messages to signal that the + // native multi-result method is unsupported. Retrying is only safe for reads; + // query-first writes and stored procedures may already have side effects. + if results == nil && len(messages) == 0 { + return true + } + return nativeReadOnlyResultsMissingTabularPayload(true, results) +} + func shouldUseNativeMultiResultBatch(dbType string, statements []string, allReadOnly bool) bool { if allReadOnly { return !shouldPreferPlainReadQueryResult(dbType) @@ -1670,7 +1686,8 @@ func shouldUseNativeMultiResultBatch(dbType string, statements []string, allRead func shouldPreferPlainReadQueryResult(dbType string) bool { switch strings.ToLower(strings.TrimSpace(dbType)) { - case "postgres", "postgresql", + case "sqlite", + "postgres", "postgresql", "oracle", "kingbase", "kingbase8", "kingbasees", "kingbasev8", "highgo", "vastbase", diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index 038ed754..fcbef2bc 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -2096,6 +2096,61 @@ func TestDBQueryMultiFallsBackWhenNativeReadOnlyBatchReturnsBlankResultSet(t *te } } +func TestDBQueryMultiFallsBackWhenSQLServerReadReturnsOnlyAffectedRowsStatus(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + query := "SELECT name FROM sys.databases" + statusOnlyResult := []connection.ResultSetData{{ + Rows: []map[string]interface{}{{"affectedRows": int64(1)}}, + Columns: []string{"affectedRows"}, + }} + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: {{"name": "master"}}, + }, + fieldMap: map[string][]string{ + query: {"name"}, + }, + multiResult: map[string][]connection.ResultSetData{ + query: statusOnlyResult, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeEmptyNativeMultiResultDB{ + fakeBatchWriteDB: baseDB, + results: statusOnlyResult, + } + newDatabaseFunc = func(dbType string) (db.Database, error) { + return fakeDB, nil + } + + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + config := connection.ConnectionConfig{Type: "custom", Driver: "mssql", Host: "127.0.0.1", Port: 1433, User: "sa"} + result := app.DBQueryMulti(config, "master", query, "sqlserver-affected-only-read-fallback-test") + if !result.Success { + t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message) + } + if fakeDB.multiCalls != 1 { + t.Fatalf("expected one top-level native multi-result attempt, got %d", fakeDB.multiCalls) + } + if baseDB.session == nil || baseDB.session.queryCalls != 2 { + t.Fatalf("expected status-only result to retry session multi then plain query, session=%#v", baseDB.session) + } + resultSets, ok := result.Data.([]connection.ResultSetData) + if !ok || len(resultSets) != 1 { + t.Fatalf("expected one fallback result set, got %#v", result.Data) + } + if got := resultSets[0].Rows[0]["name"]; got != "master" { + t.Fatalf("expected fallback SQL Server row name=master, got %#v", got) + } + if got := queryResultRowsReturned(result); got != 1 { + t.Fatalf("expected SQL audit rows returned = 1, got %d", got) + } +} + func TestDBQueryMultiFallsBackToPlainQueryWhenSequentialMultiStillReturnsBlankResultSet(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc t.Cleanup(func() { @@ -2854,6 +2909,49 @@ func TestExecuteManagedSQLTransactionStatementsPrefersPlainQueryForDamengReadRes } } +func TestExecuteManagedSQLTransactionStatementsFallsBackWhenSQLServerReadReturnsOnlyAffectedRowsStatus(t *testing.T) { + query := "SELECT name FROM sys.databases" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: {{"name": "master"}}, + }, + fieldMap: map[string][]string{ + query: {"name"}, + }, + multiResult: map[string][]connection.ResultSetData{ + query: {{ + Rows: []map[string]interface{}{{"affectedRows": int64(1)}}, + Columns: []string{"affectedRows"}, + }}, + }, + queryErr: map[string]error{}, + } + session := &fakeBatchWriteSession{parent: baseDB} + + results, err := executeManagedSQLTransactionStatements( + context.Background(), + session, + connection.ConnectionConfig{Type: "sqlserver"}, + []string{query}, + nil, + ) + if err != nil { + t.Fatalf("expected executeManagedSQLTransactionStatements success, got %v", err) + } + if session.queryCalls != 2 { + t.Fatalf("expected SQL Server status-only result plus plain query fallback, got %d calls", session.queryCalls) + } + if len(results) != 1 || len(results[0].Rows) != 1 { + t.Fatalf("expected one fallback result row, got %#v", results) + } + if got := results[0].Rows[0]["name"]; got != "master" { + t.Fatalf("expected fallback SQL Server row name=master, got %#v", got) + } + if got := queryResultRowsReturned(connection.QueryResult{Success: true, Data: results}); got != 1 { + t.Fatalf("expected SQL audit rows returned = 1, got %d", got) + } +} + func TestExecuteManagedSQLTransactionStatementsPrefersPlainQueryForOceanBaseOracleReadResults(t *testing.T) { query := "SELECT * FROM EINP_BASICINFO.AC01" baseDB := &fakeBatchWriteDB{ diff --git a/internal/app/methods_db_sqlite_agent_regression_test.go b/internal/app/methods_db_sqlite_agent_regression_test.go new file mode 100644 index 00000000..d531534f --- /dev/null +++ b/internal/app/methods_db_sqlite_agent_regression_test.go @@ -0,0 +1,252 @@ +package app + +import ( + "context" + "testing" + + "GoNavi-Wails/internal/connection" + "GoNavi-Wails/internal/db" + "GoNavi-Wails/internal/sqlaudit" +) + +// fakeUnsupportedMultiResultDB mirrors OptionalDriverAgentDB: the database-level +// multi-result method reports unsupported as nil while its pinned session only +// exposes the plain query API. +type fakeUnsupportedMultiResultDB struct { + *fakeBatchWriteDB + multiCalls int + session *fakePlainQuerySession +} + +func (f *fakeUnsupportedMultiResultDB) QueryMulti(query string) ([]connection.ResultSetData, error) { + results, _, err := f.QueryMultiWithMessages(query) + return results, err +} + +func (f *fakeUnsupportedMultiResultDB) QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error) { + return f.QueryMultiContextWithMessages(context.Background(), query) +} + +func (f *fakeUnsupportedMultiResultDB) QueryMultiContext(ctx context.Context, query string) ([]connection.ResultSetData, error) { + results, _, err := f.QueryMultiContextWithMessages(ctx, query) + return results, err +} + +func (f *fakeUnsupportedMultiResultDB) QueryMultiContextWithMessages(context.Context, string) ([]connection.ResultSetData, []string, error) { + f.multiCalls++ + return nil, nil, nil +} + +func (f *fakeUnsupportedMultiResultDB) OpenSessionExecer(context.Context) (db.StatementExecer, error) { + f.session = &fakePlainQuerySession{parent: f.fakeBatchWriteDB} + return f.session, nil +} + +type fakePlainQuerySession struct { + parent *fakeBatchWriteDB + queryCalls int +} + +func (s *fakePlainQuerySession) Query(query string) ([]map[string]interface{}, []string, error) { + return s.QueryContext(context.Background(), query) +} + +func (s *fakePlainQuerySession) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) { + s.queryCalls++ + return s.parent.QueryContext(ctx, query) +} + +func (s *fakePlainQuerySession) QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error) { + return s.QueryContextWithMessages(context.Background(), query) +} + +func (s *fakePlainQuerySession) QueryContextWithMessages(ctx context.Context, query string) ([]map[string]interface{}, []string, []string, error) { + s.queryCalls++ + return s.parent.QueryContextWithMessages(ctx, query) +} + +func (s *fakePlainQuerySession) Exec(query string) (int64, error) { + return s.ExecContext(context.Background(), query) +} + +func (s *fakePlainQuerySession) ExecContext(ctx context.Context, query string) (int64, error) { + return s.parent.ExecContext(ctx, query) +} + +func (s *fakePlainQuerySession) Close() error { return nil } + +type fakeUnsupportedMultiResultSession struct { + *fakePlainQuerySession + multiCalls int +} + +func (s *fakeUnsupportedMultiResultSession) QueryMulti(query string) ([]connection.ResultSetData, error) { + results, _, err := s.QueryMultiWithMessages(query) + return results, err +} + +func (s *fakeUnsupportedMultiResultSession) QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error) { + return s.QueryMultiContextWithMessages(context.Background(), query) +} + +func (s *fakeUnsupportedMultiResultSession) QueryMultiContext(ctx context.Context, query string) ([]connection.ResultSetData, error) { + results, _, err := s.QueryMultiContextWithMessages(ctx, query) + return results, err +} + +func (s *fakeUnsupportedMultiResultSession) QueryMultiContextWithMessages(context.Context, string) ([]connection.ResultSetData, []string, error) { + s.multiCalls++ + return nil, nil, nil +} + +func installFakeOptionalSQLiteDatabase(t *testing.T, fakeDB db.Database) { + t.Helper() + originalNewDatabaseFunc := newDatabaseFunc + originalDriverRuntimeSupportStatusFunc := driverRuntimeSupportStatusFunc + originalVerifyDriverAgentRevisionFunc := verifyDriverAgentRevisionFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + driverRuntimeSupportStatusFunc = originalDriverRuntimeSupportStatusFunc + verifyDriverAgentRevisionFunc = originalVerifyDriverAgentRevisionFunc + }) + newDatabaseFunc = func(string) (db.Database, error) { return fakeDB, nil } + driverRuntimeSupportStatusFunc = func(string) (bool, string) { return true, "" } + verifyDriverAgentRevisionFunc = func(connection.ConnectionConfig) error { return nil } +} + +func TestDBQueryMultiSQLiteAgentStyleQueryReturnsRowsAndAuditCount(t *testing.T) { + query := "SELECT id, name FROM orders" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: {{"id": 1, "name": "SQLite agent row"}}, + }, + fieldMap: map[string][]string{ + query: {"id", "name"}, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeUnsupportedMultiResultDB{fakeBatchWriteDB: baseDB} + installFakeOptionalSQLiteDatabase(t, fakeDB) + + app := newSQLAuditTestApp(t) + config := connection.ConnectionConfig{Type: "sqlite", Host: "/tmp/orders.sqlite"} + result := app.DBQueryMulti(config, "", query, "sqlite-agent-select") + if !result.Success { + t.Fatalf("SQLite agent-style SELECT returned failure: %s", result.Message) + } + resultSets, ok := result.Data.([]connection.ResultSetData) + if !ok || len(resultSets) != 1 || len(resultSets[0].Rows) != 1 { + t.Fatalf("SQLite agent-style SELECT result sets = %#v, want one row", result.Data) + } + if fakeDB.multiCalls != 0 { + t.Fatalf("SQLite plain SELECT should not probe unsupported multi-result API, calls=%d", fakeDB.multiCalls) + } + if fakeDB.session == nil || fakeDB.session.queryCalls != 1 { + t.Fatalf("SQLite plain SELECT should use one pinned-session query, session=%#v", fakeDB.session) + } + + events := loadSQLAuditEvents(t, app, sqlaudit.Filter{Search: "sqlite-agent-select", PageSize: 10}) + if len(events) != 1 || events[0].RowsReturned != 1 { + t.Fatalf("SQLite SELECT audit events = %#v, want RowsReturned=1", events) + } +} + +func TestDBQueryMultiFallsBackWhenMultiResultReportsUnsupportedAsNil(t *testing.T) { + query := "SELECT id FROM users" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: {{"id": 7}}, + }, + fieldMap: map[string][]string{ + query: {"id"}, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeUnsupportedMultiResultDB{fakeBatchWriteDB: baseDB} + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { newDatabaseFunc = originalNewDatabaseFunc }) + newDatabaseFunc = func(string) (db.Database, error) { return fakeDB, nil } + + app := NewApp() + result := app.DBQueryMulti(connection.ConnectionConfig{Type: "mysql"}, "main", query, "nil-multi-result") + if !result.Success { + t.Fatalf("DBQueryMulti returned failure: %s", result.Message) + } + resultSets, ok := result.Data.([]connection.ResultSetData) + if !ok || len(resultSets) != 1 || len(resultSets[0].Rows) != 1 { + t.Fatalf("DBQueryMulti result sets = %#v, want one row", result.Data) + } + if fakeDB.multiCalls != 2 { + t.Fatalf("expected batch and statement multi-result probes before fallback, calls=%d", fakeDB.multiCalls) + } + if fakeDB.session == nil || fakeDB.session.queryCalls != 1 { + t.Fatalf("expected one plain pinned-session fallback, session=%#v", fakeDB.session) + } +} + +func TestDBQueryMultiDoesNotRetryQueryFirstWriteWhenMultiResultReturnsNil(t *testing.T) { + query := "INSERT INTO users(id) VALUES (11) RETURNING id" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: {{"id": 11}}, + }, + fieldMap: map[string][]string{ + query: {"id"}, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeUnsupportedMultiResultDB{fakeBatchWriteDB: baseDB} + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { newDatabaseFunc = originalNewDatabaseFunc }) + newDatabaseFunc = func(string) (db.Database, error) { return fakeDB, nil } + + app := NewApp() + result := app.DBQueryMulti(connection.ConnectionConfig{Type: "postgres"}, "main", query, "nil-write-result") + if !result.Success { + t.Fatalf("query-first write returned failure: %s", result.Message) + } + if fakeDB.multiCalls != 1 { + t.Fatalf("expected the query-first write to execute once, multi calls=%d", fakeDB.multiCalls) + } + if fakeDB.session == nil || fakeDB.session.queryCalls != 0 { + t.Fatalf("query-first write must not retry through plain Query, session=%#v", fakeDB.session) + } +} + +func TestManagedTransactionFallsBackWhenMultiResultReportsUnsupportedAsNil(t *testing.T) { + query := "SELECT id FROM users" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: {{"id": 9}}, + }, + fieldMap: map[string][]string{ + query: {"id"}, + }, + queryErr: map[string]error{}, + } + session := &fakeUnsupportedMultiResultSession{ + fakePlainQuerySession: &fakePlainQuerySession{parent: baseDB}, + } + + resultSets, err := executeManagedSQLTransactionStatements( + context.Background(), + session, + connection.ConnectionConfig{Type: "mysql"}, + []string{query}, + defaultDBBackendText, + ) + if err != nil { + t.Fatalf("managed transaction SELECT returned failure: %v", err) + } + if len(resultSets) != 1 || len(resultSets[0].Rows) != 1 { + t.Fatalf("managed transaction result sets = %#v, want one row", resultSets) + } + if session.multiCalls != 1 || session.queryCalls != 1 { + t.Fatalf("expected one multi-result probe and one plain fallback, multi=%d plain=%d", session.multiCalls, session.queryCalls) + } +} + +var _ db.MultiResultQueryMessageExecer = (*fakeUnsupportedMultiResultDB)(nil) +var _ db.SessionExecerProvider = (*fakeUnsupportedMultiResultDB)(nil) +var _ db.StatementQueryMessageExecer = (*fakePlainQuerySession)(nil) +var _ db.StatementMultiResultQueryMessageExecer = (*fakeUnsupportedMultiResultSession)(nil) diff --git a/internal/app/methods_db_transaction.go b/internal/app/methods_db_transaction.go index 0ae36976..21fbcf2f 100644 --- a/internal/app/methods_db_transaction.go +++ b/internal/app/methods_db_transaction.go @@ -511,6 +511,21 @@ func executeManagedSQLTransactionStatementsWithObserver( } else { err = buildTransactionQueryUnsupportedError() } + if err == nil && usedMultiResult && shouldFallbackToPlainQueryAfterMultiResult(isReadStmt, statementResults, messages) { + logger.Warnf("托管事务多结果集返回空结果,将回退普通查询(第 %d/%d 条):类型=%s SQL片段=%q", statementIndex, statementCount, resolvedDBType, sqlSnippet(stmt)) + usedMultiResult = false + statementResults = nil + data = nil + columns = nil + messages = nil + if sessionQueryMessageTarget != nil { + data, columns, messages, err = sessionQueryMessageTarget.QueryContextWithMessages(ctx, stmt) + } else if sessionQueryTarget != nil { + data, columns, err = sessionQueryTarget.QueryContext(ctx, stmt) + } else { + err = buildTransactionQueryUnsupportedError() + } + } if err == nil { if usedMultiResult { var rowsAffected, rowsReturned int64 diff --git a/internal/db/driver_agent_revisions_gen.go b/internal/db/driver_agent_revisions_gen.go index a5a6b8df..68c6ee2b 100644 --- a/internal/db/driver_agent_revisions_gen.go +++ b/internal/db/driver_agent_revisions_gen.go @@ -9,7 +9,7 @@ func init() { "diros": "src-7d4fe439271d0c56", "starrocks": "src-ce9ee22641a32f46", "sphinx": "src-08f5ae54efb3d9df", - "sqlserver": "src-33b3b2c6dad5b3e6", + "sqlserver": "src-bd1323910fc119ac", "sqlite": "src-96dfa25b3042b2d5", "duckdb": "src-8804eb2cdbc89433", "dameng": "src-016e77082aea6718", diff --git a/internal/db/sqlserver_impl.go b/internal/db/sqlserver_impl.go index fcf5ef65..b79dd144 100644 --- a/internal/db/sqlserver_impl.go +++ b/internal/db/sqlserver_impl.go @@ -40,10 +40,33 @@ func scanSQLServerRowsWithMessages(ctx context.Context, rows *sql.Rows, retmsg * } var ( - resultSets []connection.ResultSetData - messages []string - allMessages []string + resultSets []connection.ResultSetData + messages []string + allMessages []string + currentResultScanned bool + currentResultStart int ) + // go-mssqldb can emit a result-set boundary without MsgNext. Recover the + // unread rows before advancing and keep them before any affectedRows status. + scanCurrentResultIfNeeded := func() error { + if currentResultScanned { + return nil + } + result, err := scanSQLServerFallbackResultSet(rows) + if err != nil { + return err + } + currentResultScanned = true + if len(result.Columns) == 0 && len(result.Rows) == 0 { + return nil + } + result.Messages = append([]string(nil), messages...) + resultSets = append(resultSets, connection.ResultSetData{}) + copy(resultSets[currentResultStart+1:], resultSets[currentResultStart:]) + resultSets[currentResultStart] = result + messages = nil + return nil + } active := true for active { raw := retmsg.Message(ctx) @@ -70,6 +93,7 @@ func scanSQLServerRowsWithMessages(ctx context.Context, rows *sql.Rows, retmsg * Columns: cols, Messages: append([]string(nil), messages...), }) + currentResultScanned = true messages = nil case sqlexp.MsgRowsAffected: resultSets = append(resultSets, connection.ResultSetData{ @@ -79,13 +103,23 @@ func scanSQLServerRowsWithMessages(ctx context.Context, rows *sql.Rows, retmsg * }) messages = nil case sqlexp.MsgNextResultSet: + if err := scanCurrentResultIfNeeded(); err != nil { + return resultSets, messages, err + } active = rows.NextResultSet() + if active { + currentResultScanned = false + currentResultStart = len(resultSets) + } case sqlexp.MsgError: return resultSets, messages, msg.Error default: active = false } } + if err := scanCurrentResultIfNeeded(); err != nil { + return resultSets, messages, err + } if len(messages) > 0 { resultSets = append(resultSets, connection.ResultSetData{ @@ -95,11 +129,7 @@ func scanSQLServerRowsWithMessages(ctx context.Context, rows *sql.Rows, retmsg * }) } if len(resultSets) == 0 { - fallbackResult, err := scanSQLServerFallbackResultSet(rows) - if err != nil { - return resultSets, allMessages, err - } - resultSets = []connection.ResultSetData{fallbackResult} + resultSets = []connection.ResultSetData{emptySQLServerRowsResultSet()} } if err := rows.Err(); err != nil { return resultSets, allMessages, err diff --git a/internal/db/sqlserver_impl_test.go b/internal/db/sqlserver_impl_test.go index 7f0a9f59..5d301afc 100644 --- a/internal/db/sqlserver_impl_test.go +++ b/internal/db/sqlserver_impl_test.go @@ -13,6 +13,7 @@ import ( "GoNavi-Wails/shared/i18n" + "github.com/golang-sql/sqlexp" _ "modernc.org/sqlite" ) @@ -162,6 +163,55 @@ func TestScanSQLServerFallbackResultSetPreservesColumnsWhenResultHasNoRows(t *te } } +func TestScanSQLServerRowsWithMessagesRecoversRowsWhenMessageLoopOmitsMsgNext(t *testing.T) { + dbConn, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + t.Cleanup(func() { + _ = dbConn.Close() + }) + + rows, err := dbConn.Query("SELECT 'config:roomType:add' AS menuName") + if err != nil { + t.Fatalf("query rows: %v", err) + } + defer rows.Close() + + ctx := context.Background() + retmsg := &sqlexp.ReturnMessage{} + sqlexp.ReturnMessageInit(retmsg) + for _, message := range []sqlexp.RawMessage{ + sqlexp.MsgRowsAffected{Count: 1}, + sqlexp.MsgNextResultSet{}, + } { + if err := sqlexp.ReturnMessageEnqueue(ctx, retmsg, message); err != nil { + t.Fatalf("enqueue SQL Server message: %v", err) + } + } + + resultSets, messages, err := scanSQLServerRowsWithMessages(ctx, rows, retmsg) + if err != nil { + t.Fatalf("scanSQLServerRowsWithMessages returned error: %v", err) + } + if len(messages) != 0 { + t.Fatalf("expected no SQL Server notices, got %#v", messages) + } + if len(resultSets) != 2 { + t.Fatalf("expected recovered SELECT rows plus affected-row status, got %#v", resultSets) + } + if !reflect.DeepEqual(resultSets[0].Columns, []string{"menuName"}) || + len(resultSets[0].Rows) != 1 || + resultSets[0].Rows[0]["menuName"] != "config:roomType:add" { + t.Fatalf("expected recovered SELECT rows first, got %#v", resultSets) + } + if !reflect.DeepEqual(resultSets[1].Columns, []string{"affectedRows"}) || + len(resultSets[1].Rows) != 1 || + resultSets[1].Rows[0]["affectedRows"] != int64(1) { + t.Fatalf("expected affected-row status after SELECT rows, got %#v", resultSets) + } +} + func TestSQLServerMetadataErrorsUseCurrentLanguage(t *testing.T) { SetBackendLanguage(i18n.LanguageEnUS) t.Cleanup(func() {