Skip to content

Releases: jolt-lang/db

v0.4.0

Choose a tag to compare

@yogthos yogthos released this 01 Sep 02:50
d85f391

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/getConnection is served, which is where clojure.jdbc's dbspec->connection ends.
  • Class/forName resolves the two driver classnames this library backs, so {:classname "org.sqlite.JDBC"} specs work.
  • Default transaction isolation is the driver's: sqlite answers SERIALIZABLE, postgres READ_COMMITTED. Both previously reported READ_COMMITTED.
  • executeBatch raises BatchUpdateException, the class the JVM throws.
  • PreparedStatement answers getConnection.

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

Choose a tag to compare

@yogthos yogthos released this 11 Aug 23:56
6e3474a

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

Choose a tag to compare

@yogthos yogthos released this 11 Aug 22:50
9955782

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

  • #11 Require db.pg statically so tree shaking can proceed, adopting #10 by @sundbp

v0.1.2

Choose a tag to compare

@yogthos yogthos released this 11 Aug 22:05
c3a8bed

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

Choose a tag to compare

@yogthos yogthos released this 11 Aug 17:41
cb4c4f3

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

Choose a tag to compare

@yogthos yogthos released this 11 Aug 14:41
9676b11

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 array

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

Changes

  • #5 Make SQLite errors catchable and support BLOB values, thanks to @sundbp
  • #6 Give postgres the same SQL error and binary value behavior as sqlite
  • #7 Bind postgres byte arrays in binary with an explicit bytea Oid

v0.0.1

Choose a tag to compare

@yogthos yogthos released this 04 Jul 19:20

First tagged release. Reference as a jolt git dependency via deps.edn with :git/url and this commit's SHA.