2019-03-31 23:54:34 +00:00
|
|
|
;; copyright (c) 2018-2019 Sean Corfield, all rights reserved
|
|
|
|
|
|
|
|
|
|
(ns next.jdbc.result-set
|
2019-04-01 06:17:12 +00:00
|
|
|
"An implementation of ResultSet handling functions."
|
2019-03-31 23:54:34 +00:00
|
|
|
(:require [clojure.core.protocols :as core-p]
|
|
|
|
|
[next.jdbc.prepare :as prepare]
|
|
|
|
|
[next.jdbc.protocols :as p])
|
|
|
|
|
(:import (java.sql PreparedStatement
|
|
|
|
|
ResultSet ResultSetMetaData
|
|
|
|
|
SQLException)))
|
|
|
|
|
|
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
|
|
|
|
|
|
(defn- get-column-names
|
2019-04-01 06:17:12 +00:00
|
|
|
"Given a ResultSet, return a vector of columns names, each qualified by
|
|
|
|
|
the table from which it came.
|
|
|
|
|
|
|
|
|
|
If :identifiers was specified, apply that to both the table qualifier
|
|
|
|
|
and the column name."
|
2019-04-02 06:25:10 +00:00
|
|
|
[^ResultSet rs ^ResultSetMetaData rsmeta opts]
|
|
|
|
|
(let [idxs (range 1 (inc (.getColumnCount rsmeta)))]
|
2019-03-31 23:54:34 +00:00
|
|
|
(if-let [ident-fn (:identifiers opts)]
|
|
|
|
|
(mapv (fn [^Integer i]
|
|
|
|
|
(keyword (when-let [qualifier (not-empty (.getTableName rsmeta i))]
|
|
|
|
|
(ident-fn qualifier))
|
|
|
|
|
(ident-fn (.getColumnLabel rsmeta i))))
|
|
|
|
|
idxs)
|
|
|
|
|
(mapv (fn [^Integer i]
|
|
|
|
|
(keyword (not-empty (.getTableName rsmeta i))
|
|
|
|
|
(.getColumnLabel rsmeta i)))
|
|
|
|
|
idxs))))
|
|
|
|
|
|
2019-04-02 05:19:02 +00:00
|
|
|
(defprotocol ColumnarResultSet
|
2019-04-02 06:25:10 +00:00
|
|
|
"To allow reducing functions to access a result set's column names and
|
|
|
|
|
row values, such as the as-arrays reducing function in this namespace."
|
|
|
|
|
(column-names [this] "Return the column names from a result set.")
|
|
|
|
|
(row-values [this] "Return the values from the current row of a result set."))
|
|
|
|
|
|
2019-04-02 06:32:24 +00:00
|
|
|
(defprotocol ReadableColumn
|
2019-04-02 06:25:10 +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,
|
|
|
|
|
but it can be extended to provide custom behavior for special types."
|
|
|
|
|
(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-03-31 23:54:34 +00:00
|
|
|
(defn- mapify-result-set
|
|
|
|
|
"Given a result set, return an object that wraps the current row as a hash
|
|
|
|
|
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).
|
|
|
|
|
|
|
|
|
|
Supports ILookup (keywords are treated as strings).
|
|
|
|
|
|
|
|
|
|
Supports Associative (again, keywords are treated as strings). If you assoc,
|
|
|
|
|
a full row will be realized (via seq/into).
|
|
|
|
|
|
|
|
|
|
Supports Seqable which realizes a full row of the data."
|
|
|
|
|
[^ResultSet rs opts]
|
2019-04-02 06:25:10 +00:00
|
|
|
(let [rsmeta (.getMetaData rs)
|
|
|
|
|
cols (delay (get-column-names rs rsmeta opts))]
|
2019-03-31 23:54:34 +00:00
|
|
|
(reify
|
|
|
|
|
|
|
|
|
|
clojure.lang.ILookup
|
|
|
|
|
(valAt [this k]
|
|
|
|
|
(try
|
2019-04-02 06:25:10 +00:00
|
|
|
(read-column-by-label (.getObject rs (name k)) (name k))
|
2019-03-31 23:54:34 +00:00
|
|
|
(catch SQLException _)))
|
|
|
|
|
(valAt [this k not-found]
|
|
|
|
|
(try
|
2019-04-02 06:25:10 +00:00
|
|
|
(read-column-by-label (.getObject rs (name k)) (name k))
|
2019-03-31 23:54:34 +00:00
|
|
|
(catch SQLException _
|
|
|
|
|
not-found)))
|
|
|
|
|
|
|
|
|
|
clojure.lang.Associative
|
|
|
|
|
(containsKey [this k]
|
|
|
|
|
(try
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
true
|
|
|
|
|
(catch SQLException _
|
|
|
|
|
false)))
|
|
|
|
|
(entryAt [this k]
|
|
|
|
|
(try
|
2019-04-02 06:25:10 +00:00
|
|
|
(clojure.lang.MapEntry. k (read-column-by-label
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
(name k)))
|
2019-03-31 23:54:34 +00:00
|
|
|
(catch SQLException _)))
|
|
|
|
|
(assoc [this k v]
|
|
|
|
|
(assoc (into {} (seq this)) k v))
|
|
|
|
|
|
|
|
|
|
clojure.lang.Seqable
|
|
|
|
|
(seq [this]
|
|
|
|
|
(seq (mapv (fn [^Integer i]
|
|
|
|
|
(clojure.lang.MapEntry. (nth @cols (dec i))
|
2019-04-02 06:25:10 +00:00
|
|
|
(read-column-by-index
|
|
|
|
|
(.getObject rs i)
|
|
|
|
|
rsmeta i)))
|
2019-04-02 05:19:02 +00:00
|
|
|
(range 1 (inc (count @cols))))))
|
|
|
|
|
|
|
|
|
|
ColumnarResultSet
|
|
|
|
|
(column-names [this] @cols)
|
|
|
|
|
(row-values [this]
|
2019-04-02 06:25:10 +00:00
|
|
|
(mapv (fn [^Integer i] (read-column-by-index
|
|
|
|
|
(.getObject rs i)
|
|
|
|
|
rsmeta i))
|
2019-04-02 05:19:02 +00:00
|
|
|
(range 1 (inc (count @cols))))))))
|
|
|
|
|
|
|
|
|
|
(defn as-arrays
|
|
|
|
|
"A reducing function that can be used on a result set to produce an
|
|
|
|
|
array-based representation, where the first element is a vector of the
|
|
|
|
|
column names in the result set, and subsequent elements are vectors of
|
|
|
|
|
the rows from the result set.
|
|
|
|
|
|
|
|
|
|
It should be used with a nil initial value:
|
|
|
|
|
|
|
|
|
|
(reduce rs/as-arrays nil (reducible! con sql-params))"
|
|
|
|
|
[result rs-map]
|
|
|
|
|
(if result
|
|
|
|
|
(conj result (row-values rs-map))
|
|
|
|
|
(conj [(column-names rs-map)] (row-values rs-map))))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
(defn- reduce-stmt
|
2019-04-01 06:17:12 +00:00
|
|
|
"Execute the PreparedStatement, 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
|
2019-04-05 03:25:20 +00:00
|
|
|
a hash map containing :next.jdbc/update-count and the number of rows
|
|
|
|
|
updated, with the supplied function and initial value applied."
|
2019-03-31 23:54:34 +00:00
|
|
|
[^PreparedStatement stmt f init opts]
|
|
|
|
|
(if-let [^ResultSet rs (if (.execute stmt)
|
|
|
|
|
(.getResultSet stmt)
|
|
|
|
|
(when (:return-keys opts)
|
|
|
|
|
(try
|
|
|
|
|
(.getGeneratedKeys stmt)
|
|
|
|
|
(catch Exception _))))]
|
|
|
|
|
(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
|
|
|
|
|
|
|
|
(extend-protocol p/Executable
|
|
|
|
|
java.sql.Connection
|
|
|
|
|
(-execute [this sql-params opts]
|
2019-04-01 00:30:10 +00:00
|
|
|
(let [factory (prepare/->factory opts)]
|
2019-03-31 23:54:34 +00:00
|
|
|
(reify clojure.lang.IReduceInit
|
|
|
|
|
(reduce [_ f init]
|
2019-04-01 00:30:10 +00:00
|
|
|
(with-open [stmt (prepare/create this
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
factory)]
|
2019-03-31 23:54:34 +00:00
|
|
|
(reduce-stmt stmt f init opts))))))
|
|
|
|
|
javax.sql.DataSource
|
|
|
|
|
(-execute [this sql-params opts]
|
2019-04-01 00:30:10 +00:00
|
|
|
(let [factory (prepare/->factory opts)]
|
2019-03-31 23:54:34 +00:00
|
|
|
(reify clojure.lang.IReduceInit
|
|
|
|
|
(reduce [_ f init]
|
|
|
|
|
(with-open [con (p/get-connection this opts)]
|
2019-04-01 00:30:10 +00:00
|
|
|
(with-open [stmt (prepare/create con
|
|
|
|
|
(first sql-params)
|
|
|
|
|
(rest sql-params)
|
|
|
|
|
factory)]
|
2019-03-31 23:54:34 +00:00
|
|
|
(reduce-stmt stmt f init opts)))))))
|
|
|
|
|
java.sql.PreparedStatement
|
|
|
|
|
(-execute [this _ opts]
|
|
|
|
|
(reify clojure.lang.IReduceInit
|
|
|
|
|
;; 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
|
|
|
|
|
(reduce [_ f init]
|
|
|
|
|
(reduce-stmt this f init (assoc opts :return-keys true)))))
|
|
|
|
|
Object
|
|
|
|
|
(-execute [this sql-params opts]
|
|
|
|
|
(p/-execute (p/get-datasource this) sql-params opts)))
|
|
|
|
|
|
|
|
|
|
(declare navize-row)
|
|
|
|
|
|
2019-04-01 00:30:10 +00:00
|
|
|
(defn datafiable-row
|
2019-04-01 06:17:12 +00:00
|
|
|
"Given a connectable object, return a function that knows how to turn a row
|
|
|
|
|
into a datafiable object that can be 'nav'igated."
|
2019-03-31 23:54:34 +00:00
|
|
|
[connectable opts]
|
|
|
|
|
(fn [row]
|
|
|
|
|
(into (with-meta {} {`core-p/datafy (navize-row connectable opts)}) row)))
|
|
|
|
|
|
|
|
|
|
(defn execute!
|
2019-04-01 06:17:12 +00:00
|
|
|
"Given a connectable object and SQL and parameters, execute it and reduce it
|
|
|
|
|
into a vector of processed hash maps (rows).
|
|
|
|
|
|
|
|
|
|
By default, this will create datafiable rows but :row-fn can override that."
|
2019-04-02 07:41:39 +00:00
|
|
|
[connectable sql-params f opts]
|
2019-03-31 23:54:34 +00:00
|
|
|
(into []
|
2019-04-02 07:41:39 +00:00
|
|
|
(map f)
|
2019-03-31 23:54:34 +00:00
|
|
|
(p/-execute connectable sql-params opts)))
|
|
|
|
|
|
|
|
|
|
(defn execute-one!
|
2019-04-01 06:17:12 +00:00
|
|
|
"Given a connectable object and SQL and parameters, execute it and return
|
|
|
|
|
just the first processed hash map (row).
|
|
|
|
|
|
|
|
|
|
By default, this will create a datafiable row but :row-fn can override that."
|
2019-04-02 07:41:39 +00:00
|
|
|
[connectable sql-params f opts]
|
|
|
|
|
(reduce (fn [_ row] (reduced (f row)))
|
|
|
|
|
nil
|
|
|
|
|
(p/-execute connectable sql-params opts)))
|
2019-03-31 23:54:34 +00:00
|
|
|
|
|
|
|
|
(defn- default-schema
|
|
|
|
|
"The default schema lookup rule for column names.
|
|
|
|
|
|
|
|
|
|
If a column name ends with _id or id, it is assumed to be a foreign key
|
|
|
|
|
into the table identified by the first part of the column name."
|
|
|
|
|
[col]
|
|
|
|
|
(let [[_ table] (re-find #"(?i)^(.+)_?id$" (name col))]
|
|
|
|
|
(when table
|
|
|
|
|
[(keyword table) :id])))
|
|
|
|
|
|
|
|
|
|
(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
|
|
|
|
|
into a navigable object.
|
|
|
|
|
|
|
|
|
|
A :schema option can provide a map of qualified column names (:table/column)
|
|
|
|
|
to tuples that indicate which table they are a foreign key for, the name of
|
|
|
|
|
the key within that table, and (optionality) the cardinality of that
|
|
|
|
|
relationship (:many, :one).
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Rows are looked up using 'execute!' or 'execute-one!' and the :entities
|
|
|
|
|
function, if provided, is applied to both the assumed table name and the
|
|
|
|
|
assumed foreign key column name."
|
2019-03-31 23:54:34 +00:00
|
|
|
[connectable opts]
|
|
|
|
|
(fn [row]
|
|
|
|
|
(with-meta row
|
|
|
|
|
{`core-p/nav (fn [coll k v]
|
|
|
|
|
(let [[table fk cardinality] (or (get-in opts [:schema k])
|
|
|
|
|
(default-schema k))]
|
|
|
|
|
(if fk
|
|
|
|
|
(try
|
|
|
|
|
(let [entity-fn (:entities opts identity)
|
|
|
|
|
exec-fn! (if (= :many cardinality)
|
|
|
|
|
execute!
|
|
|
|
|
execute-one!)]
|
|
|
|
|
(exec-fn! connectable
|
|
|
|
|
[(str "SELECT * FROM "
|
|
|
|
|
(entity-fn (name table))
|
|
|
|
|
" WHERE "
|
|
|
|
|
(entity-fn (name fk))
|
|
|
|
|
" = ?")
|
|
|
|
|
v]
|
2019-04-02 07:41:39 +00:00
|
|
|
(datafiable-row connectable opts)
|
2019-03-31 23:54:34 +00:00
|
|
|
opts))
|
|
|
|
|
(catch Exception _
|
|
|
|
|
;; assume an exception means we just cannot
|
|
|
|
|
;; navigate anywhere, so return just the value
|
|
|
|
|
v))
|
|
|
|
|
v)))})))
|