🐛 fix(query-editor): 修复Oracle匿名块事务托管缺失

- 区分 BEGIN 事务控制语句与 Oracle 匿名块起始
- 让包含 DML 的 Oracle like 匿名块进入 SQL 编辑器托管事务
- 补充匿名块进入事务与回滚收口的后端回归测试
This commit is contained in:
Syngnat
2026-07-06 16:59:17 +08:00
parent 19c10f2369
commit ac68729734
3 changed files with 122 additions and 2 deletions

View File

@@ -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() {

View File

@@ -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)
}

View File

@@ -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")
}
}