Skip to content

Commit ae2a61f

Browse files
benbjohnsonmattn
authored andcommitted
Add sqlite3_file_control() support
This commit adds the SQLiteConn.FileControlInt() method which calls the underlying sqlite3_file_control() function with an int argument. This can be used for low-level operations on SQLite databases such as persisting the WAL file after database close.
1 parent 671e666 commit ae2a61f

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

sqlite3.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,51 @@ const (
297297
/*SQLITE_RECURSIVE = C.SQLITE_RECURSIVE*/
298298
)
299299

300+
// Standard File Control Opcodes
301+
// See: https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html
302+
const (
303+
SQLITE_FCNTL_LOCKSTATE = int(1)
304+
SQLITE_FCNTL_GET_LOCKPROXYFILE = int(2)
305+
SQLITE_FCNTL_SET_LOCKPROXYFILE = int(3)
306+
SQLITE_FCNTL_LAST_ERRNO = int(4)
307+
SQLITE_FCNTL_SIZE_HINT = int(5)
308+
SQLITE_FCNTL_CHUNK_SIZE = int(6)
309+
SQLITE_FCNTL_FILE_POINTER = int(7)
310+
SQLITE_FCNTL_SYNC_OMITTED = int(8)
311+
SQLITE_FCNTL_WIN32_AV_RETRY = int(9)
312+
SQLITE_FCNTL_PERSIST_WAL = int(10)
313+
SQLITE_FCNTL_OVERWRITE = int(11)
314+
SQLITE_FCNTL_VFSNAME = int(12)
315+
SQLITE_FCNTL_POWERSAFE_OVERWRITE = int(13)
316+
SQLITE_FCNTL_PRAGMA = int(14)
317+
SQLITE_FCNTL_BUSYHANDLER = int(15)
318+
SQLITE_FCNTL_TEMPFILENAME = int(16)
319+
SQLITE_FCNTL_MMAP_SIZE = int(18)
320+
SQLITE_FCNTL_TRACE = int(19)
321+
SQLITE_FCNTL_HAS_MOVED = int(20)
322+
SQLITE_FCNTL_SYNC = int(21)
323+
SQLITE_FCNTL_COMMIT_PHASETWO = int(22)
324+
SQLITE_FCNTL_WIN32_SET_HANDLE = int(23)
325+
SQLITE_FCNTL_WAL_BLOCK = int(24)
326+
SQLITE_FCNTL_ZIPVFS = int(25)
327+
SQLITE_FCNTL_RBU = int(26)
328+
SQLITE_FCNTL_VFS_POINTER = int(27)
329+
SQLITE_FCNTL_JOURNAL_POINTER = int(28)
330+
SQLITE_FCNTL_WIN32_GET_HANDLE = int(29)
331+
SQLITE_FCNTL_PDB = int(30)
332+
SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = int(31)
333+
SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = int(32)
334+
SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = int(33)
335+
SQLITE_FCNTL_LOCK_TIMEOUT = int(34)
336+
SQLITE_FCNTL_DATA_VERSION = int(35)
337+
SQLITE_FCNTL_SIZE_LIMIT = int(36)
338+
SQLITE_FCNTL_CKPT_DONE = int(37)
339+
SQLITE_FCNTL_RESERVE_BYTES = int(38)
340+
SQLITE_FCNTL_CKPT_START = int(39)
341+
SQLITE_FCNTL_EXTERNAL_READER = int(40)
342+
SQLITE_FCNTL_CKSM_FILE = int(41)
343+
)
344+
300345
// SQLiteDriver implements driver.Driver.
301346
type SQLiteDriver struct {
302347
Extensions []string
@@ -1813,6 +1858,31 @@ func (c *SQLiteConn) SetLimit(id int, newVal int) int {
18131858
return int(C._sqlite3_limit(c.db, C.int(id), C.int(newVal)))
18141859
}
18151860

1861+
// SetFileControlInt invokes the xFileControl method on a given database. The
1862+
// dbName is the name of the database. It will default to "main" if left blank.
1863+
// The op is one of the opcodes prefixed by "SQLITE_FCNTL_". The arg argument
1864+
// and return code are both opcode-specific. Please see the SQLite documentation.
1865+
//
1866+
// This method is not thread-safe as the returned error code can be changed by
1867+
// another call if invoked concurrently.
1868+
//
1869+
// See: sqlite3_file_control, https://www.sqlite.org/c3ref/file_control.html
1870+
func (c *SQLiteConn) SetFileControlInt(dbName string, op int, arg int) error {
1871+
if dbName == "" {
1872+
dbName = "main"
1873+
}
1874+
1875+
cDBName := C.CString(dbName)
1876+
defer C.free(unsafe.Pointer(cDBName))
1877+
1878+
cArg := C.int(arg)
1879+
rv := C.sqlite3_file_control(c.db, cDBName, C.int(op), unsafe.Pointer(&cArg))
1880+
if rv != C.SQLITE_OK {
1881+
return c.lastError()
1882+
}
1883+
return nil
1884+
}
1885+
18161886
// Close the statement.
18171887
func (s *SQLiteStmt) Close() error {
18181888
s.mu.Lock()

sqlite3_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Use of this source code is governed by an MIT-style
44
// license that can be found in the LICENSE file.
55

6+
//go:build cgo
67
// +build cgo
78

89
package sqlite3
@@ -1722,6 +1723,43 @@ func TestAuthorizer(t *testing.T) {
17221723
}
17231724
}
17241725

1726+
func TestSetFileControlInt(t *testing.T) {
1727+
t.Run("PERSIST_WAL", func(t *testing.T) {
1728+
tempFilename := TempFilename(t)
1729+
defer os.Remove(tempFilename)
1730+
1731+
sql.Register("sqlite3_FCNTL_PERSIST_WAL", &SQLiteDriver{
1732+
ConnectHook: func(conn *SQLiteConn) error {
1733+
if err := conn.SetFileControlInt("", SQLITE_FCNTL_PERSIST_WAL, 1); err != nil {
1734+
return fmt.Errorf("Unexpected error from SetFileControlInt(): %w", err)
1735+
}
1736+
return nil
1737+
},
1738+
})
1739+
1740+
db, err := sql.Open("sqlite3_FCNTL_PERSIST_WAL", tempFilename)
1741+
if err != nil {
1742+
t.Fatal("Failed to open database:", err)
1743+
}
1744+
defer db.Close()
1745+
1746+
// Set to WAL mode & write a page.
1747+
if _, err := db.Exec(`PRAGMA journal_mode = wal`); err != nil {
1748+
t.Fatal("Failed to set journal mode:", err)
1749+
} else if _, err := db.Exec(`CREATE TABLE t (x)`); err != nil {
1750+
t.Fatal("Failed to create table:", err)
1751+
}
1752+
if err := db.Close(); err != nil {
1753+
t.Fatal("Failed to close database", err)
1754+
}
1755+
1756+
// Ensure WAL file persists after close.
1757+
if _, err := os.Stat(tempFilename + "-wal"); err != nil {
1758+
t.Fatal("Expected WAL file to be persisted after close", err)
1759+
}
1760+
})
1761+
}
1762+
17251763
func TestNonColumnString(t *testing.T) {
17261764
db, err := sql.Open("sqlite3", ":memory:")
17271765
if err != nil {

0 commit comments

Comments
 (0)