Repository: mattn/go-sqlite3
Pull request #1298 (2024-11-22 20:47:47)

Add ability to set an int64 file control
Similar to https://github.com/mattn/go-sqlite3/pull/1000,

We are using `vfs=memdb` and would like to use the `SQLITE_FCNTL_SIZE_LIMIT` opcode with `sqlite3_file_control`:

> The [SQLITE_FCNTL_SIZE_LIMIT](https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlsizelimit) opcode is used by in-memory VFS that implements [sqlite3_deserialize()](https://www.sqlite.org/c3ref/deserialize.html) to set an upper bound on the size of the in-memory database. The argument is a pointer to a [sqlite3_int64](https://www.sqlite.org/c3ref/int64.html). If the integer pointed to is negative, then it is filled in with the current limit. Otherwise the limit is set to the larger of the value of the integer pointed to and the current database size. The integer pointed to is set to the new limit.

Calling the existing `SetFileControlInt` method when go decides that `int`s are `int64` based on your architecture seem to overflow the `C.int` and in our case set the size limit of our in-memory VFS to 0. This adds a new `SetFileControlInt64` to be used with op codes that expect a `sqlite3_int64`.

rittneje: > Calling the existing SetFileControlInt method when go decides that ints are int64 based on your architecture seem to overflow the C.int and in our case set the size limit of our in-memory VFS to 0.

I filed #1299 for this bug.
jonstacks: CIFuzz failure was caused by https://github.com/mattn/go-sqlite3/pull/1300. I'll rebase this and hopefully that fixes the issue 🤞 

Patch:
diff --git a/README.md b/README.md
index 1804a89..3b43b03 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ This package follows the official [Golang Release Policy](https://golang.org/doc
   - [Android](#android)
 - [ARM](#arm)
 - [Cross Compile](#cross-compile)
-- [Google Cloud Platform](#google-cloud-platform)
+- [Compiling](#compiling)
   - [Linux](#linux)
     - [Alpine](#alpine)
     - [Fedora](#fedora)
@@ -70,7 +70,6 @@ This package can be installed with the `go get` command:
 
 _go-sqlite3_ is *cgo* package.
 If you want to build your app using go-sqlite3, you need gcc.
-However, after you have built and installed _go-sqlite3_ with `go install github.com/mattn/go-sqlite3` (which requires gcc), you can build your app without relying on gcc in future.
 
 ***Important: because this is a `CGO` enabled package, you are required to set the environment variable `CGO_ENABLED=1` and have a `gcc` compiler present within your path.***
 
@@ -228,11 +227,7 @@ Steps:
 
 Please refer to the project's [README](https://github.com/FiloSottile/homebrew-musl-cross#readme) for further information.
 
-# Google Cloud Platform
-
-Building on GCP is not possible because Google Cloud Platform does not allow `gcc` to be executed.
-
-Please work only with compiled final binaries.
+# Compiling
 
 ## Linux
 
diff --git a/callback.go b/callback.go
index b794bcd..0c518fa 100644
--- a/callback.go
+++ b/callback.go
@@ -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
 }
 
diff --git a/sqlite3.go b/sqlite3.go
index ce985ec..a967cab 100644
--- a/sqlite3.go
+++ b/sqlite3.go
@@ -381,7 +381,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.
@@ -393,12 +393,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 {
@@ -1876,6 +1876,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 == "" {
@@ -1893,6 +1896,34 @@ func (c *SQLiteConn) SetFileControlInt(dbName string, op int, arg int) 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()
@@ -2008,14 +2039,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
 }
@@ -2112,24 +2141,28 @@ 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
 }
 
@@ -2137,9 +2170,9 @@ func (rc *SQLiteRows) Close() error {
 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)))
 		}
 	}
@@ -2149,7 +2182,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))))
 		}
 	}
diff --git a/sqlite3_opt_unlock_notify.c b/sqlite3_opt_unlock_notify.c
index fc37b33..3a00f43 100644
--- a/sqlite3_opt_unlock_notify.c
+++ b/sqlite3_opt_unlock_notify.c
@@ -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);
 
diff --git a/sqlite3_opt_unlock_notify.go b/sqlite3_opt_unlock_notify.go
index 76f7bbf..3ac8050 100644
--- a/sqlite3_opt_unlock_notify.go
+++ b/sqlite3_opt_unlock_notify.go
@@ -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);
 */


Test patch:
diff --git a/sqlite3_test.go b/sqlite3_test.go
index 63c939d..ed8fa64 100644
--- a/sqlite3_test.go
+++ b/sqlite3_test.go
@@ -1864,6 +1864,32 @@ func TestSetFileControlInt(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 {
@@ -2111,6 +2137,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 {
@@ -2568,3 +2595,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)
+			}
+		}
+	})
+}