next-jdbc/src/next/jdbc/optional.clj
2020-07-05 15:57:10 -07:00

131 lines
4.8 KiB
Clojure

;; copyright (c) 2019-2020 Sean Corfield, all rights reserved
(ns next.jdbc.optional
"Builders that treat NULL SQL values as 'optional' and omit the
corresponding keys from the Clojure hash maps for the rows."
(:require [next.jdbc.result-set :as rs])
(:import (java.sql ResultSet)
(java.util Locale)))
(set! *warn-on-reflection* true)
(defrecord MapResultSetOptionalBuilder [^ResultSet rs rsmeta cols]
rs/RowBuilder
(->row [this] (transient {}))
(column-count [this] (count cols))
(with-column [this row i]
(let [v (.getObject rs ^Integer i)]
(if (nil? v)
row
(assoc! row
(nth cols (dec i))
(rs/read-column-by-index v rsmeta i)))))
(row! [this row] (persistent! row))
rs/ResultSetBuilder
(->rs [this] (transient []))
(with-row [this mrs row]
(conj! mrs row))
(rs! [this mrs] (persistent! mrs)))
(defn as-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
that produces bare vectors of hash map rows, with nil columns omitted."
[^ResultSet rs opts]
(let [rsmeta (.getMetaData rs)
cols (rs/get-column-names rsmeta opts)]
(->MapResultSetOptionalBuilder rs rsmeta cols)))
(defn as-unqualified-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
that produces bare vectors of hash map rows, with simple keys and nil
columns omitted."
[^ResultSet rs opts]
(let [rsmeta (.getMetaData rs)
cols (rs/get-unqualified-column-names rsmeta opts)]
(->MapResultSetOptionalBuilder rs rsmeta cols)))
(defn as-modified-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
that produces bare vectors of hash map rows, with modified keys and nil
columns omitted.
Requires both the `:qualifier-fn` and `:label-fn` options."
[^ResultSet rs opts]
(let [rsmeta (.getMetaData rs)
cols (rs/get-modified-column-names rsmeta opts)]
(->MapResultSetOptionalBuilder rs rsmeta cols)))
(defn as-unqualified-modified-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
that produces bare vectors of hash map rows, with simple, modified keys
and nil columns omitted.
Requires the `:label-fn` option."
[^ResultSet rs opts]
(let [rsmeta (.getMetaData rs)
cols (rs/get-unqualified-modified-column-names rsmeta opts)]
(->MapResultSetOptionalBuilder rs rsmeta cols)))
(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)))
(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 and nil
columns omitted."
[rs opts]
(as-modified-maps rs (assoc opts
:qualifier-fn lower-case
:label-fn lower-case)))
(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
and nil columns omitted."
[rs opts]
(as-unqualified-modified-maps rs (assoc opts :label-fn lower-case)))
(defn as-maps-adapter
"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.
This adapter omits SQL NULL values.
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
to call `.getObject` or some other method to read the column's value.
`read-column-by-index` is still called on the result of that read, if
it is not `nil`."
[builder-fn column-reader]
(fn [rs opts]
(let [mrsb (builder-fn rs opts)]
(reify
rs/RowBuilder
(->row [this] (rs/->row mrsb))
(column-count [this] (rs/column-count mrsb))
(with-column [this row i]
(let [v (column-reader rs (:rsmeta mrsb) i)]
(if (nil? v)
row
(assoc! row
(nth (:cols mrsb) (dec i))
(rs/read-column-by-index v (:rsmeta mrsb) i)))))
(row! [this row] (rs/row! mrsb row))
rs/ResultSetBuilder
(->rs [this] (rs/->rs mrsb))
(with-row [this mrs row] (rs/with-row mrsb mrs row))
(rs! [this mrs] (rs/rs! mrsb mrs))
clojure.lang.ILookup
(valAt [this k] (get mrsb k))
(valAt [this k not-found] (get mrsb k not-found))))))