feat(sql-audit): 新增 SQL 审计中心并完善事务追踪

- 新增脱敏审计存储、筛选、保留策略、完整性校验与 JSON/CSV 导出
- 覆盖查询编辑器、事务、导入同步、对象操作、AI/MCP 与 Web 运行时入口
- 完善事务完整语句日志、Windows 快捷键映射及 SQL 分析布局
- 补充多语言、健康状态、数据目录迁移和回归测试
This commit is contained in:
Syngnat
2026-07-12 15:24:15 +08:00
parent 0b33b3a683
commit 7c8e8d8dd3
89 changed files with 11421 additions and 194 deletions

View File

@@ -7,6 +7,7 @@ import (
"reflect"
"strings"
"testing"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
@@ -27,6 +28,9 @@ type fakeBatchWriteDB struct {
queryErr map[string]error
execErr map[string]error
execAffected map[string]int64
execDelay map[string]time.Duration
execStarted chan<- string
execRelease <-chan struct{}
session *fakeBatchWriteSession
}
@@ -173,6 +177,29 @@ func (f *fakeBatchWriteDB) ExecContext(ctx context.Context, query string) (int64
f.lastCtx = ctx
f.execCalls++
f.execQueries = append(f.execQueries, query)
if f.execStarted != nil {
select {
case f.execStarted <- query:
case <-ctx.Done():
return 0, ctx.Err()
}
}
if f.execRelease != nil {
select {
case <-f.execRelease:
case <-ctx.Done():
return 0, ctx.Err()
}
}
if delay := f.execDelay[query]; delay > 0 {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-timer.C:
case <-ctx.Done():
return 0, ctx.Err()
}
}
if err := f.execErr[query]; err != nil {
return 0, err
}
@@ -356,6 +383,126 @@ func cloneResultSets(input []connection.ResultSetData) []connection.ResultSetDat
return cloned
}
func TestDBQueryMultiInTransactionSerializesCommitWithInFlightStatement(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
t.Cleanup(func() { newDatabaseFunc = originalNewDatabaseFunc })
initialStatement := "UPDATE users SET active = 1 WHERE id = 1"
followUpStatement := "UPDATE users SET active = 0 WHERE id = 2"
fakeDB := &fakeTransactionalDB{fakeBatchWriteDB: fakeBatchWriteDB{
execAffected: map[string]int64{initialStatement: 1, followUpStatement: 1},
}}
newDatabaseFunc = func(string) (db.Database, error) { return fakeDB, nil }
app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
config := connection.ConnectionConfig{Type: "mysql", Host: "127.0.0.1", Port: 3306, Database: "main"}
started := app.DBQueryMultiTransactional(config, "main", initialStatement, "tx-serialize-start")
if !started.Success || started.TransactionID == "" {
t.Fatalf("start managed transaction: %#v", started)
}
execStarted := make(chan string, 1)
execRelease := make(chan struct{})
fakeDB.execStarted = execStarted
fakeDB.execRelease = execRelease
queryDone := make(chan connection.QueryResult, 1)
go func() {
queryDone <- app.DBQueryMultiInTransaction(started.TransactionID, followUpStatement, "tx-serialize-follow-up")
}()
select {
case statement := <-execStarted:
if statement != followUpStatement {
close(execRelease)
t.Fatalf("blocked statement = %q, want %q", statement, followUpStatement)
}
case <-time.After(2 * time.Second):
close(execRelease)
t.Fatal("follow-up statement did not start")
}
commitDone := make(chan connection.QueryResult, 1)
go func() {
commitDone <- app.DBCommitTransaction(started.TransactionID)
}()
select {
case result := <-commitDone:
close(execRelease)
t.Fatalf("commit completed while statement was still in flight: %#v", result)
case <-time.After(75 * time.Millisecond):
}
close(execRelease)
if result := <-queryDone; !result.Success {
t.Fatalf("follow-up statement failed: %#v", result)
}
if result := <-commitDone; !result.Success {
t.Fatalf("commit after follow-up failed: %#v", result)
}
if fakeDB.txSession.commitCalls != 1 || !fakeDB.txSession.closed {
t.Fatalf("transaction was not committed exactly once after execution: commitCalls=%d closed=%v", fakeDB.txSession.commitCalls, fakeDB.txSession.closed)
}
}
func TestRollbackPendingSQLTransactionsWaitsForInFlightStatement(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
t.Cleanup(func() { newDatabaseFunc = originalNewDatabaseFunc })
initialStatement := "UPDATE users SET active = 1 WHERE id = 1"
followUpStatement := "UPDATE users SET active = 0 WHERE id = 2"
fakeDB := &fakeTransactionalDB{fakeBatchWriteDB: fakeBatchWriteDB{
execAffected: map[string]int64{initialStatement: 1, followUpStatement: 1},
}}
newDatabaseFunc = func(string) (db.Database, error) { return fakeDB, nil }
app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
config := connection.ConnectionConfig{Type: "mysql", Host: "127.0.0.1", Port: 3306, Database: "main"}
started := app.DBQueryMultiTransactional(config, "main", initialStatement, "tx-shutdown-start")
if !started.Success || started.TransactionID == "" {
t.Fatalf("start managed transaction: %#v", started)
}
execStarted := make(chan string, 1)
execRelease := make(chan struct{})
fakeDB.execStarted = execStarted
fakeDB.execRelease = execRelease
queryDone := make(chan connection.QueryResult, 1)
go func() {
queryDone <- app.DBQueryMultiInTransaction(started.TransactionID, followUpStatement, "tx-shutdown-follow-up")
}()
select {
case <-execStarted:
case <-time.After(2 * time.Second):
close(execRelease)
t.Fatal("follow-up statement did not start")
}
shutdownDone := make(chan struct{})
go func() {
app.rollbackPendingSQLTransactionsOnShutdown()
close(shutdownDone)
}()
select {
case <-shutdownDone:
close(execRelease)
t.Fatal("shutdown rollback completed while statement was still in flight")
case <-time.After(75 * time.Millisecond):
}
close(execRelease)
if result := <-queryDone; !result.Success {
t.Fatalf("follow-up statement failed: %#v", result)
}
select {
case <-shutdownDone:
case <-time.After(2 * time.Second):
t.Fatal("shutdown rollback did not finish after statement completed")
}
if fakeDB.txSession.rollbackCalls != 1 || !fakeDB.txSession.closed {
t.Fatalf("transaction was not rolled back exactly once after execution: rollbackCalls=%d closed=%v", fakeDB.txSession.rollbackCalls, fakeDB.txSession.closed)
}
}
func TestDBQueryMultiKeepsOracleAnonymousBlockAsSingleStatement(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
t.Cleanup(func() {