A SQLite and PostgreSQL database library for jolt
(Clojure on Chez Scheme). It binds the system libsqlite3 and libpq
directly through jolt.ffi — jolt's foreign-function interface — and runs the
real clojure.jdbc on top of them, plus
a small next.jdbc surface. No jolt
built-in, no JVM: the native binding lives here, and the API is the published
library rather than a copy of it.
jdbc.core is clojure.jdbc itself. This library supplies the java.sql surface
it drives (db.jdbc-shim) over the native drivers, so its own documentation and
semantics apply as written.
(require '[db.jdbc]) ; registers the shim, once
(require '[jdbc.core :as jdbc])
(with-open [conn (jdbc/connection "sqlite::memory:")] ; or "postgres://user:pw@host/db"
(jdbc/execute! conn "create table p (id integer primary key, name text)")
(jdbc/insert! conn :p {:name "ada"}) ; -> (1), one result per row
(jdbc/fetch conn ["select * from p where name = ?" "ada"]))Require db.jdbc once before jdbc.core, and before anything else that pulls it
in. It has to be loaded first because clojure.jdbc's namespaces resolve the
java.sql constants as they compile, and it is what points connection
construction at the native drivers instead of DriverManager.
fetch/fetch-one, execute!, insert!/insert-multi!/update!/delete!,
prepared-statement, and atomic (transactions with nested savepoints) are
supported on both backends. Queries are strings or sqlvecs ([sql & params],
JDBC ? placeholders — rewritten to $N for postgres).
Generated keys come back through RETURNING, since neither driver has a JDBC
generated-keys channel. {:returning true} (or :all) asks for the whole row,
which is what postgres' own driver gives; a sequence of column names asks for
those. Without it there are no generated keys to report, so insert! falls back
to the update count, exactly as clojure.jdbc does on a driver that has none.
A byte array parameter binds as a SQLite blob / postgres bytea, and those
columns read back as byte arrays. The bytes round-trip exactly, so embedded NULs,
non-UTF-8 bytes, and empty payloads all survive.
(jdbc/execute! conn "create table doc (id integer primary key, body blob)")
(jdbc/insert! conn :doc {:body (byte-array [0 255 65])})
(:body (jdbc/fetch-one conn "select body from doc")) ; -> byte arrayOn postgres a byte array is sent in binary with its type given as bytea, so it
does not depend on the statement offering a bytea column for the server to infer
one from. ["select ? as c" (byte-array [1 2])] binds a bytea and reads back as
bytes rather than inferring text.
The next.jdbc namespace carries the upstream calling conventions over the
same drivers: get-datasource / get-connection, execute! / execute-one!
(rows for a result set, {:next.jdbc/update-count n} otherwise), plan (a
reducible over the rows), execute-batch! (one SQL across a seq of parameter
groups, answering per-group update counts), and with-transaction with its
options map (:isolation, :read-only, :rollback-only). Every operation
takes a connection, a datasource, or a db-spec; a datasource or spec opens a
connection owned by that call. Rows are unqualified lower-cased keyword maps —
the drivers cannot see table names, so upstream's qualified default is not
reproducible, and next.jdbc.result-set builder markers are accepted and
ignored.
A datasource is db.datasource: an explicit open-datasource / acquire /
release / close-datasource lifecycle over the drivers. It is a connection
factory, not a pool — pooling can grow behind the same surface later, and this
library deliberately does not emulate HikariCP.
Typed columns normalize to conventional values on the way out: uuid columns
read as uuids, numeric as bigdec (it used to go through parse-double,
losing precision), date/time/timestamp/timestamptz as java.time
values (LocalDate/LocalTime/LocalDateTime/OffsetDateTime — a
deliberate divergence from JDBC's java.sql.Timestamp, which jolt does not
model), and arrays of the common element types as vectors (quoting, NULL
elements and nesting honoured). A value the parser cannot read — postgres
infinity, say — keeps its text form rather than throwing on a read path.
Parameters: uuids and temporal values bind as their text form and postgres
infers the type; a vector binds as an array literal, which pairs with a cast
at the use site (?::text[]). SQLite stores anything it does not know as text.
Database errors satisfy (catch java.sql.SQLException ...), so code written
against the JDBC contract works unchanged. Migratus depends on this: its
table-exists? probe catches SQLException to decide whether it still needs to
create schema_migrations. Errors raised by the drivers themselves also carry
:jdbc/sql-error true in their ex-data.
db.sqlite/db.pg— the native drivers (jolt.ffi bindings).db.jdbc-shim— thejava.sqlsurface clojure.jdbc drives, over those drivers.db.jdbc— the entry point: loads the shim, then clojure.jdbc on top of it.jdbc.core— clojure.jdbc itself, pulled in as a dependency.next.jdbc(+.sql/.prepare/.result-set/.transaction) — the next.jdbc surface migratus and similar tools use.
jolt v0.7.3 or newer on PATH; the system libsqlite3 (preinstalled on macOS
and most Linux distros). PostgreSQL support additionally needs libpq at runtime.
The version floor is not cosmetic. The shim needs three host fixes that landed in
v0.7.3: with-open on a reify, a parenthesised (Class/FIELD) reading the
field, and a protocol extended to a library-declared class actually dispatching.
On an older jolt this library fails at load or at the first connection.
jolt -M:test # sqlite
JOLT_TEST_PG_URI=postgres://... jolt -M:test # also runs the postgres suite