-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
result_set.clj
156 lines (137 loc) · 6.77 KB
/
result_set.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
(ns toucan2.jdbc.result-set
"Implementation of a custom `next.jdbc` result set builder function, [[builder-fn]], and the default
implementation; [[reduce-result-set]] which is used to reduce results from JDBC databases."
(:require
[better-cond.core :as b]
[clojure.spec.alpha :as s]
[methodical.core :as m]
[next.jdbc.result-set :as next.jdbc.rs]
[toucan2.instance :as instance]
[toucan2.jdbc :as jdbc]
[toucan2.jdbc.read :as jdbc.read]
[toucan2.jdbc.row :as jdbc.row]
[toucan2.log :as log]
[toucan2.model :as model]
[toucan2.types :as types]
[toucan2.util :as u])
(:import
(java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true)
(comment s/keep-me
types/keep-me)
(m/defmulti builder-fn
"Return the `next.jdbc` builder function to use to create the results when querying a model. By default, this
uses [[instance-builder-fn]], and returns Toucan 2 instances; but if you want to use plain maps you can use one of the
other builder functions that ships with `next.jdbc`, or write your own custom builder function."
{:arglists '([^java.sql.Connection conn₁ model₂ ^java.sql.ResultSet rset opts])
:defmethod-arities #{4}
:dispatch-value-spec (s/nonconforming
(s/or :default ::types/dispatch-value.default
:conn-model (s/cat :conn (s/or :default ::types/dispatch-value.default
:classname symbol?)
:model ::types/dispatch-value.model)))}
u/dispatch-on-first-two-args)
(defrecord ^:no-doc InstanceBuilder [model ^ResultSet rset ^ResultSetMetaData rsmeta cols]
next.jdbc.rs/RowBuilder
(->row [_this]
(log/tracef :results "Fetching row %s" (.getRow rset))
(transient (instance/instance model)))
(column-count [_this]
(count cols))
;; this is purposefully not implemented because we should never get here; if we do it is an error and we want an
;; Exception thrown.
#_(with-column [this row i]
(println (pr-str (list 'with-column 'this 'row i)))
(next.jdbc.rs/with-column-value this row (nth cols (dec i))
(next.jdbc.rs/read-column-by-index (.getObject rset ^Integer i) rsmeta i)))
(with-column-value [_this row col v]
(assert (some? col) "Invalid col")
(assoc! row col v))
(row! [_this row]
(log/tracef :results "Converting transient row to persistent row")
(persistent! row))
next.jdbc.rs/ResultSetBuilder
(->rs [_this]
(transient []))
(with-row [_this acc row]
(conj! acc row))
(rs! [_this acc]
(persistent! acc)))
(defn- make-column-name->index [cols label-fn]
{:pre [(seq cols) (fn? label-fn)]}
(memoize
(fn [column-name]
(when (or (string? column-name)
(instance? clojure.lang.Named column-name))
(let [column-name' (keyword
(when (instance? clojure.lang.Named column-name)
(when-let [col-ns (namespace column-name)]
(label-fn (name col-ns))))
(label-fn (name column-name)))
i (when column-name'
(first (keep-indexed
(fn [i col]
(when (= col column-name')
(inc i)))
cols)))]
(log/tracef :results "Index of column named %s (originally %s) is %s" column-name' column-name i)
i)))))
(defn instance-builder-fn
"Create a result set map builder function appropriate for passing as the `:builder-fn` option to `next.jdbc` that
will create [[toucan2.instance]]s of `model` using namespaces determined
by [[toucan2.model/table-name->namespace]]."
[model ^ResultSet rset opts]
(let [table-name->ns (model/table-name->namespace model)
_ (log/debugf :results "Using table namespaces %s" table-name->ns)
label-fn (get opts :label-fn name)
qualifier-fn (memoize
(fn [table]
(let [table (label-fn (name table))
table-ns (some-> (get table-name->ns table) name)]
(log/tracef :results "Using namespace %s for columns in table %s" table-ns table)
table-ns)))
opts (merge {:label-fn label-fn
:qualifier-fn qualifier-fn}
opts)
rsmeta (.getMetaData rset)
col-names (next.jdbc.rs/get-modified-column-names rsmeta opts)]
(log/tracef :results "Column names: %s" col-names)
(constantly
(assoc (->InstanceBuilder model rset rsmeta col-names) :opts opts))))
(m/defmethod builder-fn :default
"Default `next.jdbc` builder function. Uses [[instance-builder-fn]] to return Toucan 2 instances."
[_conn model rset opts]
(let [merged-opts (jdbc/merge-options opts)]
(instance-builder-fn model rset merged-opts)))
(defn ^:no-doc reduce-result-set
"Reduce a `java.sql.ResultSet` using reducing function `rf` and initial value `init`. `conn` is an instance of
`java.sql.Connection`. `conn` and `model` are used mostly for dispatch value purposes for things like [[builder-fn]],
and for creating instances with the correct model.
Part of the low-level implementation of the JDBC query execution backend -- you probably shouldn't be using this
directly."
[rf init conn model ^ResultSet rset opts]
(log/debugf :execute "Reduce JDBC result set for model %s with rf %s and init %s" model rf init)
(let [row-num->i->thunk (jdbc.read/make-cached-row-num->i->thunk conn model rset)
builder-fn* (next.jdbc.rs/builder-adapter
(builder-fn conn model rset opts)
(jdbc.read/read-column-by-index-fn row-num->i->thunk))
builder (builder-fn* rset opts)
combined-opts (jdbc/merge-options (merge (:opts builder) opts))
label-fn (get combined-opts :label-fn)
_ (assert (fn? label-fn) "Options must include :label-fn")
col-names (get builder :cols (next.jdbc.rs/get-modified-column-names
(.getMetaData rset)
combined-opts))
col-name->index (make-column-name->index col-names label-fn)]
(loop [acc init]
(b/cond
(not (.next rset))
acc
:let [row-num (.getRow rset)
i->thunk (row-num->i->thunk row-num)
row (jdbc.row/row model rset builder i->thunk col-name->index)
acc' (rf acc row)]
(reduced? acc')
@acc'
:else
(recur acc')))))