diff --git a/CHANGELOG.md b/CHANGELOG.md index c1af9f9..3dabf83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Only accretive/fixative changes will be made from now on. +* 1.3.next in progress + * Address [#256](https://github.com/seancorfield/next-jdbc/issues/256) by adding `with-transaction+options`. Documentation TBD. + * 1.3.883 -- 2023-06-25 * Address [#254](https://github.com/seancorfield/next-jdbc/issues/254) by adding `next.jdbc/active-tx?` and adding more explanation to [**Transactions**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/transactions) about the conventions behind transactions and the limitations of thread-local tracking of active transactions in `next.jdbc`. * Address [#251](https://github.com/seancorfield/next-jdbc/issues/251) by updating `next.jdbc/with-logging` docstring. diff --git a/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/config.edn b/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/config.edn index 57911b6..cfe5a72 100644 --- a/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/config.edn +++ b/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/config.edn @@ -1,5 +1,7 @@ {:hooks {:analyze-call {next.jdbc/with-transaction - hooks.com.github.seancorfield.next-jdbc/with-transaction}} - :lint-as {next.jdbc/on-connection clojure.core/with-open}} \ No newline at end of file + hooks.com.github.seancorfield.next-jdbc/with-transaction + next.jdbc/with-transaction+options + hooks.com.github.seancorfield.next-jdbc/with-transaction+options}} + :lint-as {next.jdbc/on-connection clojure.core/with-open}} diff --git a/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/hooks/com/github/seancorfield/next_jdbc.clj_kondo b/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/hooks/com/github/seancorfield/next_jdbc.clj_kondo index daedc45..9fc398d 100644 --- a/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/hooks/com/github/seancorfield/next_jdbc.clj_kondo +++ b/resources/clj-kondo.exports/com.github.seancorfield/next.jdbc/hooks/com/github/seancorfield/next_jdbc.clj_kondo @@ -16,3 +16,19 @@ opts body))] {:node new-node}))) + +(defn with-transaction+options + "Expands (with-transaction+options [tx expr opts] body) + to (let [tx expr] opts body) per clj-kondo examples." + [{:keys [:node]}] + (let [[binding-vec & body] (rest (:children node)) + [sym val opts] (:children binding-vec)] + (when-not (and sym val) + (throw (ex-info "No sym and val provided" {}))) + (let [new-node (api/list-node + (list* + (api/token-node 'let) + (api/vector-node [sym val]) + opts + body))] + {:node new-node}))) diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index 4b8d49d..8ea448a 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -389,6 +389,9 @@ Like `with-open`, if `with-transaction` creates a new `Connection` object, it will automatically close it for you. + If you are working with default options via `with-options`, you might want + to use `with-transaction+options` instead. + The options map supports: * `:isolation` -- `:none`, `:read-committed`, `:read-uncommitted`, `:repeatable-read`, `:serializable`, @@ -419,9 +422,45 @@ return plain Java objects, so if you call any of those on this wrapped object, you'll need to re-wrap the Java object `with-options` again. See the Datasources, Connections & Transactions section of Getting Started for - more details, and some examples of use with these functions." + more details, and some examples of use with these functions. + + `with-transaction+options` exists to automatically rewrap a `Connection` + with the options from a `with-options` wrapper." [connectable opts] - (opts/->DefaultOptions connectable opts)) + (let [c (:connectable connectable) + o (:options connectable)] + (if (and c o) + (opts/->DefaultOptions c (merge o opts)) + (opts/->DefaultOptions connectable opts)))) + +(defmacro with-transaction+options + "Given a transactable object, assumed to be wrapped with options, gets a + connection, rewraps it with those options, and binds it to `sym`, then + executes the `body` in that context, committing any changes if the body + completes successfully, otherwise rolling back any changes made. + + Like `with-open`, if `with-transaction+options` creates a new `Connection` + object, it will automatically close it for you. + + Note: the bound `sym` will be a **wrapped** connectable and not a plain + Java object, so you cannot call JDBC methods directly on it like you can + with `with-transaction`. + + The options map supports: + * `:isolation` -- `:none`, `:read-committed`, `:read-uncommitted`, + `:repeatable-read`, `:serializable`, + * `:read-only` -- `true` / `false` (`true` will make the `Connection` readonly), + * `:rollback-only` -- `true` / `false` (`true` will make the transaction + rollback, even if it would otherwise succeed)." + [[sym transactable opts] & body] + (let [con (vary-meta sym assoc :tag 'java.sql.Connection)] + `(let [tx# ~transactable] + (transact tx# + (^{:once true} fn* + [con#] + (let [~con (with-options con# (:options tx# {}))] + ~@body)) + ~(or opts {}))))) (defn with-logging "Given a connectable/transactable object and a sql/params logging diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 721dcfe..36d2480 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -65,6 +65,18 @@ ds-opts ["select appearance as looks_like from fruit where id = ?" 1] jdbc/snake-kebab-opts)))) + (let [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)] + (is (= "red" (:fruit/looks-like + (jdbc/execute-one! + ds' + ["select appearance as looks_like from fruit where id = ?" 1]))))) + (jdbc/with-transaction+options [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)] + (is (= (merge (default-options) jdbc/snake-kebab-opts) + (:options ds'))) + (is (= "red" (:fruit/looks-like + (jdbc/execute-one! + ds' + ["select appearance as looks_like from fruit where id = ?" 1]))))) (is (= "red" (:looks-like (jdbc/execute-one! ds-opts