diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dabf83..3ec12d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ 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. + * Address [#256](https://github.com/seancorfield/next-jdbc/issues/256) by adding `with-transaction+options` and `on-connection+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`. 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 cfe5a72..c02325b 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 @@ -4,4 +4,5 @@ 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}} + :lint-as {next.jdbc/on-connection clojure.core/with-open + next.jdbc/on-connection+options clojure.core/with-open}} diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index 16563bb..44661f0 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -360,16 +360,50 @@ Otherwise, creates a new `Connection` object from the connectable, executes the body, and automatically closes it for you." [[sym connectable] & body] - (let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection) - con-obj connectable] - `(cond (instance? java.sql.Connection ~con-obj) - ((^{:once true} fn* [~con-sym] ~@body) ~con-obj) - (and (satisfies? p/Connectable ~con-obj) - (instance? java.sql.Connection (:connectable ~con-obj))) - ((^{:once true} fn* [~con-sym] ~@body) (:connectable ~con-obj)) + (let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection)] + `(let [con-obj# ~connectable] + (cond (instance? java.sql.Connection con-obj#) + ((^{:once true} fn* [~con-sym] ~@body) con-obj#) + (and (satisfies? p/Connectable con-obj#) + (instance? java.sql.Connection (:connectable con-obj#))) + ((^{:once true} fn* [~con-sym] ~@body) (:connectable con-obj#)) + :else + (with-open [con# (get-connection con-obj#)] + ((^{:once true} fn* [~con-sym] ~@body) con#)))))) + +(defmacro on-connection+options + "Given a connectable 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. + + This allows you to write generic, **wrapped** connectable code without + needing to know the exact type of an incoming datasource: + +```clojure + (on-connection+options [conn datasource] + (execute! conn some-insert-sql) + (execute! conn some-update-sql)) +``` + + If passed a `Connection` then that `Connection` is used as-is. + + If passed a `Connectable` that wraps a `Connection`, then that + `Connectable` is used as-is. + + Otherwise, creates a new `Connection` object from the connectable, + wraps that with options, executes the body, and automatically closes + the new `Connection` for you." + [[sym connectable] & body] + `(let [con-obj# ~connectable] + (cond (instance? java.sql.Connection con-obj#) + ((^{:once true} fn* [~sym] ~@body) con-obj#) + (and (satisfies? p/Connectable con-obj#) + (instance? java.sql.Connection (:connectable con-obj#))) + ((^{:once true} fn* [~sym] ~@body) con-obj#) :else - (with-open [con# (get-connection ~con-obj)] - ((^{:once true} fn* [~con-sym] ~@body) con#))))) + (with-open [con# (get-connection con-obj#)] + ((^{:once true} fn* [~sym] ~@body) + (with-options con# (:options con-obj# {}))))))) (defn transact "Given a transactable object and a function (taking a `Connection`), diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 36d2480..aa07adf 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -944,11 +944,49 @@ INSERT INTO fruit (name, appearance) VALUES (?,?) (deftest issue-204 (testing "against a Connection" (is (seq (with-open [con (jdbc/get-connection (ds))] - (jdbc/on-connection [x con] (jdbc/execute! x ["select * from fruit"])))))) + (jdbc/on-connection + [x con] + (jdbc/execute! x ["select * from fruit"])))))) (testing "against a wrapped Connection" (is (seq (with-open [con (jdbc/get-connection (ds))] - (jdbc/on-connection [x (jdbc/with-options con {})] (jdbc/execute! x ["select * from fruit"])))))) + (jdbc/on-connection + [x (jdbc/with-options con {})] + (jdbc/execute! x ["select * from fruit"])))))) (testing "against a wrapped Datasource" - (is (seq (jdbc/on-connection [x (jdbc/with-options (ds) {})] (jdbc/execute! x ["select * from fruit"]))))) + (is (seq (jdbc/on-connection + [x (jdbc/with-options (ds) {})] + (jdbc/execute! x ["select * from fruit"]))))) (testing "against a Datasource" - (is (seq (jdbc/on-connection [x (ds)] (jdbc/execute! x ["select * from fruit"])))))) + (is (seq (jdbc/on-connection + [x (ds)] + (jdbc/execute! x ["select * from fruit"])))))) + +(deftest issue-256 + (testing "against a Connection" + (is (seq (with-open [con (jdbc/get-connection (ds))] + (jdbc/on-connection+options + [x con] ; raw connection stays raw + (is (instance? java.sql.Connection x)) + (jdbc/execute! x ["select * from fruit"])))))) + (testing "against a wrapped Connection" + (is (seq (with-open [con (jdbc/get-connection (ds))] + (jdbc/on-connection+options + [x (jdbc/with-options con {:test-option 42})] + ;; ensure we get the same wrapped connection + (is (instance? java.sql.Connection (:connectable x))) + (is (= {:test-option 42} (:options x))) + (jdbc/execute! x ["select * from fruit"])))))) + (testing "against a wrapped Datasource" + (is (seq (jdbc/on-connection+options + [x (jdbc/with-options (ds) {:test-option 42})] + ;; ensure we get a wrapped connection + (is (instance? java.sql.Connection (:connectable x))) + (is (= {:test-option 42} (:options x))) + (jdbc/execute! x ["select * from fruit"]))))) + (testing "against a Datasource" + (is (seq (jdbc/on-connection+options + [x (ds)] ; unwrapped datasource has no options + ;; ensure we get a wrapped connection (empty options) + (is (instance? java.sql.Connection (:connectable x))) + (is (= {} (:options x))) + (jdbc/execute! x ["select * from fruit"]))))))