Files
MyGoNavi/internal/db/duckdb_applychanges_test.go
Syngnat 2254b76232 🐛 fix(duckdb): 修复无主键结果无法安全编辑
- 为 DuckDB 查询结果和表预览补充隐藏 rowid 定位列,允许无主键表安全提交修改
- DataGrid 提交变更时仅将 rowid 用作定位条件,避免把隐藏定位列写回业务字段
- DuckDB ApplyChanges 对 duckdb-rowid 改用未加引号的 rowid 条件,修复更新和删除失效
- 补充前后端回归测试,覆盖 QueryEditor、DataViewer、rowLocator 与 ApplyChanges 链路
2026-06-05 14:05:18 +08:00

161 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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])
}
}