Repository navigation
Releases: jolt-lang/db
Release list
v0.4.0
Two breaking changes for consumers.
ffi/write argument order (#14)
jolt 0.8.0 reversed ffi/write's value and offset arguments; the drivers here moved with it. deps.edn declares :jolt/min-version "0.8.0" so an older runtime refuses to run rather than writing to the wrong place — both spellings are integers, so the mistake is otherwise silent.
clojure.jdbc replaces the reimplementation (#12, #15)
jdbc.core is now the published clojure.jdbc running on the java.sql shim in db.jdbc-shim, rather than a reimplementation of that API here.
Non-standard helpers are gone. jdbc/last-insert-id in particular: clojure.jdbc has no such function, and the generated key comes back from the insert itself.
;; before
(jdbc/execute! conn (sql/format {:insert-into :t :values [{:name n}]}))
(jdbc/last-insert-id conn)
;; after
(:id (first (jdbc/insert! conn :t {:name n} {:returning true})))Require db.jdbc once before jdbc.core, and before anything else that pulls it in — clojure.jdbc's namespaces resolve the java.sql constants as they compile.
#15: the java.sql surface clojure.jdbc actually reaches for
Running upstream's own test suite against the shim for the first time turned up five gaps. That suite previously could not load at all; it now passes whole — 25 tests, 78 assertions, 0 failures.
DriverManager/getConnectionis served, which is where clojure.jdbc'sdbspec->connectionends.Class/forNameresolves the two driver classnames this library backs, so{:classname "org.sqlite.JDBC"}specs work.- Default transaction isolation is the driver's: sqlite answers
SERIALIZABLE, postgresREAD_COMMITTED. Both previously reportedREAD_COMMITTED. executeBatchraisesBatchUpdateException, the class the JVM throws.PreparedStatementanswersgetConnection.
The last needed the class hierarchy too: jdbc.impl imports the class and extends protocols to the bare PreparedStatement, and an extension written against a simple name is filed under the extending namespace unless jolt models the class — so the arm silently never fired and (jdbc/fetch conn stmt) failed with No method fetch in jdbc.proto/IFetch.
Verified against the released jolt v0.8.0: upstream's suite, this library's own suite, and examples/ring-app end to end over the wire.
v0.2.0
jdbc.core is now the real clojure.jdbc, not a reimplementation of it.
This library supplies the java.sql surface clojure.jdbc drives (db.jdbc-shim) over the native libsqlite3 and libpq bindings, and pulls clojure.jdbc itself in as a dependency. Its documentation and semantics apply as written, and there is no second implementation here to drift from it.
Upgrading
Require db.jdbc once before jdbc.core, and before anything else that pulls it in. It registers the shim, which clojure.jdbc's namespaces resolve against as they compile, and points connection construction at the native drivers instead of DriverManager.
(require '[db.jdbc])
(require '[jdbc.core :as jdbc])insert! returns one result per row rather than a bare id: a seq of generated keys where the driver has them, of update counts where it does not. Generated keys come back through RETURNING, so ask for them explicitly.
(jdbc/insert! conn :p {:name "ada"}) ; -> (1)
(jdbc/insert! conn :p {:name "ada"} {:returning true}) ; -> ({:id 1 :name "ada"})last-insert-id is gone. It was ours, not clojure.jdbc's. Use {:returning ...}.
jolt v0.7.3 or newer is required. The shim needs three host fixes from that release — with-open on a reify, a parenthesised (Class/FIELD) reading the field, and a protocol extended to a library-declared class dispatching at all. On an older jolt this fails at load or at the first connection.
What you gain
The full clojure.jdbc surface, including prepared-statement, fetch-lazy and cursors, the :identifiers and :as-rows? result options, :returning, and the isolation-level and read-only connection options. Anything its docs describe now works here, because it is the same code.
next.jdbc is unchanged and still sits on top, so migratus keeps working.
Changes
- #12 Use the real clojure.jdbc instead of reimplementing it
v0.1.3
Tree shaking works again for programs that use this library. Reported and diagnosed by @sundbp, who also wrote the original change in #10.
The problem
jdbc.core reached the postgres driver through (resolve (symbol "db.pg" n)) behind a runtime require. A reachable call to resolve stops jolt from building a sound whole-program call graph, so jolt build --tree-shake skipped shaking entirely for any program that loaded jdbc.core, including sqlite-only ones. A runtime require is also the wrong dependency edge for an AOT binary, which has no source roots to load db.pg from later.
Both jdbc.core and db.jdbc-shim now require db.pg statically and call it directly.
before: jolt build: tree-shake skipped (reachable code resolves vars at runtime)
after: jolt build: tree-shake kept 341 of 766 defs (core 242/661)
A sqlite-only app still does not need libpq
Loading db.pg only declares the FFI bindings; ffi/defcfn resolves a symbol on first call, so a program that never opens a postgres connection never touches the library. Checked directly rather than assumed: a project whose deps.edn points :jolt/native at a libpq that does not exist builds, tree-shakes, runs and returns rows.
Upgrading
No API or behaviour change. This is a build-time fix.
Verified against jolt v0.7.3, which is also the first release carrying the three host fixes from jolt-lang/jolt#585 that the java.sql shim depends on. The suite passes on sqlite and postgres 16.
Changes
v0.1.2
Groundwork for running the real clojure.jdbc on jolt instead of reimplementing its API here. Nothing is switched over yet and the jdbc.core API is unchanged, so this is a no-op for existing code.
Ordered row access
A JDBC ResultSet is read by column index and ResultSetMetaData reports labels by index, but both drivers returned keyword-keyed maps with column order already lost. db.sqlite/query-raw and db.pg/all-raw now return {:labels [...] :rows [[v ...]]}, and query and all build their maps from that, so the map-shaped API is untouched.
The ? rewriter moved into db.pg
Rewriting JDBC ? placeholders to $N moved from jdbc.core into db.pg, which is where it belongs, and db.pg/run applies it so every caller gets it rather than each one remembering to. Its lexer table moved with it and still runs in the sqlite-only build.
The java.sql shim, present but unwired
db.jdbc-shim and db.jdbc are the java.sql surface clojure.jdbc drives — Connection, Statement, PreparedStatement, ResultSet, ResultSetMetaData, DatabaseMetaData and Savepoint — over the native drivers.
They are deliberately not in use. While this repo still ships its own clj/jdbc/core.clj, that copy shadows the dependency on the classpath, so db.jdbc resolves to ours rather than to clojure.jdbc. Removing it is the switchover, and it lands with the test-suite migration in a later release.
For what it is worth, the shim already works: with the three host fixes in jolt-lang/jolt#585, the published clojure.jdbc runs unmodified against native sqlite through it, including generated keys, transactions, nested savepoints and column-ordered as-rows? results. The switchover is gated on a jolt release carrying those fixes, since CI installs jolt from its latest release.
Changes
- #9 Groundwork for running the real clojure.jdbc on a java.sql shim
v0.1.1
Two fixes on the postgres side, both found while reviewing v0.1.0.
Affected rows are reported on postgres
execute!, update! and delete! now return the number of rows they affected, read from PQcmdTuples. execute! had always promised rows affected and sqlite had always delivered it, but postgres returned nil from all three, so anything branching on the count silently saw nothing. Commands that carry no count, DDL among them, give 0, which is what sqlite3_changes reports for those too.
If you are upgrading, this is the one outward change: a nil becomes a number, and postgres now matches what sqlite already did.
Placeholders are no longer miscounted
Rewriting JDBC ? placeholders to $N only skipped single-quoted literals, so a ? in a comment, a quoted identifier or a dollar-quoted body was rewritten and, worse, consumed a parameter number, shifting every later placeholder. Both triggers are ordinary things to write in a migration: a prose comment ending in a question mark, or a plpgsql body in dollar quotes.
before: select /* ? */ ? as c -> select /* $1 */ $2 as c ($2 with one parameter)
after: select /* ? */ ? as c -> select /* ? */ $1 as c
The scanner now skips string literals, quoted identifiers, line and block comments including the nesting postgres allows, dollar-quoted bodies with or without a tag, and E escape strings. A $ inside an identifier is left alone, since postgres allows it there and a$b$c is one name rather than a dollar quote.
Placeholder rewriting is linear
It rebuilt the statement one character at a time, copying the accumulator on every character, so cost grew with the square of the length.
| sql chars | before | after |
|---|---|---|
| 500,000 | 19,343 ms | 146 ms |
Cost per character is now flat from 125KB through 2MB.
Changes
- #8 Report affected rows on postgres, and rewrite placeholders in linear time
v0.1.0
Database errors are now catchable as java.sql.SQLException on both backends, and byte arrays round-trip as SQLite blobs and postgres bytea.
Catchable SQL errors
Errors carry :jdbc/sql-error in their ex-data and satisfy (catch java.sql.SQLException ...). That is the contract migratus depends on: its table-exists? probe catches SQLException to decide whether it still needs to create schema_migrations, and on a fresh database that probe used to escape instead of letting migratus bootstrap.
Binary values
A byte array parameter binds as a blob or 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 Oid given as bytea, so the parameter carries its own type rather than depending on the statement offering a bytea column for the server to infer one from. Reads still come back in text format and handle both bytea_output encodings, hex and escape.
Upgrading
One behavior change to be aware of: a bare select ? with a byte array parameter now returns a byte array, where it previously returned the literal \x... string the driver had sent. Binding a byte array against a non-bytea column is unaffected; text columns store the same value as before, and types like integer or json errored before and error now, with a clearer message.