mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 08:43:34 +08:00
🐛 fix(query-results): 修复 SQLite 与 SQL Server 查询结果空页
- SQLite 只读查询优先普通 Query,并安全降级不支持的多结果集接口 - SQL Server 在结果集边界前恢复未扫描数据,保持 data/affectedRows 顺序 - 普通与事务查询过滤状态空壳,修正结果展示与审计返回行数 - 更新 SQL Server driver-agent revision 并补充 SQLite、MSSQL 回归测试
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
252
internal/app/methods_db_sqlite_agent_regression_test.go
Normal file
252
internal/app/methods_db_sqlite_agent_regression_test.go
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user