Commits

Anonymous committed 38a8324

add entity query API to handle large sized result sets
update CHANGES with 0.2 and beyond information

Comments (0)

Files changed (4)

 
 # Changes and TODO
 
-## 0.2 / 2010-Sep-?? (GMT+5:30)
+## 0.3 / 2010-???-?? (GMT + ?:??)
 
-- [TODO] Add support for composite primary keys
-- [TODO] Support large sized query result-sets (by adding lazy loading option)
+- [TODO] Add support for composite keys (primary key, foreign key)
 - [TODO] Optimistic locks using lock-counter field (optional column in meta)
-- [TODO] *show-sql* to be implemented for non-SELECT queries too
+- [TODO] Easy/transparent API for JDBC connection pooling (maybe Clj-DBCP?)
+- [TODO] Implement *show-sql* flag for non-SELECT queries too
+- [????] Provide 'insert' and 'update' functions
+
+## 0.2 / 2010-Sep-?? (GMT + 5:30)
+
+[TODO] Unit tests for entity functions (large result sets)
+[TODO] Documentation/tutorial
 
 - DSL for SQL clauses, Sub-query support
-- Support :groupby and :other attributes in entity/find-xxx functions
-
-- [????] Provide 'insert' and 'update' functions
+- Support for :groupby and :other attributes in entity/find-xxx functions
+- Support large sized query result-sets (with-query-results style)
+- Minor backward-incompatible change in entity API
+- *assert-args* flag for development mode (can be turned off in production)
 
 ## 0.1 / 2010-Aug-31 (GMT + 5:30)
 

src/main/clj/org/bituf/sqlrat/entity.clj

   (:use org.bituf.sqlrat.entity.internal)
   (:use org.bituf.sqlrat.util)
   (:use org.bituf.sqlrat.clause)
-  (:use [clojure.contrib.sql :as sql :only ()]))
+  (:use [clojure.contrib.sql :as sql :only ()])
+  (:import [clojure.lang IFn Keyword IPersistentMap IPersistentVector]))
 
 
 (def *show-sql* true)
 (def *show-sql-results* false)
+(def *assert-args* true)
 
 
 ; ===== Utility functions and macros =====
 
+(defn- as-vector*
+  [x]
+  (if *assert-args* (do (assert (vector? x)) x)
+    (as-vector x)))
+
+
 (defmacro in-db
   [db & body]
   `(sql/with-connection ~db
 
 
 (defn db-query
-  "Fetch rows from database table. Execute this with in-db or in-txn."
-  [query-vec]
-  (if *show-sql* (mypp "Executing SQL..." query-vec))
-  (let [result (sql/with-query-results rows
-                 query-vec
-                 (if (nil? rows) nil (into [] rows)))]
-    (if *show-sql-results* (mypp "SQL Result..." result))
-    result))
+  "Fetch rows from database table. Execute this with in-db or in-txn. If the
+   first argument is a function (f), it receives the results for processing.
+   'f' must accept one argument and must not return a lazy sequence."
+  ([^IFn f ^IPersistentVector query-vec]
+    (if *assert-args* (do
+                        (assert (fn? f))
+                        (assert (vector? query-vec))))
+    (if *show-sql* (mypp "Executing SQL..." query-vec))
+    (sql/with-query-results rows query-vec
+      (f rows)))
+  ([query-vec]
+    (let [f (fn [rows]
+              (let [result (if (nil? rows) nil (into [] rows))]
+                (if *show-sql-results* (mypp "SQL Result..." rows))
+                rows))]
+      (db-query f query-vec))))
+
+
+(defmacro with-db-query-results "Wrapper macro for db-query"
+  [results sql-params & body]
+  `(db-query (fn [~results] ~@body) ~sql-params))
 
 
 ;(defn save-row
 
 (defn to-row
   "Default implementation for to-row-fn."
-  [entity]
+  [^IPersistentMap entity]
   (into {} entity))
 
 
 
 (def star-col "*")
 
-
 (def count-col "COUNT(*) AS cnt")
 
 
 
 
 (defn find-by-sql
-  "Find entities with custom SQL/criteria. This is a free-form style."
-  [^EntityMetadata entity-meta sql-vec]
-  (let [sql-vector (as-vector sql-vec)
-        rows       (db-query sql-vector)]
-    (into [] (for [each rows] ((:from-row-fn entity-meta) each)))))
+  "Find entities with custom SQL/criteria. This is a free-form style. 'f' is
+   a function that accepts result entities as an argument and must not return
+   something that lazily processes the entities."
+  ([^IFn f ^EntityMetadata entity-meta ^IPersistentVector sql-vec]
+    (if *assert-args* (do
+                        (assert (fn? f))
+                        (assert-as entity-meta EntityMetadata)))
+    (let [sql-vector (as-vector sql-vec)]
+      (with-db-query-results results sql-vector
+        (f (map #((:from-row-fn entity-meta) %) results)))))
+  ([^EntityMetadata entity-meta sql-vec]
+    (let [f (fn [entities] (into [] entities))]
+      (find-by-sql f entity-meta sql-vec))))
 
 
-(defn find-by-criteria [^EntityMetadata entity-meta &
-                        {:keys [cols where groupby other]
-                         :or   {cols    [star-col]     ;; vector of col names
-                                where   (empty-clause) ;; clause
-                                groupby []             ;; vector of expressions
-                                other   (empty-clause) ;; clause
-                                }}]
-  "Find entities using :cols and/or :where attributes. Examples are below:
+(defmacro with-find-by-sql-results "Wrapper macro for find-by-sql"
+  [entities ^EntityMetadata entity-meta sql-vec & body]
+  `(find-by-sql (fn [~entities] ~@body) ~entity-meta ~sql-vec))
+
+
+(defn find-by-criteria
+  "Find entities using :cols, :where, :groupby and :other attributes and return
+   a lazy sequence. Examples are below:
    :cols [:title :content \"whenposted\"]
    :where [\"whenposted < ?\" (new java.util.Date)]
    >> OR >> :where (<? :whenposted (new java.util.Date))
    :other [\"ORDER BY whenposted\"]
    >> OR >> :other (merge-key-clause :order-by (cscols [:whenposted]))
-   "
-  (let [where-clause   (as-clause where)
-        other-clause   (as-clause other)
-        from-str       (name (:name entity-meta))
-        sql-vector (merge-clauses
-                     (merge-key-clause :select true (cscols cols))
-                     (merge-key-clause :from        (as-clause from-str))
-                     (merge-key-clause :where       where-clause)
-                     (merge-key-clause :group-by    (cscols groupby))
-                     other-clause)
-        ]
-    (find-by-sql entity-meta sql-vector)))
+   'f' accepts entities as the only argument and must not return something that
+   processes the entities lazily."
+  ([^IFn f ^EntityMetadata entity-meta
+    ^IPersistentMap {:keys [^IPersistentVector cols    ^IPersistentVector where
+                            ^IPersistentVector groupby ^IPersistentVector other]
+                     :or   {cols    [star-col]     ;; vector of col names
+                            where   (empty-clause) ;; clause
+                            groupby []             ;; vector of expressions
+                            other   (empty-clause) ;; clause
+                            }}]
+    (if *assert-args* (do
+                        (assert (fn? f))
+                        (assert-as entity-meta EntityMetadata)
+                        (assert (vector? cols))
+                        (assert (clause? where))
+                        (assert (vector? groupby))
+                        (assert (clause? other))))
+    (let [where-clause (as-clause where)
+          other-clause (as-clause other)
+          sql-vector   (SELECT (csnames cols)
+                         (FROM     (csnames [(:name entity-meta)]))
+                         (WHERE    where-clause)
+                         (GROUP-BY (csnames groupby))
+                         other-clause)]
+      (find-by-sql (fn [entities] (f entities)) entity-meta sql-vector)))
+  ([^EntityMetadata entity-meta
+    ^IPersistentMap {:keys [^IPersistentVector cols    ^IPersistentVector where
+                            ^IPersistentVector groupby ^IPersistentVector other]
+                     :or   {cols    [star-col]     ;; vector of col names
+                            where   (empty-clause) ;; clause
+                            groupby []             ;; vector of expressions
+                            other   (empty-clause) ;; clause
+                            }}]
+    (find-by-criteria as-vector entity-meta
+      {:cols cols :where where :groupby groupby :other other}))
+  ([^EntityMetadata entity-meta]
+    (find-by-criteria entity-meta {})))
+
+
+(defmacro with-find-by-criteria-results
+  [entities ^EntityMetadata entity-meta ^IPersistentMap criteria & body]
+  `(let [{cols# :cols where# :where groupby# :groupby other# :other} ~criteria]
+    (find-by-criteria (fn [~entities] ~@body) ~entity-meta
+      {:cols cols# :where where# :groupby groupby# :other other#})))
 
 
 (defn find-by-id
   "Find an entity of given type for a given ID. You can also pass :cols, :where,
    :groupby and :other attributes as in 'find-by-criteria' function."
   [^EntityMetadata entity-meta id &
-   {:keys [cols where groupby other]
-    :or   {cols    [star-col]     ;; vector of col names
-           where   (empty-clause) ;; clause
-           groupby []             ;; vector of expressions
-           other   (empty-clause) ;; clause
-           }}]
-  (let [old-where    (as-clause where)
-        id-clause    (=? (:id entity-meta) id)
-        where-clause (if (empty-clause? old-where) id-clause
-                       (AND id-clause old-where))
+   ^IPersistentMap {:keys [^IPersistentVector cols    ^IPersistentVector where
+                           ^IPersistentVector groupby ^IPersistentVector other]
+                    :or   {cols    [star-col]     ;; vector of col names
+                           where   (empty-clause) ;; clause
+                           groupby []             ;; vector of expressions
+                           other   (empty-clause) ;; clause
+                           }}]
+  ;; assert only args that are read before find-by-criteria is called
+  (if *assert-args* (do
+                      (assert-as entity-meta EntityMetadata)
+                      (assert (not (nil? id)))
+                      (assert (clause? where))))
+  (let [id-clause    (=? (:id entity-meta) id)
+        where-clause (if (empty-clause? where) id-clause
+                       (AND id-clause where))
         rows         (find-by-criteria entity-meta
-                       :cols    cols
-                       :where   where-clause
-                       :groupby groupby
-                       :other   other)
-        ]
+                       {:cols    cols    :where   where-clause
+                        :groupby groupby :other   other} )]
     (if (empty? rows) nil
       ((:from-row-fn entity-meta) (first rows)))))
 
 
 ;;; ===== Relationship handling functions. Execute with in-db / in-txn
 
+(defn- assert-same-type-entities
+  "Assert that all entities are of the same type."
+  [^IPersistentVector entities]
+  (assert (vector? entities))
+  (if (or
+        (nil?   entities)
+        (empty? entities)
+        (nil?   (first entities))
+        (let [entity-meta  (get-meta (first entities))
+              invalid?    #(or
+                             (nil? %)
+                             (not= entity-meta (get-meta %)))]
+          (some invalid? entities)))
+    (bad-arg# "One or more non-null entities of same type expected")))
+
+
+(defn entity-rels-map
+  "Build entity-relation map. You pass [e1 e2 e3] as entities and
+   [e1r1 e1r2 e2r1 e2r2 e2r3] as related entities, and you get back
+   {e1 [e1r1 e1r2]
+    e2 [e2r1 e2r2 e2r3]}
+   Note: e3 is not a key in the map as it has no corresponding related entities"
+  [^IPersistentVector entities ^IPersistentVector rel-entities]
+  (if *assert-args* (do
+                      (assert-same-type-entities entities)
+                      (assert-same-type-entities rel-entities)))
+  (let [entity         (first entities)
+        this-meta      (get-meta entity)
+        that-meta      (get-meta (first rel-entities))
+        that-table-map (dbrel-lookup-by-that-entity (rel-meta entity))
+        rel-data       (that-table-map (:name that-meta))
+        that-column    (:that-column rel-data)
+        this-column    (:this-column rel-data)]
+    (group-by #(get-original-entity
+                 entities this-column % that-column)
+      rel-entities)))
+
+
 (defn find-rels
   "Fetch related entities. You can use the :cols, :where, :groupby and :other
-   attributes as in find-by-criteria function. This avoids N+1 Selects. Returns
-   a map in the form
-   {entity1 [e1-rel1 e1-rel2 ...]
-    entity2 [e2-rel1 e2-rel2 e2-rel3 ...]}
-   Note: Entities with no children are not included in the map."
-  [entities-vec ^EntityMetadata that-meta &
-   {:keys [cols where groupby other]
-    :or   {cols    [star-col]     ;; vector of col names
-           where   (empty-clause) ;; clause
-           groupby []             ;; vector of expressions
-           other   (empty-clause) ;; clause
-           }}]
-  (let [entities (as-set (if (map? entities-vec) #{entities-vec}
-                           entities-vec))]
-    ;; validate (error check) the input entities
-    (if (or
-          (nil?   entities)
-          (empty? entities)
-          (nil?   (first entities))
-          (let [entity-meta  (get-meta (first entities))
-                invalid?    #(or
-                               (nil? %)
-                               (not= entity-meta (get-meta %)))]
-            (some invalid? entities)))
-      (bad-arg# "One or more non-null entities of same type expected"))
+   attributes as in find-by-criteria function. This avoids N+1 Selects. Return
+   a sequence of entity:child pairs. 'f' is a function that takes one argument
+   (the sequence) and must not return something that processes the arg lazily."
+  ([^IFn f ^IPersistentVector entities ^EntityMetadata that-meta
+    ^IPersistentMap {:keys [^IPersistentVector cols    ^IPersistentVector where
+                            ^IPersistentVector groupby ^IPersistentVector other]
+                     :or   {cols    [star-col]     ;; vector of col names
+                            where   (empty-clause) ;; clause
+                            groupby []             ;; vector of expressions
+                            other   (empty-clause) ;; clause
+                            }}]
+    (if *assert-args* (do
+                        (assert (fn? f))
+                        (assert (vector? entities))
+                        (assert-as that-meta EntityMetadata)
+                        (assert (vector? cols))
+                        (assert (clause? where))
+                        (assert (vector? groupby))
+                        (assert (clause? other))
+                        (assert-same-type-entities entities)))
     ;; actual processing
     (let [entity         (first entities)
           this-meta      (get-meta entity)
           this-column    (:this-column rel-data)
           rel-col-values (map #(this-column %) entities)
           ;; add 'that-col IN (vals-in-entities)' expression to the WHERE clause
-          old-where      (as-clause where)
-          new-clause     (in? that-column rel-col-values)
-          where-clause   (if (empty-clause? old-where) new-clause
-                           (AND new-clause old-where))
+          new-where      (in? that-column rel-col-values)
+          where-clause   (if (empty-clause? where) new-where
+                           (AND new-where where))
           ;; add 'that-col' to the cols being fetched
-          add-rel-column (fn [few-cols]
+          add-rel-column (fn [^IPersistentVector few-cols]
                            (if (some #(or
                                         (= that-column %)  ;; that-col
                                         (= star-col %))    ;; OR star-col
                                  few-cols)                 ;; found in cols?
                              few-cols                      ;; then cols are fine
                              (conj few-cols that-column))) ;; prefix otherwise
-          cols-vector    (add-rel-column (as-vector cols))
+          cols-vector    (add-rel-column cols)
           ;; add 'that-col' to GROUP BY if 'count-col' is being fetched
-          old-groupby    (as-vector groupby)
           new-groupby    (if (and
                                (some #(= count-col %)
-                                 cols-vector)        ;; count-col being fetched
-                               (< 1 (count entities));; AND more than 1 entity
+                                 cols-vector)         ;; count-col being fetched?
+                               (< 1 (count entities)) ;; AND more than 1 entity?
                                (not (some #(= that-column %)
-                                      old-groupby))) ;; AND that-col not in group-by
+                                      groupby))) ;; AND that-col not in group-by?
                            [that-column] [])
-          groupby-vector (into new-groupby old-groupby) ;; new col comes first
-          ;; fetch child entities
-          child-entities (find-by-criteria that-meta
-                           :cols    cols-vector
-                           :where   where-clause
-                           :groupby groupby-vector
-                           :other   other)
-          find-parent    (fn [child-entity]
-                           (let [foreign-key (that-column child-entity)]
-                             (first (filter #(= (this-column %) foreign-key)
-                                      entities))))]
-      (group-by find-parent child-entities))))
+          groupby-vector (into new-groupby groupby) ;; new col comes first
+          ;; criteria
+          criteria       {:cols    cols-vector    :where where-clause
+                          :groupby groupby-vector :other other}
+          ]
+      (with-find-by-criteria-results ents that-meta criteria
+        (f ents))))
+  ([^IPersistentVector entities ^EntityMetadata that-meta
+    ^IPersistentMap {:keys [^IPersistentVector cols    ^IPersistentVector where
+                            ^IPersistentVector groupby ^IPersistentVector other]
+                     :or   {cols    [star-col]     ;; vector of col names
+                            where   (empty-clause) ;; clause
+                            groupby []             ;; vector of expressions
+                            other   (empty-clause) ;; clause
+                            }}]
+    (let [criteria {:cols cols :where where :groupby groupby :other other}
+          rels     (find-rels #(into [] %) entities that-meta criteria)]
+      rels))
+  ([^IPersistentVector entities ^EntityMetadata that-meta]
+    (find-rels entities that-meta {})))
 
 
-(defn save-deps [^Entity entity deps-vector]
+(defmacro with-find-rels-results
+  [rel-entities ^IPersistentVector entities ^EntityMetadata that-meta
+   ^IPersistentMap criteria & body]
+  `(let [{cols# :cols where# :where groupby# :groupby other# :other} ~criteria]
+    (find-rels (fn [~rel-entities] ~@body) ~entities ~that-meta
+      {:cols cols# :where where# :groupby groupby# :other other#})))
+
+
+(defn find-entity-rels-map
+  "Find related entities for the given set of entities and return a map of
+   entity versus related-entities (see entity-rel-map function for details)."
+  ([^IPersistentVector entities ^EntityMetadata that-meta
+    ^IPersistentMap {:keys [^IPersistentVector cols    ^IPersistentVector where
+                            ^IPersistentVector groupby ^IPersistentVector other]
+                     :or   {cols    [star-col]     ;; vector of col names
+                            where   (empty-clause) ;; clause
+                            groupby []             ;; vector of expressions
+                            other   (empty-clause) ;; clause
+                            }}]
+    (entity-rels-map entities (find-rels entities that-meta
+                                {:cols    cols    :where where
+                                 :groupby groupby :other other})))
+  ([^IPersistentVector entities ^EntityMetadata that-meta]
+    (find-entity-rels-map entities that-meta {})))
+
+
+(defn save-deps [^Entity entity ^IPersistentVector dep-entities]
   "Save dependents (THAT table) -- 1-to-many (has-many) relationships"
-  (let [cvec (as-vector deps-vector)
-        rels (rel-meta entity)
+  (if *assert-args* (do
+                      (assert (map? entity))
+                      (assert (vector? dep-entities))
+                      (assert (not (empty? dep-entities)))
+                      (assert (every? #(map? %) dep-entities))))
+  (let [rels (rel-meta entity)
         that-table-map (dbrel-lookup-by-that-entity rels)]
-    (into [] (for [each cvec]
+    (into [] (for [each dep-entities]
       (if-let [each-rel (that-table-map (:name (get-meta each)))]
         (let [child (assoc each
                       (:that-column each-rel)
 (defn find-siblings
   "Fetch sibling entities - Many-to-1 relationships. You can use the :cols,
    :where, :groupby and :other attributes as in find-by-criteria function."
-  [^Entity entity ^Entity rel-entity &
-   {:keys [cols where groupby other]
-    :or   {cols    [star-col]     ;; vector of col names
-           where   (empty-clause) ;; clause
-           groupby []             ;; vector of expressions
-           other   (empty-clause) ;; clause
-           }}]
-  (let [this-meta       (get-meta entity)
-        that-table-map  (dbrel-lookup-by-that-entity (rel-meta entity))
-        rel-data        (that-table-map (:name (get-meta rel-entity)))
-        this-table-name (name (:name this-meta))
-        this-col-name   (name (:this-column rel-data))
-        that-id-value   ((:that-column rel-data) rel-entity)
-        ;; add 'this-col = that-id-value' expression to the WHERE clause
-        old-where (as-clause where)
-        new-where (=? this-col-name that-id-value)
-        where-clause (if (empty-clause? old-where) new-where
-                       (AND new-where old-where))
-        ]
-    (find-by-criteria this-meta
-      :cols  cols
-      :where   where-clause
-      :groupby groupby
-      :other   other)))
+  ([^IFn f ^Entity entity ^Entity rel-entity
+    ^IPersistentMap {:keys [cols where groupby other]
+                     :or   {cols    [star-col]     ;; vector of col names
+                            where   (empty-clause) ;; clause
+                            groupby []             ;; vector of expressions
+                            other   (empty-clause) ;; clause
+                            }}]
+    (let [this-meta       (get-meta entity)
+          that-table-map  (dbrel-lookup-by-that-entity (rel-meta entity))
+          rel-data        (that-table-map (:name (get-meta rel-entity)))
+          this-table-name (name (:name this-meta))
+          this-col-name   (name (:this-column rel-data))
+          that-id-value   ((:that-column rel-data) rel-entity)
+          ;; add 'this-col = that-id-value' expression to the WHERE clause
+          old-where    (as-clause where)
+          new-where    (=? this-col-name that-id-value)
+          where-clause (if (empty-clause? old-where) new-where
+                         (AND new-where old-where))
+          criteria     {:cols    cols    :where where-clause
+                        :groupby groupby :other other}
+          ]
+      (with-find-by-criteria-results ents this-meta criteria
+        (f ents))))
+  ([^Entity entity ^Entity rel-entity ^IPersistentMap criteria]
+    (find-siblings #(into [] %) entity rel-entity criteria))
+  ([^Entity entity ^Entity rel-entity]
+    (find-siblings #(into [] %) entity rel-entity {})))
+
+
+(defmacro with-find-siblings-results
+  [sibling-entities ^Entity entity ^Entity rel-entity ^IPersistentMap criteria
+   & body]
+  `(find-siblings (fn [~sibling-entities] ~@body) ~entity ~rel-entity ~criteria))
 
 
 (defn delete-cascade [entity]
   [entity]
   "Delete (cascaded) a given entity"
+  (if *assert-args* (do
+                      (assert (not (nil? entity)))
+                      (assert (map? entity))))
   (let [rels (rel-meta entity)]
     (doseq [each rels]
       (if (:that-depends? each)
-        (let [c ((find-rels entity (:that-entity each)) entity)]
+        (let [c ((find-entity-rels-map [entity] (:that-entity each)) entity)]
           (doseq [each-child c]
             (delete-cascade each-child))))))
   (delete entity))
 (defn print-entities [entities]
   "Print homogenous entities in a table format. Keys from the first entity are
    used as title. Passing an empty sequence of entities prints nothing at all."
+  (mypp "\nENTITIES ***\n" entities)
+  (if *assert-args*
+    (do
+      (assert (vector? entities))
+      (assert (every? (fn [entity] (and (map? entity)
+                         (every? (fn [col-entry]
+                                   (let [not-coll? #(not (coll? %))]
+                                     (and
+                                       (not-coll? (first col-entry))
+                                       (not-coll? (last col-entry)))))
+                           entity))) entities))))
   (if-let [rows (map to-row (as-vector (if (map? entities) [entities] entities)))]
-    (let [cols-count (count (first rows))
-          cols-width (atom (into [] (take cols-count (repeat 0))))
+    (let [cols-count  (count (first rows))
+          cols-width  (atom (into [] (take cols-count (repeat 0))))
           keys-as-str (map name (keys (first rows)))
           keys-n-vals (conj (map vals rows) keys-as-str)
           ;; translate non-printable chars http://hyperpolyglot.wikidot.com/lisp

src/main/clj/org/bituf/sqlrat/entity/internal.clj

        (binding [sql/do-prepared do-prepared-insert]
          (sql/insert-values table (keys record) (vals record)))
        result))))
+
+
+(defn get-original-entity
+  "Find parent entity for the specified child-entity from a group of entities"
+  [^IPersistentVector entities ^Keyword this-column
+   ^IPersistentMap rel-entity  ^Keyword that-column]
+  (let [foreign-key (that-column rel-entity)]
+    (first (filter #(= (this-column %) foreign-key)
+             entities))))
+
+

src/test/clj/org/bituf/sqlrat/test/dbblog.clj

 ;; =========================================================================
 
 (defn ppe ;; pretty-print-entities
-  ([e]
-    (print-entities e))
+  ([es]
+    (assert (vector? es))
+    (print-entities es))
   ([label e]
     (println label)
     (print-entities e)))
                            :whenposted (new java.util.Date)} ))
             newid (get-id-value saved)]
         (is (and (not-nil? newid) (not (zero? newid))))
-        (ppe "Saved row #1" saved)
+        (ppe "Saved row #1" [saved])
         (let [saved-again (save (assoc saved :title "Test Updated"))
               newid-again (get-id-value saved-again)]
           (is (= newid newid-again))
-          (ppe "Saved again (updated) row #1" saved-again))))
+          (ppe "Saved again (updated) row #1" [saved-again]))))
     (in-txn db
       (let [saved (save (BlogEntry. {}
                           {:title "Second post"
                            :whenposted (new java.util.Date)} ))
             newid (get-id-value saved)]
         (is (and (not-nil? newid) (not (zero? newid))))
-        (ppe "Saved row #2" saved)))
+        (ppe "Saved row #2" [saved])))
     (in-txn db
       (let [saved (save (BlogEntry. {}
                           {:title "Third post"
                            :whenposted (new java.util.Date)} ))
             newid (get-id-value saved)]
         (is (and (not-nil? newid) (not (zero? newid))))
-        (ppe "Saved row #3" saved)))))
+        (ppe "Saved row #3" [saved])))))
 
 
 (deftest test-read-entry-table
             en (find-by-id blog-entry-meta 11)
             ea (find-by-criteria blog-entry-meta)]
         (is (= 1 (get-id-value e1)))
-        (ppe "\nRow 1:" e1)
+        (ppe "\nRow 1:" [e1])
         (is (= 2 (get-id-value e2)))
-        (ppe "\nRow 2:" e2)
+        (ppe "\nRow 2:" [e2])
         (is (nil? en))
-        (ppe "\nRow 11 (non-existent):" en)
+        (println "\nRow 11 (non-existent):" en)
         (is (= 3 (count ea)))
         (ppe "\nAll rows:" ea)))))
 
     (println "** Fetching entry-comment graph **")
     (in-db db
       (let [e (find-by-id blog-entry-meta 1)
-            r ((find-rels e entry-comment-meta) e)]
+            s (find-entity-rels-map [e] entry-comment-meta)
+            _ (do (println "Debug in dblog.clj START")
+                (println s)
+                (println "Debug in dblog.clj END"))
+            r (s e)]
         (is (= 1 (count r)))
-        (ppe "\nEntry:" e)
+        (ppe "\nEntry:" [e])
         (ppe "\nRelations:" r))
-      (let [e (find-by-id blog-entry-meta 2)
-            r ((find-rels e entry-comment-meta) e)
-            rc ((find-rels e entry-comment-meta :cols [:content :name :email]) e)
-            rw ((find-rels e entry-comment-meta :where ["email=?" "hey@nospam.com"]) e)
-            rb ((find-rels e entry-comment-meta :cols [:content :name :email]
-                 :where ["email=?" "hey@nospam.com"]) e)]
+      (let [e  (find-by-id blog-entry-meta 2)
+            r  ((find-entity-rels-map [e] entry-comment-meta) e)
+            rc ((find-entity-rels-map [e] entry-comment-meta
+                  {:cols [:content :name :email]}) e)
+            rw ((find-entity-rels-map [e] entry-comment-meta
+                  {:where (as-clause ["email=?" "hey@nospam.com"])}) e)
+            rb ((find-entity-rels-map [e] entry-comment-meta
+                  {:cols [:content :name :email]
+                   :where (as-clause ["email=?" "hey@nospam.com"])}) e)]
         (is (= 2 (count r)))
         (is (<= 3 (count (keys (first rc)))))
         (is (= 1 (count rw)))
         (is (and (<= 3 (count (keys (first rb))))  (= 1 (count rb))))
-        (ppe "\nEntry:" e)
+        (ppe "\nEntry:" [e])
         (ppe "\nRelations:" r)
         (ppe "\nRelations with selected columns:" rc)
         (ppe "\nRelations with WHERE clause:" rw)
         (ppe "\nRelations with selected columns and WHERE clause:" rb))
       (println "\n*** Avoiding N+1 selects ***")
       (let [entries (find-by-criteria blog-entry-meta)
-            comments (find-rels entries entry-comment-meta)]
+            comments (find-entity-rels-map entries entry-comment-meta)]
         (ppe "\nAll entries:" entries)
         (doseq [each entries]
           (ppe (str "\nComments for entry ID: " (:autoid each))
             (comments each))))
       (let [entries (find-by-criteria blog-entry-meta)
-            comments (find-rels entries entry-comment-meta :cols count-col)]
+            comments (find-entity-rels-map entries entry-comment-meta
+                       {:cols [count-col]})]
         (ppe "\nAll entries:" entries)
         (doseq [each entries]
           (println (str "\nComments# for entry ID: " (:autoid each))
     (println "** Fetching comments using GROUP BY and LIMIT **")
     (in-db db
       (let [entries (find-by-criteria blog-entry-meta)
-            comments (find-rels entries entry-comment-meta :cols count-col
-                       :other (limit 1 2))]
+            comments (find-entity-rels-map entries entry-comment-meta
+                       {:cols [count-col] :other (limit 1 2)} )]
         (is (= 3 (count entries)))
         (ppe "\nAll entries:" entries)
         (doseq [each entries]
     (println "** Fetching siblings **")
     (in-db db
       (let [e (find-by-id blog-entry-meta 2)
-            r ((find-rels e entry-comment-meta) e)
+            r ((find-entity-rels-map [e] entry-comment-meta) e)
             c (first r)
             s (find-siblings c e)
-            sc (find-siblings c e :cols [:content :name :email])
-            sw (find-siblings c e :where ["name LIKE ?" "Phi%"])
-            sb (find-siblings c e :cols [:content :name :email] :where ["name LIKE ?" "Phi%"])]
+            sc (find-siblings c e {:cols  [:content :name :email]})
+            sw (find-siblings c e {:where (as-clause ["name LIKE ?" "Phi%"])})
+            sb (find-siblings c e {:cols  [:content :name :email]
+                                   :where (as-clause ["name LIKE ?" "Phi%"])})]
         (is (= 2 (count s)))
         (is (= 3 (count (keys (first sc)))))
         (is (= 1 (count sw)))
         (is (and (= 3 (count (keys (first sb)))) (= 1 (count sb))))
-        (ppe "\nChild: " c)
+        (ppe "\nChild: " [c])
         (ppe "\nSiblings:" s)
         (ppe "\nSiblings with selected columns:" sc)
         (ppe "\nSiblings with WHERE clause:" sw)
   (fail-on-exception
     (println "** Counting by criteria **")
     (in-db db
-      (let [r1 (find-by-criteria blog-entry-meta :cols count-col)
+      (let [r1 (find-by-criteria blog-entry-meta {:cols [count-col]})
             e  (find-by-id blog-entry-meta 2)
-            r2 ((find-rels e entry-comment-meta :cols count-col) e)
+            r2 ((find-entity-rels-map [e] entry-comment-meta {:cols [count-col]}) e)
             c  (first r2)
-            s  (find-siblings c e :cols count-col)
-            sw (find-siblings c e :cols count-col :where ["name LIKE ?" "Phi%"])]
+            s  (find-siblings c e {:cols [count-col]})
+            sw (find-siblings c e {:cols  [count-col]
+                                   :where (as-clause ["name LIKE ?" "Phi%"])})]
         (is (= 3 (read-count-col r1)))
         (is (= 2 (read-count-col r2)))
         (is (= 2 (read-count-col s)))
   (fail-on-exception
     (in-txn db
       (let [e (find-by-id blog-entry-meta 3)
-            r ((find-rels e entry-comment-meta) e)
+            r ((find-entity-rels-map [e] entry-comment-meta) e)
             c (first r)]
         (is (= 3 (count r)))
         (println "** Deleting comment **")
         (delete c)
-        (let [ra ((find-rels e entry-comment-meta :cols count-col) e)]
+        (let [ra ((find-entity-rels-map [e] entry-comment-meta
+                    {:cols [count-col]}) e)]
           (is (= 2 (read-count-col ra)))
           (println "** Deleting entry-comment graph **")
           (delete-cascade e)