Skip to content

Groundwork for running the real clojure.jdbc on a java.sql shim - #9

Merged
yogthos merged 3 commits into
mainfrom
clojure-jdbc-shim
Aug 11, 2026
Merged

Groundwork for running the real clojure.jdbc on a java.sql shim#9
yogthos merged 3 commits into
mainfrom
clojure-jdbc-shim

Conversation

@yogthos

@yogthos yogthos commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Towards running the published clojure.jdbc on jolt instead of reimplementing its API here. This is the groundwork only. It does not switch anything over yet, and everything in it is inert or internal, so the existing suite is untouched at 91 checks.

The three commits are separable on purpose.

The java.sql constants. jdbc.constants maps clojure.jdbc's keyword options onto java.sql static fields, and those were the only thing stopping its namespaces from compiling. With ResultSet, Connection and Statement registered, every clojure.jdbc namespace loads under jolt. The values match the JVM's, since callers pass them straight through: :serializable has to reach setTransactionIsolation as 8 either way.

Ordered row access, and the ? rewriter moves to db.pg. A JDBC ResultSet is read by column index and ResultSetMetaData reports labels by index, but both drivers returned keyword maps with column order already lost. query-raw and all-raw now return {:labels [...] :rows [[v ...]]}, and query/all build their maps from that, so the map-shaped API is unchanged. The ? to $N rewriter also moves 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 shim itself. Connection, Statement, PreparedStatement, ResultSet, ResultSetMetaData, DatabaseMetaData and Savepoint over db.sqlite and db.pg, as host tagged-tables. db.jdbc is the entry point: it fixes load order, since clojure.jdbc resolves the constants at compile time, and re-extends IConnection so a dbspec builds a connection over the native drivers instead of reaching for DriverManager, which has nothing to load here.

Two design points worth naming. Generated keys are RETURNING underneath, because neither driver has a JDBC generated-keys channel: :all/true asks for the whole row, which is what postgres' own driver gives, and a sequence of names asks for those columns. An empty ResultSet when nothing was requested is deliberate, since that is what makes insert! fall back to the update count the way it does on a driver without generated keys. And a ResultSet here is a cursor over rows the driver has already materialised, so a fetch that streams on the JVM is eager on this shim.

What already works

With jolt-lang/jolt#585 (merged), the published clojure.jdbc runs unmodified against native sqlite through this shim:

insert!            -> (1)
insert! :returning -> ({:id 2, :name "grace", :age nil})
insert-multi!      -> (1 1)
update!            -> 1
delete!            -> 1
as-rows? + header? -> [["id" "name"] [1 "ada"] [2 "grace"] [3 "alan"]]
atomic rollback    -> :threw   (rolled back: 0)
nested atomic      -> (1)      (committed: 1)

Generated keys, transactions, nested savepoints via SAVEPOINT, and column order preserved in as-rows?.

What is deliberately not here

clj/jdbc/core.clj still ships, and while it does it shadows the dependency on the classpath, so db.jdbc resolves to our copy rather than to clojure.jdbc. Its docstring says so. Removing it is the switchover, and that belongs with the test-suite migration rather than buried here. deps.edn also does not declare clojure.jdbc or io.github.jolt-lang/time yet; the latter is needed because jdbc.util/lower-case calls (Locale/US).

The switchover additionally needs a jolt release carrying jolt-lang/jolt#585, since CI installs jolt from its latest release and the shim depends on all three fixes in it.

Yogthos added 3 commits August 11, 2026 14:44
First step towards running the real clojure.jdbc on jolt instead of reimplementing
its API here. jdbc.constants maps clojure.jdbc's keyword options onto java.sql
static fields, and those were the only thing stopping its namespaces from
compiling: with ResultSet, Connection and Statement registered through
__register-class-statics!, every clojure.jdbc namespace loads under jolt and
jdbc.core/prepared-statement resolves from the dependency.

The values match the JVM's, since callers pass these through. :serializable has to
reach setTransactionIsolation as 8 either way.

Nothing requires this namespace yet, so it is inert and the existing suite is
unaffected. Wiring it to db.sqlite and db.pg, and routing connection creation away
from DriverManager, comes next.
Groundwork for the java.sql shim, kept separate so it can be reviewed on its own.

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. query-raw and all-raw now return {:labels [...] :rows [[v ...]]} and query
and all build their maps from that, so the map-shaped API is unchanged while an
indexed reader has something to work with.

The ? to $N rewriter also moves from jdbc.core into db.pg, which is where it
belongs: it is a postgres concern, and db.pg/run now applies it so every caller
gets it rather than each one remembering to. That also keeps it available once
jdbc.core goes away, since the shim needs it. Its lexer table moved with it and
still runs in the sqlite-only build, which is why the test namespace now requires
db.pg directly. Loading db.pg has never needed libpq present, only calling it does.
The object surface clojure.jdbc drives, over db.sqlite and db.pg: Connection,
Statement, PreparedStatement, ResultSet, ResultSetMetaData, DatabaseMetaData and
Savepoint, as host tagged-tables with their methods registered through
__register-class-methods!. __register-class! reports the java.sql class names so
that clojure.jdbc's protocols, which are extended to java.sql.Connection and
friends, dispatch on these values at all, and __register-instance-check! answers
instance? for them.

db.jdbc is the entry point. It fixes load order, since clojure.jdbc resolves the
java.sql constants when its namespaces compile, and then re-extends IConnection so
a dbspec builds a connection over the native drivers instead of reaching for
DriverManager, which has nothing to load here. Extending after jdbc.impl is what
makes ours win.

Two things worth naming. Generated keys are RETURNING underneath, because neither
driver has a JDBC generated-keys channel: :all or true asks for the whole row,
which is what postgres' own driver gives, and a sequence of names asks for those
columns. An empty ResultSet when nothing was requested is deliberate, since that
is what makes insert! fall back to the update count the way it does on a driver
without generated keys. And a ResultSet here is a cursor over rows the driver has
already materialised, so a fetch that streams on the JVM is eager on this shim.

Real clojure.jdbc already runs execute!, fetch and fetch-one on sqlite through
this. Nothing in the repo requires it yet, so the existing suite is untouched.
@yogthos
yogthos merged commit c3a8bed into main Aug 11, 2026
2 checks passed
@yogthos
yogthos deleted the clojure-jdbc-shim branch August 11, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant