From a57011a99811c53f8754012664b3c69a225f3ecf Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Sat, 9 Nov 2019 22:59:34 -0800 Subject: [PATCH] Recast the adapter/processors as a middleware Still not happy with this but it seems more "holistic". --- src/next/jdbc/middleware.clj | 84 ++++++++++++++++++++++++++++++ src/next/jdbc/result_set.clj | 39 -------------- test/next/jdbc/middleware_test.clj | 76 +++++++++++++++++++++++++++ test/next/jdbc_test.clj | 36 ------------- 4 files changed, 160 insertions(+), 75 deletions(-) create mode 100644 src/next/jdbc/middleware.clj create mode 100644 test/next/jdbc/middleware_test.clj diff --git a/src/next/jdbc/middleware.clj b/src/next/jdbc/middleware.clj new file mode 100644 index 0000000..504cc96 --- /dev/null +++ b/src/next/jdbc/middleware.clj @@ -0,0 +1,84 @@ +;; copyright (c) 2019 world singles networks llc + +(ns next.jdbc.middleware + (:require [next.jdbc.protocols :as p] + [next.jdbc.result-set :as rs])) + +(defn post-processing-adapter + "Given a builder function (e.g., `as-lower-maps`), return a new builder + function that post-processes rows and the result set. The options may + contain post-processing functions that are called on each row and on the + the result set. The options map is provided as a second parameter to these + functions, which should include `:next.jdbc/sql-params` (the vector of SQL + and parameters, in case post-processing needs it): + + * `:execute-fn` -- called immediately after the SQL operation completes + * `:row!-fn` -- called on each row as it is fully-realized + * `:rs!-fn` -- called on the whole result set once it is fully-realized + + The results of these functions are returned as the rows/result set." + [builder-fn] + (fn [rs opts] + (let [id2 (fn [x _] x) + exec-fn (get opts :execute-fn id2) + opts (exec-fn opts {}) + mrsb (builder-fn rs opts) + row!-fn (get opts :row!-fn id2) + rs!-fn (get opts :rs!-fn id2)] + (reify + rs/RowBuilder + (->row [this] (rs/->row mrsb)) + (column-count [this] (rs/column-count mrsb)) + (with-column [this row i] (rs/with-column mrsb row i)) + (row! [this row] (row!-fn (rs/row! mrsb row) opts)) + rs/ResultSetBuilder + (->rs [this] (rs/->rs mrsb)) + (with-row [this mrs row] (rs/with-row mrsb mrs row)) + (rs! [this mrs] (rs!-fn (rs/rs! mrsb mrs) opts)))))) + +(defrecord JdbcMiddleware [db global-opts] + p/Executable + (-execute [this sql-params opts] + (let [opts (merge global-opts opts) + id2 (fn [x _] x) + builder-fn (get opts :builder-fn rs/as-maps) + sql-params-fn (get opts :sql-params-fn id2) + result (sql-params-fn sql-params opts)] + (p/-execute db + (if (map? result) + (or (:next.jdbc/sql-params result) sql-params) + result) + (assoc (if (map? result) result opts) + :builder-fn + (post-processing-adapter builder-fn))))) + (-execute-one [this sql-params opts] + (let [opts (merge global-opts opts) + id2 (fn [x _] x) + builder-fn (get opts :builder-fn rs/as-maps) + sql-params-fn (get opts :sql-params-fn id2) + result (sql-params-fn sql-params opts)] + (p/-execute-one db + (if (map? result) + (or (:next.jdbc/sql-params result) sql-params) + result) + (assoc (if (map? result) result opts) + :builder-fn + (post-processing-adapter builder-fn))))) + (-execute-all [this sql-params opts] + (let [opts (merge global-opts opts) + id2 (fn [x _] x) + builder-fn (get opts :builder-fn rs/as-maps) + sql-params-fn (get opts :sql-params-fn id2) + result (sql-params-fn sql-params opts)] + (p/-execute-all db + (if (map? result) + (or (:next.jdbc/sql-params result) sql-params) + result) + (assoc (if (map? result) result opts) + :builder-fn + (post-processing-adapter builder-fn)))))) + +(defn wrapper + "" + ([db] (JdbcMiddleware. db {})) + ([db opts] (JdbcMiddleware. db opts))) diff --git a/src/next/jdbc/result_set.clj b/src/next/jdbc/result_set.clj index c244ad9..e03e974 100644 --- a/src/next/jdbc/result_set.clj +++ b/src/next/jdbc/result_set.clj @@ -356,45 +356,6 @@ (with-row [this mrs row] (with-row arsb mrs row)) (rs! [this mrs] (rs! arsb mrs)))))) -(defn builder-adapter - "Given a builder function (e.g., `as-lower-maps`) and a hash map of - (optional) processing functions, return a new builder function that - applies those functions as follows, _after_ the SQL operation completes: - - * `:sql-params-fn` -- called on the value of `:next.jdbc/sql-params` - (which is the vector of SQL and parameters passed to `plan`, `execute!`, - or `execute-one!` that was just completed) - * `:row!-fn` -- called on each row once it is fully-realized - * `:rs!-fn` -- called on the whole result set once it is fully-realized - - These functions are assumed to be side-effecting and their result is - ignored." - [builder-fn processors] - (fn [rs opts] - (when-let [f (:sql-params-fn processors)] - (when-let [sql-params (:next.jdbc/sql-params opts)] - (f sql-params))) - (let [mrsb (builder-fn rs opts)] - (reify - RowBuilder - (->row [this] (->row mrsb)) - (column-count [this] (column-count mrsb)) - (with-column [this row i] (with-column mrsb row i)) - (row! [this row] - (let [r (row! mrsb row)] - (when-let [f (:row!-fn processors)] - (f r)) - r)) - ResultSetBuilder - (->rs [this] (->rs mrsb)) - (with-row [this mrs row] (with-row mrsb mrs row)) - (rs! [this mrs] - (let [r (rs! mrsb mrs)] - (when-let [f (:rs!-fn processors)] - (f r)) - r)))))) - - (declare navize-row) (defprotocol DatafiableRow diff --git a/test/next/jdbc/middleware_test.clj b/test/next/jdbc/middleware_test.clj new file mode 100644 index 0000000..a191e45 --- /dev/null +++ b/test/next/jdbc/middleware_test.clj @@ -0,0 +1,76 @@ +;; copyright (c) 2019 world singles networks llc + +(ns next.jdbc.middleware-test + (:require [clojure.string :as str] + [clojure.test :refer [deftest is testing use-fixtures]] + [next.jdbc :as jdbc] + [next.jdbc.connection :as c] + [next.jdbc.middleware :as mw] + [next.jdbc.test-fixtures :refer [with-test-db db ds + derby? postgres?]] + [next.jdbc.prepare :as prep] + [next.jdbc.result-set :as rs] + [next.jdbc.specs :as specs]) + (:import (java.sql ResultSet ResultSetMetaData))) + +(set! *warn-on-reflection* true) + +(use-fixtures :once with-test-db) + +(specs/instrument) + +(deftest logging-test + (let [logging (atom []) + logger (fn [data _] (swap! logging conj data) data) + sql-p ["select * from fruit where id in (?,?) order by id desc" 1 4]] + (jdbc/execute! (mw/wrapper (ds)) + sql-p + {:builder-fn rs/as-lower-maps + :sql-params-fn logger + :row!-fn logger + :rs!-fn logger}) + ;; should log four things + (is (= 4 (-> @logging count))) + ;; :next.jdbc/sql-params value + (is (= sql-p (-> @logging (nth 0)))) + ;; first row (with PK 4) + (is (= 4 (-> @logging (nth 1) :fruit/id))) + ;; second row (with PK 1) + (is (= 1 (-> @logging (nth 2) :fruit/id))) + ;; full result set with two rows + (is (= 2 (-> @logging (nth 3) count))) + (is (= [4 1] (-> @logging (nth 3) (->> (map :fruit/id))))) + ;; now repeat without the row logging + (reset! logging []) + (jdbc/execute! (mw/wrapper (ds) + {:builder-fn rs/as-lower-maps + :sql-params-fn logger + :rs!-fn logger}) + sql-p) + ;; should log two things + (is (= 2 (-> @logging count))) + ;; :next.jdbc/sql-params value + (is (= sql-p (-> @logging (nth 0)))) + ;; full result set with two rows + (is (= 2 (-> @logging (nth 1) count))) + (is (= [4 1] (-> @logging (nth 1) (->> (map :fruit/id))))))) + +(deftest timing-test + (let [timing (atom {:calls 0 :total 0.0}) + start-fn (fn [sql-p opts] + (swap! (:timing opts) update :calls inc) + (assoc opts :start (System/nanoTime))) + exec-fn (fn [opts _] + (let [end (System/nanoTime)] + (swap! (:timing opts) update :total + (- end (:start opts))) + opts)) + sql-p ["select * from fruit where id in (?,?) order by id desc" 1 4]] + (jdbc/execute! (mw/wrapper (ds) {:timing timing + :sql-params-fn start-fn + :execute-fn exec-fn}) + sql-p) + (jdbc/execute! (mw/wrapper (ds) {:timing timing + :sql-params-fn start-fn + :execute-fn exec-fn}) + sql-p) + (println (:calls @timing) (long (:total @timing))))) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 89cce66..a067d8c 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -220,39 +220,3 @@ VALUES ('Pear', 'green', 49, 47) (into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"])))) (is (thrown? IllegalArgumentException (doall (take 3 (jdbc/plan (ds) ["select * from fruit"])))))) - -(deftest adapter-side-effects - (let [logging (atom []) - logger (fn [data] (swap! logging conj data)) - sql-p ["select * from fruit where id in (?,?) order by id desc" 1 4]] - (jdbc/execute! (ds) sql-p - {:builder-fn (rs/builder-adapter - rs/as-lower-maps - {:sql-params-fn logger - :row!-fn logger - :rs!-fn logger})}) - ;; should log four things - (is (= 4 (-> @logging count))) - ;; :next.jdbc/sql-params value - (is (= sql-p (-> @logging (nth 0)))) - ;; first row (with PK 4) - (is (= 4 (-> @logging (nth 1) :fruit/id))) - ;; second row (with PK 1) - (is (= 1 (-> @logging (nth 2) :fruit/id))) - ;; full result set with two rows - (is (= 2 (-> @logging (nth 3) count))) - (is (= [4 1] (-> @logging (nth 3) (->> (map :fruit/id))))) - ;; now repeat without the row logging - (reset! logging []) - (jdbc/execute! (ds) sql-p - {:builder-fn (rs/builder-adapter - rs/as-lower-maps - {:sql-params-fn logger - :rs!-fn logger})}) - ;; should log two things - (is (= 2 (-> @logging count))) - ;; :next.jdbc/sql-params value - (is (= sql-p (-> @logging (nth 0)))) - ;; full result set with two rows - (is (= 2 (-> @logging (nth 1) count))) - (is (= [4 1] (-> @logging (nth 1) (->> (map :fruit/id)))))))