Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,41 @@

A SQLite and PostgreSQL database library for [jolt](https://github.com/jolt-lang/jolt)
(Clojure on Chez Scheme). It binds the system **libsqlite3** and **libpq**
directly through `jolt.ffi` — jolt's foreign-function interface — and exposes the
[clojure.jdbc](https://github.com/yogthos/clojure.jdbc) API plus a small
[next.jdbc](https://github.com/seancorfield/next-jdbc) surface. No jolt built-in,
no JVM: the native binding lives in this library.
directly through `jolt.ffi` — jolt's foreign-function interface — and runs the
real [clojure.jdbc](https://github.com/yogthos/clojure.jdbc) on top of them, plus
a small [next.jdbc](https://github.com/seancorfield/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.

```clojure
(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"}) ; -> generated id
(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!`,
`last-insert-id`, and `atomic` (transactions with nested savepoints) are
`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.

## Binary values

A byte array parameter binds as a SQLite `blob` / postgres `bytea`, and those
Expand All @@ -39,23 +56,30 @@ bytes rather than inferring text.

## Errors

Database errors are `ex-info` values carrying `:jdbc/sql-error true` in their
`ex-data`, and they also satisfy `(catch java.sql.SQLException ...)` so code
written against the JDBC contract works unchanged. Migratus depends on this: its
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`.
create `schema_migrations`. Errors raised by the drivers themselves also carry
`:jdbc/sql-error true` in their `ex-data`.

## Layout

- `db.sqlite` / `db.pg` — the native drivers (jolt.ffi bindings).
- `jdbc.core` — the clojure.jdbc API over them.
- `db.jdbc-shim` — the `java.sql` surface 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.

## Requirements

`jolt` on PATH; the system `libsqlite3` (preinstalled on macOS and most Linux
distros). PostgreSQL support additionally needs `libpq` at runtime.
`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.

## Test

Expand Down
32 changes: 25 additions & 7 deletions clj-test/jdbc/core_test.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
(ns jdbc.core-test
(:require [jdbc.core :as jdbc]
;; db.jdbc first: it registers the java.sql shim, which clojure.jdbc's
;; namespaces resolve against as they compile, and points connection
;; construction at the native drivers. jdbc.core below is the published
;; clojure.jdbc running on top of it.
(:require [db.jdbc]
[jdbc.core :as jdbc]
;; the placeholder rewriter lives in the pg driver; requiring it here
;; lets the sqlite-only run cover the lexer table. Loading db.pg does
;; not need libpq present, only calling into it does.
Expand All @@ -18,10 +23,17 @@
(with-open [conn (jdbc/connection "sqlite::memory:")]
(check "execute! ddl" 0
(jdbc/execute! conn "create table person (id integer primary key, name text, zip integer)"))
(check "insert! returns the id" 1 (jdbc/insert! conn :person {:name "ada" :zip 94546}))
(check "insert-multi! ids" [2 3]
;; clojure.jdbc's insert! returns one result per row. Without :returning the
;; shim hands back an empty generated-keys set, so each row reports its update
;; count, which is how the API behaves on a driver with no generated keys.
(check "insert! returns a result per row" '(1)
(jdbc/insert! conn :person {:name "ada" :zip 94546}))
(check "insert-multi! returns a result per row" '(1 1)
(jdbc/insert-multi! conn :person [{:name "grace" :zip 94546}
{:name "alan" :zip 10001}]))
(check "insert! :returning gives the inserted row" {:id 4 :name "edsger" :zip 1}
(first (jdbc/insert! conn :person {:name "edsger" :zip 1} {:returning true})))
(jdbc/delete! conn :person ["name = ?" "edsger"])
(check "fetch sqlvec with params" [{:id 1 :name "ada" :zip 94546}]
(jdbc/fetch conn ["select * from person where name = ?" "ada"]))
(check "fetch-one" {:id 3 :name "alan" :zip 10001}
Expand All @@ -40,7 +52,8 @@
(doseq [[label payload] [["embedded NULs" (byte-array [65 0 66 0 67])]
["non-UTF-8 bytes" (byte-array [-1 -2])]
["empty payload" (byte-array [])]]]
(let [id (jdbc/insert! conn :payload {:content payload})
(let [id (:id (first (jdbc/insert! conn :payload {:content payload}
{:returning true})))
actual (:content
(jdbc/fetch-one conn
["select content from payload where id = ?" id]))]
Expand Down Expand Up @@ -82,7 +95,7 @@
(count (jdbc/fetch conn ["select * from person where name = ?" "marked"]))))

(println "dbspec parsing")
(check "map spec works" 1
(check "map spec works" '(1)
(with-open [c (jdbc/connection {:vendor "sqlite" :name ":memory:"})]
(jdbc/execute! c "create table t (x integer)")
(jdbc/insert! c :t {:x 7})))
Expand Down Expand Up @@ -144,7 +157,11 @@
(with-open [conn (jdbc/connection pg-uri)]
(jdbc/execute! conn "drop table if exists jolt_person")
(jdbc/execute! conn "create table jolt_person (id serial primary key, name text, zip integer)")
(check "pg insert! returns the id" 1 (jdbc/insert! conn :jolt_person {:name "ada" :zip 94546}))
(check "pg insert! returns a result per row" '(1)
(jdbc/insert! conn :jolt_person {:name "ada" :zip 94546}))
(check "pg insert! :returning gives the inserted row" {:id 2 :name "hopper" :zip 3}
(first (jdbc/insert! conn :jolt_person {:name "hopper" :zip 3} {:returning true})))
(jdbc/delete! conn :jolt_person ["name = ?" "hopper"])
(check "pg fetch with ? params" [{:id 1 :name "ada" :zip 94546}]
(jdbc/fetch conn ["select * from jolt_person where name = ?" "ada"]))
(jdbc/insert! conn :jolt_person {:name "grace" :zip 94546})
Expand Down Expand Up @@ -212,7 +229,8 @@
["every byte value" (byte-array (mapv (fn [i] (if (> i 127) (- i 256) i))
(range 256)))]
["empty payload" (byte-array [])]]]
(let [id (jdbc/insert! conn :jolt_payload {:content payload})
(let [id (:id (first (jdbc/insert! conn :jolt_payload {:content payload}
{:returning true})))
actual (:content
(jdbc/fetch-one conn
["select content from jolt_payload where id = ?" id]))]
Expand Down
10 changes: 8 additions & 2 deletions clj/db/jdbc_shim.clj
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,18 @@
"java.sql.DatabaseMetaData" :jdbc/dbmeta
"java.sql.Savepoint" :jdbc/savepoint})

;; Answer true or nil, never false. nil means "not one of mine, keep looking",
;; while false settles the question for every other library's check as well: the
;; first non-nil answer wins. next.jdbc registers its own check so its connection
;; wrapper answers instance? java.sql.Connection, which is how migratus picks its
;; Connection branch, and returning false here silently overruled it.
(clojure.core/__register-instance-check!
(fn [cn val]
(when-let [tag (get class-tags cn)]
;; a PreparedStatement is a Statement too
(boolean (or (tagged? val tag)
(and (= cn "java.sql.Statement") (tagged? val :jdbc/prepared)))))))
(when (or (tagged? val tag)
(and (= cn "java.sql.Statement") (tagged? val :jdbc/prepared)))
true))))

;; Report the java.sql class name for (class x) and, more importantly, so a
;; protocol extended to java.sql.Connection dispatches on these values.
Expand Down
Loading
Loading