From 1fd986cc81137f408dd90bb52224453fb75060e6 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Tue, 11 Jun 2024 23:12:28 -0400 Subject: [PATCH] Support disabling checkpoint-on-close Because Go doesn't support calling varadic C functions, this required a wrapper. --- sqlite3.go | 16 +++++++++ sqlite3_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/sqlite3.go b/sqlite3.go index 4ff905b5..9a8a6788 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -84,6 +84,12 @@ _sqlite3_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) { return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT); } +static int +_sqlite3_db_config_no_ckpt_on_close(sqlite3 *db) { + int v; + return sqlite3_db_config(db, SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, 1, &v); +} + #include #include @@ -1898,6 +1904,16 @@ func (c *SQLiteConn) SetFileControlInt(dbName string, op int, arg int) error { return nil } +// DBConfigNoCkptOnClose disables checkpointing on database close. +// See http://sqlite.org/c3ref/db_config.html +func (c *SQLiteConn) DBConfigNoCkptOnClose() error { + rv := C._sqlite3_db_config_no_ckpt_on_close(c.db) + 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 63c939d3..5e2c8ca9 100644 --- a/sqlite3_test.go +++ b/sqlite3_test.go @@ -10,6 +10,7 @@ package sqlite3 import ( "bytes" + "context" "database/sql" "database/sql/driver" "errors" @@ -1864,6 +1865,92 @@ func TestSetFileControlInt(t *testing.T) { }) } +func TestDBConfigNoCkptOnClose(t *testing.T) { + fname := TempFilename(t) + defer os.Remove(fname) + db, err := sql.Open("sqlite3", fname) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + // Enable WAL mode. + if _, err := db.Exec(`PRAGMA journal_mode = wal`); err != nil { + t.Fatal(err) + } + + // Write some data. + _, err = db.Exec("create table foo (department integer, profits integer)") + if err != nil { + t.Fatal("Failed to create table:", err) + } + + // Confirm WAL file exists. + if _, err := os.Stat(fname + "-wal"); err != nil { + t.Fatal("Expected WAL file to exist", err) + } + + // Close the database, and confirm WAL file is removed. + if err := db.Close(); err != nil { + t.Fatal("Failed to close database", err) + } + if _, err := os.Stat(fname + "-wal"); err == nil { + t.Fatal("Expected WAL file to be removed after close") + } + + // Now do it again, but with the DBConfig option set. + db, err = sql.Open("sqlite3", fname) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + // Insert a record, confirm a WAL file appears. + if _, err := db.Exec(`insert into foo values (1, 2)`); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(fname + "-wal"); err != nil { + t.Fatal("Expected WAL file to exist", err) + } + + // Disable checkpoint-on-close. + f := func(driverConn interface{}) error { + c := driverConn.(*SQLiteConn) + return c.DBConfigNoCkptOnClose() + } + conn, err := db.Conn(context.Background()) + if err != nil { + t.Fatal(err) + } + if err := conn.Raw(f); err != nil { + t.Fatal(err) + } + + // Read the SQLite file into a byte slice for comparison later. + bufPre, err := os.ReadFile(fname) + if err != nil { + t.Fatal(err) + } + + // Close the database, and confirm WAL file is still present. + if err := db.Close(); err != nil { + t.Fatal("Failed to close database", err) + } + if _, err := os.Stat(fname + "-wal"); err != nil { + t.Fatal("Expected WAL file to be present after close", err) + } + + // Confirm the SQLite file is the same as before since no checkpoint + // was performed. + bufPost, err := os.ReadFile(fname) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(bufPre, bufPost) { + t.Fatal("Expected SQLite file to be unchanged after close") + } +} + func TestNonColumnString(t *testing.T) { db, err := sql.Open("sqlite3", ":memory:") if err != nil {