diff --git a/src/sqlite4clj/core.clj b/src/sqlite4clj/core.clj index c245a85..c8460c9 100644 --- a/src/sqlite4clj/core.clj +++ b/src/sqlite4clj/core.clj @@ -224,6 +224,32 @@ :pragma (merge pragma writer-pragma) :vfs vfs :default-result-set-fn default-result-set-fn}) + ;; Defensive: force -wal/-shm files to materialize before opening + ;; the read-only reader pool. On a fresh WAL database the writer + ;; pool sets journal_mode=WAL but no -shm/-wal files exist yet + ;; (no write has happened). Per SQLite WAL docs §5, a + ;; SQLITE_OPEN_READONLY connection can attach to a WAL DB only if + ;; -shm/-wal already exist or the database is opened immutable. + ;; macOS's stock sqlite3 3.51.0 demonstrates this deterministically: + ;; + ;; $ sqlite3 fresh.db "PRAGMA journal_mode=WAL;" + ;; $ sqlite3 -readonly fresh.db "PRAGMA cache_size=15625;" + ;; Error: in prepare, unable to open database file (14) + ;; + ;; The bundled SQLite 3.51.3 in this library is more permissive in + ;; isolated tests, but the failure has been observed in production + ;; on macOS — likely a timing/VFS interaction that does not + ;; reliably reproduce in a fresh JVM. A no-op write tx here is + ;; the cheapest way to satisfy the docs' condition (1) regardless + ;; of which SQLite/OS quirks are in play. + _ (when-not (= ":memory:" url) + (let [conn-pool (:conn-pool writer) + conn (BlockingQueue/.take conn-pool)] + (try + (q* conn ["BEGIN IMMEDIATE"] default-result-set-fn) + (q* conn ["COMMIT"] default-result-set-fn) + (finally + (BlockingQueue/.offer conn-pool conn))))) ;; Pool of read connections reader (if (= ":memory:" url) writer diff --git a/test/sqlite4clj/core_test.clj b/test/sqlite4clj/core_test.clj index 4b96561..317fba4 100644 --- a/test/sqlite4clj/core_test.clj +++ b/test/sqlite4clj/core_test.clj @@ -124,3 +124,23 @@ ["select length(data) from raw_bytes where id = 1"])] (is (= (seq payload) (seq stored-bytes))) (is (= (inc (alength payload)) stored-length))))))) + +(deftest init-db-on-fresh-wal-database + (testing "init-db! works on a brand-new file with default WAL pragmas. + + This is a smoke test, not a strict regression test: the fresh-WAL failure + this change defends against (see the comment in init-db!) does not + reliably reproduce against the bundled SQLite in a fresh JVM, even though + it has been observed in production on macOS and is reproducible at the + SQLite CLI level (3.51.0). This test just verifies that the warmup tx + doesn't break the normal happy path." + (let [path "test-data/fresh-wal.db"] + (clojure.java.io/delete-file (clojure.java.io/file path) true) + (clojure.java.io/delete-file (clojure.java.io/file (str path "-shm")) true) + (clojure.java.io/delete-file (clojure.java.io/file (str path "-wal")) true) + (with-db [db (d/init-db! path {:pool-size 2})] + (is (some? (:writer db))) + (is (some? (:reader db))) + (d/q (:writer db) ["CREATE TABLE t (x INT)"]) + (d/q (:writer db) ["INSERT INTO t VALUES (1)"]) + (is (= [1] (d/q (:reader db) ["SELECT x FROM t"])))))))