From 6a9df0f4aa958d46722c3098e63ccf78e5f3c498 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Sat, 25 May 2019 19:16:30 -0700 Subject: [PATCH 1/2] Fix #22 by adding next.jdbc.optional Includes four `as*maps` builders that omit `NULL` columns completely from the returned row hash maps. --- CHANGELOG.md | 2 +- doc/migration-from-clojure-java-jdbc.md | 2 +- doc/result-set-builders.md | 4 ++ src/next/jdbc/optional.clj | 62 +++++++++++++++++++++++++ test/next/jdbc/optional_test.clj | 44 ++++++++++++++++++ test/next/jdbc/result_set_test.clj | 8 +++- test/next/jdbc/test_fixtures.clj | 15 +++--- 7 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 src/next/jdbc/optional.clj create mode 100644 test/next/jdbc/optional_test.clj diff --git a/CHANGELOG.md b/CHANGELOG.md index ace9d7e..871aff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/doc/migration-from-clojure-java-jdbc.md b/doc/migration-from-clojure-java-jdbc.md index 1986e8c..3688f12 100644 --- a/doc/migration-from-clojure-java-jdbc.md +++ b/doc/migration-from-clojure-java-jdbc.md @@ -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` (or the unqualified or lower variant, as appropriate). ## Primary API diff --git a/doc/result-set-builders.md b/doc/result-set-builders.md index 7e0c066..26f75c6 100644 --- a/doc/result-set-builders.md +++ b/doc/result-set-builders.md @@ -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. diff --git a/src/next/jdbc/optional.clj b/src/next/jdbc/optional.clj new file mode 100644 index 0000000..245e162 --- /dev/null +++ b/src/next/jdbc/optional.clj @@ -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))) diff --git a/test/next/jdbc/optional_test.clj b/test/next/jdbc/optional_test.clj new file mode 100644 index 0000000..8ba85fa --- /dev/null +++ b/test/next/jdbc/optional_test.clj @@ -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)))))) diff --git a/test/next/jdbc/result_set_test.clj b/test/next/jdbc/result_set_test.clj index 93c0984..802007f 100644 --- a/test/next/jdbc/result_set_test.clj +++ b/test/next/jdbc/result_set_test.clj @@ -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" diff --git a/test/next/jdbc/test_fixtures.clj b/test/next/jdbc/test_fixtures.clj index bcd0c98..a304114 100644 --- a/test/next/jdbc/test_fixtures.clj +++ b/test/next/jdbc/test_fixtures.clj @@ -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))))) From d4e5ed6ee4c4e95c8a4ced7eb3cbc44dcc435879 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Sat, 25 May 2019 19:19:19 -0700 Subject: [PATCH 2/2] Correct :as-arrays? migration note --- doc/migration-from-clojure-java-jdbc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/migration-from-clojure-java-jdbc.md b/doc/migration-from-clojure-java-jdbc.md index 3688f12..eb1bce9 100644 --- a/doc/migration-from-clojure-java-jdbc.md +++ b/doc/migration-from-clojure-java-jdbc.md @@ -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 most likely want to use a `:builder-fn` option of `next.jdbc.result-set/as-unqualified-lower-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