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).
This commit is contained in:
Sean Corfield 2019-08-02 12:24:04 -07:00
parent 22b7e6df61
commit 0fd8bf1a88
3 changed files with 120 additions and 42 deletions

View file

@ -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: 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). * Improved documentation around connection pooling (HikariCP caveats).
## Stable Builds ## Stable Builds

View file

@ -288,36 +288,37 @@
(range 1 (inc (column-count builder)))) (range 1 (inc (column-count builder))))
(row! builder))) (row! builder)))
(definterface MapifiedResultSet)
(defn- mapify-result-set (defn- mapify-result-set
"Given a `ResultSet`, return an object that wraps the current row as a hash "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 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). this wrapper so operations need to be eager (and fairly limited).
In particular, this does not satisfy `map?` because it does not implement Supports `IPersistentMap` in full. Any operation that requires a full hash
all of `IPersistentMap`. 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
Supports `ILookup` (keywords are treated as strings). you want the row to be datafiable/navigable, use `datafiable-row` to
realize the full row explicitly before performing other
Supports `Associative` (again, keywords are treated as strings). If you `assoc`, (metadata-preserving) operations on it."
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)."
[^ResultSet rs opts] [^ResultSet rs opts]
(let [builder (delay ((get opts :builder-fn as-maps) rs opts))] (let [builder (delay ((get opts :builder-fn as-maps) rs opts))]
(reify (reify
clojure.lang.ILookup MapifiedResultSet
(valAt [this k] ;; marker, just for printing resolution
(try
(read-column-by-label (.getObject rs (name k)) (name k)) clojure.lang.IPersistentMap
(catch SQLException _))) (assoc [this k v]
(valAt [this k not-found] (assoc (row-builder @builder) k v))
(try (assocEx [this k v]
(read-column-by-label (.getObject rs (name k)) (name k)) (.assocEx ^clojure.lang.IPersistentMap (row-builder @builder) k v))
(catch SQLException _ (without [this k]
not-found))) (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 clojure.lang.Associative
(containsKey [this k] (containsKey [this k]
@ -332,8 +333,29 @@
(.getObject rs (name k)) (.getObject rs (name k))
(name k))) (name k)))
(catch SQLException _))) (catch SQLException _)))
(assoc [this k v]
(assoc (row-builder @builder) k v)) 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 clojure.lang.Seqable
(seq [this] (seq [this]
@ -345,7 +367,21 @@
(row-builder @builder) (row-builder @builder)
{`core-p/datafy (navize-row connectable opts)})) {`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 (extend-protocol
DatafiableRow DatafiableRow

View file

@ -113,12 +113,12 @@
(deftest test-mapify (deftest test-mapify
(testing "no row builder is used" (testing "no row builder is used"
(is (= [false] (is (= [true]
(into [] (map map?) ; it is not a real map (into [] (map map?) ; it looks like a real map now
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) ["select * from fruit where id = ?" 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= ["Apple"] (is (= ["Apple"]
(into [] (map :name) ; but keyword selection works (into [] (map :name) ; keyword selection works
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) ["select * from fruit where id = ?" 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [[2 [:name "Banana"]]] (is (= [[2 [:name "Banana"]]]
@ -136,14 +136,55 @@
:unnamed) :unnamed)
(get % :id 0))) ; get with not-found works (get % :id 0))) ; get with not-found works
(p/-execute (ds) ["select * from fruit where id = ?" 4] (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)}))))) {: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))) (is (map? (reduce (fn [_ row] (reduced (assoc row :x 1)))
nil nil
(p/-execute (ds) ["select * from fruit"] {})))) (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))) (is (seq? (reduce (fn [_ row] (reduced (seq row)))
nil nil
(p/-execute (ds) ["select * from fruit"] {})))) (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))) (is (every? map-entry? (reduce (fn [_ row] (reduced (seq row)))
nil nil
(p/-execute (ds) ["select * from fruit"] {}))))) (p/-execute (ds) ["select * from fruit"] {})))))