Liking cljdoc? Tell your friends :D

rsqlite

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.

Getting connected

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

Using it

(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.

Guarantees

  • No uncommitted visibility — recomputes are scheduled only after a statement leaves autocommit, and read committed state.
  • No rollback publication — rolled-back mutations never produce results (ROLLBACK TO savepoints included).
  • The writer is never blocked — hooks only append cheap event data; recomputation and notification run off the writing thread (background thread on the JVM, microtask on JS).
  • Change-only delivery — results are diffed; equal rows deliver nothing.
  • Failure retention — a failing re-evaluation keeps the last published result and retries on the next flush.
  • Eventual convergence — after writes stop, every subscription reflects the latest committed state.

Boundaries by design

  • Column references bind to tables (through aliases and CTE bodies; single-table queries are exact); bare references in multi-table scopes resolve against the schema when unambiguous, and conservatively match any queried table otherwise.
  • Virtual tables are invisible to invalidation (SQLite's hooks do not fire for them), and sqlite_sequence staying invisible is correct: internal bookkeeping must not invalidate your queries.
  • WITHOUT ROWID tables produce events without rowids; matching is table/column-shaped, so they still invalidate correctly.
  • On JS, integers beyond 2^53 lose precision (the JVM keeps full longs).
  • intel macOS is unsupported - no bundled native. Apple Silicon, linux x64/aarch64 and Windows x64 ship bundled.

Development

./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

  • Ninja on every platform (mingw-w64 gcc on Windows, gcc/clang elsewhere; it bootstraps the single ninja binary itself where missing). It patches exactly one thing into the binding: CApi's static 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.

License

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

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close