Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions cgosqlite/cgosqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@ func (db *DB) DisableFunction(name string, numArgs int) error {
return errCode(C.ts_sqlite3_disable_function(db.db, cName, C.int(numArgs)))
}

func (db *DB) FileControlInt(dbName string, op sqliteh.FileControlOp, arg *int) error {
var cDB *C.char
if dbName != "" {
cDB = C.CString(dbName)
defer C.free(unsafe.Pointer(cDB))
}
if arg == nil {
return errCode(C.sqlite3_file_control(db.db, cDB, C.int(op), nil))
}
cArg := C.int(*arg)
res := C.sqlite3_file_control(db.db, cDB, C.int(op), unsafe.Pointer(&cArg))
*arg = int(cArg)
return errCode(res)
}

func (stmt *Stmt) DBHandle() sqliteh.DB {
cdb := C.sqlite3_db_handle(stmt.stmt)
if cdb != nil {
Expand Down
29 changes: 29 additions & 0 deletions sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,35 @@ func DisableFunction(sqlconn SQLConn, name string, numArgs int) error {
})
}

// fileControlInt calls sqlite3_file_control on the underlying connection.
func fileControlInt(sqlconn SQLConn, dbName string, op sqliteh.FileControlOp, arg *int) error {
return sqlconn.Raw(func(driverConn any) error {
c, ok := driverConn.(*conn)
if !ok {
return fmt.Errorf("sqlite.FileControl: sql.Conn is not the sqlite driver: %T", driverConn)
}
return c.db.FileControlInt(dbName, op, arg)
})
}

// SetReserveBytes sets the number of reserved bytes at the end of each
// database page, using sqlite3_file_control with SQLITE_FCNTL_RESERVE_BYTES opcode.
func SetReserveBytes(sqlconn SQLConn, dbName string, bytes int) error {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As our higher-level interface to the underlying sqlite3_file_control method, maybe this method could also call the VACUUM for us?

if bytes < 0 || bytes > 255 {
return fmt.Errorf("reserved bytes must be between 0 and 255, got %d", bytes)
}
return fileControlInt(sqlconn, dbName, sqliteh.SQLITE_FCNTL_RESERVE_BYTES, &bytes)
}

// GetReserveBytes gets the number of reserved bytes at the end of each
// database page.
func GetReserveBytes(sqlconn SQLConn, dbName string) (int, error) {
var bytes int
bytes = -1 // pass a negative value to query the current setting.
err := fileControlInt(sqlconn, dbName, sqliteh.SQLITE_FCNTL_RESERVE_BYTES, &bytes)
return bytes, err
}

// WithPersist makes a ctx instruct the sqlite driver to persist a prepared query.
//
// This should be used with recurring queries to avoid constant parsing and
Expand Down
64 changes: 64 additions & 0 deletions sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
"database/sql/driver"
"expvar"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
Expand Down Expand Up @@ -1625,3 +1627,65 @@ func TestExpandedSQL(t *testing.T) {
t.Errorf("wrong sql: got %q, want %q", got, want)
}
}

func TestFileControlReserveBytes(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
db, err := sql.Open("sqlite3", "file:"+dbPath)
if err != nil {
t.Fatal(err)
}

conn, err := db.Conn(context.Background())
if err != nil {
t.Fatal(err)
}

if _, err := db.Exec("CREATE TABLE t (id INTEGER)"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("INSERT INTO t (id) VALUES (1);"); err != nil {
t.Fatal(err)
}

if got, err := GetReserveBytes(conn, "main"); err != nil {
t.Fatal(err)
} else if got != 0 {
t.Fatalf("initial reserved bytes=%d, want 0", got)
}

reserve := 16
if err := SetReserveBytes(conn, "", reserve); err != nil {
t.Fatal(err)
}

if err := ExecScript(conn, "VACUUM;"); err != nil {
t.Fatal(err)
}

if got, err := GetReserveBytes(conn, "main"); err != nil {
t.Fatal(err)
} else if got != reserve {
t.Fatalf("reserved bytes=%d, want %d", got, reserve)
}

if err := conn.Close(); err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}

header := make([]byte, 100)
file, err := os.Open(dbPath)
if err != nil {
t.Fatal(err)
}
defer file.Close()
if _, err := io.ReadFull(file, header); err != nil {
t.Fatal(err)
}
if got := int(header[20]); got != reserve {
t.Fatalf("reserved bytes header=%d, want %d", got, reserve)
}
}
10 changes: 10 additions & 0 deletions sqliteh/sqliteh.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ type DB interface {
// function's signature.
//
DisableFunction(name string, numArgs int) error
// FileControlInt is sqlite3_file_control for opcodes that take an
// integer argument.
FileControlInt(dbName string, op FileControlOp, arg *int) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dbName might deserve a type with enumerated const values for the pre-defined names main and temp.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I am not quite following. It's a common argument for many functions here - do you think FileControlInt calls for a custom type specifically? I imagine the most common value for dbName will be empty string (which defaults to the main db).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I hadn't noticed this pattern was already used elsewhere in the API.

}

// Stmt is an sqlite3_stmt* database connection object.
Expand Down Expand Up @@ -351,6 +354,13 @@ func (o OpenFlags) String() string {
return string(flags)
}

// FileControlOp is an opcode for sqlite3_file_control.
type FileControlOp int

const (
SQLITE_FCNTL_RESERVE_BYTES FileControlOp = 38
)

// Checkpoint is a WAL checkpoint mode.
// It is used by sqlite3_wal_checkpoint_v2.
//
Expand Down
Loading