Skip to content
3 changes: 2 additions & 1 deletion callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ func callbackRetText(ctx *C.sqlite3_context, v reflect.Value) error {
if v.Type().Kind() != reflect.String {
return fmt.Errorf("cannot convert %s to TEXT", v.Type())
}
C._sqlite3_result_text(ctx, C.CString(v.Interface().(string)))
cstr := C.CString(v.Interface().(string))
C._sqlite3_result_text(ctx, cstr)
return nil
}

Expand Down
13,600 changes: 8,510 additions & 5,090 deletions sqlite3-binding.c

Large diffs are not rendered by default.

494 changes: 344 additions & 150 deletions sqlite3-binding.h

Large diffs are not rendered by default.

79 changes: 56 additions & 23 deletions sqlite3.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ type SQLiteStmt struct {
s *C.sqlite3_stmt
t string
closed bool
cls bool
cls bool // True if the statement was created by SQLiteConn.Query
}

// SQLiteResult implements sql.Result.
Expand All @@ -408,12 +408,12 @@ type SQLiteResult struct {
// SQLiteRows implements driver.Rows.
type SQLiteRows struct {
s *SQLiteStmt
nc int
nc int32 // Number of columns
cls bool // True if we need to close the parent statement in Close
cols []string
decltype []string
cls bool
closed bool
ctx context.Context // no better alternative to pass context into Next() method
closemu sync.Mutex
}

type functionInfo struct {
Expand Down Expand Up @@ -1891,6 +1891,9 @@ func (c *SQLiteConn) SetLimit(id int, newVal int) int {
// This method is not thread-safe as the returned error code can be changed by
// another call if invoked concurrently.
//
// Use SetFileControlInt64 instead if the argument for the opcode is documented
// as a pointer to a sqlite3_int64.
//
// See: sqlite3_file_control, https://www.sqlite.org/c3ref/file_control.html
func (c *SQLiteConn) SetFileControlInt(dbName string, op int, arg int) error {
if dbName == "" {
Expand Down Expand Up @@ -1918,6 +1921,34 @@ func (c *SQLiteConn) DBConfigNoCkptOnClose() error {
return nil
}

// SetFileControlInt64 invokes the xFileControl method on a given database. The
// dbName is the name of the database. It will default to "main" if left blank.
// The op is one of the opcodes prefixed by "SQLITE_FCNTL_". The arg argument
// and return code are both opcode-specific. Please see the SQLite documentation.
//
// This method is not thread-safe as the returned error code can be changed by
// another call if invoked concurrently.
//
// Only use this method if the argument for the opcode is documented as a pointer
// to a sqlite3_int64.
//
// See: sqlite3_file_control, https://www.sqlite.org/c3ref/file_control.html
func (c *SQLiteConn) SetFileControlInt64(dbName string, op int, arg int64) error {
if dbName == "" {
dbName = "main"
}

cDBName := C.CString(dbName)
defer C.free(unsafe.Pointer(cDBName))

cArg := C.sqlite3_int64(arg)
rv := C.sqlite3_file_control(c.db, cDBName, C.int(op), unsafe.Pointer(&cArg))
if rv != C.SQLITE_OK {
return c.lastError()
}
return nil
}

// Close the statement.
func (s *SQLiteStmt) Close() error {
s.mu.Lock()
Expand Down Expand Up @@ -2033,14 +2064,12 @@ func (s *SQLiteStmt) query(ctx context.Context, args []driver.NamedValue) (drive

rows := &SQLiteRows{
s: s,
nc: int(C.sqlite3_column_count(s.s)),
nc: int32(C.sqlite3_column_count(s.s)),
cls: s.cls,
cols: nil,
decltype: nil,
cls: s.cls,
closed: false,
ctx: ctx,
}
runtime.SetFinalizer(rows, (*SQLiteRows).Close)

return rows, nil
}
Expand Down Expand Up @@ -2137,34 +2166,38 @@ func (s *SQLiteStmt) Readonly() bool {

// Close the rows.
func (rc *SQLiteRows) Close() error {
rc.s.mu.Lock()
if rc.s.closed || rc.closed {
rc.s.mu.Unlock()
rc.closemu.Lock()
defer rc.closemu.Unlock()
s := rc.s
if s == nil {
return nil
}
rc.s = nil // remove reference to SQLiteStmt
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
rc.closed = true
if rc.cls {
rc.s.mu.Unlock()
return rc.s.Close()
s.mu.Unlock()
return s.Close()
}
rv := C.sqlite3_reset(rc.s.s)
rv := C.sqlite3_reset(s.s)
if rv != C.SQLITE_OK {
rc.s.mu.Unlock()
return rc.s.c.lastError()
s.mu.Unlock()
return s.c.lastError()
}
rc.s.mu.Unlock()
rc.s = nil
runtime.SetFinalizer(rc, nil)
s.mu.Unlock()
return nil
}

// Columns return column names.
func (rc *SQLiteRows) Columns() []string {
rc.s.mu.Lock()
defer rc.s.mu.Unlock()
if rc.s.s != nil && rc.nc != len(rc.cols) {
if rc.s.s != nil && int(rc.nc) != len(rc.cols) {
rc.cols = make([]string, rc.nc)
for i := 0; i < rc.nc; i++ {
for i := 0; i < int(rc.nc); i++ {
rc.cols[i] = C.GoString(C.sqlite3_column_name(rc.s.s, C.int(i)))
}
}
Expand All @@ -2174,7 +2207,7 @@ func (rc *SQLiteRows) Columns() []string {
func (rc *SQLiteRows) declTypes() []string {
if rc.s.s != nil && rc.decltype == nil {
rc.decltype = make([]string, rc.nc)
for i := 0; i < rc.nc; i++ {
for i := 0; i < int(rc.nc); i++ {
rc.decltype[i] = strings.ToLower(C.GoString(C.sqlite3_column_decltype(rc.s.s, C.int(i))))
}
}
Expand Down
4 changes: 4 additions & 0 deletions sqlite3_opt_unlock_notify.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
#include <stdio.h>
#ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h"
#else
#include <sqlite3.h>
#endif

extern int unlock_notify_wait(sqlite3 *db);

Expand Down
4 changes: 4 additions & 0 deletions sqlite3_opt_unlock_notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ package sqlite3
#cgo CFLAGS: -DSQLITE_ENABLE_UNLOCK_NOTIFY
#include <stdlib.h>
#ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h"
#else
#include <sqlite3.h>
#endif
extern void unlock_notify_callback(void *arg, int argc);
*/
Expand Down
44 changes: 44 additions & 0 deletions sqlite3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1951,6 +1951,32 @@ func TestDBConfigNoCkptOnClose(t *testing.T) {
}
}

func TestSetFileControlInt64(t *testing.T) {
const GiB = 1024 * 1024 * 1024

t.Run("", func(t *testing.T) {

sql.Register("sqlite3_FCNTL_SIZE_LIMIT", &SQLiteDriver{
ConnectHook: func(conn *SQLiteConn) error {
if err := conn.SetFileControlInt64("", SQLITE_FCNTL_SIZE_LIMIT, 4*GiB); err != nil {
return fmt.Errorf("Unexpected error from SetFileControlInt64(): %w", err)
}
return nil
},
})

db, err := sql.Open("sqlite3", "file:/dbname?vfs=memdb")
if err != nil {
t.Fatal("Failed to open database:", err)
}
err = db.Ping()
if err != nil {
t.Fatal("Failed to ping", err)
}
db.Close()
})
}

func TestNonColumnString(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
Expand Down Expand Up @@ -2198,6 +2224,7 @@ var benchmarks = []testing.InternalBenchmark{
{Name: "BenchmarkStmt", F: benchmarkStmt},
{Name: "BenchmarkRows", F: benchmarkRows},
{Name: "BenchmarkStmtRows", F: benchmarkStmtRows},
{Name: "BenchmarkQueryParallel", F: benchmarkQueryParallel},
}

func (db *TestDB) mustExec(sql string, args ...any) sql.Result {
Expand Down Expand Up @@ -2655,3 +2682,20 @@ func benchmarkStmtRows(b *testing.B) {
}
}
}

func benchmarkQueryParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
db.SetMaxOpenConns(runtime.NumCPU())
defer db.Close()
var i int64
for pb.Next() {
if err := db.QueryRow("SELECT 1, 2, 3, 4").Scan(&i, &i, &i, &i); err != nil {
panic(err)
}
}
})
}
Loading