shantanuk  committed 817c41b

cleanup, documentation, assert-xx and read-first-count-col functions

  • Participants
  • Parent commits 4154c2a

Comments (0)

Files changed (3)

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

 (ns org.bituf.sqlrat.entity
+  "Support Data Types (defrecord) as database entities and provide functions to
+   carry out database operations using the entities."
   (:use org.bituf.sqlrat.entity.internal)
   (:use org.bituf.sqlrat.util)
   (:use org.bituf.sqlrat.clause)
 ; ===== Utility functions and macros =====
 (defn- as-vector*
+  "Convert anything to a vector."
   (if *assert-args* (do (assert (vector? x)) x)
     (as-vector x)))
 (defmacro in-db
+  "Create the context for executing database operations inside the macro body.
+   It provides with a database connection, which is automatically closed once
+   the body is executed completely. You must not return a lazy sequence that
+   tries to access the database *after* the body is executed.
+   Example: See the 'db-query' function
+   See also: in-txn"
   [db & body]
   `(sql/with-connection ~db
 (defmacro in-txn
+  "Same as 'in-db' macro, but it creates a transaction in which the database
+   operations can take place. You should use this for executing write-operations
+   in transactions.
+   Example: See the 'save-row' function
+   See also: in-db"
   [db & body]
   `(sql/with-connection ~db
 (defn db-query
-  "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]
+  "Fetch rows from database. Execute this with 'in-db' or 'in-txn'. When the
+   first argument is not a function (f) it simply collects the rows into a
+   vector an returns it - amount of memory consumed varies with the row count.
+   Arguments:
+     f     Is called with 'rows' as the only argument for processing them. It
+           must not return a lazy sequence that tries to access the database
+           *after* the 'db-query' function is executed.
+     query A vector containing the SQL in clojure.contrib.sql format. Examples
+           are: [\"SELECT * FROM emp\"] and [\"SELECT * FROM emp WHERE id=?\" 56]
+   Example:
+     (in-db mysql
+       (db-query (fn [rows] (count rows))
+         [\"SELECT * FROM emp WHERE id=?\" 56]))
+     => 135 ;; returns the row-count
+     (in-db mysql
+       (db-query [\"SELECT * FROM emp WHERE id=?\" 56]))
+     => [{<row1 data>} {<row2 data>} ...] ;; returns the rows as a vector
+   See also: with-db-query-results"
+  ([^IFn f ^IPersistentVector query]
     (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
+                        (assert (vector? query))))
+    (if *show-sql* (mypp "Executing SQL..." query))
+    (sql/with-query-results rows query
       (f rows)))
-  ([query-vec]
+  ([^IPersistentVector query]
     (let [f (fn [rows]
               (let [result (if (nil? rows) nil (into [] rows))]
                 (if *show-sql-results* (mypp "SQL Result..." rows))
-      (db-query f query-vec))))
+      (db-query f query))))
-(defmacro with-db-query-results "Wrapper macro for db-query"
-  [results sql-params & body]
-  `(db-query (fn [~results] ~@body) ~sql-params))
+(defmacro with-db-query-results
+  "Wrapper macro for 'db-query'.
+   Arguments:
+     rows  Is bound to the rows fetched as a result of running the query
+     query The SQL query as described in 'db-query' function
+   Example:
+     (in-db mysql
+       (with-db-query-results rows [\"SELECT * FROM emp\"]
+         (println rows)))
+     => <rows-data>
+   See also: db-query"
+  [rows ^IPersistentVector sql-params & body]
+  `(db-query (fn [~rows] ~@body) ~sql-params))
 ;(defn save-row
 (defn save-row
   "Save given row. Table should be specified as :tablename (keyword).
-  Row is simply a map of :columnName to value. Execute with in-txn or in-db.
-  Returns the saved row, which may have generated ID (if applicable)."
-  [^Keyword table ^Map row ^Keyword id-column]
+   Row is simply a map of :columnName to value. Execute with in-txn or in-db.
+   Returns the saved row, which may have generated ID (if applicable).
+   Arguments:
+     table     The database table name (keyword)
+     row       The row (map of column-name to column-value) to be saved
+     id-column The primary ID column (keyword)
+   Examples:
+     (in-txn mysql
+       (save-row :emp {:code 9008 :name \"Joe Walker\"} :empid))
+     => {:empid 197 :code 9008 :name \"Joe Walker\"} ;; 197 is the generated ID
+     (in-txn mysql
+       (save-row :emp {:empid 197 :code 9667 :name \"Joe Hacker\"} :empid))
+     => {:empid 197 :code 1337 :name \"Joe Hacker\"} ;; updated code and name
+   See also: save"
+  [^Keyword table ^IPersistentMap row ^Keyword id-column]
   (let [id=? (str (name id-column) "=?")]
     (let [result (update-or-insert-values-returnid
                    table [id=? (id-column row)] row)]
 (defn relation
+  "Factory function for creating a RelationMetadata instance. A relation is
+   defined between 'this' and 'that' entities. RelationMetadata is associated
+   with a certain 'this' entity, hence you need not specify 'this' entity.
+   Arguments:
+     this-col      (Keyword) The column in 'this' entity
+     that-ent      (EntityMetadata) The other entity
+     that-col      (Keyword) The column in 'that' entity
+     that-depends? (Boolean, optional, default false) Whether 'that' entity
+                   logically depends on 'this' entity
+   Example:
+     (relation :orderid item-metadata :itemid true) ;; order to item relation
+   See also: one-to-many, many-to-one, one-to-one, one-to-one-depends"
   ([^Keyword this-col ^EntityMetadata that-ent ^Keyword that-col that-depends?]
     (RelationMetadata. this-col that-ent that-col that-depends?))
   ([^Keyword this-col ^EntityMetadata that-ent ^Keyword that-col]
     (RelationMetadata. this-col that-ent that-col false)))
-(defn one-to-many [^Keyword this-col ^EntityMetadata that-ent-meta ^Keyword that-col]
+(defn one-to-many
+  "Create one-to-many relation metadata.
+   Arguments: See 'relation' function
+   Example:
+     TODO
+   See also: relation"
+  [^Keyword this-col ^EntityMetadata that-ent-meta ^Keyword that-col]
   (relation this-col that-ent-meta that-col true))
-(defn many-to-one [^Keyword this-col ^EntityMetadata that-ent-meta ^Keyword that-col]
+(defn many-to-one
+  "Create many-to-one relation metadata.
+   Arguments: See 'relation' function
+   Example:
+     TODO
+   See also: relation"
+  [^Keyword this-col ^EntityMetadata that-ent-meta ^Keyword that-col]
   (relation this-col that-ent-meta that-col false))
-(def one-to-one-depends one-to-many)
+(def ^{:doc "An alias to 'one-to-many' function"}
+      one-to-one-depends one-to-many)
-(def one-to-one         many-to-one)
+(def ^{:doc "An alias to 'many-to-one' function"}
+      one-to-one         many-to-one)
 (defn rel-impl
-  "Returns implementation for the Relation protocol."
-  [rels-vector]
+  "Return implementation for the Relation protocol.
+   Arguments:
+     rels-vector  (Vector) of relation specs
+   Example: See extend-entity
+   See also: relation, extend-entity"
+  [^IPersistentVector rels-vector]
   {:rel-meta (fn [this] (as-vector rels-vector))} )
   (into {} entity))
-(defmacro from-row [& entity-creator]
+(defmacro from-row
+  "Return a function that merges a value-map into a data type instance.
+   Arguments:
+     entity-creator  Body of code that creates/returns a data type instance
+   Example:
+     (from-row OrderItem.)
+   See also: entity-meta"
+  [& entity-creator]
   `#(~@entity-creator {} %))
 (defn entity-meta
-  "Factory function to create entity metadata."
-  [name id from-row-fn
-   & {:keys [cols to-row-fn]
+  "Factory function to create entity metadata. Arguments 'from-row-fn' and
+   'to-row-fn' let you abstract the row data away from the entity (useful when
+   entities cover a different perspective than the rows, for example during
+   Domain-driven design).
+   Arguments:
+     name        (Keyword) table name
+     id-col      (Keyword) ID column
+     from-row-fn (Function) that accepts a row (col-value map) as the only
+                 argument and converts it into an entity (data type instance).
+   Optional arguments:
+     :cols <v>      (Vector) of column specs. Each colum spec is a vector too.
+                    This is required *only* for the 'create-table' function.
+     :to-row-fn <v> (Function) that accepts entity (data type instance) as the
+                    only argument and converts into a row (key-value map).
+   Example:
+     (defrecord BlogEntry [])
+     (def blog-entry-meta
+       (entity-meta :entry :autoid (from-row BlogEntry.)
+         :cols [[:autoid     :int           \"NOT NULL PRIMARY KEY AUTO_INCREMENT\"]
+                [:title      \"varchar(30)\"  \"NOT NULL\"]
+                [:content    \"varchar(500)\" \"NOT NULL\"]
+                [:whenposted \"DATETIME\"     \"NOT NULL\"]
+                [:isdeleted  \"BOOLEAN\"      \"NOT NULL DEFAULT false\"]] ))
+   See also: Functions take entity metadata as argument."
+  [^Keyword name ^Keyword id ^IFn from-row-fn
+   & {:keys [^IPersistentVector cols ^IFn to-row-fn]
       :or   {to-row-fn to-row}}]
   (EntityMetadata. name id from-row-fn
     {} {:cols cols :to-row-fn to-row-fn}))
   (get-meta [this] "Get entity metadata"))
+(defn entity?
+  "Tell whether specified object is an entity"
+  [obj]
+  (and
+    (extends? Entity (type obj))
+    (map? obj)))
 (defn entity-impl
-  "Returns implementation for Entity protocol."
-  [^EntityMetadata e-meta]
-  {:get-meta (fn [this] e-meta)} )
+  "Factory function to create implementation for Entity protocol.
+   Arguments:
+     ent-metadata  (EntityMetadata) the Entity metadata
+   Example:
+     (entity-impl e-meta) ;; where e-meta is the entity metadata
+   See also: entity-meta"
+  [^EntityMetadata ent-metadata]
+  {:get-meta (fn [this] ent-metadata)} )
 (defn extend-entity
-  ([record ent-meta]
-    (extend record
-      Entity (entity-impl ent-meta)))
-  ([record ent-meta rel-metadata]
-    (extend record
-      Entity   (entity-impl ent-meta)
-      Relation (rel-impl rel-metadata))))
+  "Associate an entity data type (hence all instances) with entity metadata and
+   relation metadata. This function may typically be called only once after the
+   entity data type is defined.
+   Arguments:
+     ent-type     The entity data type (not an instance)
+     ent-metadata Entity metadata
+     rel-metadata Relation metadata
+   Example:
+     (extend-entity BlogEntry blog-entry-meta
+       [(one-to-many :autoid  entry-comment-meta :entryid)] )
+   See also: entity-meta"
+  ([^Class ent-type ^EntityMetadata ent-metadata]
+    (extend ent-type
+      Entity   (entity-impl ent-metadata)))
+  ([^Class ent-type ^EntityMetadata ent-metadata
+    ^IPersistentVector rel-metadata]
+    (extend ent-type
+      Entity   (entity-impl ent-metadata)
+      Relation (rel-impl    rel-metadata))))
-(def star-col "*")
+(def ^{:doc "The * (all columns) specifier"}
+      star-col "*")
-(def count-col "COUNT(*) AS cnt")
+(def ^{:doc "The count-column expression clause"}
+      count-col "COUNT(*) AS sqlratcnt")
 (defn read-count-col
-  "Read the value of count-col from specified row or entity"
-  [row-or-entity]
-  (:cnt (first row-or-entity)))
+  "Read the value of count-col from specified row or entity. Return nil if the
+   the argument is nil."
+  [^IPersistentMap row-or-entity]
+  (if *assert-args* (assert-arg #(or (nil? %) (map? %)) row-or-entity))
+  (:sqlratcnt row-or-entity))
+(defn read-first-count-col
+  "Read the value of count-col from the first row/entity of a vector. Return nil
+   if the the argument is nil. Useful when the count-col is not grouped by some
+   column and hence there is just one row in the result set."
+  [^IPersistentVector row-vector]
+  (if *assert-args* (assert-arg #(or (nil? %) (vector? %)) row-vector))
+  (read-count-col (first row-vector)))
 (defn get-id-column
   "Return ID column from entity"
-  [entity]
+  [^IPersistentMap entity]
+  (if *assert-args* (assert-arg entity? entity))
   (:id (get-meta entity)))
 (defn get-id-value
   "Return ID column value from entity"
-  [entity]
+  [^IPersistentMap entity]
   ((get-id-column entity) entity))

File src/main/clj/org/bituf/sqlrat/util.clj

 (ns org.bituf.sqlrat.util
-  "Common utility functions")
+  "Common utility functions"
+  (:use clojure.repl)
+  (:import [clojure.lang IFn Keyword IPersistentMap IPersistentVector]))
 (defn bad-arg#
   (throw (IllegalArgumentException. (apply str reason more))))
+(defn vstr
+  "Verbose str"
+  [s]
+  (if-let [x s] x "<nil>"))
+(defn arg
+  "Apply f? (must return Boolean) to arg - return true when asserted true,
+   throw exception otherwise."
+  [^IFn f? arg]
+  (if (f? arg) true
+    (bad-arg# "Invalid argument " (vstr arg) " (Expected: " (:name (meta f?))
+      ", Found: " (vstr (type arg)) ")")))
+(defn assert-arg
+  "Assert specified argument using the function f?, which must return Boolean."
+  [^IFn f? param]
+  (assert (arg f? param)))
 (defn assert-as
   [item ^Class expected-type]
+  (assert (instance? Class expected-type))
     (assert (isa? (type item) expected-type))
     (catch AssertionError e

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

         (ppe "\nAll entries:" entries)
         (doseq [each entries]
           (println (str "\nComments# for entry ID: " (:autoid each))
-            (read-count-col (comments each))))))))
+            (read-first-count-col (comments each))))))))
 (deftest test-comment-groupby-other
         (is (= 3 (count entries)))
         (ppe "\nAll entries:" entries)
         (doseq [each entries]
-          (is (= (read-count-col (comments each)) ({2 2 3 3} (:autoid each))))
+          (is (= (read-first-count-col (comments each)) ({2 2 3 3} (:autoid each))))
           (println (str "\nComments# for entry ID: " (:autoid each))
-            (read-count-col (comments each))))))))
+            (read-first-count-col (comments each))))))))
 (deftest test-fetch-comment-siblings
             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)))
-        (is (= 1 (read-count-col sw)))))))
+        (is (= 3 (read-first-count-col r1)))
+        (is (= 2 (read-first-count-col r2)))
+        (is (= 2 (read-first-count-col s)))
+        (is (= 1 (read-first-count-col sw)))))))
 (deftest test-delete-entities
         (delete c)
         (let [ra ((find-entity-rels-map [e] entry-comment-meta
                     {:cols [count-col]}) e)]
-          (is (= 2 (read-count-col ra)))
+          (is (= 2 (read-first-count-col ra)))
           (println "** Deleting entry-comment graph **")
           (delete-cascade e)
           (let [ne (find-by-id blog-entry-meta 3)]