Merge pull request #23 from seancorfield/issue-22

Fix #22 by adding next.jdbc.optional
This commit is contained in:
Sean Corfield 2019-05-25 19:20:30 -07:00 committed by GitHub
commit d23e91221f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 127 additions and 10 deletions

View file

@ -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: 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.

View file

@ -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. `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 ## Primary API

View file

@ -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!`). 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 # 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. 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.

View 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)))

View 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))))))

View file

@ -1,7 +1,7 @@
;; copyright (c) 2019 Sean Corfield, all rights reserved ;; copyright (c) 2019 Sean Corfield, all rights reserved
(ns next.jdbc.result-set-test (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: What's left to be tested:
* ReadableColumn protocol extension point" * ReadableColumn protocol extension point"
@ -63,6 +63,8 @@
["select * from fruit where id = ?" 1] ["select * from fruit where id = ?" 1]
{})] {})]
(is (map? row)) (is (map? row))
(is (contains? row :FRUIT/GRADE))
(is (nil? (:FRUIT/GRADE row)))
(is (= 1 (:FRUIT/ID row))) (is (= 1 (:FRUIT/ID row)))
(is (= "Apple" (:FRUIT/NAME row)))) (is (= "Apple" (:FRUIT/NAME row))))
(let [rs (p/-execute-all (ds) (let [rs (p/-execute-all (ds)
@ -78,6 +80,8 @@
["select * from fruit where id = ?" 2] ["select * from fruit where id = ?" 2]
{:builder-fn rs/as-unqualified-maps})] {:builder-fn rs/as-unqualified-maps})]
(is (map? row)) (is (map? row))
(is (contains? row :COST))
(is (nil? (:COST row)))
(is (= 2 (:ID row))) (is (= 2 (:ID row)))
(is (= "Banana" (:NAME row))))) (is (= "Banana" (:NAME row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
@ -85,6 +89,8 @@
["select * from fruit where id = ?" 3] ["select * from fruit where id = ?" 3]
{:builder-fn rs/as-lower-maps})] {:builder-fn rs/as-lower-maps})]
(is (map? row)) (is (map? row))
(is (contains? row :fruit/appearance))
(is (nil? (:fruit/appearance row)))
(is (= 3 (:fruit/id row))) (is (= 3 (:fruit/id row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" (:fruit/name row)))))
(testing "lower-case row builder" (testing "lower-case row builder"

View file

@ -1,6 +1,7 @@
;; copyright (c) 2019 Sean Corfield, all rights reserved ;; copyright (c) 2019 Sean Corfield, all rights reserved
(ns next.jdbc.test-fixtures (ns next.jdbc.test-fixtures
"Multi-database testing fixtures."
(:require [next.jdbc :as jdbc] (:require [next.jdbc :as jdbc]
[next.jdbc.sql :as sql])) [next.jdbc.sql :as sql]))
@ -56,15 +57,15 @@
CREATE TABLE FRUIT ( CREATE TABLE FRUIT (
ID INTEGER " auto-inc-pk ", ID INTEGER " auto-inc-pk ",
NAME VARCHAR(32), NAME VARCHAR(32),
APPEARANCE VARCHAR(32), APPEARANCE VARCHAR(32) DEFAULT NULL,
COST INT, COST INT DEFAULT NULL,
GRADE REAL GRADE REAL DEFAULT NULL
)")]) )")])
(sql/insert-multi! con :fruit (sql/insert-multi! con :fruit
[:name :appearance :cost :grade] [:name :appearance :cost :grade]
[["Apple" "red" 59 87] [["Apple" "red" 59 nil]
["Banana","yellow",29,92.2] ["Banana" "yellow" nil 92.2]
["Peach","fuzzy",139,90.0] ["Peach" nil 139 90.0]
["Orange","juicy",89,88.6]] ["Orange" "juicy" 89 88.6]]
{:return-keys false}) {:return-keys false})
(t))))) (t)))))