Skip to content

Serve the java.sql surface clojure.jdbc actually reaches for - #15

Merged
yogthos merged 1 commit into
mainfrom
feat/driver-manager-shim
Sep 1, 2026
Merged

yogthos merged 1 commit into
mainfrom
feat/driver-manager-shim

Conversation

@yogthos

@yogthos yogthos commented Sep 1, 2026

Copy link
Copy Markdown
Member

Running clojure.jdbc's own test suite against this shim for the first time turned up five gaps. Upstream's suite could not previously load at all; it now passes whole.

Ran 25 tests. 78 assertions passed, 0 failures, 0 errors.
Gap Fix
Connections fell through to DriverManager serve DriverManager/getConnection — where a JVM driver is reached once registered
{:classname "org.sqlite.JDBC"} threw ClassNotFoundException register the two driver names this library backs
sqlite reported isolation READ_COMMITTED sqlite serializes by nature and answers SERIALIZABLE; postgres stays READ_COMMITTED
executeBatch threw a bare SQLException raise BatchUpdateException, the class the JVM throws
PreparedStatement had no getConnection java.sql.Statement declares it; clojure.jdbc's lazy cursor calls it

The one worth reading

jdbc.impl imports the class and extends protocols to the bare name:

(:import java.sql.PreparedStatement)
(extend-protocol proto/IFetch PreparedStatement (fetch [stmt conn opts] …))

An extension written against a simple name is filed under that name only when jolt models the class. Otherwise it is localized to the extending namespace — jdbc.impl.PreparedStatement, a tag no value can carry — and the arm silently never fires, while the fully-qualified arms beside it work. (jdbc/fetch conn stmt) was dying with No method fetch in jdbc.proto/IFetch even though class and instance? were both correct.

Fixed by grafting the java.sql names onto the modeled hierarchy via the existing jolt.host/register-class-supers! seam, and reporting the simple spelling alongside the qualified one.

Verified

Against the released jolt v0.8.0 (no unreleased runtime changes needed):

  • upstream clojure.jdbc suite — 25 tests, 78 assertions, 0 failures
  • this library's own suite — all checks passed
  • examples/ring-app end to end, live server over the wire — all passed

Note for consumers

A consumer must still (require '[db.jdbc]) before jdbc.core, as the README says. A jolt-side change to make that require optional is written but deliberately not part of this PR.

Upstream's suite is not yet wired in as a gate here — worth doing so these do not regress.

Running clojure.jdbc's own test suite against this shim for the first time
turned up five gaps. Upstream's suite could not even load before; it now
passes whole — 25 tests, 78 assertions, 0 failures.

- DriverManager/getConnection. clojure.jdbc's dbspec->connection ends there,
  which is where a JVM driver is reached once it has registered itself. Serving
  it is what lets a program that only requires jdbc.core work: extending
  proto/IConnection cannot, because that extension has to load AFTER
  clojure.jdbc's own to win while this namespace has to load BEFORE them.

- Class/forName on a driver classname. A dbspec may carry
  {:classname "org.sqlite.JDBC"}, and clojure.jdbc loads it before connecting.
  The two drivers this library backs are registered so the class resolves; one
  it does not back still throws ClassNotFoundException.

- The default transaction isolation is the driver's, not a constant. SQLite
  serializes by nature and its JDBC driver answers SERIALIZABLE; postgres
  starts at READ_COMMITTED. Reporting 2 for both said sqlite was weaker.

- executeBatch raises BatchUpdateException, the class the JVM throws and the
  one a caller catches to tell a failed batch entry from any other SQL error.

- PreparedStatement answers getConnection, which java.sql.Statement declares
  and clojure.jdbc's lazy cursor calls.

The last one needed the hierarchy too. jdbc.impl imports the class and extends
protocols to the bare name:

    (:import java.sql.PreparedStatement)
    (extend-protocol proto/IFetch PreparedStatement (fetch [stmt conn opts] …))

An extension written against a SIMPLE name is filed under that name only when
jolt models the class; otherwise it is localized to the extending namespace, as
"jdbc.impl.PreparedStatement" — a tag no value can carry — and the arm silently
never fires while the fully-qualified arms beside it work. So the java.sql
names are grafted onto the modeled hierarchy through
jolt.host/register-class-supers!, and values report the simple spelling
alongside the qualified one. (jdbc/fetch conn stmt) was failing with "No method
fetch in jdbc.proto/IFetch".

Verified against the released jolt v0.8.0: upstream clojure.jdbc's suite, this
library's own suite, and examples/ring-app end to end over the wire.
@yogthos
yogthos merged commit d85f391 into main Sep 1, 2026
2 checks passed
@yogthos
yogthos deleted the feat/driver-manager-shim branch September 1, 2026 02:49
yogthos added a commit that referenced this pull request Sep 1, 2026
This library's job is to run the published clojure.jdbc unmodified on a
java.sql shim. Only upstream's tests can say whether it still does: the suite
in clj-test is ours, so it agrees with our own idea of the API and kept passing
while five real gaps sat behind it — the ones #15 fixed once upstream's suite
was pointed at the shim for the first time.

The tests come from upstream's tree at the sha deps.edn already pins, read out
of deps.edn rather than repeated in the script, so a bump to the clojure.jdbc
pin moves this gate with it instead of leaving it on an older tree that still
passes. Copying the test files in here would drift the other way: they would go
on agreeing with whatever the shim had become.

Upstream ships its tests outside :paths, so they cannot be reached by an alias
in our deps.edn. The script assembles a throwaway project with db as a
:local/root and the checkout's test/ on the source roots.

The namespaces are the sqlite-only set upstream nominates in its own
tests-jolt.edn; jdbc.postgres-test needs a live postgres and hikari-cp, and
db's own suite covers the postgres driver.

Verified both ways: green as it stands (25 tests, 78 assertions), and exit 1
naming the regression when PreparedStatement's getConnection is removed.

Co-authored-by: Yogthos <yogthos@gmail.com>
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