Reactive SQLite for Clojure and ClojureScript: register a query, get an atom; write through the engine; the atom always derefs to the query's fresh result. The architecture is posh's (for Datascript) on SQLite's preupdate hook — the full design lives in doc/intro.md.
JVM — one dependency, zero setup. The jar embeds SQLite's official JNI binding and precompiled natives for linux x64/aarch64, Apple Silicon and Windows; they extract and load at runtime.
;; deps.edn
{org.clojars.ebifriedrice/rsqlite {:mvn/version "0.2.0"}}
ClojureScript — the engine runs on
@sqlite.org/sqlite-wasm. You own the
npm install and the module initialization (it's your build
toolchain's import, and sqlite3InitModule runs once per app);
connect takes the initialized module:
npm install @sqlite.org/sqlite-wasm
;; shadow-cljs.edn :modules/:builds as usual; then in your code
(ns app (:require ["@sqlite.org/sqlite-wasm" :default init-sqlite3]
[ebifriedrice.rsqlite :as rsql]))
(defonce engine (atom nil))
(defn init! []
(-> (init-sqlite3)
(.then (fn [sqlite3] (reset! engine (rsql/connect {:sqlite3 sqlite3}))))))
connect accepts a uri string, an opts map, or nothing:
(rsql/connect) ; ":memory:" — in-memory
(rsql/connect "app.db") ; file
(rsql/connect "file:app.db?mode=memory&cache=shared") ; uri spellings
(rsql/connect {:uri "app.db" ; or any mix:
:schedule my-dispatch-fn ; flush dispatch override
:sqlite3 sqlite3-module}) ; cljs: the wasm module
(def eng (rsql/connect))
;; DDL and writes go through the engine — one statement per exec!
(rsql/exec! eng "create table person (id integer primary key, name text, age int)")
;; Register a query: trailing args bind positional ?s.
;; Returns a plain atom, already holding the current rows.
(def adults (rsql/q! eng "select id, name from person where age >= ?" 18))
@adults
;; => []
;; Recomputation is scheduled off the writing thread and the atom
;; updates asynchronously; deref and watch like any atom.
(add-watch adults :log (fn [_ _ old new] (println "adults:" old "->" new)))
(rsql/exec! eng "insert into person values (1, 'Ann', 33)")
;; ... async: @adults => [{"id" 1, "name" "Ann"}]
;; Invalidation is dependency-aware. Columns count wherever they
;; appear - select list, WHERE, joins - so this re-runs the query,
;; but the rows come back equal and nothing is delivered. A column
;; the query never mentions anywhere (add one and try) does not
;; re-run it at all.
(rsql/exec! eng "update person set age = ? where id = ?" 34 1)
;; Transactions: a vector of [sql & params] or bare strings.
;; One consolidated invalidation after COMMIT; rollback publishes
;; nothing (savepoints are checkpoint/restore-correct too).
(rsql/transact! eng
["insert into person values (2, 'Bea', 41)"]
["insert into person values (3, 'Cy', 7)"])
;; In tests you usually want deterministic reads:
(rsql/sync! eng) ; flush pending recomputes now
;; Reagent integration is just an atom deref in a component:
;; (defn adults-view [] [:div (count @adults)] ...)
;; the re-render happens when the atom changes, which only happens
;; when the rows changed.
;; Done with a subscription / the engine:
(rsql/unq! eng adults)
(rsql/close! eng)
Rows are maps keyed by column name (strings). JVM values are
long/double/String/byte[]/nil; on JS they are
numbers/strings/typed arrays (int64 beyond 2^53 loses precision).
The contract: writes go through the engine (exec!/transact!)
— the preupdate hook observes only its own connection; one
connection per engine (engines are not pools); queries are SELECTs
and multi-statement strings are not split (give transact! a
vector); DDL re-derives every dependency profile on the next
flush.
ROLLBACK TO savepoints included).sqlite_sequence staying invisible is
correct: internal bookkeeping must not invalidate your queries../script/native-setup.sh # build pinned org.sqlite.jni (needed for JVM tests)
clojure -T:build test # JVM suite
npm install
npx shadow-cljs compile test && node target/cljs-test.js # CLJS suite
clojure -T:build ci # assemble target/rsqlite-<version>.jar
script/native-setup.sh pins SQLite 3.54.1 (tag version-3.54.1,
sha3-verified) and builds the native library uniformly with CMake
System.loadLibrary becomes a NativeShim class so
loading is owned by ebifriedrice.rsqlite.natives (modern JVMs
snapshot java.library.path at startup, making a jar-bundled
library otherwise unreachable). See resources/NOTICE.Releases are tag-driven: git tag vX.Y.Z main && git push origin vX.Y.Z, and CI builds the natives matrix, embeds everything into
the jar, publishes to Clojars and attaches the jar to a GitHub
Release.
Eclipse Public License 2.0. The embedded SQLite artifacts are
public domain — see resources/NOTICE.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |