-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
result_set.clj
163 lines (144 loc) · 7.28 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
157
158
159
160
161
162
163
(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 ::types/dispatch-value.keyword-or-class
: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))
;; TODO FIXME -- it seems like the column name we get here has already went thru the label fn/qualifying
;; functions. The `(originally ...)` in the log message is wrong. Are we applying label function twice?!
(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)
(when-not i
(log/warnf :results "Could not determine index of column name %s. Found: %s" column-name cols))
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)
label-fn (get opts :label-fn name)
qualifier-fn (memoize
(fn [table]
(let [table (some-> table not-empty name label-fn)
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)
_ (log/debugf :results "Getting modified column names with next.jdbc options %s" opts)
col-names (next.jdbc.rs/get-modified-column-names rsmeta opts)]
(log/debugf :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)]
(log/tracef :results "column name -> index = %s" col-name->index)
(loop [acc init]
(b/cond
(not (.next rset))
(do
(log/tracef :results "Result set has no more rows.")
acc)
:let [row-num (.getRow rset)
_ (log/tracef :results "Fetch row %s" row-num)
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')))))