Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clj-kondo/inline-configs/sqlite4clj.core.clj/config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:lint-as {sqlite4clj.core/with-stmt-reset clojure.core/with-open, sqlite4clj.core/with-conn clojure.core/with-open, sqlite4clj.core/with-read-tx clojure.core/with-open, sqlite4clj.core/with-write-tx clojure.core/with-open}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:lint-as {sqlite4clj.test-common/with-db clojure.core/with-open}}
31 changes: 25 additions & 6 deletions src/sqlite4clj/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,22 @@
(api/sqlite3-limit pdb limit-id (int v))))
limits))

(defn pragma->set-pragma-query [pragma]
(conj (->> (merge default-pragma pragma)
(mapv (fn [[k v]] [(str "pragma " (name k) "=" v)])))))
(def ^:private read-only-skipped-pragmas
"Pragmas that can require a database header write when set. Converting the
journal mode to WAL, or setting a page size on an empty database, writes the
header, which fails on a connection opened with SQLITE_OPEN_READONLY.
Read-only connections skip them; the remaining pragmas are
connection-local and safe to set."
#{:journal_mode :page_size})

(defn pragma->set-pragma-query
([pragma]
(pragma->set-pragma-query pragma false))
([pragma read-only]
(->> (merge default-pragma pragma)
(remove (fn [[k _]]
(and read-only (contains? read-only-skipped-pragmas k))))
(mapv (fn [[k v]] [(str "pragma " (name k) "=" v)])))))

(defn no-unwrap-result-set-fn
[_col-metadata result-set]
Expand Down Expand Up @@ -206,7 +219,7 @@
:stmt-cache statement-cache
:default-result-set-fn default-result-set-fn}]
(set-limits conn limits)
(->> (pragma->set-pragma-query pragma)
(->> (pragma->set-pragma-query pragma read-only)
(run! #(q* conn % default-result-set-fn)))
conn))

Expand All @@ -233,8 +246,13 @@

(defn init-db!
"A db consists of a read pool of size :pool-size and a write pool of size 1.
The same pragma are set for both pools."
[url & [{:keys [pool-size pragma writer-pragma vfs
The same pragma are set for both pools.

When :read-only is true, every connection — the writer pool included —
opens with SQLITE_OPEN_READONLY, so writes through any connection fail at
the SQLite level. Read-only connections skip the :journal_mode and
:page_size pragmas, which can require a database header write."
[url & [{:keys [pool-size pragma writer-pragma read-only vfs
default-result-set-fn limits writer-limits]
:or {default-result-set-fn unwrap-result-set-fn
pool-size (Runtime/.availableProcessors
Expand All @@ -244,6 +262,7 @@
writer
(init-pool! url
{:pool-size 1
:read-only read-only
:pragma (merge pragma writer-pragma)
:limits (merge limits writer-limits)
:vfs vfs
Expand Down
30 changes: 29 additions & 1 deletion test/sqlite4clj/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
(:require
[clojure.test :refer [deftest is testing use-fixtures]]
[sqlite4clj.core :as d]
[sqlite4clj.test-common :refer [test-db test-fixture with-db]]))
[sqlite4clj.test-common :refer [test-db test-db-path test-fixture with-db]]))

(use-fixtures :once test-fixture)

Expand Down Expand Up @@ -140,3 +140,31 @@
#"too many attached databases - max 0"
(d/q (:reader db)
["ATTACH DATABASE 'test-data/test.db' AS other"]))))))

(deftest read-only-db-init
(testing "A db opened with :read-only true serves reads and rejects writes
through the writer pool."
(with-db [db (test-db)]
(d/q (:writer db) ["create table ro (id integer primary key, data text)"])
(d/q (:writer db) ["insert into ro (id, data) values (1, 'one')"]))
(with-db [db (d/init-db! test-db-path {:pool-size 2 :read-only true})]
(is (= [1] (d/q (:reader db) ["select id from ro"])))
(is (= [1] (d/q (:writer db) ["select id from ro"])))
(is (thrown-with-msg? clojure.lang.ExceptionInfo
#"readonly database"
(d/q (:writer db)
["insert into ro (id, data) values (2, 'two')"]))))))

(deftest read-only-opens-rollback-journal-dbs
(testing "Read-only connections skip journal_mode and page_size pragmas, so
a rollback-journal database opens read-only without error."
;; A file of its own: journal_mode=delete cannot be set on a database
;; with a leftover write-ahead log, and earlier tests leave the shared
;; test.db in WAL mode.
(let [path "test-data/rollback-test.db"]
(with-db [db (d/init-db! path {:pool-size 2 :pragma {:journal_mode "delete"}})]
(d/q (:writer db) ["create table ro_journal (id integer primary key)"])
(d/q (:writer db) ["insert into ro_journal (id) values (1)"]))
(with-db [db (d/init-db! path {:pool-size 2 :read-only true})]
(is (= [1] (d/q (:reader db) ["select id from ro_journal"])))
(is (= "delete" (first (d/q (:writer db) ["pragma journal_mode"]))))))))