Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7d534a0
Release vtable and cursor handles when SQLite destroys them
mattn Jul 28, 2026
1f0b756
Do not clobber SQLite's default cost estimates in BestIndex
mattn Jul 28, 2026
c2ecdac
Translate SQL NULL filter arguments to nil like goVUpdate
mattn Jul 28, 2026
1a1bc9e
Identify updated row by argv 0 in goVUpdate
mattn Jul 29, 2026
518016c
Ignore Used for constraints SQLite marked not usable
mattn Jul 29, 2026
cad8b71
Reject nil module and nil BestIndex result
mattn Jul 29, 2026
6443b7e
Fix off-by-one truncating SQL in fuzz target
mattn Jul 29, 2026
322330f
Fail upgrade tool on download and write errors
mattn Jul 29, 2026
3b8cebf
Fix wrong results and cursor state sharing in series example
mattn Jul 29, 2026
baab344
Use the table name from xCreate args in vtable example
mattn Jul 29, 2026
80cf381
Close leaked rows in hook example
mattn Jul 29, 2026
084f3d3
Close prepared statement and fail if limit is not enforced in limit e…
mattn Jul 29, 2026
88a4797
Handle NULL and BLOB values in json example Scan
mattn Jul 29, 2026
f65452a
Use unique driver name in leak test
mattn Jul 29, 2026
3babc1f
Merge pull request #1429 from mattn/fix-vtable-handle-leak
mattn Jul 29, 2026
2897b83
Clamp fractional row estimates to at least 1
mattn Jul 29, 2026
b330252
Merge pull request #1430 from mattn/fix-vtable-estimated-cost
mattn Jul 29, 2026
a39da35
Merge pull request #1431 from mattn/fix-vtable-filter-null
mattn Jul 29, 2026
726af39
Reject rowid-changing updates on virtual tables
mattn Jul 29, 2026
eba871f
Merge pull request #1432 from mattn/fix-vtable-update-rowid
mattn Jul 29, 2026
2730212
Merge pull request #1433 from mattn/fix-vtable-bestindex-usable
mattn Jul 29, 2026
fadf4a7
Merge pull request #1434 from mattn/fix-vtable-nil-checks
mattn Jul 29, 2026
6ebb665
Close merge destination before removing the source
mattn Jul 29, 2026
df9873f
Merge pull request #1435 from mattn/fix-upgrade-error-handling
mattn Jul 29, 2026
2195c5d
Merge pull request #1436 from mattn/fix-fuzz-off-by-one
mattn Jul 29, 2026
e664cc6
Merge pull request #1437 from mattn/fix-series-vtable-example
mattn Jul 29, 2026
c0861de
Quote table name before interpolating into DeclareVTab
mattn Jul 29, 2026
dccd45f
Merge pull request #1438 from mattn/fix-vtable-example-declare
mattn Jul 29, 2026
210e260
Merge pull request #1439 from mattn/fix-hook-example-rows-leak
mattn Jul 29, 2026
18f2ec3
Assert the expected too many SQL variables error
mattn Jul 29, 2026
c1a1466
Merge pull request #1440 from mattn/fix-limit-example-stmt-leak
mattn Jul 29, 2026
18308a8
Upgrade SQLite to version 3053004
mattn Jul 29, 2026
cc41b8c
Merge pull request #1442 from mattn/sqlite-amalgamation-3053004
mattn Jul 29, 2026
9415230
Merge pull request #1441 from mattn/fix-json-example-scan
mattn Jul 29, 2026
485f31f
Add workflow to create releases on tag push
mattn Jul 29, 2026
a93a209
Fail closed on release lookup errors and serialize runs per tag
mattn Jul 29, 2026
f9fc7aa
Merge pull request #1443 from mattn/add-release-workflow
mattn Jul 30, 2026
e27b5e9
fix: keep vtab handle registered when xDestroy fails
canesin Aug 17, 2026
58c8e14
Merge pull request #1446 from 0xCarbon/fix/vtable-destroy-handle
mattn Aug 17, 2026
ef27dfa
Merge remote-tracking branch 'mattn/master' into merge-upstream
otoolep Aug 25, 2026
50bd7a4
Remove GitHub workflows file
otoolep Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _example/fuzz/fuzz_openexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 7 additions & 3 deletions _example/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
12 changes: 11 additions & 1 deletion _example/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
15 changes: 8 additions & 7 deletions _example/limit/limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -89,11 +90,13 @@ 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")
}
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
sqlite3conn.SetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, bigLimitVariableNumber)
Expand All @@ -104,9 +107,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")
Expand Down
13 changes: 12 additions & 1 deletion _example/vtable/vtable.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/mattn/go-sqlite3"
)
Expand All @@ -20,13 +21,20 @@ 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)
}
tableName := `"` + strings.ReplaceAll(args[2], `"`, `""`) + `"`
err := c.DeclareVTab(fmt.Sprintf(`
CREATE TABLE %s (
id INT,
full_name TEXT,
description TEXT,
html_url TEXT
)`, args[0]))
)`, tableName))
if err != nil {
return nil, err
}
Expand All @@ -49,6 +57,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 {
Expand Down
30 changes: 21 additions & 9 deletions _example/vtable_eponymous_only/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
93 changes: 61 additions & 32 deletions _example/vtable_eponymous_only/vtable.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"errors"
"fmt"
"strings"

"github.com/mattn/go-sqlite3"
)
Expand All @@ -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) {
Expand All @@ -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
}
Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}

Expand Down
21 changes: 21 additions & 0 deletions callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading