diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5751bc..14590d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: - name: Test GORM run: gormlite/test.sh - - name: Test coverage + - name: Collect coverage run: | go install github.com/dave/courtney@latest courtney diff --git a/config.go b/config.go index 0342be7..84f0881 100644 --- a/config.go +++ b/config.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ncruces/go-sqlite3/internal/util" + "github.com/ncruces/go-sqlite3/vfs" "github.com/tetratelabs/wazero/api" ) @@ -56,6 +57,99 @@ func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) { } } +// FileControl allows low-level control of database files. +// Only a subset of opcodes are supported. +// +// https://sqlite.org/c3ref/file_control.html +func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, error) { + defer c.arena.mark()() + + var schemaPtr uint32 + if schema != "" { + schemaPtr = c.arena.string(schema) + } + + switch op { + case FCNTL_RESET_CACHE: + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), 0) + return nil, c.error(r) + + case FCNTL_PERSIST_WAL, FCNTL_POWERSAFE_OVERWRITE: + var flag int + switch { + case len(arg) == 0: + flag = -1 + case arg[0]: + flag = 1 + } + ptr := c.arena.new(4) + util.WriteUint32(c.mod, ptr, uint32(flag)) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + return util.ReadUint32(c.mod, ptr) != 0, c.error(r) + + case FCNTL_CHUNK_SIZE: + ptr := c.arena.new(4) + util.WriteUint32(c.mod, ptr, uint32(arg[0].(int))) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + return nil, c.error(r) + + case FCNTL_RESERVE_BYTES: + bytes := -1 + if len(arg) > 0 { + bytes = arg[0].(int) + } + ptr := c.arena.new(4) + util.WriteUint32(c.mod, ptr, uint32(bytes)) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + return int(util.ReadUint32(c.mod, ptr)), c.error(r) + + case FCNTL_DATA_VERSION: + ptr := c.arena.new(4) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + return util.ReadUint32(c.mod, ptr), c.error(r) + + case FCNTL_LOCKSTATE: + ptr := c.arena.new(4) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + return vfs.LockLevel(util.ReadUint32(c.mod, ptr)), c.error(r) + + case FCNTL_VFS_POINTER: + ptr := c.arena.new(4) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + const zNameOffset = 16 + ptr = util.ReadUint32(c.mod, ptr) + ptr = util.ReadUint32(c.mod, ptr+zNameOffset) + name := util.ReadString(c.mod, ptr, _MAX_NAME) + return vfs.Find(name), c.error(r) + + case FCNTL_FILE_POINTER, FCNTL_JOURNAL_POINTER: + ptr := c.arena.new(4) + r := c.call("sqlite3_file_control", + uint64(c.handle), uint64(schemaPtr), + uint64(op), uint64(ptr)) + const fileHandleOffset = 4 + ptr = util.ReadUint32(c.mod, ptr) + ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset) + return util.GetHandle(c.ctx, ptr), c.error(r) + } + + return nil, MISUSE +} + // Limit allows the size of various constructs to be // limited on a connection by connection basis. // diff --git a/conn.go b/conn.go index 39870b1..78efc0a 100644 --- a/conn.go +++ b/conn.go @@ -227,7 +227,6 @@ func (c *Conn) Filename(schema string) *vfs.Filename { defer c.arena.mark()() ptr = c.arena.string(schema) } - r := c.call("sqlite3_db_filename", uint64(c.handle), uint64(ptr)) return vfs.OpenFilename(c.ctx, c.mod, uint32(r), vfs.OPEN_MAIN_DB) } diff --git a/const.go b/const.go index 2bb5365..a3bd395 100644 --- a/const.go +++ b/const.go @@ -229,6 +229,24 @@ const ( DBCONFIG_REVERSE_SCANORDER DBConfig = 1019 ) +// FcntlOpcode are the available opcodes for [Conn.FileControl]. +// +// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html +type FcntlOpcode uint32 + +const ( + FCNTL_LOCKSTATE FcntlOpcode = 1 + FCNTL_CHUNK_SIZE FcntlOpcode = 6 + FCNTL_FILE_POINTER FcntlOpcode = 7 + FCNTL_PERSIST_WAL FcntlOpcode = 10 + FCNTL_POWERSAFE_OVERWRITE FcntlOpcode = 13 + FCNTL_VFS_POINTER FcntlOpcode = 27 + FCNTL_JOURNAL_POINTER FcntlOpcode = 28 + FCNTL_DATA_VERSION FcntlOpcode = 35 + FCNTL_RESERVE_BYTES FcntlOpcode = 38 + FCNTL_RESET_CACHE FcntlOpcode = 42 +) + // LimitCategory are the available run-time limit categories. // // https://sqlite.org/c3ref/c_limit_attached.html diff --git a/embed/exports.txt b/embed/exports.txt index b3cb158..f9c4761 100644 --- a/embed/exports.txt +++ b/embed/exports.txt @@ -66,6 +66,7 @@ sqlite3_errmsg sqlite3_error_offset sqlite3_errstr sqlite3_exec +sqlite3_file_control sqlite3_filename_database sqlite3_filename_journal sqlite3_filename_wal diff --git a/embed/sqlite3.wasm b/embed/sqlite3.wasm index cc57767..4700bbf 100755 Binary files a/embed/sqlite3.wasm and b/embed/sqlite3.wasm differ diff --git a/tests/conn_test.go b/tests/conn_test.go index aba68c1..0b85f79 100644 --- a/tests/conn_test.go +++ b/tests/conn_test.go @@ -12,6 +12,7 @@ import ( "github.com/ncruces/go-sqlite3" _ "github.com/ncruces/go-sqlite3/embed" _ "github.com/ncruces/go-sqlite3/internal/testcfg" + "github.com/ncruces/go-sqlite3/vfs" _ "github.com/ncruces/go-sqlite3/vfs/memdb" ) @@ -337,6 +338,147 @@ func TestConn_ConfigLog(t *testing.T) { } } +func TestConn_FileControl(t *testing.T) { + t.Parallel() + + file := filepath.Join(t.TempDir(), "test.db") + db, err := sqlite3.Open(file) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + t.Run("MISUSE", func(t *testing.T) { + _, err := db.FileControl("main", 0) + if !errors.Is(err, sqlite3.MISUSE) { + t.Errorf("got %v, want MISUSE", err) + } + }) + t.Run("FCNTL_RESET_CACHE", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_RESET_CACHE) + if err != nil { + t.Fatal(err) + } + if o != nil { + t.Errorf("got %v, want nil", o) + } + }) + + t.Run("FCNTL_PERSIST_WAL", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_PERSIST_WAL) + if err != nil { + t.Fatal(err) + } + if o != false { + t.Errorf("got %v, want false", o) + } + + o, err = db.FileControl("", sqlite3.FCNTL_PERSIST_WAL, true) + if err != nil { + t.Fatal(err) + } + if o != true { + t.Errorf("got %v, want true", o) + } + + o, err = db.FileControl("", sqlite3.FCNTL_PERSIST_WAL) + if err != nil { + t.Fatal(err) + } + if o != true { + t.Errorf("got %v, want true", o) + } + }) + + t.Run("FCNTL_CHUNK_SIZE", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_CHUNK_SIZE, 1024*1024) + if !errors.Is(err, sqlite3.NOTFOUND) { + t.Errorf("got %v, want NOTFOUND", err) + } + if o != nil { + t.Errorf("got %v, want nil", o) + } + }) + + t.Run("FCNTL_RESERVE_BYTES", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_RESERVE_BYTES, 4) + if err != nil { + t.Fatal(err) + } + if o != 0 { + t.Errorf("got %v, want 0", o) + } + + o, err = db.FileControl("", sqlite3.FCNTL_RESERVE_BYTES) + if err != nil { + t.Fatal(err) + } + if o != 4 { + t.Errorf("got %v, want 4", o) + } + }) + + t.Run("FCNTL_DATA_VERSION", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_DATA_VERSION) + if err != nil { + t.Fatal(err) + } + if o != uint32(2) { + t.Errorf("got %v, want 2", o) + } + }) + + t.Run("FCNTL_VFS_POINTER", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_VFS_POINTER) + if err != nil { + t.Fatal(err) + } + if o != vfs.Find("os") { + t.Errorf("got %v, want os", o) + } + }) + + t.Run("FCNTL_FILE_POINTER", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_FILE_POINTER) + if err != nil { + t.Fatal(err) + } + if _, ok := o.(vfs.File); !ok { + t.Errorf("got %v, want File", o) + } + }) + + t.Run("FCNTL_JOURNAL_POINTER", func(t *testing.T) { + o, err := db.FileControl("", sqlite3.FCNTL_JOURNAL_POINTER) + if err != nil { + t.Fatal(err) + } + if o != nil { + t.Errorf("got %v, want nil", o) + } + }) + + t.Run("FCNTL_LOCKSTATE", func(t *testing.T) { + if !vfs.SupportsFileLocking { + t.Skip("skipping without locks") + } + + txn, err := db.BeginExclusive() + if err != nil { + t.Fatal(err) + } + defer txn.End(&err) + + o, err := db.FileControl("", sqlite3.FCNTL_LOCKSTATE) + if err != nil { + t.Fatal(err) + } + if o != vfs.LOCK_EXCLUSIVE { + t.Errorf("got %v, want LOCK_EXCLUSIVE", o) + } + }) +} + func TestConn_Limit(t *testing.T) { t.Parallel()