2020-01-01 21:13:41 +00:00
|
|
|
;; copyright (c) 2018-2020 Sean Corfield, all rights reserved
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
(ns next.jdbc.result-set
|
2019-04-21 23:13:52 +00:00
|
|
|
"An implementation of `ResultSet` handling functions.
|
2019-04-18 06:43:32 +00:00
|
|
|
|
|
|
|
|
Defines the following protocols:
|
2019-04-21 23:13:52 +00:00
|
|
|
* `DatafiableRow` -- for turning a row into something datafiable
|
|
|
|
|
* `ReadableColumn` -- to read column values by label or index
|
|
|
|
|
* `RowBuilder` -- for materializing a row
|
|
|
|
|
* `ResultSetBuilder` -- for materializing a result set
|
2019-04-18 06:43:32 +00:00
|
|
|
|
2019-08-24 18:25:19 +00:00
|
|
|
A broad range of result set builder implementation functions are provided.
|
|
|
|
|
|
2019-04-21 23:13:52 +00:00
|
|
|
Also provides the default implemenations for `Executable` and
|
2020-04-07 21:29:37 +00:00
|
|
|
the default `datafy`/`nav` behavior for rows from a result set.
|
|
|
|
|
|
|
|
|
|
See also https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.date-time
|
|
|
|
|
for implementations of `ReadableColumn` that provide automatic
|
|
|
|
|
conversion of some SQL data types to Java Time objects."
|
2019-03-31 23:54:34 +00:00
|
|
|
(:require [clojure.core.protocols :as core-p]
|
2020-06-27 00:38:58 +00:00
|
|
|
[clojure.core.reducers :as r]
|
2020-05-31 07:19:48 +00:00
|
|
|
[clojure.datafy :as d]
|
2019-03-31 23:54:34 +00:00
|
|
|
[next.jdbc.prepare :as prepare]
|
|
|
|
|
[next.jdbc.protocols :as p])
|
2019-10-14 18:13:36 +00:00
|
|
|
(:import (java.sql Clob
|
|
|
|
|
PreparedStatement
|
2019-03-31 23:54:34 +00:00
|
|
|
ResultSet ResultSetMetaData
|
2019-11-15 00:15:52 +00:00
|
|
|
Statement
|
2019-08-09 00:01:23 +00:00
|
|
|
SQLException)
|
2020-06-27 19:21:02 +00:00
|
|
|
(java.util Locale)
|
|
|
|
|
(java.util.concurrent ForkJoinPool ForkJoinTask)))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
|
|
2019-04-18 06:43:32 +00:00
|
|
|
(defn get-column-names
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given `ResultSetMetaData`, return a vector of column names, each qualified by
|
2019-04-18 15:12:12 +00:00
|
|
|
the table from which it came."
|
2020-05-22 01:39:23 +00:00
|
|
|
[^ResultSetMetaData rsmeta _]
|
2020-02-26 19:48:42 +00:00
|
|
|
(mapv (fn [^Integer i]
|
|
|
|
|
(if-let [q (not-empty (.getTableName rsmeta i))]
|
|
|
|
|
(keyword q (.getColumnLabel rsmeta i))
|
|
|
|
|
(keyword (.getColumnLabel rsmeta i))))
|
2020-08-20 19:10:41 +00:00
|
|
|
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0)))))
|
2019-04-01 06:17:12 +00:00
|
|
|
|
2019-04-18 15:12:12 +00:00
|
|
|
(defn get-unqualified-column-names
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given `ResultSetMetaData`, return a vector of unqualified column names."
|
2020-05-22 01:39:23 +00:00
|
|
|
[^ResultSetMetaData rsmeta _]
|
2019-04-18 15:12:12 +00:00
|
|
|
(mapv (fn [^Integer i] (keyword (.getColumnLabel rsmeta i)))
|
2020-08-20 19:10:41 +00:00
|
|
|
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0)))))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn get-modified-column-names
|
|
|
|
|
"Given `ResultSetMetaData`, return a vector of modified column names, each
|
|
|
|
|
qualified by the table from which it came.
|
|
|
|
|
|
|
|
|
|
Requires both the `:qualifier-fn` and `:label-fn` options."
|
2019-04-21 05:00:40 +00:00
|
|
|
[^ResultSetMetaData rsmeta opts]
|
2020-02-26 16:55:32 +00:00
|
|
|
(let [qf (:qualifier-fn opts)
|
|
|
|
|
lf (:label-fn opts)]
|
|
|
|
|
(assert qf ":qualifier-fn is required")
|
|
|
|
|
(assert lf ":label-fn is required")
|
2020-02-26 19:48:42 +00:00
|
|
|
(mapv (fn [^Integer i]
|
|
|
|
|
(if-let [q (some-> (.getTableName rsmeta i) (qf) (not-empty))]
|
|
|
|
|
(keyword q (-> (.getColumnLabel rsmeta i) (lf)))
|
|
|
|
|
(keyword (-> (.getColumnLabel rsmeta i) (lf)))))
|
2020-08-20 19:10:41 +00:00
|
|
|
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0))))))
|
2019-04-21 05:00:40 +00:00
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn get-unqualified-modified-column-names
|
|
|
|
|
"Given `ResultSetMetaData`, return a vector of unqualified modified column
|
|
|
|
|
names.
|
|
|
|
|
|
|
|
|
|
Requires the `:label-fn` option."
|
2019-04-21 05:00:40 +00:00
|
|
|
[^ResultSetMetaData rsmeta opts]
|
2020-02-26 16:55:32 +00:00
|
|
|
(let [lf (:label-fn opts)]
|
|
|
|
|
(assert lf ":label-fn is required")
|
|
|
|
|
(mapv (fn [^Integer i] (keyword (lf (.getColumnLabel rsmeta i))))
|
2020-08-20 19:10:41 +00:00
|
|
|
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0))))))
|
2019-04-21 05:00:40 +00:00
|
|
|
|
2019-08-09 00:01:23 +00:00
|
|
|
(defn- lower-case
|
|
|
|
|
"Converts a string to lower case in the US locale to avoid problems in
|
|
|
|
|
locales where the lower case version of a character is not a valid SQL
|
|
|
|
|
entity name (e.g., Turkish)."
|
|
|
|
|
[^String s]
|
|
|
|
|
(.toLowerCase s (Locale/US)))
|
|
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn get-lower-column-names
|
|
|
|
|
"Given `ResultSetMetaData`, return a vector of lower-case column names, each
|
|
|
|
|
qualified by the table from which it came."
|
|
|
|
|
[rsmeta opts]
|
|
|
|
|
(get-modified-column-names rsmeta (assoc opts
|
2019-08-09 00:01:23 +00:00
|
|
|
:qualifier-fn lower-case
|
|
|
|
|
:label-fn lower-case)))
|
2019-06-05 01:01:19 +00:00
|
|
|
|
|
|
|
|
(defn get-unqualified-lower-column-names
|
|
|
|
|
"Given `ResultSetMetaData`, return a vector of unqualified column names."
|
|
|
|
|
[rsmeta opts]
|
|
|
|
|
(get-unqualified-modified-column-names rsmeta
|
2019-08-09 00:01:23 +00:00
|
|
|
(assoc opts :label-fn lower-case)))
|
2019-06-05 01:01:19 +00:00
|
|
|
|
2020-07-07 21:19:12 +00:00
|
|
|
(defprotocol ReadableColumn :extend-via-metadata true
|
2019-04-21 23:13:52 +00:00
|
|
|
"Protocol for reading objects from the `java.sql.ResultSet`. Default
|
|
|
|
|
implementations (for `Object` and `nil`) return the argument, and the
|
|
|
|
|
`Boolean` implementation ensures a canonicalized `true`/`false` value,
|
2020-07-07 21:19:12 +00:00
|
|
|
but it can be extended to provide custom behavior for special types.
|
|
|
|
|
Extension via metadata is supported."
|
2019-04-02 06:25:10 +00:00
|
|
|
(read-column-by-label [val label]
|
|
|
|
|
"Function for transforming values after reading them via a column label.")
|
|
|
|
|
(read-column-by-index [val rsmeta idx]
|
|
|
|
|
"Function for transforming values after reading them via a column index."))
|
|
|
|
|
|
2019-04-02 06:32:24 +00:00
|
|
|
(extend-protocol ReadableColumn
|
2019-04-02 06:25:10 +00:00
|
|
|
Object
|
|
|
|
|
(read-column-by-label [x _] x)
|
|
|
|
|
(read-column-by-index [x _2 _3] x)
|
|
|
|
|
|
|
|
|
|
Boolean
|
|
|
|
|
(read-column-by-label [x _] (if (= true x) true false))
|
|
|
|
|
(read-column-by-index [x _2 _3] (if (= true x) true false))
|
|
|
|
|
|
|
|
|
|
nil
|
|
|
|
|
(read-column-by-label [_1 _2] nil)
|
|
|
|
|
(read-column-by-index [_1 _2 _3] nil))
|
2019-04-02 05:19:02 +00:00
|
|
|
|
2019-04-11 06:59:19 +00:00
|
|
|
(defprotocol RowBuilder
|
2019-04-23 00:41:31 +00:00
|
|
|
"Protocol for building rows in various representations.
|
2019-04-18 21:06:14 +00:00
|
|
|
|
2019-04-21 23:13:52 +00:00
|
|
|
The default implementation for building hash maps: `MapResultSetBuilder`"
|
2019-04-23 00:41:31 +00:00
|
|
|
(->row [_]
|
|
|
|
|
"Called once per row to create the basis of each row.")
|
|
|
|
|
(column-count [_]
|
|
|
|
|
"Return the number of columns in each row.")
|
|
|
|
|
(with-column [_ row i]
|
|
|
|
|
"Called with the row and the index of the column to be added;
|
|
|
|
|
this is expected to read the column value from the `ResultSet`!")
|
2020-07-07 21:19:12 +00:00
|
|
|
(with-column-value [_ row col v]
|
|
|
|
|
"Called with the row, the column name, and the value to be added;
|
|
|
|
|
this is a low-level function, typically used by `with-column`.")
|
2019-04-23 00:41:31 +00:00
|
|
|
(row! [_ row]
|
|
|
|
|
"Called once per row to finalize each row once it is complete."))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
|
|
|
|
(defprotocol ResultSetBuilder
|
2019-04-23 00:41:31 +00:00
|
|
|
"Protocol for building result sets in various representations.
|
2019-04-18 21:06:14 +00:00
|
|
|
|
2019-04-23 00:41:31 +00:00
|
|
|
Default implementations for building vectors of hash maps and vectors of
|
|
|
|
|
column names and row values: `MapResultSetBuilder` & `ArrayResultSetBuilder`"
|
|
|
|
|
(->rs [_]
|
|
|
|
|
"Called to create the basis of the result set.")
|
|
|
|
|
(with-row [_ rs row]
|
|
|
|
|
"Called with the result set and the row to be added.")
|
|
|
|
|
(rs! [_ rs]
|
|
|
|
|
"Called to finalize the result set once it is complete."))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
2020-07-07 21:19:12 +00:00
|
|
|
(defn builder-adapter
|
|
|
|
|
"Given any builder function (e.g., `as-lower-maps`) and a column reading
|
|
|
|
|
function, return a new builder function that uses that column reading
|
|
|
|
|
function instead of `.getObject` and `read-column-by-index` so you can
|
|
|
|
|
override the default behavior.
|
|
|
|
|
|
|
|
|
|
The default column-by-index-fn behavior would be equivalent to:
|
|
|
|
|
|
|
|
|
|
(defn default-column-by-index-fn
|
|
|
|
|
[builder ^ResultSet rs ^Integer i]
|
|
|
|
|
(read-column-by-index (.getObject rs i) (:rsmeta builder) i))
|
|
|
|
|
|
|
|
|
|
Your column-by-index-fn can use the result set metadata `(:rsmeta builder)`
|
|
|
|
|
and/or the (processed) column name `(nth (:cols builder) (dec i))` to
|
|
|
|
|
determine whether to call `.getObject` or some other method to read the
|
|
|
|
|
column's value, and can choose whether or not to use the `ReadableColumn`
|
|
|
|
|
protocol-based value processor (and could add metadata to the value to
|
|
|
|
|
satify that protocol on a per-instance basis)."
|
|
|
|
|
[builder-fn column-by-index-fn]
|
|
|
|
|
(fn [rs opts]
|
|
|
|
|
(let [builder (builder-fn rs opts)]
|
|
|
|
|
(reify
|
|
|
|
|
RowBuilder
|
|
|
|
|
(->row [this] (->row builder))
|
|
|
|
|
(column-count [this] (column-count builder))
|
|
|
|
|
(with-column [this row i]
|
|
|
|
|
(with-column-value this row (nth (:cols builder) (dec i))
|
|
|
|
|
(column-by-index-fn builder rs i)))
|
|
|
|
|
(with-column-value [this row col v]
|
|
|
|
|
(with-column-value builder row col v))
|
|
|
|
|
(row! [this row] (row! builder row))
|
|
|
|
|
ResultSetBuilder
|
|
|
|
|
(->rs [this] (->rs builder))
|
|
|
|
|
(with-row [this mrs row] (with-row builder mrs row))
|
|
|
|
|
(rs! [this mrs] (rs! builder mrs))
|
|
|
|
|
clojure.lang.ILookup
|
|
|
|
|
(valAt [this k] (get builder k))
|
|
|
|
|
(valAt [this k not-found] (get builder k not-found))))))
|
|
|
|
|
|
2019-04-18 15:12:12 +00:00
|
|
|
(defrecord MapResultSetBuilder [^ResultSet rs rsmeta cols]
|
|
|
|
|
RowBuilder
|
|
|
|
|
(->row [this] (transient {}))
|
|
|
|
|
(column-count [this] (count cols))
|
|
|
|
|
(with-column [this row i]
|
2020-07-07 21:19:12 +00:00
|
|
|
(with-column-value this row (nth cols (dec i))
|
|
|
|
|
(read-column-by-index (.getObject rs ^Integer i) rsmeta i)))
|
|
|
|
|
(with-column-value [this row col v]
|
|
|
|
|
(assoc! row col v))
|
2019-04-18 15:12:12 +00:00
|
|
|
(row! [this row] (persistent! row))
|
|
|
|
|
ResultSetBuilder
|
|
|
|
|
(->rs [this] (transient []))
|
|
|
|
|
(with-row [this mrs row]
|
|
|
|
|
(conj! mrs row))
|
|
|
|
|
(rs! [this mrs] (persistent! mrs)))
|
|
|
|
|
|
|
|
|
|
(defn as-maps
|
2019-04-27 05:42:27 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces bare vectors of hash map rows.
|
|
|
|
|
|
|
|
|
|
This is the default `:builder-fn` option."
|
2019-04-18 06:34:31 +00:00
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
|
|
|
|
cols (get-column-names rsmeta opts)]
|
2019-04-18 15:12:12 +00:00
|
|
|
(->MapResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
|
|
|
|
(defn as-unqualified-maps
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-04-18 15:12:12 +00:00
|
|
|
that produces bare vectors of hash map rows, with simple keys."
|
2019-04-18 06:34:31 +00:00
|
|
|
[^ResultSet rs opts]
|
2019-04-11 06:59:19 +00:00
|
|
|
(let [rsmeta (.getMetaData rs)
|
2019-04-18 15:12:12 +00:00
|
|
|
cols (get-unqualified-column-names rsmeta opts)]
|
|
|
|
|
(->MapResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn as-modified-maps
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-06-05 01:01:19 +00:00
|
|
|
that produces bare vectors of hash map rows, with modified keys.
|
|
|
|
|
|
|
|
|
|
Requires both the `:qualifier-fn` and `:label-fn` options."
|
2019-04-21 05:00:40 +00:00
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
2019-06-05 01:01:19 +00:00
|
|
|
cols (get-modified-column-names rsmeta opts)]
|
2019-04-21 05:00:40 +00:00
|
|
|
(->MapResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn as-unqualified-modified-maps
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-06-05 01:01:19 +00:00
|
|
|
that produces bare vectors of hash map rows, with simple, modified keys.
|
|
|
|
|
|
|
|
|
|
Requires the `:label-fn` option."
|
2019-04-21 05:00:40 +00:00
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
2019-06-05 01:01:19 +00:00
|
|
|
cols (get-unqualified-modified-column-names rsmeta opts)]
|
2019-04-21 05:00:40 +00:00
|
|
|
(->MapResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn as-lower-maps
|
|
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces bare vectors of hash map rows, with lower-case keys."
|
|
|
|
|
[rs opts]
|
|
|
|
|
(as-modified-maps rs (assoc opts
|
2019-08-09 00:01:23 +00:00
|
|
|
:qualifier-fn lower-case
|
|
|
|
|
:label-fn lower-case)))
|
2019-06-05 01:01:19 +00:00
|
|
|
|
|
|
|
|
(defn as-unqualified-lower-maps
|
|
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces bare vectors of hash map rows, with simple, lower-case keys."
|
|
|
|
|
[rs opts]
|
2019-08-09 00:01:23 +00:00
|
|
|
(as-unqualified-modified-maps rs (assoc opts :label-fn lower-case)))
|
2019-06-05 01:01:19 +00:00
|
|
|
|
2020-07-08 18:52:22 +00:00
|
|
|
(defmacro ^:private def-snake-kebab []
|
|
|
|
|
(try
|
|
|
|
|
(let [kebab-case (requiring-resolve 'camel-snake-kebab.core/->kebab-case)]
|
|
|
|
|
`(do
|
|
|
|
|
(defn ~'as-kebab-maps
|
|
|
|
|
{:doc "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces bare vectors of hash map rows, with kebab-case keys."
|
|
|
|
|
:arglists '([~'rs ~'opts])}
|
|
|
|
|
[rs# opts#]
|
|
|
|
|
(as-modified-maps rs# (assoc opts#
|
|
|
|
|
:qualifier-fn ~kebab-case
|
|
|
|
|
:label-fn ~kebab-case)))
|
|
|
|
|
(defn ~'as-unqualified-kebab-maps
|
|
|
|
|
{:doc "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces bare vectors of hash map rows, with simple, kebab-case keys."
|
|
|
|
|
:arglists '([~'rs ~'opts])}
|
|
|
|
|
[rs# opts#]
|
|
|
|
|
(as-unqualified-modified-maps rs# (assoc opts# :label-fn ~kebab-case)))))
|
|
|
|
|
(catch Throwable _)))
|
|
|
|
|
(def-snake-kebab)
|
|
|
|
|
|
2019-08-21 21:19:32 +00:00
|
|
|
(defn as-maps-adapter
|
2019-08-21 21:47:55 +00:00
|
|
|
"Given a map builder function (e.g., `as-lower-maps`) and a column reading
|
|
|
|
|
function, return a new builder function that uses that column reading
|
|
|
|
|
function instead of `.getObject` so you can override the default behavior.
|
2019-08-21 21:19:32 +00:00
|
|
|
|
|
|
|
|
The default column-reader behavior would be equivalent to:
|
|
|
|
|
|
|
|
|
|
(defn default-column-reader
|
|
|
|
|
[^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
|
|
|
|
|
(.getObject rs i))
|
|
|
|
|
|
|
|
|
|
Your column-reader can use the result set metadata to determine whether
|
2019-08-24 18:25:19 +00:00
|
|
|
to call `.getObject` or some other method to read the column's value.
|
|
|
|
|
|
2020-07-07 21:19:12 +00:00
|
|
|
`read-column-by-index` is still called on the result of that read.
|
|
|
|
|
|
|
|
|
|
Note: this is different behavior to `builder-adapter`'s `column-by-index-fn`."
|
2019-08-21 21:19:32 +00:00
|
|
|
[builder-fn column-reader]
|
2020-07-07 21:19:12 +00:00
|
|
|
(builder-adapter builder-fn
|
|
|
|
|
(fn [builder rs i]
|
|
|
|
|
(let [^ResultSetMetaData rsmeta (:rsmeta builder)]
|
|
|
|
|
(read-column-by-index (column-reader rs rsmeta i)
|
|
|
|
|
rsmeta
|
|
|
|
|
i)))))
|
2019-08-21 21:19:32 +00:00
|
|
|
|
2019-10-14 18:13:36 +00:00
|
|
|
(defn clob->string
|
|
|
|
|
"Given a CLOB column value, read it as a string."
|
|
|
|
|
[^Clob clob]
|
2019-10-18 01:02:37 +00:00
|
|
|
(with-open [rdr (.getCharacterStream clob)]
|
2019-10-14 18:13:36 +00:00
|
|
|
(slurp rdr)))
|
|
|
|
|
|
|
|
|
|
(defn clob-column-reader
|
|
|
|
|
"An example column-reader that still uses `.getObject` but expands CLOB
|
|
|
|
|
columns into strings."
|
|
|
|
|
[^ResultSet rs ^ResultSetMetaData _ ^Integer i]
|
|
|
|
|
(when-let [value (.getObject rs i)]
|
|
|
|
|
(cond-> value
|
|
|
|
|
(instance? Clob value)
|
|
|
|
|
(clob->string))))
|
|
|
|
|
|
2019-04-18 15:12:12 +00:00
|
|
|
(defrecord ArrayResultSetBuilder [^ResultSet rs rsmeta cols]
|
|
|
|
|
RowBuilder
|
|
|
|
|
(->row [this] (transient []))
|
|
|
|
|
(column-count [this] (count cols))
|
|
|
|
|
(with-column [this row i]
|
2020-07-07 21:19:12 +00:00
|
|
|
(with-column-value this row nil
|
|
|
|
|
(read-column-by-index (.getObject rs ^Integer i) rsmeta i)))
|
|
|
|
|
(with-column-value [this row _ v]
|
|
|
|
|
(conj! row v))
|
2019-04-18 15:12:12 +00:00
|
|
|
(row! [this row] (persistent! row))
|
|
|
|
|
ResultSetBuilder
|
|
|
|
|
(->rs [this] (transient [cols]))
|
|
|
|
|
(with-row [this ars row]
|
|
|
|
|
(conj! ars row))
|
|
|
|
|
(rs! [this ars] (persistent! ars)))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
|
|
|
|
(defn as-arrays
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-04-18 06:34:31 +00:00
|
|
|
that produces a vector of column names followed by vectors of row values."
|
|
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
|
|
|
|
cols (get-column-names rsmeta opts)]
|
2019-04-18 15:12:12 +00:00
|
|
|
(->ArrayResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
|
|
|
|
(defn as-unqualified-arrays
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-04-18 15:12:12 +00:00
|
|
|
that produces a vector of simple column names followed by vectors of row
|
|
|
|
|
values."
|
|
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
|
|
|
|
cols (get-unqualified-column-names rsmeta opts)]
|
|
|
|
|
(->ArrayResultSetBuilder rs rsmeta cols)))
|
2019-04-11 06:59:19 +00:00
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn as-modified-arrays
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-06-05 01:01:19 +00:00
|
|
|
that produces a vector of modified column names followed by vectors of
|
|
|
|
|
row values.
|
|
|
|
|
|
|
|
|
|
Requires both the `:qualifier-fn` and `:label-fn` options."
|
2019-04-21 05:00:40 +00:00
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
2019-06-05 01:01:19 +00:00
|
|
|
cols (get-modified-column-names rsmeta opts)]
|
2019-04-21 05:00:40 +00:00
|
|
|
(->ArrayResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn as-unqualified-modified-arrays
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
2019-06-05 01:01:19 +00:00
|
|
|
that produces a vector of simple, modified column names followed by
|
|
|
|
|
vectors of row values.
|
|
|
|
|
|
|
|
|
|
Requires the `:label-fn` option."
|
2019-04-21 05:00:40 +00:00
|
|
|
[^ResultSet rs opts]
|
|
|
|
|
(let [rsmeta (.getMetaData rs)
|
2019-06-05 01:01:19 +00:00
|
|
|
cols (get-unqualified-modified-column-names rsmeta opts)]
|
2019-04-21 05:00:40 +00:00
|
|
|
(->ArrayResultSetBuilder rs rsmeta cols)))
|
|
|
|
|
|
2019-06-05 01:01:19 +00:00
|
|
|
(defn as-lower-arrays
|
|
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces a vector of lower-case column names followed by vectors of
|
|
|
|
|
row values."
|
|
|
|
|
[rs opts]
|
|
|
|
|
(as-modified-arrays rs (assoc opts
|
2019-08-09 00:01:23 +00:00
|
|
|
:qualifier-fn lower-case
|
|
|
|
|
:label-fn lower-case)))
|
2019-06-05 01:01:19 +00:00
|
|
|
|
|
|
|
|
(defn as-unqualified-lower-arrays
|
|
|
|
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
|
|
|
|
that produces a vector of simple, lower-case column names followed by
|
|
|
|
|
vectors of row values."
|
|
|
|
|
[rs opts]
|
2019-08-09 00:01:23 +00:00
|
|
|
(as-unqualified-modified-arrays rs (assoc opts :label-fn lower-case)))
|
2019-06-05 01:01:19 +00:00
|
|
|
|
2019-08-21 21:47:55 +00:00
|
|
|
(defn as-arrays-adapter
|
|
|
|
|
"Given an array builder function (e.g., `as-unqualified-arrays`) and a column
|
|
|
|
|
reading function, return a new builder function that uses that column reading
|
|
|
|
|
function instead of `.getObject` so you can override the default behavior.
|
|
|
|
|
|
|
|
|
|
The default column-reader behavior would be equivalent to:
|
|
|
|
|
|
|
|
|
|
(defn default-column-reader
|
|
|
|
|
[^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
|
|
|
|
|
(.getObject rs i))
|
|
|
|
|
|
|
|
|
|
Your column-reader can use the result set metadata to determine whether
|
2019-08-24 18:25:19 +00:00
|
|
|
to call `.getObject` or some other method to read the column's value.
|
|
|
|
|
|
2020-07-07 21:19:12 +00:00
|
|
|
`read-column-by-index` is still called on the result of that read.
|
|
|
|
|
|
|
|
|
|
Note: this is different behavior to `builder-adapter`'s `column-by-index-fn`."
|
2019-08-21 21:47:55 +00:00
|
|
|
[builder-fn column-reader]
|
2020-07-07 21:19:12 +00:00
|
|
|
(builder-adapter builder-fn
|
|
|
|
|
(fn [builder rs i]
|
|
|
|
|
(let [^ResultSetMetaData rsmeta (:rsmeta builder)]
|
|
|
|
|
(read-column-by-index (column-reader rs rsmeta i)
|
|
|
|
|
rsmeta
|
|
|
|
|
i)))))
|
2019-08-21 21:47:55 +00:00
|
|
|
|
2019-04-11 06:59:19 +00:00
|
|
|
(declare navize-row)
|
|
|
|
|
|
2019-04-18 21:06:14 +00:00
|
|
|
(defprotocol DatafiableRow
|
2019-04-23 00:41:31 +00:00
|
|
|
"Protocol for making rows datafiable and therefore navigable.
|
|
|
|
|
|
|
|
|
|
The default implementation just adds metadata so that `datafy` can be
|
|
|
|
|
called on the row, which will produce something that `nav` can be called
|
|
|
|
|
on, to lazily navigate through foreign key relationships into other tables.
|
|
|
|
|
|
|
|
|
|
If `datafiable-row` is called when reducing the result set produced by
|
2019-05-22 23:22:14 +00:00
|
|
|
`next.jdbc/plan`, the row is fully-realized from the `ResultSet`
|
2020-02-28 18:54:28 +00:00
|
|
|
first, using the `:builder-fn` (or `as-maps` by default)."
|
2019-04-23 00:41:31 +00:00
|
|
|
(datafiable-row [this connectable opts]
|
|
|
|
|
"Produce a datafiable representation of a row from a `ResultSet`."))
|
2019-04-18 21:06:14 +00:00
|
|
|
|
2019-04-18 06:34:31 +00:00
|
|
|
(defn- row-builder
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `RowBuilder` -- a row materialization strategy -- produce a fully
|
2019-04-18 06:34:31 +00:00
|
|
|
materialized row from it."
|
2019-04-24 21:22:35 +00:00
|
|
|
[builder]
|
|
|
|
|
(->> (reduce (fn [r i] (with-column builder r i))
|
|
|
|
|
(->row builder)
|
|
|
|
|
(range 1 (inc (column-count builder))))
|
|
|
|
|
(row! builder)))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
2019-08-02 19:24:04 +00:00
|
|
|
(definterface MapifiedResultSet)
|
|
|
|
|
|
2020-05-23 00:11:36 +00:00
|
|
|
(defprotocol InspectableMapifiedResultSet :extend-via-metadata true
|
2020-05-23 03:16:40 +00:00
|
|
|
"Protocol for exposing aspects of the (current) result set via functions.
|
|
|
|
|
|
|
|
|
|
The intent here is to expose information that is associated with either
|
|
|
|
|
the (current row of the) result set or the result set metadata, via
|
|
|
|
|
functions that can be called inside a reducing function being used over
|
|
|
|
|
`next.jdbc/plan`, including situations where the reducing function has
|
|
|
|
|
to realize a row by calling `datafiable-row` but still wants to call
|
|
|
|
|
these functions on the (realized) row."
|
2020-05-23 00:11:36 +00:00
|
|
|
(row-number [this]
|
2020-06-07 16:39:04 +00:00
|
|
|
"Return the current 1-based row number, if available.
|
|
|
|
|
|
|
|
|
|
Should not cause any row realization.")
|
2020-05-23 00:11:36 +00:00
|
|
|
(column-names [this]
|
2020-06-07 16:39:04 +00:00
|
|
|
"Return a vector of the column names from the result set.
|
|
|
|
|
|
|
|
|
|
Reifies the result builder, in order to construct column names,
|
|
|
|
|
but should not cause any row realization.")
|
2020-05-31 06:44:18 +00:00
|
|
|
(metadata [this]
|
|
|
|
|
"Return the raw `ResultSetMetaData` object from the result set.
|
|
|
|
|
|
2020-06-07 16:39:04 +00:00
|
|
|
Should not cause any row realization.
|
|
|
|
|
|
|
|
|
|
If `next.jdbc.datafy` has been required, this metadata will be
|
|
|
|
|
fully-realized as a Clojure data structure, otherwise this should
|
|
|
|
|
not be allowed to 'leak' outside of the reducing function as it may
|
|
|
|
|
depend on the connection remaining open, in order to be valid."))
|
2020-05-23 00:11:36 +00:00
|
|
|
|
2019-03-31 23:54:34 +00:00
|
|
|
(defn- mapify-result-set
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `ResultSet`, return an object that wraps the current row as a hash
|
2019-03-31 23:54:34 +00:00
|
|
|
map. Note that a result set is mutable and the current row will change behind
|
|
|
|
|
this wrapper so operations need to be eager (and fairly limited).
|
|
|
|
|
|
2019-08-02 19:24:04 +00:00
|
|
|
Supports `IPersistentMap` in full. Any operation that requires a full hash
|
|
|
|
|
map (`assoc`, `dissoc`, `cons`, `seq`, etc) will cause a full row to be
|
|
|
|
|
realized (via `row-builder` above). The result will be a regular map: if
|
|
|
|
|
you want the row to be datafiable/navigable, use `datafiable-row` to
|
|
|
|
|
realize the full row explicitly before performing other
|
|
|
|
|
(metadata-preserving) operations on it."
|
2019-03-31 23:54:34 +00:00
|
|
|
[^ResultSet rs opts]
|
2019-04-24 21:22:35 +00:00
|
|
|
(let [builder (delay ((get opts :builder-fn as-maps) rs opts))]
|
2019-03-31 23:54:34 +00:00
|
|
|
(reify
|
|
|
|
|
|
2019-08-02 19:24:04 +00:00
|
|
|
MapifiedResultSet
|
|
|
|
|
;; marker, just for printing resolution
|
|
|
|
|
|
2020-05-23 00:11:36 +00:00
|
|
|
InspectableMapifiedResultSet
|
|
|
|
|
(row-number [this] (.getRow rs))
|
|
|
|
|
(column-names [this] (:cols @builder))
|
2020-06-07 16:39:04 +00:00
|
|
|
(metadata [this] (d/datafy (.getMetaData rs)))
|
2020-05-23 00:11:36 +00:00
|
|
|
|
2019-08-02 19:24:04 +00:00
|
|
|
clojure.lang.IPersistentMap
|
|
|
|
|
(assoc [this k v]
|
|
|
|
|
(assoc (row-builder @builder) k v))
|
|
|
|
|
(assocEx [this k v]
|
|
|
|
|
(.assocEx ^clojure.lang.IPersistentMap (row-builder @builder) k v))
|
|
|
|
|
(without [this k]
|
|
|
|
|
(dissoc (row-builder @builder) k))
|
|
|
|
|
|
|
|
|
|
java.lang.Iterable ; Java 7 compatible: no forEach / spliterator
|
|
|
|
|
(iterator [this]
|
|
|
|
|
(.iterator ^java.lang.Iterable (row-builder @builder)))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
clojure.lang.Associative
|
|
|
|
|
(containsKey [this k]
|
2019-08-02 19:24:04 +00:00
|
|
|
(try
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
true
|
|
|
|
|
(catch SQLException _
|
|
|
|
|
false)))
|
2019-03-31 23:54:34 +00:00
|
|
|
(entryAt [this k]
|
2019-08-02 19:24:04 +00:00
|
|
|
(try
|
|
|
|
|
(clojure.lang.MapEntry. k (read-column-by-label
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
(name k)))
|
|
|
|
|
(catch SQLException _)))
|
|
|
|
|
|
|
|
|
|
clojure.lang.Counted
|
|
|
|
|
(count [this]
|
|
|
|
|
(column-count @builder))
|
|
|
|
|
|
|
|
|
|
clojure.lang.IPersistentCollection
|
|
|
|
|
(cons [this obj]
|
|
|
|
|
(cons obj (seq (row-builder @builder))))
|
|
|
|
|
(empty [this]
|
|
|
|
|
{})
|
|
|
|
|
(equiv [this obj]
|
|
|
|
|
(.equiv ^clojure.lang.IPersistentCollection (row-builder @builder) obj))
|
|
|
|
|
|
2020-05-23 07:01:39 +00:00
|
|
|
;; we support get with a numeric key for array-based builders:
|
2019-08-02 19:24:04 +00:00
|
|
|
clojure.lang.ILookup
|
|
|
|
|
(valAt [this k]
|
|
|
|
|
(try
|
2020-05-23 07:01:39 +00:00
|
|
|
(if (number? k)
|
|
|
|
|
(let [^Integer i (inc k)]
|
|
|
|
|
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
|
|
|
|
|
(read-column-by-label (.getObject rs (name k)) (name k)))
|
2019-08-02 19:24:04 +00:00
|
|
|
(catch SQLException _)))
|
|
|
|
|
(valAt [this k not-found]
|
|
|
|
|
(try
|
2020-05-23 07:01:39 +00:00
|
|
|
(if (number? k)
|
|
|
|
|
(let [^Integer i (inc k)]
|
|
|
|
|
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
|
|
|
|
|
(read-column-by-label (.getObject rs (name k)) (name k)))
|
|
|
|
|
(catch SQLException _
|
|
|
|
|
not-found)))
|
|
|
|
|
|
|
|
|
|
;; we support nth for array-based builders (i is primitive int here!):
|
|
|
|
|
clojure.lang.Indexed
|
|
|
|
|
(nth [this i]
|
|
|
|
|
(try
|
|
|
|
|
(let [i (inc i)]
|
|
|
|
|
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
|
|
|
|
|
(catch SQLException _)))
|
|
|
|
|
(nth [this i not-found]
|
|
|
|
|
(try
|
|
|
|
|
(let [i (inc i)]
|
|
|
|
|
(read-column-by-index (.getObject rs i) (:rsmeta @builder) i))
|
2019-08-02 19:24:04 +00:00
|
|
|
(catch SQLException _
|
|
|
|
|
not-found)))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
clojure.lang.Seqable
|
|
|
|
|
(seq [this]
|
2019-08-02 19:24:04 +00:00
|
|
|
(seq (row-builder @builder)))
|
2019-04-11 06:59:19 +00:00
|
|
|
|
|
|
|
|
DatafiableRow
|
|
|
|
|
(datafiable-row [this connectable opts]
|
2020-05-23 00:11:36 +00:00
|
|
|
;; since we have to call these eagerly, we trap any exceptions so
|
|
|
|
|
;; that they can be thrown when the actual functions are called
|
|
|
|
|
(let [row (try (.getRow rs) (catch Throwable t t))
|
2020-05-31 06:44:18 +00:00
|
|
|
cols (try (:cols @builder) (catch Throwable t t))
|
2020-06-07 16:39:04 +00:00
|
|
|
meta (try (d/datafy (.getMetaData rs)) (catch Throwable t t))]
|
2020-05-23 00:11:36 +00:00
|
|
|
(with-meta
|
|
|
|
|
(row-builder @builder)
|
|
|
|
|
{`core-p/datafy
|
|
|
|
|
(navize-row connectable opts)
|
|
|
|
|
`row-number
|
|
|
|
|
(fn [_] (if (instance? Throwable row) (throw row) row))
|
|
|
|
|
`column-names
|
2020-05-31 06:44:18 +00:00
|
|
|
(fn [_] (if (instance? Throwable cols) (throw cols) cols))
|
|
|
|
|
`metadata
|
|
|
|
|
(fn [_] (if (instance? Throwable meta) (throw meta) meta))})))
|
2019-08-02 19:24:04 +00:00
|
|
|
|
|
|
|
|
(toString [_]
|
|
|
|
|
(try
|
|
|
|
|
(str (row-builder @builder))
|
|
|
|
|
(catch Throwable _
|
|
|
|
|
"{row} from `plan` -- missing `map` or `reduce`?"))))))
|
|
|
|
|
|
2019-08-02 19:42:00 +00:00
|
|
|
(defmethod print-dup MapifiedResultSet [rs ^java.io.Writer w]
|
|
|
|
|
(.write w (str rs)))
|
2019-08-02 19:24:04 +00:00
|
|
|
|
|
|
|
|
(prefer-method print-dup MapifiedResultSet clojure.lang.IPersistentMap)
|
|
|
|
|
|
2019-08-02 19:42:00 +00:00
|
|
|
(defmethod print-method MapifiedResultSet [rs ^java.io.Writer w]
|
|
|
|
|
(.write w (str rs)))
|
2019-06-11 23:47:58 +00:00
|
|
|
|
2019-08-02 19:24:04 +00:00
|
|
|
(prefer-method print-method MapifiedResultSet clojure.lang.IPersistentMap)
|
2019-04-11 06:59:19 +00:00
|
|
|
|
|
|
|
|
(extend-protocol
|
|
|
|
|
DatafiableRow
|
2019-04-18 21:06:14 +00:00
|
|
|
clojure.lang.IObj ; assume we can "navigate" anything that accepts metadata
|
|
|
|
|
;; in reality, this is going to be over-optimistic and will like cause `nav`
|
|
|
|
|
;; to fail on attempts to navigate into result sets that are not hash maps
|
2019-04-11 06:59:19 +00:00
|
|
|
(datafiable-row [this connectable opts]
|
2019-04-18 15:12:12 +00:00
|
|
|
(with-meta this
|
2019-04-18 21:06:14 +00:00
|
|
|
{`core-p/datafy (navize-row connectable opts)})))
|
2019-04-11 06:59:19 +00:00
|
|
|
|
2019-06-08 22:09:42 +00:00
|
|
|
(defn datafiable-result-set
|
|
|
|
|
"Given a ResultSet, a connectable, and an options hash map, return a fully
|
|
|
|
|
realized, datafiable result set per the `:builder-fn` option passed in.
|
|
|
|
|
If no `:builder-fn` option is provided, `as-maps` is used as the default.
|
|
|
|
|
|
|
|
|
|
This can be used to process regular result sets or metadata result sets."
|
|
|
|
|
[^java.sql.ResultSet rs connectable opts]
|
|
|
|
|
(let [builder-fn (get opts :builder-fn as-maps)
|
|
|
|
|
builder (builder-fn rs opts)]
|
|
|
|
|
(loop [rs' (->rs builder) more? (.next rs)]
|
|
|
|
|
(if more?
|
|
|
|
|
(recur (with-row builder rs'
|
|
|
|
|
(datafiable-row (row-builder builder) connectable opts))
|
|
|
|
|
(.next rs))
|
|
|
|
|
(rs! builder rs')))))
|
|
|
|
|
|
2019-04-18 06:34:31 +00:00
|
|
|
(defn- stmt->result-set
|
2019-04-21 23:13:52 +00:00
|
|
|
"Given a `PreparedStatement` and options, execute it and return a `ResultSet`
|
2019-04-18 06:34:31 +00:00
|
|
|
if possible."
|
|
|
|
|
^ResultSet
|
|
|
|
|
[^PreparedStatement stmt opts]
|
|
|
|
|
(if (.execute stmt)
|
|
|
|
|
(.getResultSet stmt)
|
|
|
|
|
(when (:return-keys opts)
|
|
|
|
|
(try
|
|
|
|
|
(.getGeneratedKeys stmt)
|
|
|
|
|
(catch Exception _)))))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
2020-06-07 19:13:32 +00:00
|
|
|
(defn- stmt->result-set-update-count
|
|
|
|
|
"Given a connectable, a `Statement`, a flag indicating there might
|
|
|
|
|
be a result set, and options, return a (datafiable) result set if possible
|
|
|
|
|
(either from the statement or from generated keys). If no result set is
|
|
|
|
|
available, return a 'fake' result set containing the update count.
|
|
|
|
|
|
|
|
|
|
If no update count is available either, return nil."
|
|
|
|
|
[connectable ^Statement stmt go opts]
|
|
|
|
|
(if-let [^ResultSet
|
|
|
|
|
rs (if go
|
|
|
|
|
(.getResultSet stmt)
|
|
|
|
|
(when (:return-keys opts)
|
|
|
|
|
(try
|
|
|
|
|
(.getGeneratedKeys stmt)
|
|
|
|
|
(catch Exception _))))]
|
|
|
|
|
(datafiable-result-set rs connectable opts)
|
|
|
|
|
(let [n (.getUpdateCount stmt)]
|
|
|
|
|
(when-not (= -1 n)
|
|
|
|
|
[{:next.jdbc/update-count n}]))))
|
2020-06-06 16:32:03 +00:00
|
|
|
|
2019-03-31 23:54:34 +00:00
|
|
|
(defn- reduce-stmt
|
2019-04-21 23:13:52 +00:00
|
|
|
"Execute the `PreparedStatement`, attempt to get either its `ResultSet` or
|
|
|
|
|
its generated keys (as a `ResultSet`), and reduce that using the supplied
|
2019-04-01 06:17:12 +00:00
|
|
|
function and initial value.
|
|
|
|
|
|
2019-04-21 23:13:52 +00:00
|
|
|
If the statement yields neither a `ResultSet` nor generated keys, return
|
|
|
|
|
a hash map containing `:next.jdbc/update-count` and the number of rows
|
2019-04-05 03:25:20 +00:00
|
|
|
updated, with the supplied function and initial value applied."
|
2019-03-31 23:54:34 +00:00
|
|
|
[^PreparedStatement stmt f init opts]
|
2019-04-18 06:34:31 +00:00
|
|
|
(if-let [rs (stmt->result-set stmt opts)]
|
2019-03-31 23:54:34 +00:00
|
|
|
(let [rs-map (mapify-result-set rs opts)]
|
|
|
|
|
(loop [init' init]
|
|
|
|
|
(if (.next rs)
|
|
|
|
|
(let [result (f init' rs-map)]
|
|
|
|
|
(if (reduced? result)
|
|
|
|
|
@result
|
|
|
|
|
(recur result)))
|
|
|
|
|
init')))
|
2019-04-05 03:25:20 +00:00
|
|
|
(f init {:next.jdbc/update-count (.getUpdateCount stmt)})))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
2020-06-27 19:21:02 +00:00
|
|
|
;; ForkJoinTask wrappers copied in from clojure.core.reducers to avoid
|
|
|
|
|
;; relying on private functionality that might possibly change over time
|
|
|
|
|
|
|
|
|
|
(defn- fjtask [^Callable f]
|
|
|
|
|
(ForkJoinTask/adapt f))
|
|
|
|
|
|
|
|
|
|
(defn- fjinvoke
|
|
|
|
|
"For now, this still relies on clojure.core.reducers/pool which is
|
|
|
|
|
public but undocumented."
|
|
|
|
|
[f]
|
|
|
|
|
(if (ForkJoinTask/inForkJoinPool)
|
|
|
|
|
(f)
|
|
|
|
|
(.invoke ^ForkJoinPool @r/pool ^ForkJoinTask (fjtask f))))
|
|
|
|
|
|
|
|
|
|
(defn- fjfork [task] (.fork ^ForkJoinTask task))
|
|
|
|
|
|
|
|
|
|
(defn- fjjoin [task] (.join ^ForkJoinTask task))
|
|
|
|
|
|
2020-06-27 00:38:58 +00:00
|
|
|
(defn- fold-stmt
|
|
|
|
|
"Execute the `PreparedStatement`, attempt to get either its `ResultSet` or
|
|
|
|
|
its generated keys (as a `ResultSet`), and fold that using the supplied
|
|
|
|
|
batch size, combining function, and reducing function.
|
|
|
|
|
|
|
|
|
|
If the statement yields neither a `ResultSet` nor generated keys, produce
|
|
|
|
|
a hash map containing `:next.jdbc/update-count` and the number of rows
|
|
|
|
|
updated, and fold that as a single element collection."
|
|
|
|
|
[^PreparedStatement stmt n combinef reducef connectable opts]
|
|
|
|
|
(if-let [rs (stmt->result-set stmt opts)]
|
|
|
|
|
(let [rs-map (mapify-result-set rs opts)
|
2020-06-27 19:21:02 +00:00
|
|
|
chunk (fn [batch] (fjtask #(r/reduce reducef (combinef) batch)))
|
2020-06-27 00:38:58 +00:00
|
|
|
realize (fn [row] (datafiable-row row connectable opts))]
|
2020-06-27 05:21:49 +00:00
|
|
|
(loop [batch [] task nil]
|
2020-06-27 00:38:58 +00:00
|
|
|
(if (.next rs)
|
|
|
|
|
(if (= n (count batch))
|
2020-06-27 05:21:49 +00:00
|
|
|
(recur [(realize rs-map)]
|
2020-06-27 19:21:02 +00:00
|
|
|
(let [t (fjfork (chunk batch))]
|
2020-06-27 05:21:49 +00:00
|
|
|
(if task
|
2020-06-27 19:21:02 +00:00
|
|
|
(fjfork
|
|
|
|
|
(fjtask #(combinef (fjjoin task)
|
|
|
|
|
(fjjoin t))))
|
2020-06-27 05:21:49 +00:00
|
|
|
t)))
|
|
|
|
|
(recur (conj batch (realize rs-map)) task))
|
|
|
|
|
(if (seq batch)
|
2020-06-27 19:21:02 +00:00
|
|
|
(let [t (fjfork (chunk batch))]
|
|
|
|
|
(fjinvoke
|
|
|
|
|
#(combinef (if task (fjjoin task) (combinef))
|
|
|
|
|
(fjjoin t))))
|
2020-06-27 05:21:49 +00:00
|
|
|
(if task
|
2020-06-27 19:21:02 +00:00
|
|
|
(fjinvoke
|
|
|
|
|
#(combinef (combinef) (fjjoin task)))
|
2020-06-27 05:21:49 +00:00
|
|
|
(combinef))))))
|
2020-06-27 00:38:58 +00:00
|
|
|
(reducef (combinef) {:next.jdbc/update-count (.getUpdateCount stmt)})))
|
|
|
|
|
|
2019-11-15 00:15:52 +00:00
|
|
|
(defn- stmt-sql->result-set
|
2020-06-07 19:35:03 +00:00
|
|
|
"Given a `Statement`, a SQL command, execute it and return a
|
2020-06-07 19:13:32 +00:00
|
|
|
`ResultSet` if possible. We always attempt to return keys."
|
2019-11-15 00:15:52 +00:00
|
|
|
^ResultSet
|
2020-06-27 06:04:27 +00:00
|
|
|
[^Statement stmt ^String sql]
|
2019-11-15 00:15:52 +00:00
|
|
|
(if (.execute stmt sql)
|
|
|
|
|
(.getResultSet stmt)
|
2020-06-27 06:04:27 +00:00
|
|
|
(try
|
|
|
|
|
(.getGeneratedKeys stmt)
|
|
|
|
|
(catch Exception _))))
|
2019-11-15 00:15:52 +00:00
|
|
|
|
|
|
|
|
(defn- reduce-stmt-sql
|
|
|
|
|
"Execute the SQL command on the given `Statement`, attempt to get either
|
|
|
|
|
its `ResultSet` or its generated keys (as a `ResultSet`), and reduce
|
|
|
|
|
that using the supplied function and initial value.
|
|
|
|
|
|
|
|
|
|
If the statement yields neither a `ResultSet` nor generated keys, return
|
|
|
|
|
a hash map containing `:next.jdbc/update-count` and the number of rows
|
|
|
|
|
updated, with the supplied function and initial value applied."
|
|
|
|
|
[^Statement stmt sql f init opts]
|
2020-06-27 06:04:27 +00:00
|
|
|
(if-let [rs (stmt-sql->result-set stmt sql)]
|
2019-11-15 00:15:52 +00:00
|
|
|
(let [rs-map (mapify-result-set rs opts)]
|
|
|
|
|
(loop [init' init]
|
|
|
|
|
(if (.next rs)
|
|
|
|
|
(let [result (f init' rs-map)]
|
|
|
|
|
(if (reduced? result)
|
|
|
|
|
@result
|
|
|
|
|
(recur result)))
|
|
|
|
|
init')))
|
|
|
|
|
(f init {:next.jdbc/update-count (.getUpdateCount stmt)})))
|
|
|
|
|
|
2020-06-27 02:03:57 +00:00
|
|
|
(defn- fold-stmt-sql
|
|
|
|
|
"Execute the SQL command on the given `Statement`, attempt to get either
|
|
|
|
|
its `ResultSet` or its generated keys (as a `ResultSet`), and fold that
|
|
|
|
|
using the supplied batch size, combining function, and reducing function.
|
|
|
|
|
|
|
|
|
|
If the statement yields neither a `ResultSet` nor generated keys, produce
|
|
|
|
|
a hash map containing `:next.jdbc/update-count` and the number of rows
|
|
|
|
|
updated, and fold that as a single element collection."
|
|
|
|
|
[^Statement stmt sql n combinef reducef connectable opts]
|
2020-06-27 06:04:27 +00:00
|
|
|
(if-let [rs (stmt-sql->result-set stmt sql)]
|
2020-06-27 02:03:57 +00:00
|
|
|
(let [rs-map (mapify-result-set rs opts)
|
2020-06-27 19:21:02 +00:00
|
|
|
chunk (fn [batch] (fjtask #(r/reduce reducef (combinef) batch)))
|
2020-06-27 02:03:57 +00:00
|
|
|
realize (fn [row] (datafiable-row row connectable opts))]
|
2020-06-27 05:21:49 +00:00
|
|
|
(loop [batch [] task nil]
|
2020-06-27 02:03:57 +00:00
|
|
|
(if (.next rs)
|
|
|
|
|
(if (= n (count batch))
|
2020-06-27 05:21:49 +00:00
|
|
|
(recur [(realize rs-map)]
|
2020-06-27 19:21:02 +00:00
|
|
|
(let [t (fjfork (chunk batch))]
|
2020-06-27 05:21:49 +00:00
|
|
|
(if task
|
2020-06-27 19:21:02 +00:00
|
|
|
(fjfork
|
|
|
|
|
(fjtask #(combinef (fjjoin task)
|
|
|
|
|
(fjjoin t))))
|
2020-06-27 05:21:49 +00:00
|
|
|
t)))
|
|
|
|
|
(recur (conj batch (realize rs-map)) task))
|
|
|
|
|
(if (seq batch)
|
2020-06-27 19:21:02 +00:00
|
|
|
(let [t (fjfork (chunk batch))]
|
|
|
|
|
(fjinvoke
|
|
|
|
|
#(combinef (if task (fjjoin task) (combinef))
|
|
|
|
|
(fjjoin t))))
|
2020-06-27 05:21:49 +00:00
|
|
|
(if task
|
2020-06-27 19:21:02 +00:00
|
|
|
(fjinvoke
|
|
|
|
|
#(combinef (combinef) (fjjoin task)))
|
2020-06-27 05:21:49 +00:00
|
|
|
(combinef))))))
|
2020-06-27 02:03:57 +00:00
|
|
|
(reducef (combinef) {:next.jdbc/update-count (.getUpdateCount stmt)})))
|
|
|
|
|
|
2019-03-31 23:54:34 +00:00
|
|
|
(extend-protocol p/Executable
|
|
|
|
|
java.sql.Connection
|
|
|
|
|
(-execute [this sql-params opts]
|
2020-06-27 02:03:57 +00:00
|
|
|
(reify
|
|
|
|
|
clojure.lang.IReduceInit
|
2019-04-11 04:46:38 +00:00
|
|
|
(reduce [_ f init]
|
2020-06-27 02:03:57 +00:00
|
|
|
(with-open [stmt (prepare/create this
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
|
|
|
|
(reduce-stmt stmt f init opts)))
|
|
|
|
|
r/CollFold
|
|
|
|
|
(coll-fold [_ n combinef reducef]
|
|
|
|
|
(with-open [stmt (prepare/create this
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
|
|
|
|
(fold-stmt stmt n combinef reducef this opts)))
|
2019-06-11 23:47:58 +00:00
|
|
|
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
2019-04-18 06:34:31 +00:00
|
|
|
(-execute-one [this sql-params opts]
|
|
|
|
|
(with-open [stmt (prepare/create this
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
|
|
|
|
(if-let [rs (stmt->result-set stmt opts)]
|
2019-04-24 21:22:35 +00:00
|
|
|
(let [builder-fn (get opts :builder-fn as-maps)
|
|
|
|
|
builder (builder-fn rs opts)]
|
2019-04-18 07:28:23 +00:00
|
|
|
(when (.next rs)
|
2019-04-24 21:22:35 +00:00
|
|
|
(datafiable-row (row-builder builder) this opts)))
|
2019-04-18 06:34:31 +00:00
|
|
|
{:next.jdbc/update-count (.getUpdateCount stmt)})))
|
|
|
|
|
(-execute-all [this sql-params opts]
|
|
|
|
|
(with-open [stmt (prepare/create this
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
2020-06-07 19:13:32 +00:00
|
|
|
(if (:multi-rs opts)
|
2020-06-24 19:25:25 +00:00
|
|
|
(loop [go (.execute stmt) acc []]
|
2020-06-07 19:13:32 +00:00
|
|
|
(if-let [rs (stmt->result-set-update-count this stmt go opts)]
|
2020-06-24 19:25:25 +00:00
|
|
|
(recur (.getMoreResults stmt) (conj acc rs))
|
2020-06-07 19:13:32 +00:00
|
|
|
acc))
|
|
|
|
|
(if-let [rs (stmt->result-set stmt opts)]
|
|
|
|
|
(datafiable-result-set rs this opts)
|
|
|
|
|
[{:next.jdbc/update-count (.getUpdateCount stmt)}]))))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
2019-04-11 04:46:38 +00:00
|
|
|
javax.sql.DataSource
|
|
|
|
|
(-execute [this sql-params opts]
|
2020-06-27 00:38:58 +00:00
|
|
|
(reify
|
|
|
|
|
clojure.lang.IReduceInit
|
2019-04-11 04:46:38 +00:00
|
|
|
(reduce [_ f init]
|
2020-06-27 00:38:58 +00:00
|
|
|
(with-open [con (p/get-connection this opts)
|
|
|
|
|
stmt (prepare/create con
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
|
|
|
|
(reduce-stmt stmt f init opts)))
|
|
|
|
|
r/CollFold
|
|
|
|
|
(coll-fold [_ n combinef reducef]
|
|
|
|
|
(with-open [con (p/get-connection this opts)
|
|
|
|
|
stmt (prepare/create con
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
2020-06-27 02:03:57 +00:00
|
|
|
(fold-stmt stmt n combinef reducef this opts)))
|
2019-06-11 23:47:58 +00:00
|
|
|
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
2019-04-18 06:34:31 +00:00
|
|
|
(-execute-one [this sql-params opts]
|
2019-07-09 03:48:56 +00:00
|
|
|
(with-open [con (p/get-connection this opts)
|
|
|
|
|
stmt (prepare/create con
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
2019-04-18 06:34:31 +00:00
|
|
|
(if-let [rs (stmt->result-set stmt opts)]
|
2019-04-24 21:22:35 +00:00
|
|
|
(let [builder-fn (get opts :builder-fn as-maps)
|
|
|
|
|
builder (builder-fn rs opts)]
|
2019-04-19 06:03:09 +00:00
|
|
|
(when (.next rs)
|
2019-04-24 21:22:35 +00:00
|
|
|
(datafiable-row (row-builder builder) this opts)))
|
2019-07-09 03:48:56 +00:00
|
|
|
{:next.jdbc/update-count (.getUpdateCount stmt)})))
|
2019-04-18 06:34:31 +00:00
|
|
|
(-execute-all [this sql-params opts]
|
2019-07-09 03:48:56 +00:00
|
|
|
(with-open [con (p/get-connection this opts)
|
|
|
|
|
stmt (prepare/create con
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
opts)]
|
2020-06-07 19:13:32 +00:00
|
|
|
(if (:multi-rs opts)
|
2020-06-24 19:25:25 +00:00
|
|
|
(loop [go (.execute stmt) acc []]
|
2020-06-07 19:13:32 +00:00
|
|
|
(if-let [rs (stmt->result-set-update-count this stmt go opts)]
|
2020-06-24 19:25:25 +00:00
|
|
|
(recur (.getMoreResults stmt) (conj acc rs))
|
2020-06-07 19:13:32 +00:00
|
|
|
acc))
|
2019-04-18 06:34:31 +00:00
|
|
|
(if-let [rs (stmt->result-set stmt opts)]
|
2019-06-08 22:09:42 +00:00
|
|
|
(datafiable-result-set rs this opts)
|
2020-06-06 16:32:03 +00:00
|
|
|
[{:next.jdbc/update-count (.getUpdateCount stmt)}]))))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
2019-03-31 23:54:34 +00:00
|
|
|
java.sql.PreparedStatement
|
2019-04-18 06:34:31 +00:00
|
|
|
;; we can't tell if this PreparedStatement will return generated
|
|
|
|
|
;; keys so we pass a truthy value to at least attempt it if we
|
|
|
|
|
;; do not get a ResultSet back from the execute call
|
2019-03-31 23:54:34 +00:00
|
|
|
(-execute [this _ opts]
|
2020-06-27 02:03:57 +00:00
|
|
|
(reify
|
|
|
|
|
clojure.lang.IReduceInit
|
2019-03-31 23:54:34 +00:00
|
|
|
(reduce [_ f init]
|
2020-06-27 02:03:57 +00:00
|
|
|
(reduce-stmt this f init (assoc opts :return-keys true)))
|
|
|
|
|
r/CollFold
|
|
|
|
|
(coll-fold [_ n combinef reducef]
|
|
|
|
|
(fold-stmt this n combinef reducef (.getConnection this)
|
|
|
|
|
(assoc opts :return-keys true)))
|
2019-06-11 23:47:58 +00:00
|
|
|
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
2019-04-18 06:34:31 +00:00
|
|
|
(-execute-one [this _ opts]
|
|
|
|
|
(if-let [rs (stmt->result-set this (assoc opts :return-keys true))]
|
2019-04-24 21:22:35 +00:00
|
|
|
(let [builder-fn (get opts :builder-fn as-maps)
|
|
|
|
|
builder (builder-fn rs opts)]
|
2019-04-23 00:41:31 +00:00
|
|
|
(when (.next rs)
|
2019-04-24 21:22:35 +00:00
|
|
|
(datafiable-row (row-builder builder)
|
2019-04-23 00:41:31 +00:00
|
|
|
(.getConnection this) opts)))
|
2019-04-18 06:34:31 +00:00
|
|
|
{:next.jdbc/update-count (.getUpdateCount this)}))
|
2019-04-21 05:28:21 +00:00
|
|
|
(-execute-all [this _ opts]
|
2020-06-07 19:13:32 +00:00
|
|
|
(if (:multi-rs opts)
|
2020-06-24 19:25:25 +00:00
|
|
|
(loop [go (.execute this) acc []]
|
2020-06-07 19:35:03 +00:00
|
|
|
(if-let [rs (stmt->result-set-update-count
|
|
|
|
|
(.getConnection this) this go (assoc opts :return-keys true))]
|
2020-06-24 19:25:25 +00:00
|
|
|
(recur (.getMoreResults this) (conj acc rs))
|
2020-06-07 19:13:32 +00:00
|
|
|
acc))
|
2020-06-07 19:35:03 +00:00
|
|
|
(if-let [rs (stmt->result-set this (assoc opts :return-keys true))]
|
2020-06-07 19:13:32 +00:00
|
|
|
(datafiable-result-set rs (.getConnection this) opts)
|
|
|
|
|
[{:next.jdbc/update-count (.getUpdateCount this)}])))
|
2019-11-15 00:15:52 +00:00
|
|
|
|
|
|
|
|
java.sql.Statement
|
|
|
|
|
(-execute [this sql-params opts]
|
|
|
|
|
(assert (= 1 (count sql-params))
|
|
|
|
|
"Parameters cannot be provided when executing a non-prepared Statement")
|
2020-06-27 02:03:57 +00:00
|
|
|
(reify
|
|
|
|
|
clojure.lang.IReduceInit
|
2019-11-15 00:15:52 +00:00
|
|
|
(reduce [_ f init]
|
2020-06-27 06:04:27 +00:00
|
|
|
(reduce-stmt-sql this (first sql-params) f init opts))
|
2020-06-27 02:03:57 +00:00
|
|
|
r/CollFold
|
|
|
|
|
(coll-fold [_ n combinef reducef]
|
|
|
|
|
(fold-stmt-sql this (first sql-params) n combinef reducef
|
2020-06-27 06:04:27 +00:00
|
|
|
(.getConnection this) opts))
|
2019-11-15 00:15:52 +00:00
|
|
|
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
|
|
|
|
(-execute-one [this sql-params opts]
|
|
|
|
|
(assert (= 1 (count sql-params))
|
|
|
|
|
"Parameters cannot be provided when executing a non-prepared Statement")
|
2020-06-27 06:04:27 +00:00
|
|
|
(if-let [rs (stmt-sql->result-set this (first sql-params))]
|
2019-11-15 00:15:52 +00:00
|
|
|
(let [builder-fn (get opts :builder-fn as-maps)
|
|
|
|
|
builder (builder-fn rs opts)]
|
|
|
|
|
(when (.next rs)
|
|
|
|
|
(datafiable-row (row-builder builder)
|
|
|
|
|
(.getConnection this) opts)))
|
|
|
|
|
{:next.jdbc/update-count (.getUpdateCount this)}))
|
|
|
|
|
(-execute-all [this sql-params opts]
|
|
|
|
|
(assert (= 1 (count sql-params))
|
|
|
|
|
"Parameters cannot be provided when executing a non-prepared Statement")
|
2020-06-07 19:13:32 +00:00
|
|
|
(if (:multi-rs opts)
|
2020-06-24 19:25:25 +00:00
|
|
|
(loop [go (.execute this (first sql-params)) acc []]
|
2020-06-07 19:13:32 +00:00
|
|
|
(if-let [rs (stmt->result-set-update-count
|
|
|
|
|
(.getConnection this) this go (assoc opts :return-keys true))]
|
2020-06-24 19:25:25 +00:00
|
|
|
(recur (.getMoreResults this) (conj acc rs))
|
2020-06-07 19:13:32 +00:00
|
|
|
acc))
|
2020-06-27 06:15:03 +00:00
|
|
|
(if-let [rs (stmt-sql->result-set this (first sql-params))]
|
2020-06-07 19:13:32 +00:00
|
|
|
(datafiable-result-set rs (.getConnection this) opts)
|
|
|
|
|
[{:next.jdbc/update-count (.getUpdateCount this)}])))
|
2019-04-18 06:34:31 +00:00
|
|
|
|
2019-03-31 23:54:34 +00:00
|
|
|
Object
|
|
|
|
|
(-execute [this sql-params opts]
|
2019-04-18 06:34:31 +00:00
|
|
|
(p/-execute (p/get-datasource this) sql-params opts))
|
|
|
|
|
(-execute-one [this sql-params opts]
|
|
|
|
|
(p/-execute-one (p/get-datasource this) sql-params opts))
|
|
|
|
|
(-execute-all [this sql-params opts]
|
|
|
|
|
(p/-execute-all (p/get-datasource this) sql-params opts)))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
(defn- default-schema
|
|
|
|
|
"The default schema lookup rule for column names.
|
|
|
|
|
|
2019-04-21 23:13:52 +00:00
|
|
|
If a column name ends with `_id` or `id`, it is assumed to be a foreign key
|
2019-03-31 23:54:34 +00:00
|
|
|
into the table identified by the first part of the column name."
|
|
|
|
|
[col]
|
2019-04-19 05:43:19 +00:00
|
|
|
(let [[_ table] (re-find #"(?i)^(.+?)_?id$" (name col))]
|
2019-03-31 23:54:34 +00:00
|
|
|
(when table
|
|
|
|
|
[(keyword table) :id])))
|
|
|
|
|
|
2019-09-09 00:05:07 +00:00
|
|
|
(defn- expand-schema
|
|
|
|
|
"Given a (possibly nil) schema entry, return it expanded to a triple of:
|
|
|
|
|
|
|
|
|
|
[table fk cardinality]
|
|
|
|
|
|
|
|
|
|
Possibly schema entry input formats are:
|
|
|
|
|
* [table fk] => cardinality :one
|
|
|
|
|
* [table fk cardinality] -- no change
|
|
|
|
|
* :table/fk => [:table :fk :one]
|
|
|
|
|
* [:table/fk] => [:table :fk :many]"
|
|
|
|
|
[k entry]
|
|
|
|
|
(when entry
|
|
|
|
|
(if-let [mapping
|
|
|
|
|
(cond
|
|
|
|
|
(keyword? entry)
|
|
|
|
|
[(keyword (namespace entry)) (keyword (name entry)) :one]
|
|
|
|
|
|
|
|
|
|
(coll? entry)
|
|
|
|
|
(let [[table fk cardinality] entry]
|
|
|
|
|
(cond (and table fk cardinality)
|
|
|
|
|
entry
|
|
|
|
|
|
|
|
|
|
(and table fk)
|
|
|
|
|
[table fk :one]
|
|
|
|
|
|
|
|
|
|
(keyword? table)
|
|
|
|
|
[(keyword (namespace table)) (keyword (name table)) :many])))]
|
|
|
|
|
|
|
|
|
|
mapping
|
|
|
|
|
(throw (ex-info (str "Invalid schema entry for: " (name k)) {:entry entry})))))
|
|
|
|
|
|
|
|
|
|
(comment
|
|
|
|
|
(expand-schema :user/statusid nil)
|
|
|
|
|
(expand-schema :user/statusid :status/id)
|
|
|
|
|
(expand-schema :user/statusid [:status :id])
|
|
|
|
|
(expand-schema :user/email [:deliverability :email :many])
|
|
|
|
|
(expand-schema :user/email [:deliverability/email]))
|
|
|
|
|
|
2019-03-31 23:54:34 +00:00
|
|
|
(defn- navize-row
|
2019-04-01 06:17:12 +00:00
|
|
|
"Given a connectable object, return a function that knows how to turn a row
|
2019-04-21 23:13:52 +00:00
|
|
|
into a `nav`igable object.
|
2019-04-01 06:17:12 +00:00
|
|
|
|
2019-04-18 21:06:14 +00:00
|
|
|
A `:schema` option can provide a map from qualified column names
|
|
|
|
|
(`:<table>/<column>`) to tuples that indicate for which table they are a
|
|
|
|
|
foreign key, the name of the key within that table, and (optionality) the
|
|
|
|
|
cardinality of that relationship (`:many`, `:one`).
|
2019-04-01 06:17:12 +00:00
|
|
|
|
2019-04-21 23:13:52 +00:00
|
|
|
If no `:schema` item is provided for a column, the convention of `<table>id` or
|
|
|
|
|
`<table>_id` is used, and the assumption is that such columns are foreign keys
|
|
|
|
|
in the `<table>` portion of their name, the key is called `id`, and the
|
|
|
|
|
cardinality is `:one`.
|
2019-04-01 06:17:12 +00:00
|
|
|
|
2019-04-21 23:13:52 +00:00
|
|
|
Rows are looked up using `-execute-all` or `-execute-one`, and the `:table-fn`
|
2019-04-18 21:06:14 +00:00
|
|
|
option, if provided, is applied to both the assumed table name and the
|
2019-04-01 06:17:12 +00:00
|
|
|
assumed foreign key column name."
|
2019-03-31 23:54:34 +00:00
|
|
|
[connectable opts]
|
|
|
|
|
(fn [row]
|
|
|
|
|
(with-meta row
|
2020-05-22 01:39:23 +00:00
|
|
|
{`core-p/nav (fn [_ k v]
|
2019-04-18 21:06:14 +00:00
|
|
|
(try
|
2019-09-09 00:05:07 +00:00
|
|
|
(let [[table fk cardinality]
|
|
|
|
|
(expand-schema k (or (get-in opts [:schema k])
|
|
|
|
|
(default-schema k)))]
|
2019-04-18 21:06:14 +00:00
|
|
|
(if fk
|
2019-04-18 15:12:12 +00:00
|
|
|
(let [entity-fn (:table-fn opts identity)
|
2019-03-31 23:54:34 +00:00
|
|
|
exec-fn! (if (= :many cardinality)
|
2019-04-18 06:56:44 +00:00
|
|
|
p/-execute-all
|
|
|
|
|
p/-execute-one)]
|
2019-03-31 23:54:34 +00:00
|
|
|
(exec-fn! connectable
|
|
|
|
|
[(str "SELECT * FROM "
|
|
|
|
|
(entity-fn (name table))
|
|
|
|
|
" WHERE "
|
|
|
|
|
(entity-fn (name fk))
|
|
|
|
|
" = ?")
|
|
|
|
|
v]
|
|
|
|
|
opts))
|
2019-04-18 21:06:14 +00:00
|
|
|
v))
|
|
|
|
|
(catch Exception _
|
|
|
|
|
;; assume an exception means we just cannot
|
|
|
|
|
;; navigate anywhere, so return just the value
|
2019-03-31 23:54:34 +00:00
|
|
|
v)))})))
|