Merge pull request #23 from seancorfield/issue-22
Fix #22 by adding next.jdbc.optional
This commit is contained in:
commit
d23e91221f
7 changed files with 127 additions and 10 deletions
|
|
@ -23,4 +23,4 @@ Only accretive/fixative changes will be made from now on (Beta 1).
|
|||
|
||||
The following changes have been committed to the **master** branch and will be in the next release:
|
||||
|
||||
* None at this time.
|
||||
* Fix #22 by adding `next.jdbc.optional` with four map builders that omit `NULL` columns from the row hash maps.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ This page attempts to list all of the differences between [clojure.java.jdbc](ht
|
|||
|
||||
`clojure.java.jdbc` returned result sets (and generated keys) as hash maps with simple, lower-case keys by default. `next.jdbc` returns result sets (and generated keys) as hash maps with qualified, as-is keys by default: each key is qualified by the name of table from which it is drawn, if known. The as-is default is chosen to a) improve performance and b) not mess with the data. Using a `:builder-fn` option of `next.jdbc.result-set/as-unqualified-maps` will produce simple, as-is keys. Using a `:builder-fn` option of `next.jdbc.result-set/as-unqualified-lower-maps` will produce simple, lower-case keys -- the most compatible with `clojure.java.jdbc`'s default behavior.
|
||||
|
||||
If you used `:as-arrays? true`, you will need to use a `:builder-fn` option of `next.jdbc.result-set/as-arrays` (or the unqualified or lower variant, as appropriate).
|
||||
If you used `:as-arrays? true`, you will most likely want to use a `:builder-fn` option of `next.jdbc.result-set/as-unqualified-lower-arrays`.
|
||||
|
||||
## Primary API
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ The options hash map for any `next.jdbc` function can contain a `:builder-fn` ke
|
|||
|
||||
The options hash map passed to the builder function will contain a `:next.jdbc/sql-params` key, whose value is the SQL + parameters vector passed into the top-level `next.jdbc` functions (`plan`, `execute!`, and `execute-one!`).
|
||||
|
||||
## `next.jdbc.optional`
|
||||
|
||||
This namespace contains variants of the four `as-maps`-style builders above that omit keys from the row hash maps if the corresponding column is `NULL`. This is in keeping with Clojure's views of "optionality" -- that optional elements should simply be omitted -- and is provided as an "opt-in" style of rows and result sets.
|
||||
|
||||
# ReadableColumn
|
||||
|
||||
As mentioned above, when `with-column` is called, the expectation is that the row builder will call `.getObject` on the current state of the `ResultSet` object with the column index and will then call `read-column-by-index`, passing the column value, the `ResultSetMetaData`, and the column index. That function is part of the `ReadableColumn` protocol that you can extend to handle conversion of arbitrary database-specific types to Clojure values.
|
||||
|
|
|
|||
62
src/next/jdbc/optional.clj
Normal file
62
src/next/jdbc/optional.clj
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
;; copyright (c) 2019 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)))
|
||||
|
||||
(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-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."
|
||||
[^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (rs/get-lower-column-names rsmeta opts)]
|
||||
(->MapResultSetOptionalBuilder rs rsmeta cols)))
|
||||
|
||||
(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."
|
||||
[^ResultSet rs opts]
|
||||
(let [rsmeta (.getMetaData rs)
|
||||
cols (rs/get-unqualified-lower-column-names rsmeta opts)]
|
||||
(->MapResultSetOptionalBuilder rs rsmeta cols)))
|
||||
44
test/next/jdbc/optional_test.clj
Normal file
44
test/next/jdbc/optional_test.clj
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
;; copyright (c) 2019 Sean Corfield, all rights reserved
|
||||
|
||||
(ns next.jdbc.optional-test
|
||||
"Test namespace for the optional builder functions."
|
||||
(:require [clojure.string :as str]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[next.jdbc.optional :as opt]
|
||||
[next.jdbc.protocols :as p]
|
||||
[next.jdbc.test-fixtures :refer [with-test-db ds]]))
|
||||
|
||||
(use-fixtures :once with-test-db)
|
||||
|
||||
(deftest test-map-row-builder
|
||||
(testing "default row builder"
|
||||
(let [row (p/-execute-one (ds)
|
||||
["select * from fruit where id = ?" 1]
|
||||
{:builder-fn opt/as-maps})]
|
||||
(is (map? row))
|
||||
(is (not (contains? row :FRUIT/GRADE)))
|
||||
(is (= 1 (:FRUIT/ID row)))
|
||||
(is (= "Apple" (:FRUIT/NAME row)))))
|
||||
(testing "unqualified row builder"
|
||||
(let [row (p/-execute-one (ds)
|
||||
["select * from fruit where id = ?" 2]
|
||||
{:builder-fn opt/as-unqualified-maps})]
|
||||
(is (map? row))
|
||||
(is (not (contains? row :COST)))
|
||||
(is (= 2 (:ID row)))
|
||||
(is (= "Banana" (:NAME row)))))
|
||||
(testing "lower-case row builder"
|
||||
(let [row (p/-execute-one (ds)
|
||||
["select * from fruit where id = ?" 3]
|
||||
{:builder-fn opt/as-lower-maps})]
|
||||
(is (map? row))
|
||||
(is (not (contains? row :fruit/appearance)))
|
||||
(is (= 3 (:fruit/id row)))
|
||||
(is (= "Peach" (:fruit/name row)))))
|
||||
(testing "lower-case row builder"
|
||||
(let [row (p/-execute-one (ds)
|
||||
["select * from fruit where id = ?" 4]
|
||||
{:builder-fn opt/as-unqualified-lower-maps})]
|
||||
(is (map? row))
|
||||
(is (= 4 (:id row)))
|
||||
(is (= "Orange" (:name row))))))
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
;; copyright (c) 2019 Sean Corfield, all rights reserved
|
||||
|
||||
(ns next.jdbc.result-set-test
|
||||
"Stub test namespace for the result set functions.
|
||||
"Test namespace for the result set functions.
|
||||
|
||||
What's left to be tested:
|
||||
* ReadableColumn protocol extension point"
|
||||
|
|
@ -63,6 +63,8 @@
|
|||
["select * from fruit where id = ?" 1]
|
||||
{})]
|
||||
(is (map? row))
|
||||
(is (contains? row :FRUIT/GRADE))
|
||||
(is (nil? (:FRUIT/GRADE row)))
|
||||
(is (= 1 (:FRUIT/ID row)))
|
||||
(is (= "Apple" (:FRUIT/NAME row))))
|
||||
(let [rs (p/-execute-all (ds)
|
||||
|
|
@ -78,6 +80,8 @@
|
|||
["select * from fruit where id = ?" 2]
|
||||
{:builder-fn rs/as-unqualified-maps})]
|
||||
(is (map? row))
|
||||
(is (contains? row :COST))
|
||||
(is (nil? (:COST row)))
|
||||
(is (= 2 (:ID row)))
|
||||
(is (= "Banana" (:NAME row)))))
|
||||
(testing "lower-case row builder"
|
||||
|
|
@ -85,6 +89,8 @@
|
|||
["select * from fruit where id = ?" 3]
|
||||
{:builder-fn rs/as-lower-maps})]
|
||||
(is (map? row))
|
||||
(is (contains? row :fruit/appearance))
|
||||
(is (nil? (:fruit/appearance row)))
|
||||
(is (= 3 (:fruit/id row)))
|
||||
(is (= "Peach" (:fruit/name row)))))
|
||||
(testing "lower-case row builder"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
;; copyright (c) 2019 Sean Corfield, all rights reserved
|
||||
|
||||
(ns next.jdbc.test-fixtures
|
||||
"Multi-database testing fixtures."
|
||||
(:require [next.jdbc :as jdbc]
|
||||
[next.jdbc.sql :as sql]))
|
||||
|
||||
|
|
@ -56,15 +57,15 @@
|
|||
CREATE TABLE FRUIT (
|
||||
ID INTEGER " auto-inc-pk ",
|
||||
NAME VARCHAR(32),
|
||||
APPEARANCE VARCHAR(32),
|
||||
COST INT,
|
||||
GRADE REAL
|
||||
APPEARANCE VARCHAR(32) DEFAULT NULL,
|
||||
COST INT DEFAULT NULL,
|
||||
GRADE REAL DEFAULT NULL
|
||||
)")])
|
||||
(sql/insert-multi! con :fruit
|
||||
[:name :appearance :cost :grade]
|
||||
[["Apple" "red" 59 87]
|
||||
["Banana","yellow",29,92.2]
|
||||
["Peach","fuzzy",139,90.0]
|
||||
["Orange","juicy",89,88.6]]
|
||||
[["Apple" "red" 59 nil]
|
||||
["Banana" "yellow" nil 92.2]
|
||||
["Peach" nil 139 90.0]
|
||||
["Orange" "juicy" 89 88.6]]
|
||||
{:return-keys false})
|
||||
(t)))))
|
||||
|
|
|
|||
Loading…
Reference in a new issue