🐛 fix(sql-editor): 修复 Oracle 事务结束并补充 Redis 拓扑提示

- SQL 编辑器:Oracle 托管事务优先使用 transaction provider 完成提交和回滚

- Redis:拆分 Key 浏览工具栏并展示 Cluster/Sentinel 拓扑上下文

- 测试:补充 Oracle 事务结束和 Redis 拓扑头部回归用例
This commit is contained in:
Syngnat
2026-06-12 08:48:08 +08:00
parent 781a80e03f
commit fce50b513c
5 changed files with 327 additions and 63 deletions

View File

@@ -683,6 +683,89 @@ func TestDBQueryMultiTransactionalUsesImplicitSessionTransactionForOracle(t *tes
}
}
func TestDBQueryMultiTransactionalOraclePrefersTransactionProviderForFinish(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
t.Cleanup(func() {
newDatabaseFunc = originalNewDatabaseFunc
})
for _, tt := range []struct {
name string
finish func(*App, string) connection.QueryResult
wantCommitCalls int
wantRollbackCalls int
}{
{
name: "commit",
finish: func(app *App, transactionID string) connection.QueryResult {
return app.DBCommitTransaction(transactionID)
},
wantCommitCalls: 1,
},
{
name: "rollback",
finish: func(app *App, transactionID string) connection.QueryResult {
return app.DBRollbackTransaction(transactionID)
},
wantRollbackCalls: 1,
},
} {
t.Run(tt.name, func(t *testing.T) {
stmt := "UPDATE users SET name = 'new' WHERE id = 1"
fakeDB := &fakeTransactionalDB{
fakeBatchWriteDB: fakeBatchWriteDB{
execAffected: map[string]int64{
stmt: 1,
},
execErr: map[string]error{
"COMMIT": errors.New("oracle commit rows affected unavailable"),
"ROLLBACK": errors.New("oracle rollback rows affected unavailable"),
},
},
}
newDatabaseFunc = func(dbType string) (db.Database, error) {
return fakeDB, nil
}
app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
config := connection.ConnectionConfig{Type: "oracle", Host: "127.0.0.1", Port: 1521, User: "app"}
result := app.DBQueryMultiTransactional(config, "ORCLPDB1", stmt, "oracle-provider-finish-"+tt.name)
if !result.Success {
t.Fatalf("expected Oracle transactional query success, got failure: %s", result.Message)
}
if result.TransactionID == "" || !result.TransactionPending {
t.Fatalf("expected pending transaction metadata, got id=%q pending=%v", result.TransactionID, result.TransactionPending)
}
if fakeDB.session != nil {
t.Fatal("expected Oracle to use transaction provider instead of plain session provider")
}
if fakeDB.txSession == nil {
t.Fatal("expected Oracle to open a transaction provider session")
}
finishResult := tt.finish(app, result.TransactionID)
if !finishResult.Success {
t.Fatalf("expected Oracle transaction %s success through transaction provider, got failure: %s", tt.name, finishResult.Message)
}
if fakeDB.txSession.commitCalls != tt.wantCommitCalls {
t.Fatalf("expected commitCalls=%d, got %d", tt.wantCommitCalls, fakeDB.txSession.commitCalls)
}
if fakeDB.txSession.rollbackCalls != tt.wantRollbackCalls {
t.Fatalf("expected rollbackCalls=%d, got %d", tt.wantRollbackCalls, fakeDB.txSession.rollbackCalls)
}
if !fakeDB.txSession.closed {
t.Fatal("expected transaction provider session to close after finish")
}
for _, query := range fakeDB.execQueries {
if query == "COMMIT" || query == "ROLLBACK" {
t.Fatalf("expected finish to avoid plain ExecContext(%q), got exec queries %#v", query, fakeDB.execQueries)
}
}
})
}
}
func TestDBQueryMultiTransactionalUsesOracleImplicitSessionForOceanBaseOracleProtocol(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
originalVerifyDriverAgentRevisionFunc := verifyDriverAgentRevisionFunc

View File

@@ -68,21 +68,7 @@ func (a *App) DBQueryMultiTransactional(config connection.ConnectionConfig, dbNa
transactionCancel context.CancelFunc
startTextTransaction bool
)
if implicitTextTransaction {
provider, ok := dbInst.(db.SessionExecerProvider)
if !ok {
return connection.QueryResult{
Success: false,
Message: fmt.Sprintf("当前数据源(%s不支持 SQL 编辑器托管事务", transactionDBType),
QueryID: queryID,
}
}
sessionExecer, err = provider.OpenSessionExecer(ctx)
if err != nil {
logger.Error(err, "DBQueryMultiTransactional 打开隐式事务会话失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
}
} else if provider, ok := dbInst.(db.TransactionExecerProvider); ok {
if provider, ok := dbInst.(db.TransactionExecerProvider); ok {
// database/sql rolls back a BeginTx transaction when its context is cancelled.
// SQL editor transactions must outlive the execution RPC and be ended only by
// explicit commit, rollback, or shutdown cleanup.
@@ -96,6 +82,20 @@ func (a *App) DBQueryMultiTransactional(config connection.ConnectionConfig, dbNa
}
sessionExecer = transactionExecer
transactor = transactionExecer
} else if implicitTextTransaction {
provider, ok := dbInst.(db.SessionExecerProvider)
if !ok {
return connection.QueryResult{
Success: false,
Message: fmt.Sprintf("当前数据源(%s不支持 SQL 编辑器托管事务", transactionDBType),
QueryID: queryID,
}
}
sessionExecer, err = provider.OpenSessionExecer(ctx)
if err != nil {
logger.Error(err, "DBQueryMultiTransactional 打开隐式事务会话失败:%s SQL片段=%q", formatConnSummary(runConfig), sqlSnippet(query))
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
}
} else {
if !hasTextTransaction {
return connection.QueryResult{