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:
|
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.
|
`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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
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
|
;; 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"
|
||||||
|
|
|
||||||
|
|
@ -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)))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue