Release vtable and cursor handles when SQLite destroys them

This commit is contained in:
Yasuhiro Matsumoto
2026-07-29 01:58:50 +09:00
parent 0cfec60306
commit 7d534a0306
3 changed files with 66 additions and 0 deletions

View File

@@ -131,6 +131,27 @@ func lookupHandle(handle unsafe.Pointer) any {
return lookupHandleVal(handle).val 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) { func deleteHandles(db *SQLiteConn) {
handleLock.Lock() handleLock.Lock()
defer handleLock.Unlock() defer handleLock.Unlock()

View File

@@ -423,6 +423,9 @@ func goVRelease(pVTab unsafe.Pointer, isDestroy C.int) *C.char {
} else { } else {
err = vt.vTab.Disconnect() 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 { if err != nil {
return mPrintf("%s", err.Error()) 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 { func goVClose(pCursor unsafe.Pointer) *C.char {
vtc := lookupHandle(pCursor).(*sqliteVTabCursor) vtc := lookupHandle(pCursor).(*sqliteVTabCursor)
err := vtc.vTabCursor.Close() 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 { if err != nil {
return mPrintf("%s", err.Error()) return mPrintf("%s", err.Error())
} }
@@ -502,6 +508,7 @@ func goVClose(pCursor unsafe.Pointer) *C.char {
func goMDestroy(pClientData unsafe.Pointer) { func goMDestroy(pClientData unsafe.Pointer) {
m := lookupHandle(pClientData).(*sqliteModule) m := lookupHandle(pClientData).(*sqliteModule)
m.module.DestroyModule() m.module.DestroyModule()
deleteHandle(pClientData)
} }
//export goVFilter //export goVFilter

View File

@@ -0,0 +1,38 @@
//go:build sqlite_vtable
package sqlite3
import (
"database/sql"
"testing"
)
func TestVtabCursorHandleRelease(t *testing.T) {
sql.Register("sqlite3_HandleLeakCheck", &SQLiteDriver{
ConnectHook: func(conn *SQLiteConn) error {
return conn.CreateModule("test", &testModule{t: t, intarray: []int{1, 2, 3}})
},
})
db, err := sql.Open("sqlite3_HandleLeakCheck", ":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)
}
}