diff --git a/_example/fuzz/fuzz_openexec.go b/_example/fuzz/fuzz_openexec.go index 532604490..b36c9f281 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 } diff --git a/_example/hook/hook.go b/_example/hook/hook.go index 602318157..a728e3cf4 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) + } } diff --git a/_example/json/json.go b/_example/json/json.go index 08e75cd25..1c7c6c765 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) { diff --git a/_example/limit/limit.go b/_example/limit/limit.go index c1adfe812..4ac33c24f 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,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) @@ -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") diff --git a/_example/vtable/vtable.go b/_example/vtable/vtable.go index c65535b7b..8f5a957a6 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" ) @@ -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 } @@ -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 { diff --git a/_example/vtable_eponymous_only/main.go b/_example/vtable_eponymous_only/main.go index 17b58afac..134317e8e 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 9f22ebccc..d87c4d66a 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 } diff --git a/callback.go b/callback.go index 8ea17db9d..b7df2be7f 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-binding.c b/sqlite3-binding.c index 337af9e47..4a3437f57 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 9417bce8d..aecd0f838 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 diff --git a/sqlite3_opt_vtable.go b/sqlite3_opt_vtable.go index 9e916ea8c..90a025648 100644 --- a/sqlite3_opt_vtable.go +++ b/sqlite3_opt_vtable.go @@ -423,6 +423,16 @@ func goVRelease(pVTab unsafe.Pointer, isDestroy C.int) *C.char { } else { err = vt.vTab.Disconnect() } + 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()) } @@ -451,6 +461,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", "") } @@ -460,7 +473,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++ @@ -482,8 +499,23 @@ 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 if rows = int64(res.EstimatedRows); rows < 1 { + // A positive fractional estimate must not truncate to 0. + rows = 1 + } + info.estimatedRows = C.sqlite3_int64(rows) + } return nil } @@ -492,6 +524,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 +537,7 @@ func goVClose(pCursor unsafe.Pointer) *C.char { func goMDestroy(pClientData unsafe.Pointer) { m := lookupHandle(pClientData).(*sqliteModule) m.module.DestroyModule() + deleteHandle(pClientData) } //export goVFilter @@ -514,7 +550,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 { @@ -608,7 +651,15 @@ 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 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:]) + } } } @@ -708,5 +759,5 @@ func (c *SQLiteConn) CreateModule(moduleName string, module Module) error { } return nil } - return nil + return fmt.Errorf("sqlite3: CreateModule requires a non-nil module") } diff --git a/sqlite3_opt_vtable_destroy_test.go b/sqlite3_opt_vtable_destroy_test.go new file mode 100644 index 000000000..2470b0011 --- /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) + } +} diff --git a/sqlite3_opt_vtable_leak_test.go b/sqlite3_opt_vtable_leak_test.go new file mode 100644 index 000000000..d2b704779 --- /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) + } +} diff --git a/sqlite3_opt_vtable_test.go b/sqlite3_opt_vtable_test.go index 64511e23d..3afa155e7 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 { diff --git a/upgrade/upgrade.go b/upgrade/upgrade.go index 352d4fef7..4a6553165 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,41 +78,40 @@ 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 { 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 } - return nil + // 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 + } + + // Only remove the source once it has been merged successfully. + fmt.Printf("Removing: %s\n", src) + return os.Remove(src) } func main() { @@ -170,6 +175,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 +185,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 +211,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())) }