From 66e0f1a529239252c4f0f43d9086895013a18c53 Mon Sep 17 00:00:00 2001 From: Reynold PJ Date: Fri, 15 May 2026 15:07:48 +0000 Subject: [PATCH] add generic _pragma URI parameter support Implements the _pragma DSN parameter (repeatable, executed in declaration order on every connection) to fix connection-pool PRAGMA persistence. Matches the interface already supported by modernc and ncruces drivers. Co-Authored-By: Claude Sonnet 4.6 --- sqlite3.go | 12 ++++++++ sqlite3_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/sqlite3.go b/sqlite3.go index b9deb72a..5c990e42 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -1206,6 +1206,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { writableSchema := -1 vfsName := "" var cacheSize *int64 + var pragmas []string stmtCacheSize := 0 pos := strings.IndexRune(dsn, '?') @@ -1295,6 +1296,9 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { } } + // _pragma (generic, repeatable — executed in order on every connection) + pragmas = params["_pragma"] + // Busy Timeout (_busy_timeout) // // https://www.sqlite.org/pragma.html#pragma_busy_timeout @@ -1880,6 +1884,14 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { } } + // Generic _pragma parameters — run last so they override specific defaults + for _, p := range pragmas { + if err := exec(fmt.Sprintf("PRAGMA %s;", p)); err != nil { + C.sqlite3_close_v2(db) + return nil, err + } + } + if len(d.Extensions) > 0 { if err := conn.loadExtensions(d.Extensions); err != nil { conn.Close() diff --git a/sqlite3_test.go b/sqlite3_test.go index ac81458c..0ace51d5 100644 --- a/sqlite3_test.go +++ b/sqlite3_test.go @@ -2131,6 +2131,87 @@ func BenchmarkCustomFunctions(b *testing.B) { } } +func TestGenericPragma(t *testing.T) { + // Single _pragma applied correctly + t.Run("single", func(t *testing.T) { + tempFilename := TempFilename(t) + defer os.Remove(tempFilename) + db, err := sql.Open("sqlite3", tempFilename+"?_pragma=synchronous(NORMAL)") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + var mode int + if err := db.QueryRow("PRAGMA synchronous").Scan(&mode); err != nil { + t.Fatal(err) + } + if mode != 1 { // 1 = NORMAL + t.Errorf("expected synchronous=1 (NORMAL), got %d", mode) + } + }) + + // Multiple _pragma values applied in order + t.Run("multiple", func(t *testing.T) { + tempFilename := TempFilename(t) + defer os.Remove(tempFilename) + db, err := sql.Open("sqlite3", tempFilename+"?_pragma=synchronous(NORMAL)&_pragma=busy_timeout(1234)") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + var sync, timeout int + if err := db.QueryRow("PRAGMA synchronous").Scan(&sync); err != nil { + t.Fatal(err) + } + if err := db.QueryRow("PRAGMA busy_timeout").Scan(&timeout); err != nil { + t.Fatal(err) + } + if sync != 1 { + t.Errorf("expected synchronous=1 (NORMAL), got %d", sync) + } + if timeout != 1234 { + t.Errorf("expected busy_timeout=1234, got %d", timeout) + } + }) + + // PRAGMAs are re-applied on every connection the pool opens + t.Run("pool_reapply", func(t *testing.T) { + tempFilename := TempFilename(t) + defer os.Remove(tempFilename) + db, err := sql.Open("sqlite3", tempFilename+"?_pragma=synchronous(NORMAL)") + if err != nil { + t.Fatal(err) + } + defer db.Close() + // Force the pool to open multiple distinct connections + db.SetMaxOpenConns(4) + + const workers = 4 + errc := make(chan error, workers) + for i := 0; i < workers; i++ { + go func() { + var mode int + if err := db.QueryRow("PRAGMA synchronous").Scan(&mode); err != nil { + errc <- err + return + } + if mode != 1 { + errc <- fmt.Errorf("connection got synchronous=%d, want 1 (NORMAL)", mode) + return + } + errc <- nil + }() + } + for i := 0; i < workers; i++ { + if err := <-errc; err != nil { + t.Error(err) + } + } + }) +} + func TestSuite(t *testing.T) { initializeTestDB(t) defer freeTestDB()