diff --git a/callback.go b/callback.go index 8ea17db..b7df2be 100644 --- a/callback.go +++ b/callback.go @@ -131,6 +131,27 @@ func lookupHandle(handle unsafe.Pointer) any { return lookupHandleVal(handle).val } +// deleteHandle releases a single handle created by newHandle. It is a no-op +// if the handle is unknown (e.g. already released). +func deleteHandle(handle unsafe.Pointer) { + handleLock.Lock() + defer handleLock.Unlock() + + current := loadHandleVals() + if _, ok := current[handle]; !ok { + return + } + next := make(map[unsafe.Pointer]handleVal, len(current)-1) + for h, v := range current { + if h == handle { + continue + } + next[h] = v + } + handleVals.Store(next) + C.free(handle) +} + func deleteHandles(db *SQLiteConn) { handleLock.Lock() defer handleLock.Unlock() diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea..d6e3ef6 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -423,6 +423,9 @@ func goVRelease(pVTab unsafe.Pointer, isDestroy C.int) *C.char { } else { err = vt.vTab.Disconnect() } + // The vtab is gone as far as SQLite is concerned regardless of the + // callback result, so release the handle either way. + deleteHandle(pVTab) if err != nil { return mPrintf("%s", err.Error()) } @@ -492,6 +495,9 @@ func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { func goVClose(pCursor unsafe.Pointer) *C.char { vtc := lookupHandle(pCursor).(*sqliteVTabCursor) err := vtc.vTabCursor.Close() + // The cursor is gone as far as SQLite is concerned regardless of the + // callback result, so release the handle either way. + deleteHandle(pCursor) if err != nil { return mPrintf("%s", err.Error()) } @@ -502,6 +508,7 @@ func goVClose(pCursor unsafe.Pointer) *C.char { func goMDestroy(pClientData unsafe.Pointer) { m := lookupHandle(pClientData).(*sqliteModule) m.module.DestroyModule() + deleteHandle(pClientData) } //export goVFilter diff --git a/sqlite3_opt_vtable_leak_test.go b/sqlite3_opt_vtable_leak_test.go new file mode 100644 index 0000000..d2b7047 --- /dev/null +++ b/sqlite3_opt_vtable_leak_test.go @@ -0,0 +1,45 @@ +//go:build sqlite_vtable + +package sqlite3 + +import ( + "database/sql" + "fmt" + "sync/atomic" + "testing" +) + +var leakCheckDriverSeq int32 + +func TestVtabCursorHandleRelease(t *testing.T) { + // Use a unique driver name so repeated runs (e.g. -count=2) do not + // panic on duplicate registration. + driverName := fmt.Sprintf("sqlite3_HandleLeakCheck_%d", atomic.AddInt32(&leakCheckDriverSeq, 1)) + sql.Register(driverName, &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + return conn.CreateModule("test", &testModule{t: t, intarray: []int{1, 2, 3}}) + }, + }) + db, err := sql.Open(driverName, ":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + if _, err := db.Exec("CREATE VIRTUAL TABLE vtab USING test('1', 2, three)"); err != nil { + t.Fatal(err) + } + var before, after int + for i := 0; i < 50; i++ { + var n int + if err := db.QueryRow("SELECT count(*) FROM vtab").Scan(&n); err != nil { + t.Fatal(err) + } + if i == 0 { + before = len(loadHandleVals()) + } + } + after = len(loadHandleVals()) + if after > before { + t.Fatalf("handle map grew from %d to %d over repeated cursor open/close", before, after) + } +}