From 81fab81d1b6262e5ba66e49ba87a943c17694740 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 11 Jun 2026 22:49:34 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sql):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20Oracle=20=E6=89=98=E7=AE=A1=E4=BA=8B=E5=8A=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E5=9B=9E=E6=BB=9A=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/methods_db_multi_test.go | 66 ++++++++++++++++++++++++++ internal/app/methods_db_transaction.go | 6 +-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index e29fda4..c763291 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -682,6 +682,72 @@ func TestDBQueryMultiTransactionalUsesDriverTransactionForOracle(t *testing.T) { } } +func TestDBQueryMultiTransactionalOracleDriverTransactionOutlivesAppContextCancellation(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + for _, tt := range []struct { + name string + finish func(*App, string) connection.QueryResult + wantCommits int + wantRollbacks int + }{ + { + name: "commit", + finish: func(app *App, transactionID string) connection.QueryResult { + return app.DBCommitTransaction(transactionID) + }, + wantCommits: 1, + }, + { + name: "rollback", + finish: func(app *App, transactionID string) connection.QueryResult { + return app.DBRollbackTransaction(transactionID) + }, + wantRollbacks: 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, + }, + }, + } + newDatabaseFunc = func(dbType string) (db.Database, error) { + return fakeDB, nil + } + + appCtx, cancelAppCtx := context.WithCancel(context.Background()) + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + app.ctx = appCtx + config := connection.ConnectionConfig{Type: "oracle", Host: "127.0.0.1", Port: 1521, User: "app"} + + result := app.DBQueryMultiTransactional(config, "ORCLPDB1", stmt, "oracle-tx-context-"+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) + } + + cancelAppCtx() + finishResult := tt.finish(app, result.TransactionID) + if !finishResult.Success { + t.Fatalf("expected Oracle transaction %s success after app context cancellation, got failure: %s", tt.name, finishResult.Message) + } + if fakeDB.txSession.commitCalls != tt.wantCommits || fakeDB.txSession.rollbackCalls != tt.wantRollbacks { + t.Fatalf("expected commits=%d rollbacks=%d, got commits=%d rollbacks=%d", + tt.wantCommits, tt.wantRollbacks, fakeDB.txSession.commitCalls, fakeDB.txSession.rollbackCalls) + } + }) + } +} + func TestDBQueryMultiTransactionalRollsBackOracleDriverTransactionOnDMLFailure(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc t.Cleanup(func() { diff --git a/internal/app/methods_db_transaction.go b/internal/app/methods_db_transaction.go index dba4436..d5752c8 100644 --- a/internal/app/methods_db_transaction.go +++ b/internal/app/methods_db_transaction.go @@ -59,10 +59,10 @@ func (a *App) DBQueryMultiTransactional(config connection.ConnectionConfig, dbNa startTextTransaction bool ) 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. transactionContext := context.Background() - if a.ctx != nil { - transactionContext = a.ctx - } transactionContext, transactionCancel = context.WithCancel(transactionContext) transactionExecer, err := provider.OpenTransactionExecer(transactionContext) if err != nil {