From 7d534a0306dfd4cfc873a9d155b1c6eaef33a489 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 01:58:50 +0900 Subject: [PATCH 01/24] Release vtable and cursor handles when SQLite destroys them --- callback.go | 21 ++++++++++++++++++ sqlite3_opt_vtable.go | 7 ++++++ sqlite3_opt_vtable_leak_test.go | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 sqlite3_opt_vtable_leak_test.go diff --git a/callback.go b/callback.go index 8ea17db9..b7df2be7 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 9e916ea8..d6e3ef64 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 00000000..3d8b28e7 --- /dev/null +++ b/sqlite3_opt_vtable_leak_test.go @@ -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) + } +} From 1f0b756378a2b5cfe09b5a496e14df274688b541 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 08:57:25 +0900 Subject: [PATCH 02/24] Do not clobber SQLite's default cost estimates in BestIndex --- sqlite3_opt_vtable.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea8..33de0dc1 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -482,8 +482,22 @@ func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { if res.AlreadyOrdered { info.orderByConsumed = C.int(1) } - info.estimatedCost = C.double(res.EstimatedCost) - info.estimatedRows = C.sqlite3_int64(res.EstimatedRows) + // SQLite pre-initializes estimatedCost and estimatedRows with sensible + // defaults; overwriting them with the Go zero value would make every + // candidate plan look free and break query planning, so only pass + // values the implementation actually set. + if res.EstimatedCost > 0 { + info.estimatedCost = C.double(res.EstimatedCost) + } + if res.EstimatedRows > 0 { + var rows int64 + if res.EstimatedRows >= float64(math.MaxInt64) { + rows = math.MaxInt64 + } else { + rows = int64(res.EstimatedRows) + } + info.estimatedRows = C.sqlite3_int64(rows) + } return nil } From c2ecdac1df24dfeccb602c2c0f50df88ef863b8e Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 08:59:49 +0900 Subject: [PATCH 03/24] Translate SQL NULL filter arguments to nil like goVUpdate --- sqlite3_opt_vtable.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea8..ba788f0a 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -514,7 +514,14 @@ func goVFilter(pCursor unsafe.Pointer, idxNum C.int, idxName *C.char, argc C.int if err != nil { return mPrintf("%s", err.Error()) } - vals = append(vals, conv.Interface()) + + // work around for SQLITE_NULL + x := conv.Interface() + if z, ok := x.([]byte); ok && z == nil { + x = nil + } + + vals = append(vals, x) } err := vtc.vTabCursor.Filter(int(idxNum), C.GoString(idxName), vals) if err != nil { From 1a1bc9e7783ec8ae99770996bd2edbc7c653568c Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:01:10 +0900 Subject: [PATCH 04/24] Identify updated row by argv 0 in goVUpdate --- sqlite3_opt_vtable.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea8..2c17d30b 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -608,7 +608,9 @@ func goVUpdate(pVTab unsafe.Pointer, argc C.int, argv **C.sqlite3_value, pRowid } case argc > 1: - err = v.Update(vals[1], vals[2:]) + // Per the xUpdate contract argv[0] identifies the row being + // updated while argv[1] is its (possibly changed) new rowid. + err = v.Update(vals[0], vals[2:]) } } From 518016c8440ddb347f791cb5fd151afabcb0c76e Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:02:34 +0900 Subject: [PATCH 05/24] Ignore Used for constraints SQLite marked not usable --- sqlite3_opt_vtable.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea8..695f38dc 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -460,7 +460,11 @@ func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { slice := unsafe.Slice(info.aConstraintUsage, int(info.nConstraint)) index := 1 for i := range slice { - if res.Used[i] { + // SQLite returns "xBestIndex malfunction" when an argvIndex is + // assigned to a constraint it marked as not usable, so ignore + // Used for those; they may become usable on a later xBestIndex + // invocation for a different plan. + if res.Used[i] && csts[i].Usable { slice[i].argvIndex = C.int(index) slice[i].omit = C.uchar(1) index++ From cad8b719434efb6ddf78c1b8d0a2c2fce1eba44d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:04:45 +0900 Subject: [PATCH 06/24] Reject nil module and nil BestIndex result --- sqlite3_opt_vtable.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea8..8095d26a 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -451,6 +451,9 @@ func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { if err != nil { return mPrintf("%s", err.Error()) } + if res == nil { + return mPrintf("%s", "BestIndex returned a nil IndexResult") + } if len(res.Used) != len(csts) { return mPrintf("Result.Used != expected value", "") } @@ -708,5 +711,5 @@ func (c *SQLiteConn) CreateModule(moduleName string, module Module) error { } return nil } - return nil + return fmt.Errorf("sqlite3: CreateModule requires a non-nil module") } From 6443b7e879a1b4e8988d3f457c2f3440f27ff0b0 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:06:26 +0900 Subject: [PATCH 07/24] Fix off-by-one truncating SQL in fuzz target --- _example/fuzz/fuzz_openexec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_example/fuzz/fuzz_openexec.go b/_example/fuzz/fuzz_openexec.go index 53260449..b36c9f28 100644 --- a/_example/fuzz/fuzz_openexec.go +++ b/_example/fuzz/fuzz_openexec.go @@ -22,7 +22,7 @@ func FuzzOpenExec(data []byte) int { return 0 } defer db.Close() - _, err = db.Exec(string(data[:sep-1])) + _, err = db.Exec(string(data[:sep])) if err != nil { return 0 } From 322330f1474f35671e4384e510f23f3e10e67944 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:07:31 +0900 Subject: [PATCH 08/24] Fail upgrade tool on download and write errors --- upgrade/upgrade.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/upgrade/upgrade.go b/upgrade/upgrade.go index 352d4fef..aa862e34 100644 --- a/upgrade/upgrade.go +++ b/upgrade/upgrade.go @@ -26,6 +26,9 @@ func download(prefix string) (url string, content []byte, err error) { return "", nil, err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", nil, fmt.Errorf("downloading download.html: %s", resp.Status) + } b, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -60,10 +63,13 @@ func download(prefix string) (url string, content []byte, err error) { if err != nil { return "", nil, err } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", nil, fmt.Errorf("downloading %s: %s", url, resp.Status) + } // Ready Body Content content, err = ioutil.ReadAll(resp.Body) - defer resp.Body.Close() if err != nil { return "", nil, err } @@ -72,17 +78,6 @@ func download(prefix string) (url string, content []byte, err error) { } func mergeFile(src string, dst string) error { - defer func() error { - fmt.Printf("Removing: %s\n", src) - err := os.Remove(src) - - if err != nil { - return err - } - - return nil - }() - // Open destination fdst, err := os.OpenFile(dst, os.O_APPEND|os.O_WRONLY, 0666) if err != nil { @@ -106,7 +101,9 @@ func mergeFile(src string, dst string) error { return err } - return nil + // Only remove the source once it has been merged successfully. + fmt.Printf("Removing: %s\n", src) + return os.Remove(src) } func main() { @@ -170,6 +167,7 @@ func main() { log.Fatal(err) } scanner := bufio.NewScanner(zr) + var werr error for scanner.Scan() { text := scanner.Text() if text == `#include "sqlite3.h"` { @@ -179,11 +177,19 @@ func main() { #endif ` } - _, err = fmt.Fprintln(f, text) - if err != nil { + _, werr = fmt.Fprintln(f, text) + if werr != nil { break } } + // A write failure must not be masked by scanner.Err(), which + // reports nil in that case; either way a truncated output file + // must never be reported as successfully extracted. + if werr != nil { + zr.Close() + f.Close() + log.Fatal(werr) + } err = scanner.Err() if err != nil { zr.Close() @@ -197,7 +203,9 @@ func main() { log.Fatal(err) } zr.Close() - f.Close() + if err := f.Close(); err != nil { + log.Fatal(err) + } fmt.Printf("Extracted: %v\n", filepath.Base(f.Name())) } From 3b8cebf19bd2c63fef4b51e3b60c45fd422ff5e7 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:46:32 +0900 Subject: [PATCH 09/24] Fix wrong results and cursor state sharing in series example --- _example/vtable_eponymous_only/main.go | 30 +++++--- _example/vtable_eponymous_only/vtable.go | 93 ++++++++++++++++-------- 2 files changed, 82 insertions(+), 41 deletions(-) diff --git a/_example/vtable_eponymous_only/main.go b/_example/vtable_eponymous_only/main.go index 17b58afa..134317e8 100644 --- a/_example/vtable_eponymous_only/main.go +++ b/_example/vtable_eponymous_only/main.go @@ -20,14 +20,26 @@ func main() { } defer db.Close() - rows, err := db.Query("select * from series") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - for rows.Next() { - var value int - rows.Scan(&value) - fmt.Printf("value: %d\n", value) + for _, query := range []string{ + "select value from series where value between 3 and 5", + "select value from series where start = 10 and stop = 20 and step = 5", + "select value from series where start = 3 and stop = -3 and step = -2", + } { + fmt.Println(query) + rows, err := db.Query(query) + if err != nil { + log.Fatal(err) + } + for rows.Next() { + var value int + if err := rows.Scan(&value); err != nil { + log.Fatal(err) + } + fmt.Printf("value: %d\n", value) + } + if err := rows.Err(); err != nil { + log.Fatal(err) + } + rows.Close() } } diff --git a/_example/vtable_eponymous_only/vtable.go b/_example/vtable_eponymous_only/vtable.go index 9f22ebcc..d87c4d66 100644 --- a/_example/vtable_eponymous_only/vtable.go +++ b/_example/vtable_eponymous_only/vtable.go @@ -1,7 +1,9 @@ package main import ( + "errors" "fmt" + "strings" "github.com/mattn/go-sqlite3" ) @@ -21,7 +23,7 @@ func (m *seriesModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTa if err != nil { return nil, err } - return &seriesTable{0, 0, 1}, nil + return &seriesTable{}, nil } func (m *seriesModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { @@ -30,27 +32,46 @@ func (m *seriesModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VT func (m *seriesModule) DestroyModule() {} -type seriesTable struct { - start int64 - stop int64 - step int64 -} +type seriesTable struct{} func (v *seriesTable) Open() (sqlite3.VTabCursor, error) { - return &seriesCursor{v, 0}, nil + return &seriesCursor{}, nil } func (v *seriesTable) BestIndex(csts []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) { + // Consume equality constraints on the hidden parameter columns + // (start, stop, step) only. A constraint on the value column must be + // left to SQLite, otherwise it would be dropped from the WHERE clause + // and the query would return wrong results. used := make([]bool, len(csts)) + var params []byte for c, cst := range csts { - if cst.Usable && cst.Op == sqlite3.OpEQ { - used[c] = true + if !cst.Usable || cst.Op != sqlite3.OpEQ { + continue } + var p byte + switch cst.Column { + case 1: + p = 's' // start + case 2: + p = 'e' // stop + case 3: + p = 't' // step + default: + continue + } + if strings.IndexByte(string(params), p) >= 0 { + continue + } + used[c] = true + params = append(params, p) } + // IdxStr records which parameter each Filter argument holds, in + // argument order. return &sqlite3.IndexResult{ IdxNum: 0, - IdxStr: "default", + IdxStr: string(params), Used: used, }, nil } @@ -59,7 +80,9 @@ func (v *seriesTable) Disconnect() error { return nil } func (v *seriesTable) Destroy() error { return nil } type seriesCursor struct { - *seriesTable + start int64 + stop int64 + step int64 value int64 } @@ -68,35 +91,38 @@ func (vc *seriesCursor) Column(c *sqlite3.SQLiteContext, col int) error { case 0: c.ResultInt64(vc.value) case 1: - c.ResultInt64(vc.seriesTable.start) + c.ResultInt64(vc.start) case 2: - c.ResultInt64(vc.seriesTable.stop) + c.ResultInt64(vc.stop) case 3: - c.ResultInt64(vc.seriesTable.step) + c.ResultInt64(vc.step) } return nil } func (vc *seriesCursor) Filter(idxNum int, idxStr string, vals []any) error { - switch { - case len(vals) < 1: - vc.seriesTable.start = 0 - vc.seriesTable.stop = 1000 - vc.value = vc.seriesTable.start - case len(vals) < 2: - vc.seriesTable.start = vals[0].(int64) - vc.seriesTable.stop = 1000 - vc.value = vc.seriesTable.start - case len(vals) < 3: - vc.seriesTable.start = vals[0].(int64) - vc.seriesTable.stop = vals[1].(int64) - vc.value = vc.seriesTable.start - case len(vals) < 4: - vc.seriesTable.start = vals[0].(int64) - vc.seriesTable.stop = vals[1].(int64) - vc.seriesTable.step = vals[2].(int64) + start, stop, step := int64(0), int64(1000), int64(1) + for i := 0; i < len(idxStr) && i < len(vals); i++ { + n, ok := vals[i].(int64) + if !ok { + return fmt.Errorf("series: argument %d must be an integer", i+1) + } + switch idxStr[i] { + case 's': + start = n + case 'e': + stop = n + case 't': + step = n + } } - + if step == 0 { + return errors.New("series: step must not be zero") + } + vc.start = start + vc.stop = stop + vc.step = step + vc.value = start return nil } @@ -106,6 +132,9 @@ func (vc *seriesCursor) Next() error { } func (vc *seriesCursor) EOF() bool { + if vc.step < 0 { + return vc.value < vc.stop + } return vc.value > vc.stop } From baab34464dedb7aae7191b21b8a24ea63d89232d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:47:29 +0900 Subject: [PATCH 10/24] Use the table name from xCreate args in vtable example --- _example/vtable/vtable.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/_example/vtable/vtable.go b/_example/vtable/vtable.go index c65535b7..0e608274 100644 --- a/_example/vtable/vtable.go +++ b/_example/vtable/vtable.go @@ -20,13 +20,19 @@ type githubModule struct { } func (m *githubModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + // args holds the raw xCreate arguments: args[0] is the module name, + // args[1] the database name, args[2] the virtual table name, and any + // user arguments follow. + if len(args) < 3 { + return nil, fmt.Errorf("githubModule: unexpected arguments: %v", args) + } err := c.DeclareVTab(fmt.Sprintf(` CREATE TABLE %s ( id INT, full_name TEXT, description TEXT, html_url TEXT - )`, args[0])) + )`, args[2])) if err != nil { return nil, err } @@ -49,6 +55,9 @@ func (v *ghRepoTable) Open() (sqlite3.VTabCursor, error) { return nil, err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetching repositories: %s", resp.Status) + } body, err := ioutil.ReadAll(resp.Body) if err != nil { From 80cf381fee0349ebd5897a084e7b10a735762826 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:48:21 +0900 Subject: [PATCH 11/24] Close leaked rows in hook example --- _example/hook/hook.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/_example/hook/hook.go b/_example/hook/hook.go index 60231815..a728e3cf 100644 --- a/_example/hook/hook.go +++ b/_example/hook/hook.go @@ -45,10 +45,11 @@ func main() { if err != nil { log.Fatal(err) } - _, err = srcDb.Query("select * from foo") + rows, err := srcDb.Query("select * from foo") if err != nil { log.Fatal(err) } + rows.Close() destDb, err := sql.Open("sqlite3_with_hook_example", "./bar.db") if err != nil { log.Fatal(err) @@ -65,14 +66,17 @@ func main() { if err != nil { log.Fatal(err) } - _, err = destDb.Query("select * from foo") + rows, err = destDb.Query("select * from foo") if err != nil { log.Fatal(err) } + rows.Close() _, err = destDb.Exec("insert into foo values(3, 'bar')") if err != nil { log.Fatal(err) } - bk.Finish() + if err := bk.Finish(); err != nil { + log.Fatal(err) + } } From 084f3d386f9fbe21b85a7cd17d0744dedf9c31ae Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:48:47 +0900 Subject: [PATCH 12/24] Close prepared statement and fail if limit is not enforced in limit example --- _example/limit/limit.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/_example/limit/limit.go b/_example/limit/limit.go index c1adfe81..16bf0256 100644 --- a/_example/limit/limit.go +++ b/_example/limit/limit.go @@ -32,6 +32,7 @@ func bulkInsert(db *sql.DB, query string, args []any) (err error) { if err != nil { return } + defer stmt.Close() _, err = stmt.Exec(args...) if err != nil { @@ -89,11 +90,10 @@ func main() { query, args = createBulkInsertQuery(num, num) err = bulkInsert(db, query, args) - if err != nil { - if err != nil { - log.Printf("expect failed since SQLITE_LIMIT_VARIABLE_NUMBER is too small: %v", err) - } + if err == nil { + log.Fatal("expected failure since SQLITE_LIMIT_VARIABLE_NUMBER is too small, but insert succeeded") } + log.Printf("expect failed since SQLITE_LIMIT_VARIABLE_NUMBER is too small: %v", err) bigLimitVariableNumber := 999999 sqlite3conn.SetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, bigLimitVariableNumber) @@ -104,9 +104,7 @@ func main() { query, args = createBulkInsertQuery(500, num+num) err = bulkInsert(db, query, args) if err != nil { - if err != nil { - log.Fatal(err) - } + log.Fatal(err) } log.Println("no error if SQLITE_LIMIT_VARIABLE_NUMBER > 999") From 88a47972a56842318bcd8bcf8dc24413a62804db Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 09:52:47 +0900 Subject: [PATCH 13/24] Handle NULL and BLOB values in json example Scan --- _example/json/json.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/_example/json/json.go b/_example/json/json.go index 08e75cd2..1c7c6c76 100644 --- a/_example/json/json.go +++ b/_example/json/json.go @@ -16,7 +16,17 @@ type Tag struct { } func (t *Tag) Scan(value interface{}) error { - return json.Unmarshal([]byte(value.(string)), t) + switch v := value.(type) { + case nil: + *t = Tag{} + return nil + case string: + return json.Unmarshal([]byte(v), t) + case []byte: + return json.Unmarshal(v, t) + default: + return fmt.Errorf("unsupported type %T for Tag", value) + } } func (t *Tag) Value() (driver.Value, error) { From f65452a23dce8eec5a5d841530ff0e13adc98a0d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 10:04:39 +0900 Subject: [PATCH 14/24] Use unique driver name in leak test --- sqlite3_opt_vtable_leak_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sqlite3_opt_vtable_leak_test.go b/sqlite3_opt_vtable_leak_test.go index 3d8b28e7..d2b70477 100644 --- a/sqlite3_opt_vtable_leak_test.go +++ b/sqlite3_opt_vtable_leak_test.go @@ -4,16 +4,23 @@ package sqlite3 import ( "database/sql" + "fmt" + "sync/atomic" "testing" ) +var leakCheckDriverSeq int32 + func TestVtabCursorHandleRelease(t *testing.T) { - sql.Register("sqlite3_HandleLeakCheck", &SQLiteDriver{ + // 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("sqlite3_HandleLeakCheck", ":memory:") + db, err := sql.Open(driverName, ":memory:") if err != nil { t.Fatal(err) } From 2897b83d1848f1a9b191b125a6c614de9bba7efb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 11:10:45 +0900 Subject: [PATCH 15/24] Clamp fractional row estimates to at least 1 --- sqlite3_opt_vtable.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 33de0dc1..aded1b4e 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -493,8 +493,9 @@ func goVBestIndex(pVTab unsafe.Pointer, icp unsafe.Pointer) *C.char { var rows int64 if res.EstimatedRows >= float64(math.MaxInt64) { rows = math.MaxInt64 - } else { - rows = int64(res.EstimatedRows) + } else if rows = int64(res.EstimatedRows); rows < 1 { + // A positive fractional estimate must not truncate to 0. + rows = 1 } info.estimatedRows = C.sqlite3_int64(rows) } From 726af39fbde1414e10ecf3d45df54a7c2d70a78d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 13:14:19 +0900 Subject: [PATCH 16/24] Reject rowid-changing updates on virtual tables --- sqlite3_opt_vtable.go | 10 ++++++++-- sqlite3_opt_vtable_test.go | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 2c17d30b..25ae3edb 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -609,8 +609,14 @@ func goVUpdate(pVTab unsafe.Pointer, argc C.int, argv **C.sqlite3_value, pRowid case argc > 1: // Per the xUpdate contract argv[0] identifies the row being - // updated while argv[1] is its (possibly changed) new rowid. - err = v.Update(vals[0], vals[2:]) + // updated while argv[1] is its new rowid. VTabUpdater has no + // way to convey a rowid change, so reject it instead of + // silently updating values under the old rowid. + if vals[0] != vals[1] { + err = fmt.Errorf("virtual %s table %sdoes not support changing the rowid", vt.module.name, tname) + } else { + err = v.Update(vals[0], vals[2:]) + } } } diff --git a/sqlite3_opt_vtable_test.go b/sqlite3_opt_vtable_test.go index 64511e23..3afa155e 100644 --- a/sqlite3_opt_vtable_test.go +++ b/sqlite3_opt_vtable_test.go @@ -280,6 +280,19 @@ func TestVUpdate(t *testing.T) { t.Fatalf("expected table vt entry 1 to be [117 f e], instead: %v", vt.data[1]) } + // a rowid-changing update cannot be expressed via VTabUpdater and + // must be rejected instead of updating the wrong row + _, err = db.Exec(`update vt set rowid = rowid + 10 where f1 = 117`) + if err == nil { + t.Fatalf("expected error on rowid-changing update, got nil") + } + if !strings.Contains(err.Error(), "does not support changing the rowid") { + t.Fatalf("unexpected error on rowid-changing update: %v", err) + } + if !reflect.DeepEqual(vt.data[1], []any{int64(117), "f", "e"}) { + t.Fatalf("expected table vt entry 1 to be unchanged, instead: %v", vt.data[1]) + } + // delete from vt res, err = db.Exec(`delete from vt where f1 = 117`) if err != nil { From 6ebb665a622d5d5dc33885f7c07d78a8174cf068 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 14:05:50 +0900 Subject: [PATCH 17/24] Close merge destination before removing the source --- upgrade/upgrade.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/upgrade/upgrade.go b/upgrade/upgrade.go index aa862e34..4a655316 100644 --- a/upgrade/upgrade.go +++ b/upgrade/upgrade.go @@ -83,21 +83,29 @@ func mergeFile(src string, dst string) error { if err != nil { return err } - defer fdst.Close() // Read source content content, err := ioutil.ReadFile(src) if err != nil { + fdst.Close() return err } // Add Additional newline if _, err := fdst.WriteString("\n"); err != nil { + fdst.Close() return err } fmt.Printf("Merging: %s into %s\n", src, dst) if _, err = fdst.Write(content); err != nil { + fdst.Close() + return err + } + + // Close may surface deferred write errors; the source must survive + // unless the merge fully reached the destination. + if err := fdst.Close(); err != nil { return err } From c0861dee5367fe00bdb956cb97b1ac4cade7db2d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 14:57:27 +0900 Subject: [PATCH 18/24] Quote table name before interpolating into DeclareVTab --- _example/vtable/vtable.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/_example/vtable/vtable.go b/_example/vtable/vtable.go index 0e608274..8f5a957a 100644 --- a/_example/vtable/vtable.go +++ b/_example/vtable/vtable.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "github.com/mattn/go-sqlite3" ) @@ -26,13 +27,14 @@ func (m *githubModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTa if len(args) < 3 { return nil, fmt.Errorf("githubModule: unexpected arguments: %v", args) } + tableName := `"` + strings.ReplaceAll(args[2], `"`, `""`) + `"` err := c.DeclareVTab(fmt.Sprintf(` CREATE TABLE %s ( id INT, full_name TEXT, description TEXT, html_url TEXT - )`, args[2])) + )`, tableName)) if err != nil { return nil, err } From 18f2ec37d384d0ee65be85b3c8ae1b4aa58168b2 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 15:50:02 +0900 Subject: [PATCH 19/24] Assert the expected too many SQL variables error --- _example/limit/limit.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_example/limit/limit.go b/_example/limit/limit.go index 16bf0256..4ac33c24 100644 --- a/_example/limit/limit.go +++ b/_example/limit/limit.go @@ -93,6 +93,9 @@ func main() { if err == nil { log.Fatal("expected failure since SQLITE_LIMIT_VARIABLE_NUMBER is too small, but insert succeeded") } + if !strings.Contains(err.Error(), "too many SQL variables") { + log.Fatalf("expected too many SQL variables error, got: %v", err) + } log.Printf("expect failed since SQLITE_LIMIT_VARIABLE_NUMBER is too small: %v", err) bigLimitVariableNumber := 999999 From 18308a843a370f9416c3909a515f2120fae4566e Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 16:05:01 +0900 Subject: [PATCH 20/24] Upgrade SQLite to version 3053004 --- sqlite3-binding.c | 140 +++++++++++++++++++++++++++++----------------- sqlite3-binding.h | 10 ++-- 2 files changed, 93 insertions(+), 57 deletions(-) diff --git a/sqlite3-binding.c b/sqlite3-binding.c index 337af9e4..4a3437f5 100644 --- a/sqlite3-binding.c +++ b/sqlite3-binding.c @@ -1,7 +1,7 @@ #ifndef USE_LIBSQLITE3 /****************************************************************************** ** This file is an amalgamation of many separate C source files from SQLite -** version 3.53.3. By combining all the individual C code files into this +** version 3.53.4. By combining all the individual C code files into this ** single large file, the entire code can be compiled as a single translation ** unit. This allows many compilers to do optimizations that would not be ** possible if the files were compiled separately. Performance improvements @@ -19,7 +19,7 @@ ** separate file. This file contains only code for the core SQLite library. ** ** The content in this amalgamation comes from Fossil check-in -** d4c0e51e4aeb96955b99185ab9cde75c339e with changes in files: +** bf7c7f30031888f4e796e429ab3978879485 with changes in files: ** ** */ @@ -468,12 +468,12 @@ extern "C" { ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ -#define SQLITE_VERSION "3.53.3" -#define SQLITE_VERSION_NUMBER 3053003 -#define SQLITE_SOURCE_ID "2026-06-26 20:14:12 d4c0e51e4aeb96955b99185ab9cde75c339e2c29c3f3f12428d364a10d782c62" +#define SQLITE_VERSION "3.53.4" +#define SQLITE_VERSION_NUMBER 3053004 +#define SQLITE_SOURCE_ID "2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc" #define SQLITE_SCM_BRANCH "branch-3.53" -#define SQLITE_SCM_TAGS "release version-3.53.3" -#define SQLITE_SCM_DATETIME "2026-06-26T20:14:12.354Z" +#define SQLITE_SCM_TAGS "release version-3.53.4" +#define SQLITE_SCM_DATETIME "2026-07-24T19:02:57.525Z" /* ** CAPI3REF: Run-Time Library Version Numbers @@ -27592,7 +27592,7 @@ SQLITE_PRIVATE int sqlite3OsCurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *p }else{ double r; rc = pVfs->xCurrentTime(pVfs, &r); - *pTimeOut = (sqlite3_int64)(r*86400000.0); + *pTimeOut = sqlite3RealToI64(r*86400000.0); } return rc; } @@ -60970,7 +60970,7 @@ static int readSuperJournal(sqlite3_file *pJrnl, u64 nSuper, char **pzSuper){ cksum -= zOut[u]; } } - if( rc!=SQLITE_OK || cksum ){ + if( rc!=SQLITE_OK || cksum || zOut[0]==0 ){ /* If the checksum doesn't add up, then one or more of the disk sectors ** containing the super-journal filename is corrupted. This means ** definitely roll back, so just return SQLITE_OK and report a (nul) @@ -111438,11 +111438,20 @@ static SQLITE_NOINLINE void resolveSetExprSubtypeArg(ExprList *pList){ nn = pList ? pList->nExpr : 0; for(ii=0; iia[ii].pExpr; - ExprSetProperty(pExpr, EP_SubtArg); - if( pExpr->op==TK_SELECT ){ - assert( ExprUseXSelect(pExpr) ); - assert( pExpr->x.pSelect!=0 ); - resolveSetExprSubtypeArg(pExpr->x.pSelect->pEList); + while( 1 /*exit-by-break*/ ){ + ExprSetProperty(pExpr, EP_SubtArg); + if( pExpr->op==TK_SELECT ){ + assert( ExprUseXSelect(pExpr) ); + assert( pExpr->x.pSelect!=0 ); + resolveSetExprSubtypeArg(pExpr->x.pSelect->pEList); + break; + } + if( pExpr->op==TK_UPLUS ){ + pExpr = pExpr->pLeft; + assert( pExpr!=0 ); + }else{ + break; + } } } } @@ -176955,7 +176964,7 @@ static void nth_valueStepFunc( break; case SQLITE_FLOAT: { double fVal = sqlite3_value_double(apArg[1]); - if( ((i64)fVal)!=fVal ) goto error_out; + if( sqlite3RealToI64(fVal)!=fVal ) goto error_out; iVal = (i64)fVal; break; } @@ -210777,8 +210786,8 @@ static int fts3StringAppend( ** to grow the buffer until so that it is big enough to accommodate the ** appended data. */ - if( pStr->n+nAppend+1>=pStr->nAlloc ){ - sqlite3_int64 nAlloc = pStr->nAlloc+(sqlite3_int64)nAppend+100; + if( (i64)pStr->n+(i64)nAppend+1>=(i64)pStr->nAlloc ){ + i64 nAlloc = pStr->nAlloc+(i64)nAppend+100; char *zNew = sqlite3_realloc64(pStr->z, nAlloc); if( !zNew ){ return SQLITE_NOMEM; @@ -214982,7 +214991,8 @@ static u32 jsonTranslateBlobToText( if( sz==0 ) goto malformed_jsonb; if( zIn[0]=='-' ){ jsonAppendChar(pOut, '-'); - k++; + if( sz<=1 ) goto malformed_jsonb; + k = 1; } if( zIn[k]=='.' ){ jsonAppendChar(pOut, '0'); @@ -217946,7 +217956,9 @@ static int jsonSkipLabel(JsonEachCursor *p){ if( p->eType==JSONB_OBJECT ){ u32 sz = 0; u32 n = jsonbPayloadSize(&p->sParse, p->i, &sz); - return p->i + n + sz; + sz += p->i + n; + if( sz >= p->sParse.nBlob ) sz = p->i; + return sz; }else{ return p->i; } @@ -226907,8 +226919,8 @@ static int rbuDeltaApply( int lenDelta, /* Length of the delta */ char *zOut /* Write the output into this preallocated buffer */ ){ - unsigned int limit; - unsigned int total = 0; + sqlite3_uint64 limit; + sqlite3_uint64 total = 0; #if RBU_ENABLE_DELTA_CKSUM char *zOrigOut = zOut; #endif @@ -226918,8 +226930,8 @@ static int rbuDeltaApply( /* ERROR: size integer not terminated by "\n" */ return -1; } - zDelta++; lenDelta--; - while( *zDelta && lenDelta>0 ){ + zDelta++; lenDelta--; /* Skip the \n */ + while( lenDelta>0 && zDelta[0] ){ unsigned int cnt, ofst; cnt = rbuDeltaGetInt(&zDelta, &lenDelta); if( lenDelta<=0 ) return -1; @@ -226927,7 +226939,7 @@ static int rbuDeltaApply( case '@': { zDelta++; lenDelta--; ofst = rbuDeltaGetInt(&zDelta, &lenDelta); - if( lenDelta>0 || zDelta[0]!=',' ){ + if( lenDelta>0 && zDelta[0]!=',' ){ /* ERROR: copy command not terminated by ',' */ return -1; } @@ -226952,7 +226964,7 @@ static int rbuDeltaApply( /* ERROR: insert command gives an output larger than predicted */ return -1; } - if( (i64)cnt>(i64)lenDelta ){ + if( cnt>lenDelta ){ /* ERROR: insert count exceeds size of delta */ return -1; } @@ -228965,13 +228977,13 @@ static int rbuGetUpdateStmt( char *zUpdate = 0; pUp->zMask = (char*)&pUp[1]; - memcpy(pUp->zMask, zMask, pIter->nTblCol); pUp->pNext = pIter->pRbuUpdate; pIter->pRbuUpdate = pUp; if( zSet ){ const char *zPrefix = ""; - + assert( p->rc==SQLITE_OK ); + memcpy(pUp->zMask, zMask, pIter->nTblCol); if( pIter->eType!=RBU_PK_VTAB ) zPrefix = "rbu_imp_"; zUpdate = sqlite3_mprintf("UPDATE \"%s%w\" SET %s WHERE %s", zPrefix, pIter->zTbl, zSet, zWhere @@ -229061,6 +229073,9 @@ static RbuState *rbuLoadState(sqlite3rbu *p){ case RBU_STATE_ROW: pRet->nRow = sqlite3_column_int(pStmt, 1); + if( pRet->nRow<0 ){ + rc = SQLITE_CORRUPT; + } break; case RBU_STATE_PROGRESS: @@ -240297,7 +240312,12 @@ static int sessionChangesetToHash( pIter->in.bNoDiscard = 1; while( SQLITE_ROW==(sessionChangesetNext(pIter, &aRec, &nRec, 0)) ){ - rc = sessionOneChangeIterToHash(pGrp, pIter, bRebase); + if( bRebase && pIter->bPatchset ){ + /* A patchset may not be used as a rebase */ + rc = SQLITE_ERROR; + }else{ + rc = sessionOneChangeIterToHash(pGrp, pIter, bRebase); + } if( rc!=SQLITE_OK ) break; } @@ -240674,13 +240694,14 @@ static void sessionAppendPartialUpdate( int i; u8 *a1 = aRec; u8 *a2 = aChange; + u8 *a2Eof = &a2[nChange]; *pOut++ = SQLITE_UPDATE; *pOut++ = pIter->bIndirect; for(i=0; inCol; i++){ int n1 = sessionSerialLen(a1); - int n2 = sessionSerialLen(a2); - if( pIter->abPK[i] || a2[0]==0 ){ + int n2 = (a2>=a2Eof) ? 0 : sessionSerialLen(a2); + if( n2<=0 || pIter->abPK[i] || a2[0]==0 ){ if( !pIter->abPK[i] && a1[0] ) bData = 1; memcpy(pOut, a1, n1); pOut += n1; @@ -240881,8 +240902,8 @@ SQLITE_API int sqlite3rebaser_configure( sqlite3_rebaser *p, int nRebase, const void *pRebase ){ - sqlite3_changeset_iter *pIter = 0; /* Iterator opened on pData/nData */ int rc; /* Return code */ + sqlite3_changeset_iter *pIter = 0; /* Iterator opened on pData/nData */ rc = sqlite3changeset_start(&pIter, nRebase, (void*)pRebase); if( rc==SQLITE_OK ){ rc = sessionChangesetToHash(pIter, &p->grp, 1); @@ -251702,6 +251723,7 @@ static Fts5Data *fts5DataRead(Fts5Index *p, i64 iRowid){ pRet = (Fts5Data*)sqlite3_malloc64(nAlloc); if( pRet ){ pRet->nn = nByte; + pRet->szLeaf = 0; aOut = pRet->p = (u8*)pRet + szData; }else{ rc = SQLITE_NOMEM; @@ -251714,10 +251736,8 @@ static Fts5Data *fts5DataRead(Fts5Index *p, i64 iRowid){ sqlite3_free(pRet); pRet = 0; }else{ - /* TODO1: Fix this */ pRet->p[nByte] = 0x00; pRet->p[nByte+1] = 0x00; - pRet->szLeaf = fts5GetU16(&pRet->p[2]); } } p->rc = rc; @@ -251738,9 +251758,17 @@ static void fts5DataRelease(Fts5Data *pData){ sqlite3_free(pData); } +/* +** Read a leaf-page record. This is similar to fts5DataRead(), except that +** it fills in the Fts5Data.szLeaf value before returning. +*/ static Fts5Data *fts5LeafRead(Fts5Index *p, i64 iRowid){ Fts5Data *pRet = fts5DataRead(p, iRowid); if( pRet ){ + assert( pRet->szLeaf==0 ); + if( pRet->nn>=4 ){ + pRet->szLeaf = fts5GetU16(&pRet->p[2]); + } if( pRet->szLeaf<4 || pRet->szLeaf>pRet->nn ){ FTS5_CORRUPT_ROWID(p, iRowid); fts5DataRelease(pRet); @@ -253650,6 +253678,10 @@ static void fts5SegIterNextInit( pIter->iPgidxOff = pIter->pLeaf->szLeaf; pIter->iPgidxOff += fts5GetVarint32(&a[pIter->iPgidxOff], iTermOff); + if( iTermOff > pIter->pLeaf->szLeaf ){ + p->rc = FTS5_CORRUPT; + return; + } pIter->iLeafOffset = iTermOff; fts5SegIterLoadTerm(p, pIter, 0); fts5SegIterLoadNPos(p, pIter); @@ -255976,7 +256008,7 @@ static void fts5SecureDeleteOverflow( int iNext = 0; u8 *aPg = 0; - pLeaf = fts5DataRead(p, iRowid); + pLeaf = fts5LeafRead(p, iRowid); if( pLeaf==0 ) break; aPg = pLeaf->p; @@ -255984,7 +256016,7 @@ static void fts5SecureDeleteOverflow( if( iNext!=0 ){ *pbLastInDoclist = 0; } - if( iNext==0 && pLeaf->szLeaf!=pLeaf->nn ){ + if( iNext==0 && pLeaf->szLeafnn ){ fts5GetVarint32(&aPg[pLeaf->szLeaf], iNext); } @@ -256271,7 +256303,7 @@ static void fts5DoSecureDelete( /* The entry being removed may be the only position list in ** its doclist. */ for(iPgno=pSeg->iLeafPgno-1; iPgno>pSeg->iTermLeafPgno; iPgno-- ){ - Fts5Data *pPg = fts5DataRead(p, FTS5_SEGMENT_ROWID(iSegid, iPgno)); + Fts5Data *pPg = fts5LeafRead(p, FTS5_SEGMENT_ROWID(iSegid, iPgno)); int bEmpty = (pPg && pPg->nn==4); fts5DataRelease(pPg); if( bEmpty==0 ) break; @@ -256279,7 +256311,7 @@ static void fts5DoSecureDelete( if( iPgno==pSeg->iTermLeafPgno ){ i64 iId = FTS5_SEGMENT_ROWID(iSegid, pSeg->iTermLeafPgno); - Fts5Data *pTerm = fts5DataRead(p, iId); + Fts5Data *pTerm = fts5LeafRead(p, iId); if( pTerm && pTerm->szLeaf==pSeg->iTermLeafOffset ){ u8 *aTermIdx = &pTerm->p[pTerm->szLeaf]; int nTermIdx = pTerm->nn - pTerm->szLeaf; @@ -259231,7 +259263,7 @@ static void fts5IndexIntegrityCheckEmpty( /* Now check that the iter.nEmpty leaves following the current leaf ** (a) exist and (b) contain no terms. */ for(i=iFirst; p->rc==SQLITE_OK && i<=iLast; i++){ - Fts5Data *pLeaf = fts5DataRead(p, FTS5_SEGMENT_ROWID(pSeg->iSegid, i)); + Fts5Data *pLeaf = fts5LeafRead(p, FTS5_SEGMENT_ROWID(pSeg->iSegid, i)); if( pLeaf ){ if( !fts5LeafIsTermless(pLeaf) || (i>=iNoRowid && 0!=fts5LeafFirstRowidOff(pLeaf)) @@ -259359,7 +259391,7 @@ static void fts5IndexIntegrityCheckSegment( FTS5_CORRUPT_ROWID(p, iRow); }else{ iOff += fts5GetVarint32(&pLeaf->p[iOff], nTerm); - if( iOff+nTerm>pLeaf->szLeaf ){ + if( (i64)iOff+(i64)nTerm>(i64)pLeaf->szLeaf ){ FTS5_CORRUPT_ROWID(p, iRow); }else{ res = fts5Memcmp(&pLeaf->p[iOff], zIdxTerm, MIN(nTerm, nIdxTerm)); @@ -263087,19 +263119,23 @@ static int fts5ApiPhraseFirstColumn( if( pConfig->eDetail==FTS5_DETAIL_COLUMNS ){ Fts5Sorter *pSorter = pCsr->pSorter; - int n; - if( pSorter ){ - int i1 = (iPhrase==0 ? 0 : pSorter->aIdx[iPhrase-1]); - n = pSorter->aIdx[iPhrase] - i1; - pIter->a = &pSorter->aPoslist[i1]; + if( iPhrase<0 || iPhrase>=sqlite3Fts5ExprPhraseCount(pCsr->pExpr) ){ + rc = SQLITE_RANGE; }else{ - rc = sqlite3Fts5ExprPhraseCollist(pCsr->pExpr, iPhrase, &pIter->a, &n); - } - if( rc==SQLITE_OK ){ - assert( pIter->a || n==0 ); - pIter->b = (pIter->a ? &pIter->a[n] : 0); - *piCol = 0; - fts5ApiPhraseNextColumn(pCtx, pIter, piCol); + int n; + if( pSorter ){ + int i1 = (iPhrase==0 ? 0 : pSorter->aIdx[iPhrase-1]); + n = pSorter->aIdx[iPhrase] - i1; + pIter->a = &pSorter->aPoslist[i1]; + }else{ + rc = sqlite3Fts5ExprPhraseCollist(pCsr->pExpr, iPhrase, &pIter->a, &n); + } + if( rc==SQLITE_OK ){ + assert( pIter->a || n==0 ); + pIter->b = (pIter->a ? &pIter->a[n] : 0); + *piCol = 0; + fts5ApiPhraseNextColumn(pCtx, pIter, piCol); + } } }else{ int n; @@ -264007,7 +264043,7 @@ static void fts5SourceIdFunc( ){ assert( nArg==0 ); UNUSED_PARAM2(nArg, apUnused); - sqlite3_result_text(pCtx, "fts5: 2026-06-26 20:14:12 d4c0e51e4aeb96955b99185ab9cde75c339e2c29c3f3f12428d364a10d782c62", -1, SQLITE_TRANSIENT); + sqlite3_result_text(pCtx, "fts5: 2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc", -1, SQLITE_TRANSIENT); } /* diff --git a/sqlite3-binding.h b/sqlite3-binding.h index 9417bce8..aecd0f83 100644 --- a/sqlite3-binding.h +++ b/sqlite3-binding.h @@ -147,12 +147,12 @@ extern "C" { ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ -#define SQLITE_VERSION "3.53.3" -#define SQLITE_VERSION_NUMBER 3053003 -#define SQLITE_SOURCE_ID "2026-06-26 20:14:12 d4c0e51e4aeb96955b99185ab9cde75c339e2c29c3f3f12428d364a10d782c62" +#define SQLITE_VERSION "3.53.4" +#define SQLITE_VERSION_NUMBER 3053004 +#define SQLITE_SOURCE_ID "2026-07-24 19:02:57 bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc" #define SQLITE_SCM_BRANCH "branch-3.53" -#define SQLITE_SCM_TAGS "release version-3.53.3" -#define SQLITE_SCM_DATETIME "2026-06-26T20:14:12.354Z" +#define SQLITE_SCM_TAGS "release version-3.53.4" +#define SQLITE_SCM_DATETIME "2026-07-24T19:02:57.525Z" /* ** CAPI3REF: Run-Time Library Version Numbers From 485f31f6cafa26bd27052c4e87074e86e96af703 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 16:34:28 +0900 Subject: [PATCH 21/24] Add workflow to create releases on tag push --- .github/workflows/release.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..2984088a --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: Create release + runs-on: ubuntu-latest + steps: + - name: Create release with generated notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + echo "Release $GITHUB_REF_NAME already exists, skipping" + exit 0 + fi + gh release create "$GITHUB_REF_NAME" \ + --repo "$GITHUB_REPOSITORY" \ + --title "${GITHUB_REF_NAME#v}" \ + --verify-tag \ + --generate-notes From a93a2092a3603a78f8add560eb03e7190b9e32eb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 29 Jul 2026 16:44:34 +0900 Subject: [PATCH 22/24] Fail closed on release lookup errors and serialize runs per tag --- .github/workflows/release.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2984088a..5db21a25 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,6 +8,10 @@ on: permissions: contents: write +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: release: name: Create release @@ -17,9 +21,15 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + set -euo pipefail + if output=$(gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" 2>&1); then echo "Release $GITHUB_REF_NAME already exists, skipping" exit 0 + elif ! grep -qi "release not found" <<< "$output"; then + # A lookup failure other than "not found" (auth, network, API) + # must not fall through to the create step. + echo "$output" >&2 + exit 1 fi gh release create "$GITHUB_REF_NAME" \ --repo "$GITHUB_REPOSITORY" \ From e27b5e9fc34ea20ee959fe5757ccd7c19a464e40 Mon Sep 17 00:00:00 2001 From: Fabio Canesin Date: Mon, 17 Aug 2026 04:15:21 -0300 Subject: [PATCH 23/24] fix: keep vtab handle registered when xDestroy fails SQLite retains p->pVtab when xDestroy returns an error and later calls xDisconnect on the same object (during sqlite3VtabUnlock or close). Deleting the Go-side handle on the xDestroy error path made that later xDisconnect panic with a nil type assertion in goVRelease. Release the handle only after a successful xDestroy (or any xDisconnect, where SQLite always discards the object). Found in the 0xCarbon fork's SQLCipher sync; regression test panics on the previous code. --- sqlite3_opt_vtable.go | 11 +++- sqlite3_opt_vtable_destroy_test.go | 85 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 sqlite3_opt_vtable_destroy_test.go diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index b2b2404b..90a02564 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -423,8 +423,15 @@ 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. + if err != nil && isDestroy == 1 { + // SQLite retains p->pVtab when xDestroy fails and later calls + // xDisconnect on the same object; keep the handle registered so + // that call can resolve it (a missing handle panics above). + return mPrintf("%s", err.Error()) + } + // On xDisconnect (success or error) SQLite discards the vtab object, + // and on successful xDestroy it clears p->pVtab; release the handle + // in both cases. deleteHandle(pVTab) if err != nil { return mPrintf("%s", err.Error()) diff --git a/sqlite3_opt_vtable_destroy_test.go b/sqlite3_opt_vtable_destroy_test.go new file mode 100644 index 00000000..2470b001 --- /dev/null +++ b/sqlite3_opt_vtable_destroy_test.go @@ -0,0 +1,85 @@ +//go:build sqlite_vtable || vtable +// +build sqlite_vtable vtable + +package sqlite3 + +import ( + "database/sql" + "errors" + "fmt" + "path/filepath" + "sync/atomic" + "testing" +) + +type destroyErrModule struct{} + +type destroyErrVTab struct{} + +func (m destroyErrModule) Create(c *SQLiteConn, args []string) (VTab, error) { + if err := c.DeclareVTab("CREATE TABLE x(test TEXT)"); err != nil { + return nil, err + } + return &destroyErrVTab{}, nil +} +func (m destroyErrModule) Connect(c *SQLiteConn, args []string) (VTab, error) { + return m.Create(c, args) +} +func (m destroyErrModule) DestroyModule() {} + +func (v *destroyErrVTab) BestIndex(cst []InfoConstraint, ob []InfoOrderBy) (*IndexResult, error) { + return &IndexResult{Used: make([]bool, len(cst)), EstimatedCost: 100}, nil +} +func (v *destroyErrVTab) Disconnect() error { return nil } +func (v *destroyErrVTab) Destroy() error { return errors.New("boom: destroy refused") } +func (v *destroyErrVTab) Open() (VTabCursor, error) { + return &destroyErrCursor{}, nil +} + +type destroyErrCursor struct{} + +func (c *destroyErrCursor) Close() error { return nil } +func (c *destroyErrCursor) Filter(idxNum int, idxStr string, vals []any) error { + return nil +} +func (c *destroyErrCursor) Next() error { return nil } +func (c *destroyErrCursor) EOF() bool { return true } +func (c *destroyErrCursor) Column(*SQLiteContext, int) error { return nil } +func (c *destroyErrCursor) Rowid() (int64, error) { return 0, nil } + +var destroyErrDriverSeq int32 + +// TestVTabDestroyErrorThenClose: when xDestroy fails, SQLite retains the +// vtab object and later calls xDisconnect on it during close. The handle +// must stay registered for that call; deleting it on the error path made +// Close panic with a failed type assertion. +func TestVTabDestroyErrorThenClose(t *testing.T) { + // Unique driver name so repeated runs (e.g. -count=2) do not panic on + // duplicate registration. + driverName := fmt.Sprintf("sqlite3_destroy_err_%d", atomic.AddInt32(&destroyErrDriverSeq, 1)) + sql.Register(driverName, &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + return conn.CreateModule("boom", destroyErrModule{}) + }, + }) + db, err := sql.Open(driverName, filepath.Join(t.TempDir(), "vtab.db")) + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec("CREATE VIRTUAL TABLE vtab USING boom()"); err != nil { + t.Fatal(err) + } + rows, err := db.Query("SELECT * FROM vtab") + if err != nil { + t.Fatal(err) + } + rows.Close() + + if _, err := db.Exec("DROP TABLE vtab"); err == nil { + t.Fatal("DROP TABLE unexpectedly succeeded with an erroring Destroy") + } + // Must not panic. + if err := db.Close(); err != nil { + t.Logf("close err: %v", err) + } +} From 50bd7a456aecc70f19be8a06986eb61472960558 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Tue, 25 Aug 2026 07:34:45 -0400 Subject: [PATCH 24/24] Remove GitHub workflows file --- .github/workflows/release.yaml | 38 ---------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 5db21a25..00000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false - -jobs: - release: - name: Create release - runs-on: ubuntu-latest - steps: - - name: Create release with generated notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - if output=$(gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" 2>&1); then - echo "Release $GITHUB_REF_NAME already exists, skipping" - exit 0 - elif ! grep -qi "release not found" <<< "$output"; then - # A lookup failure other than "not found" (auth, network, API) - # must not fall through to the create step. - echo "$output" >&2 - exit 1 - fi - gh release create "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --title "${GITHUB_REF_NAME#v}" \ - --verify-tag \ - --generate-notes