diff --git a/sqlite3.go b/sqlite3.go index 31b2735..e037857 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -297,6 +297,51 @@ const ( /*SQLITE_RECURSIVE = C.SQLITE_RECURSIVE*/ ) +// Standard File Control Opcodes +// See: https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html +const ( + SQLITE_FCNTL_LOCKSTATE = int(1) + SQLITE_FCNTL_GET_LOCKPROXYFILE = int(2) + SQLITE_FCNTL_SET_LOCKPROXYFILE = int(3) + SQLITE_FCNTL_LAST_ERRNO = int(4) + SQLITE_FCNTL_SIZE_HINT = int(5) + SQLITE_FCNTL_CHUNK_SIZE = int(6) + SQLITE_FCNTL_FILE_POINTER = int(7) + SQLITE_FCNTL_SYNC_OMITTED = int(8) + SQLITE_FCNTL_WIN32_AV_RETRY = int(9) + SQLITE_FCNTL_PERSIST_WAL = int(10) + SQLITE_FCNTL_OVERWRITE = int(11) + SQLITE_FCNTL_VFSNAME = int(12) + SQLITE_FCNTL_POWERSAFE_OVERWRITE = int(13) + SQLITE_FCNTL_PRAGMA = int(14) + SQLITE_FCNTL_BUSYHANDLER = int(15) + SQLITE_FCNTL_TEMPFILENAME = int(16) + SQLITE_FCNTL_MMAP_SIZE = int(18) + SQLITE_FCNTL_TRACE = int(19) + SQLITE_FCNTL_HAS_MOVED = int(20) + SQLITE_FCNTL_SYNC = int(21) + SQLITE_FCNTL_COMMIT_PHASETWO = int(22) + SQLITE_FCNTL_WIN32_SET_HANDLE = int(23) + SQLITE_FCNTL_WAL_BLOCK = int(24) + SQLITE_FCNTL_ZIPVFS = int(25) + SQLITE_FCNTL_RBU = int(26) + SQLITE_FCNTL_VFS_POINTER = int(27) + SQLITE_FCNTL_JOURNAL_POINTER = int(28) + SQLITE_FCNTL_WIN32_GET_HANDLE = int(29) + SQLITE_FCNTL_PDB = int(30) + SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = int(31) + SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = int(32) + SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = int(33) + SQLITE_FCNTL_LOCK_TIMEOUT = int(34) + SQLITE_FCNTL_DATA_VERSION = int(35) + SQLITE_FCNTL_SIZE_LIMIT = int(36) + SQLITE_FCNTL_CKPT_DONE = int(37) + SQLITE_FCNTL_RESERVE_BYTES = int(38) + SQLITE_FCNTL_CKPT_START = int(39) + SQLITE_FCNTL_EXTERNAL_READER = int(40) + SQLITE_FCNTL_CKSM_FILE = int(41) +) + // SQLiteDriver implements driver.Driver. type SQLiteDriver struct { Extensions []string @@ -1813,6 +1858,31 @@ func (c *SQLiteConn) SetLimit(id int, newVal int) int { return int(C._sqlite3_limit(c.db, C.int(id), C.int(newVal))) } +// SetFileControlInt 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. +// +// 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 == "" { + dbName = "main" + } + + cDBName := C.CString(dbName) + defer C.free(unsafe.Pointer(cDBName)) + + cArg := C.int(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() diff --git a/sqlite3_test.go b/sqlite3_test.go index c5deb3e..c86aba4 100644 --- a/sqlite3_test.go +++ b/sqlite3_test.go @@ -3,6 +3,7 @@ // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. +//go:build cgo // +build cgo package sqlite3 @@ -1722,6 +1723,43 @@ func TestAuthorizer(t *testing.T) { } } +func TestSetFileControlInt(t *testing.T) { + t.Run("PERSIST_WAL", func(t *testing.T) { + tempFilename := TempFilename(t) + defer os.Remove(tempFilename) + + sql.Register("sqlite3_FCNTL_PERSIST_WAL", &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + if err := conn.SetFileControlInt("", SQLITE_FCNTL_PERSIST_WAL, 1); err != nil { + return fmt.Errorf("Unexpected error from SetFileControlInt(): %w", err) + } + return nil + }, + }) + + db, err := sql.Open("sqlite3_FCNTL_PERSIST_WAL", tempFilename) + if err != nil { + t.Fatal("Failed to open database:", err) + } + defer db.Close() + + // Set to WAL mode & write a page. + if _, err := db.Exec(`PRAGMA journal_mode = wal`); err != nil { + t.Fatal("Failed to set journal mode:", err) + } else if _, err := db.Exec(`CREATE TABLE t (x)`); err != nil { + t.Fatal("Failed to create table:", err) + } + if err := db.Close(); err != nil { + t.Fatal("Failed to close database", err) + } + + // Ensure WAL file persists after close. + if _, err := os.Stat(tempFilename + "-wal"); err != nil { + t.Fatal("Expected WAL file to be persisted after close", err) + } + }) +} + func TestNonColumnString(t *testing.T) { db, err := sql.Open("sqlite3", ":memory:") if err != nil {