From 04237c89ea26417f607076315e397c78637d773d Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Fri, 4 Oct 2019 13:52:23 -0700 Subject: [PATCH] Add optional maps adapter --- CHANGELOG.md | 1 + src/next/jdbc/optional.clj | 38 ++++++++++++++++++++ test/next/jdbc/optional_test.clj | 60 +++++++++++++++++++++++++++++++- 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b13bf3e..43c1968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The following changes have been committed to the **master** branch since the 1.0 * Address #68 by clarifying that builder functions do not affect the "fake result set" containing `:next.jdbc/update-count`. * Fix #67 by adding `:jdbcUrl` version spec. +* Add `next.jdbc.optional/as-maps-adapter` to provide a way to override the default result set reading behavior of using `.getObject` when omitting SQL `NULL` values from result set maps. ## Stable Builds diff --git a/src/next/jdbc/optional.clj b/src/next/jdbc/optional.clj index 8986e56..29ef73d 100644 --- a/src/next/jdbc/optional.clj +++ b/src/next/jdbc/optional.clj @@ -91,3 +91,41 @@ 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)))))) diff --git a/test/next/jdbc/optional_test.clj b/test/next/jdbc/optional_test.clj index 67d8a56..7496766 100644 --- a/test/next/jdbc/optional_test.clj +++ b/test/next/jdbc/optional_test.clj @@ -6,7 +6,8 @@ [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 postgres?]])) + [next.jdbc.test-fixtures :refer [with-test-db ds postgres?]]) + (:import (java.sql ResultSet ResultSetMetaData))) (set! *warn-on-reflection* true) @@ -54,3 +55,60 @@ (is (not (contains? row (if (postgres?) :fruit/appearance :FRUIT/appearance)))) (is (= 3 ((if (postgres?) :fruit/id :FRUIT/id) row))) (is (= "Peach" ((if (postgres?) :fruit/name :FRUIT/name) row)))))) + +(defn- default-column-reader + [^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i] + (.getObject rs i)) + +(deftest test-map-row-adapter + (testing "default row builder" + (let [row (p/-execute-one (ds) + ["select * from fruit where id = ?" 1] + {:builder-fn (opt/as-maps-adapter + opt/as-maps + default-column-reader)})] + (is (map? row)) + (is (not (contains? row (if (postgres?) :fruit/grade :FRUIT/GRADE)))) + (is (= 1 ((if (postgres?) :fruit/id :FRUIT/ID) row))) + (is (= "Apple" ((if (postgres?) :fruit/name :FRUIT/NAME) row))))) + (testing "unqualified row builder" + (let [row (p/-execute-one (ds) + ["select * from fruit where id = ?" 2] + {:builder-fn (opt/as-maps-adapter + opt/as-unqualified-maps + default-column-reader)})] + (is (map? row)) + (is (not (contains? row (if (postgres?) :cost :COST)))) + (is (= 2 ((if (postgres?) :id :ID) row))) + (is (= "Banana" ((if (postgres?) :name :NAME) row))))) + (testing "lower-case row builder" + (let [row (p/-execute-one (ds) + ["select * from fruit where id = ?" 3] + {:builder-fn (opt/as-maps-adapter + opt/as-lower-maps + default-column-reader)})] + (is (map? row)) + (is (not (contains? row :fruit/appearance))) + (is (= 3 (:fruit/id row))) + (is (= "Peach" (:fruit/name row))))) + (testing "unqualified lower-case row builder" + (let [row (p/-execute-one (ds) + ["select * from fruit where id = ?" 4] + {:builder-fn (opt/as-maps-adapter + opt/as-unqualified-lower-maps + default-column-reader)})] + (is (map? row)) + (is (= 4 (:id row))) + (is (= "Orange" (:name row))))) + (testing "custom row builder" + (let [row (p/-execute-one (ds) + ["select * from fruit where id = ?" 3] + {:builder-fn (opt/as-maps-adapter + opt/as-modified-maps + default-column-reader) + :label-fn str/lower-case + :qualifier-fn identity})] + (is (map? row)) + (is (not (contains? row (if (postgres?) :fruit/appearance :FRUIT/appearance)))) + (is (= 3 ((if (postgres?) :fruit/id :FRUIT/id) row))) + (is (= "Peach" ((if (postgres?) :fruit/name :FRUIT/name) row))))))