Recast the adapter/processors as a middleware

Still not happy with this but it seems more "holistic".
This commit is contained in:
Sean Corfield 2019-11-09 22:59:34 -08:00
parent b2b7696973
commit a57011a998
4 changed files with 160 additions and 75 deletions

View file

@ -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)))

View file

@ -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

View file

@ -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)))))

View file

@ -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)))))))