From ac68729734e9c37659df114e9d80a9a531e6da9a Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 6 Jul 2026 16:59:17 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DOracle=E5=8C=BF=E5=90=8D=E5=9D=97=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=E6=89=98=E7=AE=A1=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 区分 BEGIN 事务控制语句与 Oracle 匿名块起始 - 让包含 DML 的 Oracle like 匿名块进入 SQL 编辑器托管事务 - 补充匿名块进入事务与回滚收口的后端回归测试 --- internal/app/methods_db_multi_test.go | 57 +++++++++++++++++++++ internal/app/methods_db_transaction.go | 44 +++++++++++++++- internal/app/methods_db_transaction_test.go | 23 +++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index 605d89f..9cb6291 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -1033,6 +1033,63 @@ func TestDBQueryMultiTransactionalUsesImplicitSessionTransactionForOracle(t *tes } } +func TestDBQueryMultiTransactionalKeepsOracleAnonymousBlockTransactionOpenUntilRollback(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + stmt := `BEGIN + UPDATE users SET name = 'new' WHERE id = 1; + DELETE FROM audit_logs WHERE user_id = 1; +END;` + fakeDB := &fakeBatchWriteDB{ + execAffected: map[string]int64{ + stmt: 2, + }, + } + 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-anonymous-block-tx-query") + if !result.Success { + t.Fatalf("expected Oracle anonymous block 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 anonymous block to open a pinned session") + } + if fakeDB.session.closed { + t.Fatal("expected Oracle anonymous block transaction session to stay open before rollback") + } + if len(fakeDB.execQueries) != 1 || fakeDB.execQueries[0] != stmt { + t.Fatalf("expected Oracle anonymous block to execute as a single statement before rollback, got %#v", fakeDB.execQueries) + } + + rollbackResult := app.DBRollbackTransaction(result.TransactionID) + if !rollbackResult.Success { + t.Fatalf("expected Oracle anonymous block rollback success, got failure: %s", rollbackResult.Message) + } + if !fakeDB.session.closed { + t.Fatal("expected Oracle anonymous block transaction session to close after rollback") + } + wantExecs := []string{stmt, "ROLLBACK"} + if len(fakeDB.execQueries) != len(wantExecs) { + t.Fatalf("expected Oracle anonymous block rollback on pinned session, got %#v", fakeDB.execQueries) + } + for i, want := range wantExecs { + if fakeDB.execQueries[i] != want { + t.Fatalf("expected exec query %d = %q, got %q", i, want, fakeDB.execQueries[i]) + } + } +} + func TestDBQueryMultiTransactionalOraclePrefersTransactionProviderForFinish(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 0bd885e..ac9c77c 100644 --- a/internal/app/methods_db_transaction.go +++ b/internal/app/methods_db_transaction.go @@ -394,6 +394,10 @@ func shouldUseManagedSQLTransaction(dbType string, query string) bool { if isReadOnlySQLQuery(dbType, stmt) { continue } + if isOracleLikeAnonymousBlockManagedWrite(dbType, stmt) { + hasManagedWrite = true + continue + } if isBatchableWriteSQLStatement(dbType, stmt) { hasManagedWrite = true continue @@ -425,9 +429,13 @@ func sqlEditorImplicitTransactionSQL(dbType string) (commitSQL string, rollbackS } func isSQLTransactionControlStatement(stmt string) bool { - switch leadingSQLKeyword(stmt) { + keyword, keywordEnd := nextSQLKeyword(stmt, 0) + switch keyword { case "begin", "commit", "rollback", "savepoint", "release": - return true + if keyword != "begin" { + return true + } + return isBeginTransactionControlStatement(stmt, keywordEnd) case "start": return strings.Contains(strings.ToLower(stmt), "transaction") default: @@ -435,6 +443,38 @@ func isSQLTransactionControlStatement(stmt string) bool { } } +func isBeginTransactionControlStatement(stmt string, keywordEnd int) bool { + switch nextSQLSignificantByte(stmt, keywordEnd) { + case 0, ';': + return true + } + + switch nextSQLSignificantToken(stmt, keywordEnd) { + case "transaction", "tran", "work", "isolation", "read", "write", "deferred", "immediate", "exclusive", "distributed": + return true + default: + return false + } +} + +func isOracleLikeAnonymousBlockManagedWrite(dbType string, stmt string) bool { + if !isOracleLikeDBType(dbType) { + return false + } + + switch nextSQLSignificantToken(strings.TrimSpace(stmt), 0) { + case "begin", "declare": + return sqlContainsKeyword(stmt, "insert") || + sqlContainsKeyword(stmt, "update") || + sqlContainsKeyword(stmt, "delete") || + sqlContainsKeyword(stmt, "merge") || + sqlContainsKeyword(stmt, "replace") || + sqlContainsKeyword(stmt, "upsert") + default: + return false + } +} + func (a *App) DBCommitTransaction(transactionID string) connection.QueryResult { return a.finishManagedSQLTransaction(transactionID, true) } diff --git a/internal/app/methods_db_transaction_test.go b/internal/app/methods_db_transaction_test.go index fe766cf..f57f069 100644 --- a/internal/app/methods_db_transaction_test.go +++ b/internal/app/methods_db_transaction_test.go @@ -27,3 +27,26 @@ func TestShouldUseManagedSQLTransaction_UnsupportedTypesUsePlainExecution(t *tes }) } } + +func TestShouldUseManagedSQLTransaction_OracleAnonymousBlockWithDMLUsesManagedTransaction(t *testing.T) { + t.Parallel() + + query := `BEGIN + UPDATE users SET name = 'new' WHERE id = 1; + DELETE FROM audit_logs WHERE user_id = 1; +END;` + if !shouldUseManagedSQLTransaction("oracle", query) { + t.Fatal("expected Oracle anonymous block with DML to use SQL editor managed transaction") + } +} + +func TestShouldUseManagedSQLTransaction_OracleReadOnlyAnonymousBlockStaysUnmanaged(t *testing.T) { + t.Parallel() + + query := `BEGIN + NULL; +END;` + if shouldUseManagedSQLTransaction("oracle", query) { + t.Fatal("expected Oracle read-only anonymous block to stay unmanaged") + } +}