//go:build gonavi_full_drivers || gonavi_duckdb_driver package db import ( "context" "database/sql" "database/sql/driver" "fmt" "sync" "testing" "GoNavi-Wails/internal/connection" ) const duckdbRecordingDriverName = "gonavi_duckdb_recording" var ( registerDuckDBRecordingDriverOnce sync.Once duckdbRecordingDriverMu sync.Mutex duckdbRecordingDriverSeq int duckdbRecordingDriverStates = map[string]*duckdbRecordingState{} ) type duckdbRecordingState struct { mu sync.Mutex execQueries []string execArgs [][]driver.NamedValue } func (s *duckdbRecordingState) snapshotExecQueries() []string { s.mu.Lock() defer s.mu.Unlock() return append([]string(nil), s.execQueries...) } func (s *duckdbRecordingState) snapshotExecArgs() [][]driver.NamedValue { s.mu.Lock() defer s.mu.Unlock() result := make([][]driver.NamedValue, len(s.execArgs)) for i, args := range s.execArgs { result[i] = append([]driver.NamedValue(nil), args...) } return result } type duckdbRecordingDriver struct{} func (duckdbRecordingDriver) Open(name string) (driver.Conn, error) { duckdbRecordingDriverMu.Lock() state := duckdbRecordingDriverStates[name] duckdbRecordingDriverMu.Unlock() if state == nil { return nil, fmt.Errorf("recording state not found: %s", name) } return &duckdbRecordingConn{state: state}, nil } type duckdbRecordingConn struct { state *duckdbRecordingState } func (c *duckdbRecordingConn) Prepare(query string) (driver.Stmt, error) { return nil, fmt.Errorf("prepare not supported in duckdb recording driver: %s", query) } func (c *duckdbRecordingConn) Close() error { return nil } func (c *duckdbRecordingConn) Begin() (driver.Tx, error) { return duckdbRecordingTx{}, nil } func (c *duckdbRecordingConn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) { c.state.mu.Lock() defer c.state.mu.Unlock() c.state.execQueries = append(c.state.execQueries, query) c.state.execArgs = append(c.state.execArgs, append([]driver.NamedValue(nil), args...)) return driver.RowsAffected(1), nil } var _ driver.ExecerContext = (*duckdbRecordingConn)(nil) type duckdbRecordingTx struct{} func (duckdbRecordingTx) Commit() error { return nil } func (duckdbRecordingTx) Rollback() error { return nil } func openDuckDBRecordingDB(t *testing.T) (*sql.DB, *duckdbRecordingState) { t.Helper() registerDuckDBRecordingDriverOnce.Do(func() { sql.Register(duckdbRecordingDriverName, duckdbRecordingDriver{}) }) duckdbRecordingDriverMu.Lock() duckdbRecordingDriverSeq++ dsn := fmt.Sprintf("duckdb-recording-%d", duckdbRecordingDriverSeq) state := &duckdbRecordingState{} duckdbRecordingDriverStates[dsn] = state duckdbRecordingDriverMu.Unlock() dbConn, err := sql.Open(duckdbRecordingDriverName, dsn) if err != nil { t.Fatalf("打开 duckdb recording db 失败: %v", err) } t.Cleanup(func() { _ = dbConn.Close() duckdbRecordingDriverMu.Lock() delete(duckdbRecordingDriverStates, dsn) duckdbRecordingDriverMu.Unlock() }) return dbConn, state } func TestDuckDBApplyChangesUsesUnquotedRowIDLocator(t *testing.T) { t.Parallel() dbConn, state := openDuckDBRecordingDB(t) duckdb := &DuckDB{conn: dbConn} changes := connection.ChangeSet{ Updates: []connection.UpdateRow{{ Keys: map[string]interface{}{ "rowid": 17, }, Values: map[string]interface{}{ "name": "renamed", }, }}, Deletes: []map[string]interface{}{ {"rowid": 21}, }, LocatorStrategy: "duckdb-rowid", } if err := duckdb.ApplyChanges("main.events", changes); err != nil { t.Fatalf("ApplyChanges 返回错误: %v", err) } queries := state.snapshotExecQueries() if len(queries) != 2 { t.Fatalf("期望执行 2 条 SQL,实际=%d %#v", len(queries), queries) } if queries[0] != `DELETE FROM "main"."events" WHERE rowid = ?` { t.Fatalf("删除 SQL 不符合预期: %s", queries[0]) } if queries[1] != `UPDATE "main"."events" SET "name" = ? WHERE rowid = ?` { t.Fatalf("更新 SQL 不符合预期: %s", queries[1]) } args := state.snapshotExecArgs() if len(args) != 2 || len(args[0]) != 1 || len(args[1]) != 2 { t.Fatalf("执行参数数量不符合预期: %#v", args) } if args[0][0].Value != 21 { t.Fatalf("删除 rowid 参数错误: %#v", args[0]) } if args[1][0].Value != "renamed" || args[1][1].Value != 17 { t.Fatalf("更新参数错误: %#v", args[1]) } }