From 0fd8bf1a886bdd01fb6d73166cbbe48fe2462062 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Fri, 2 Aug 2019 12:24:04 -0700 Subject: [PATCH] Fixes #51 by implementing IPersistentMap in full * `dissoc`, `cons`, `=` -- both realize a full row. * `count`, `empty` -- do not realize rows, `empty` doesn't use the builder at all. * `str` -- attempts to realize a row (else returns the same "helpful" string as before). --- CHANGELOG.md | 1 + src/next/jdbc/result_set.clj | 112 +++++++++++++++++++---------- test/next/jdbc/result_set_test.clj | 49 +++++++++++-- 3 files changed, 120 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ede2ac..6404ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Only accretive/fixative changes will be made from now on. The following changes have been committed to the **master** branch since the 1.0.4 release: +* Fix #51 by implementing `IPersistentMap` fully for the "mapified" result set inside `plan`. This adds support for `dissoc` and `cons` (which will both realize a row), `count` (which returns the column count but does not realize a row), `empty` (returns an empty hash map without realizing a row), etc. * Improved documentation around connection pooling (HikariCP caveats). ## Stable Builds diff --git a/src/next/jdbc/result_set.clj b/src/next/jdbc/result_set.clj index 5cceaff..047edbf 100644 --- a/src/next/jdbc/result_set.clj +++ b/src/next/jdbc/result_set.clj @@ -288,64 +288,100 @@ (range 1 (inc (column-count builder)))) (row! builder))) +(definterface MapifiedResultSet) + (defn- mapify-result-set "Given a `ResultSet`, return an object that wraps the current row as a hash map. Note that a result set is mutable and the current row will change behind this wrapper so operations need to be eager (and fairly limited). - In particular, this does not satisfy `map?` because it does not implement - all of `IPersistentMap`. - - Supports `ILookup` (keywords are treated as strings). - - Supports `Associative` (again, keywords are treated as strings). If you `assoc`, - a full row will be realized (via `row-builder` above). - - Supports `Seqable` which realizes a full row of the data. - - Supports `DatafiableRow` (which realizes a full row of the data)." + Supports `IPersistentMap` in full. Any operation that requires a full hash + map (`assoc`, `dissoc`, `cons`, `seq`, etc) will cause a full row to be + realized (via `row-builder` above). The result will be a regular map: if + you want the row to be datafiable/navigable, use `datafiable-row` to + realize the full row explicitly before performing other + (metadata-preserving) operations on it." [^ResultSet rs opts] (let [builder (delay ((get opts :builder-fn as-maps) rs opts))] (reify - clojure.lang.ILookup - (valAt [this k] - (try - (read-column-by-label (.getObject rs (name k)) (name k)) - (catch SQLException _))) - (valAt [this k not-found] - (try - (read-column-by-label (.getObject rs (name k)) (name k)) - (catch SQLException _ - not-found))) + MapifiedResultSet + ;; marker, just for printing resolution + + clojure.lang.IPersistentMap + (assoc [this k v] + (assoc (row-builder @builder) k v)) + (assocEx [this k v] + (.assocEx ^clojure.lang.IPersistentMap (row-builder @builder) k v)) + (without [this k] + (dissoc (row-builder @builder) k)) + + java.lang.Iterable ; Java 7 compatible: no forEach / spliterator + (iterator [this] + (.iterator ^java.lang.Iterable (row-builder @builder))) clojure.lang.Associative (containsKey [this k] - (try - (.getObject rs (name k)) - true - (catch SQLException _ - false))) + (try + (.getObject rs (name k)) + true + (catch SQLException _ + false))) (entryAt [this k] - (try - (clojure.lang.MapEntry. k (read-column-by-label - (.getObject rs (name k)) - (name k))) - (catch SQLException _))) - (assoc [this k v] - (assoc (row-builder @builder) k v)) + (try + (clojure.lang.MapEntry. k (read-column-by-label + (.getObject rs (name k)) + (name k))) + (catch SQLException _))) + + clojure.lang.Counted + (count [this] + (column-count @builder)) + + clojure.lang.IPersistentCollection + (cons [this obj] + (cons obj (seq (row-builder @builder)))) + (empty [this] + {}) + (equiv [this obj] + (.equiv ^clojure.lang.IPersistentCollection (row-builder @builder) obj)) + + clojure.lang.ILookup + (valAt [this k] + (try + (read-column-by-label (.getObject rs (name k)) (name k)) + (catch SQLException _))) + (valAt [this k not-found] + (try + (read-column-by-label (.getObject rs (name k)) (name k)) + (catch SQLException _ + not-found))) clojure.lang.Seqable (seq [this] - (seq (row-builder @builder))) + (seq (row-builder @builder))) DatafiableRow (datafiable-row [this connectable opts] - (with-meta - (row-builder @builder) - {`core-p/datafy (navize-row connectable opts)})) + (with-meta + (row-builder @builder) + {`core-p/datafy (navize-row connectable opts)})) - (toString [_] "{row} from `plan` -- missing `map` or `reduce`?")))) + (toString [_] + (try + (str (row-builder @builder)) + (catch Throwable _ + "{row} from `plan` -- missing `map` or `reduce`?")))))) + +(defmethod print-dup MapifiedResultSet [_ ^java.io.Writer w] + (.write w "{row} from `plan` -- missing `map` or `reduce`?")) + +(prefer-method print-dup MapifiedResultSet clojure.lang.IPersistentMap) + +(defmethod print-method MapifiedResultSet [_ ^java.io.Writer w] + (.write w "{row} from `plan` -- missing `map` or `reduce`?")) + +(prefer-method print-method MapifiedResultSet clojure.lang.IPersistentMap) (extend-protocol DatafiableRow diff --git a/test/next/jdbc/result_set_test.clj b/test/next/jdbc/result_set_test.clj index 9937d7f..ede5374 100644 --- a/test/next/jdbc/result_set_test.clj +++ b/test/next/jdbc/result_set_test.clj @@ -113,12 +113,12 @@ (deftest test-mapify (testing "no row builder is used" - (is (= [false] - (into [] (map map?) ; it is not a real map + (is (= [true] + (into [] (map map?) ; it looks like a real map now (p/-execute (ds) ["select * from fruit where id = ?" 1] {:builder-fn (constantly nil)})))) (is (= ["Apple"] - (into [] (map :name) ; but keyword selection works + (into [] (map :name) ; keyword selection works (p/-execute (ds) ["select * from fruit where id = ?" 1] {:builder-fn (constantly nil)})))) (is (= [[2 [:name "Banana"]]] @@ -136,14 +136,55 @@ :unnamed) (get % :id 0))) ; get with not-found works (p/-execute (ds) ["select * from fruit where id = ?" 4] + {:builder-fn (constantly nil)})))) + (is (= [{}] + (into [] (map empty) ; return empty map without building + (p/-execute (ds) ["select * from fruit where id = ?" 1] {:builder-fn (constantly nil)}))))) - (testing "assoc and seq build maps" + (testing "count does not build a map" + (let [count-builder (fn [_1 _2] + (reify rs/RowBuilder + (column-count [_] 13)))] + (is (= [13] + (into [] (map count) ; count relies on columns, not row fields + (p/-execute (ds) ["select * from fruit where id = ?" 1] + {:builder-fn count-builder})))))) + (testing "assoc, dissoc, cons, seq, and = build maps" (is (map? (reduce (fn [_ row] (reduced (assoc row :x 1))) nil (p/-execute (ds) ["select * from fruit"] {})))) + (is (= 6 (count (reduce (fn [_ row] (reduced (assoc row :x 1))) + nil + (p/-execute (ds) ["select * from fruit"] {}))))) + (is (map? (reduce (fn [_ row] (reduced + (dissoc row + (if (postgres?) + :fruit/name + :FRUIT/NAME)))) + nil + (p/-execute (ds) ["select * from fruit"] {})))) + (is (= 4 (count (reduce (fn [_ row] (reduced + (dissoc row + (if (postgres?) + :fruit/name + :FRUIT/NAME)))) + nil + (p/-execute (ds) ["select * from fruit"] {}))))) (is (seq? (reduce (fn [_ row] (reduced (seq row))) nil (p/-execute (ds) ["select * from fruit"] {})))) + (is (seq? (reduce (fn [_ row] (reduced (cons :seq row))) + nil + (p/-execute (ds) ["select * from fruit"] {})))) + (is (= :seq (first (reduce (fn [_ row] (reduced (cons :seq row))) + nil + (p/-execute (ds) ["select * from fruit"] {}))))) + (is (false? (reduce (fn [_ row] (reduced (= row {}))) + nil + (p/-execute (ds) ["select * from fruit"] {})))) + (is (map-entry? (second (reduce (fn [_ row] (reduced (cons :seq row))) + nil + (p/-execute (ds) ["select * from fruit"] {}))))) (is (every? map-entry? (reduce (fn [_ row] (reduced (seq row))) nil (p/-execute (ds) ["select * from fruit"] {})))))