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:
* 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

View file

@ -288,36 +288,37 @@
(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]
@ -332,8 +333,29 @@
(.getObject rs (name k))
(name k)))
(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
(seq [this]
@ -345,7 +367,21 @@
(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

View file

@ -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"] {})))))