Commits

Shantanu Kumar committed 3e8999b

add `Spring Batch` features - lazy-loading of result-set rows and fetch-by-range (pagination)

Comments (0)

Files changed (8)

 * [TODO] Database metadata assisted Stored Procedures
 
 
+## 0.3 / 2011-July-??
+
+* Use Clj-DBSpec 0.3
+* [TODO] Support :fetch-size and :query-timeout in SimpleJdbcTemplate
+* Spring-Batch features
+  * [TODO] Lazy loading of SELECT query results to conserve memory
+  * [TODO] Range based lazy fetching of query results for pagination
+
+
 ## 0.2 / 2011-Apr-01
 
 * Use Clj-DBSpec 0.2
   <groupId>org.bituf</groupId>
   <artifactId>fountain-jdbc</artifactId>
   <packaging>jar</packaging>
-  <version>0.2</version>
+  <version>0.3-SNAPSHOT</version>
   <name>fountain-jdbc</name>
   <description>
     Fountain-JDBC is a Clojure wrapper for Spring-JDBC component of the Spring framework.
     <dependency>
       <groupId>org.clojure</groupId>
       <artifactId>clojure</artifactId>
-      <version>1.2.0</version>
+      <version>1.3.0-beta1</version>
       <optional>true</optional>
     </dependency>
     <dependency>
         <version>${org.springframework.version}</version>
     </dependency>
     <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-context</artifactId>
+        <version>${org.springframework.version}</version>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-aop</artifactId>
+        <version>${org.springframework.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.batch</groupId>
+      <artifactId>spring-batch-core</artifactId>
+      <version>2.1.8.RELEASE</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.springframework</groupId>
+          <artifactId>spring</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
       <groupId>org.bituf</groupId>
       <artifactId>clj-miscutil</artifactId>
       <version>0.3</version>
     <dependency>
       <groupId>org.bituf</groupId>
       <artifactId>clj-dbspec</artifactId>
-      <version>0.2</version>
+      <version>0.3-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.bituf</groupId>
       <plugin>
         <groupId>com.theoryinpractise</groupId>
         <artifactId>clojure-maven-plugin</artifactId>
-        <version>1.3.6</version>
+        <version>1.3.7</version>
         <executions>
           <execution>
             <id>compile</id>

src/main/clj/org/bituf/fountain/batch.clj

+(ns org.bituf.fountain.batch
+  (:require
+    [clojure.pprint :as pp]
+    [org.bituf.clj-dbspec    :as sp]
+    [org.bituf.clj-miscutil  :as mu]
+    [org.bituf.fountain.jdbc :as jd])
+  (:import
+    (java.sql   PreparedStatement)
+    (javax.sql  DataSource)
+    (org.springframework.jdbc.core                   ColumnMapRowMapper
+                                                     PreparedStatementSetter)
+    (org.springframework.jdbc.core.namedparam        NamedParameterUtils)
+    (org.springframework.batch.item                  ExecutionContext
+                                                     ItemReader)
+    (org.springframework.batch.item.database         JdbcCursorItemReader
+                                                     JdbcPagingItemReader)
+    (org.springframework.batch.item.database.support SqlPagingQueryProviderFactoryBean)))
+
+
+(defn row-seq
+  "Return a lazy sequence of database rows. Each row is a map of column names
+  to respective values."
+  [reader-fn]
+  (let [db-to-clj (:db-to-clj sp/*dbspec*)]
+    (map (fn [row] (mu/map-keys db-to-clj row))
+         (take-while mu/not-nil? (mu/repeat-exec reader-fn)))))
+
+
+(defn ^PreparedStatementSetter
+       make-prepared-statement-setter
+  "Return a PreparedStatementSetter object for given args"
+  [arg-coll] {:pre [(or (coll? arg-coll)
+                        (mu/array? arg-coll))]}
+  (proxy [PreparedStatementSetter] []
+    (setValues [^PreparedStatement ps]
+               (doall (map (fn [idx arg]
+                             (println (format "Setting arg %d = %s" idx arg))
+                             (.setObject ps idx arg))
+                           (iterate inc 1) arg-coll)))))
+
+
+(defn jdbc-cursor-reader
+  "Return a no-arg reader function that implicitly executes given SQL statement
+  and can be used to read rows from the ResultSet, where each row is a map of
+  column-names to respective values.
+  Example:
+    (println (row-seq (jdbc-cursor-reader \"SELECT * FROM emp\")))
+    ;; OR
+    (with-open [r (JdbcCursorItemReader.)]
+      (println (row-seq (jdbc-cursor-reader \"SELECT * FROM emp\"
+                          :jci-reader r))))
+  See also:
+    row-seq"
+  [sql
+   & {:keys [datasource
+             jci-reader      ; JdbcCursorItemReader object (default: creates new)
+             reader-fn       ; 1-arg fn to read from ItemReader (default: re-throws exception)
+             ignore-warnings ; boolean
+             fetch-size      ; int (0 = ignored by driver)
+             max-rows        ; int (0 = no limit)
+             query-timeout   ; int (seconds)
+             verify-cursor-position ; boolean (default: true, should be false)
+             save-state      ; boolean (default: true)
+             driver-supports-absolute ; boolean (default: false)
+             use-shared-extended-connection ; boolean (default: false)
+             ]
+      :as opt}]
+  {:post [(mu/verify-cond (fn %))]
+   :pre  [(mu/verify-opt #{:datasource :jci-reader :reader-fn :ignore-warnings
+                           :fetch-size :max-rows :query-timeout
+                           :verify-cursor-position :save-state
+                           :driver-supports-absolute
+                           :use-shared-extended-connection} opt)]}
+  (let [ds (or datasource (:datasource sp/*dbspec*))
+        ^JdbcCursorItemReader
+        ir (or jci-reader (JdbcCursorItemReader.))
+        [sql & args] (mu/as-vector sql)
+        arg-1        (first args)
+        arg-map      (and (= 1 (count args)) (map? arg-1) arg-1)]
+    (doto ir
+      (.setDataSource ds)
+      (.setSql sql)
+      (.setRowMapper (ColumnMapRowMapper.)))
+    (cond
+      ;; named parameters
+      arg-map               (let [sql-jdbc  (NamedParameterUtils/parseSqlStatementIntoString sql)
+                                  arg-array (NamedParameterUtils/buildValueArray
+                                              sql (mu/map-keys (:clj-to-db sp/*dbspec*) arg-map))]
+                              (doto ir
+                                (.setSql sql-jdbc)
+                                (.setPreparedStatementSetter
+                                  (make-prepared-statement-setter arg-array))))
+      ;; positional parameters
+      (mu/not-empty? args)  (do
+                              (.setPreparedStatementSetter
+                                ir (make-prepared-statement-setter args))))
+    (let [fetch-size    (or fetch-size    (:fetch-size    sp/*dbspec*))
+          query-timeout (or query-timeout (:query-timeout sp/*dbspec*))]
+      (when (mu/not-nil? ignore-warnings)                (.setIgnoreWarnings              ir ignore-warnings))
+      (when (mu/not-nil? fetch-size)                     (.setFetchSize                   ir fetch-size))
+      (when (mu/not-nil? max-rows)                       (.setMaxRows                     ir max-rows))
+      (when (mu/not-nil? query-timeout)                  (.setQueryTimeout                ir query-timeout))
+      (when (mu/not-nil? verify-cursor-position)         (.setVerifyCursorPosition        ir verify-cursor-position))
+      (when (mu/not-nil? save-state)                     (.setSaveState                   ir save-state))
+      (when (mu/not-nil? driver-supports-absolute)       (.setDriverSupportsAbsolute      ir driver-supports-absolute))
+      (when (mu/not-nil? use-shared-extended-connection) (.setUseSharedExtendedConnection ir use-shared-extended-connection)))
+    ;; open reader
+    (.open ir (ExecutionContext.))
+    (jd/show-sql sql)
+    (partial
+      (or reader-fn (fn [^ItemReader r] (.read r)))
+      ir)))
+
+
+(defrecord SelectCriteria [select    ; "SELECT DISTINCT e.name, e.born_on, d.name as dept_name"
+                           from      ; "FROM emp e, dept d"
+                           where     ; "WHERE e.name NOT LIKE 'S%'"
+                           sort-key  ; "dept_name, born_on"
+                           sort-asc  ; true
+                           args])
+
+
+(defn nil-or?
+  [preds x]
+  {:pre [(or (fn? preds)
+             (and (coll? preds)
+                  (every? fn? preds)))]}
+  (or (nil? x)
+      (some #(% x) (mu/as-vector preds))))
+
+
+(defn make-criteia
+  [& {:keys [select from where sort-key sort-asc args]
+      :as criteria}]
+  {:pre [(mu/verify-arg (string?     select))
+         (mu/verify-arg (string?     from))
+         (mu/verify-arg (nil-or? string?     where))
+         (mu/verify-arg (string?     sort-key))
+         (mu/verify-arg (nil-or? mu/boolean? sort-asc))
+         (mu/verify-arg (nil-or? coll?       args))]}
+  (SelectCriteria. select from where sort-key (if (nil? sort-asc) true sort-asc)
+                   args))
+
+
+(defn select-criteria?
+  [x]
+  (instance? SelectCriteria x))
+
+
+(defn ^JdbcPagingItemReader set-range!
+  "Set `start` (1 based; inclusive) and `stop` (1 based; exclusive) range for a
+  JdbcPagingItemReader to fetch rows from."
+  [^JdbcPagingItemReader jpi-reader start stop]
+  {:pre [(mu/verify-arg (instance? JdbcPagingItemReader jpi-reader))
+         (mu/verify-arg (mu/posnum? start))
+         (mu/verify-arg (mu/posnum? stop))
+         (mu/verify-arg (<= start stop))]}
+  (doto jpi-reader
+    (.setCurrentItemCount start)
+    (.setMaxItemCount     stop))
+  jpi-reader)
+
+
+(defn ^JdbcPagingItemReader set-page!
+  "Set `page-size` and `which-page` (1 based) to read next from a
+  JdbcPagingItemReader object."
+  [^JdbcPagingItemReader jpi-reader page-size current-page]
+  {:pre [(mu/verify-arg (instance? JdbcPagingItemReader jpi-reader))
+         (mu/verify-arg (mu/posnum? page-size))
+         (mu/verify-arg (mu/posnum? current-page))]}
+  (let [start-pos (inc (* page-size (dec current-page)))
+        max-count (+ start-pos page-size)]
+    (set-range! jpi-reader start-pos max-count)))
+
+
+(defn jdbc-paging-reader
+  [^SelectCriteria criteria
+   & {:keys [datasource
+             jpi-reader  ; instance of JdbcPagingItemReader
+             reader-fn   ; 1-arg fn to read from ItemReader (default: re-throws exception)
+             fetch-size  ; int
+             save-state
+             start-pos   ; starting row position for reading (pagination)
+             max-count   ; max row count to fetch (pagination)
+             ]
+      :as opt}]
+  {:post [(mu/verify-cond (fn? %))]
+   :pre [(mu/verify-opt #{:datasource  :jpi-reader :reader-fn
+                           :fetch-size :max-rows   :query-timeout
+                           :verify-cursor-position :save-state
+                           :start-pos  :max-count} opt)
+         (mu/verify-arg (select-criteria?   criteria))
+         (mu/verify-arg (string? (:select   criteria)))
+         (mu/verify-arg (string? (:from     criteria)))
+         (mu/verify-arg (string? (:sort-key criteria)))
+         ;; non-criteria args
+         (mu/verify-arg (nil-or? #(instance? DataSource %) datasource))
+         (mu/verify-arg (nil-or? fn?         reader-fn))
+         (mu/verify-arg (nil-or? number?     fetch-size))
+         (mu/verify-arg (nil-or? mu/boolean? save-state))
+         (mu/verify-arg (nil-or? integer?    start-pos))
+         (mu/verify-arg (nil-or? integer?    max-count))]}
+  (let [ds (or datasource (:datasource sp/*dbspec*))
+        ^JdbcPagingItemReader
+        ir (or jpi-reader (JdbcPagingItemReader.))
+        qf (SqlPagingQueryProviderFactoryBean.)]
+    (doto qf
+      (.setDataSource   ds)
+      (.setSelectClause (:select   criteria))
+      (.setFromClause   (:from     criteria))
+      (.setSortKey      (:sort-key criteria)))
+    (let [wh (:where    criteria)] (when wh (.setWhereClause qf wh)))
+    (let [sa (:sort-asc criteria)] (when (mu/not-nil? sa)
+                                     (.setAscending qf sa)))
+    (doto ir
+      (.setDataSource ds)
+      (.setRowMapper (ColumnMapRowMapper.))
+      (.setQueryProvider (.getObject qf)))
+    ;; query parameters
+    (when-let [args (:args criteria)]
+      (let [arg-map (if (map? args)
+                      (mu/map-keys (:clj-to-db sp/*dbspec*) args)
+                      (zipmap (map str (range 1 (inc (count args)))) args))]
+        (.setParameterValues ir arg-map)))
+    ; opt params
+    (let []
+      (when (mu/not-nil? fetch-size) (.setFetchSize ir fetch-size))
+      (when (mu/not-nil? save-state) (.setSaveState ir save-state))
+      (when (mu/not-nil? start-pos)  (.setCurrentItemCount ir start-pos))
+      (when (mu/not-nil? max-count)  (.setMaxItemCount     ir max-count)))
+    ;; open reader
+    (.afterPropertiesSet ir)
+    (.open ir (ExecutionContext.))
+    (jd/show-sql criteria)
+    (partial
+      (or reader-fn (fn [^ItemReader r] (.read r)))
+      ir)))

src/main/clj/org/bituf/fountain/jdbc.clj

   (:import
     (java.util List Map)
     (javax.sql DataSource)
+    (org.springframework.jdbc.core        JdbcTemplate)
     (org.springframework.jdbc.core.simple SimpleJdbcTemplate SimpleJdbcInsert
                                           SimpleJdbcCall)
     (org.springframework.jdbc.support     KeyHolder))
   See also:
     with-context
     clj-dbspec/*dbspec*"
-  [& {:keys [^DataSource datasource]
+  [& {:keys [^DataSource datasource fetch-size query-timeout]
       :or   {datasource  nil}
       :as opt}]
   {:post [(mu/verify-cond (map? %))]
-   :pre  [(mu/verify-opt #{:datasource} opt)]}
+   :pre  [(mu/verify-opt #{:datasource :fetch-size :query-timeout} opt)]}
   (let [ds  (or datasource (:datasource sp/*dbspec*)
               (mu/illegal-arg "No valid DataSource found/supplied"))
-        sjt ^SimpleJdbcTemplate (SimpleJdbcTemplate. ^DataSource ds)]
+        fs  (if (contains? opt :fetch-size)    fetch-size    (:fetch-size    sp/*dbspec*))
+        qt  (if (contains? opt :query-timeout) query-timeout (:query-timeout sp/*dbspec*))
+        jt  (JdbcTemplate. ^DataSource ds)
+        _   (when fs (.setFetchSize    jt fs))
+        _   (when qt (.setQueryTimeout jt qt))
+        sjt ^SimpleJdbcTemplate (SimpleJdbcTemplate. ^JdbcTemplate jt)]
      {:fountain.jdbc.sjt sjt}))
 
 
     (query-for-long sql {})))
 
 
-(defn query-for-map
+(defn query-for-row
   "Execute query and return a row (expressed as a map)."
   ([^String sql args]
     (with-query-args [qargs args]
           (show-sql sql)
           (.queryForMap (get-sjt) sql qargs)))))
   ([sql]
-    (query-for-map sql {})))
+    (query-for-row sql {})))
 
 
 (defn query-for-list
              gencols  []
              catalog  (:catalog sp/*dbspec*)
              schema   (:schema  sp/*dbspec*)
-             use-meta true}}]
+             use-meta true}
+      :as opt}]
+  {:post [(mu/verify-cond (instance? SimpleJdbcInsert %))]
+   :pre  [(mu/verify-opt #{:datasource :gencols :catalog :schema
+                           :use-meta} opt)]}
   (let [v-gencols (mu/as-vector gencols)]
-    (-> (SimpleJdbcInsert. datasource)
+    (-> (SimpleJdbcInsert. ^DataSource datasource)
       (.withTableName (sp/db-iden table-name))
       (#(if (mu/not-empty? v-gencols)
           (.usingGeneratedKeyColumns ^SimpleJdbcInsert %

src/test/clj/org/bituf/fountain/test_batch.clj

+(ns org.bituf.fountain.test-batch
+  (:require
+    [clojure.java.io          :as io]
+    [clojure.pprint           :as pp]
+    ;[org.bituf.fountain.jdbc :as jd]
+    [org.bituf.fountain.batch :as ba]
+    [org.bituf.clj-dbcp       :as cp]
+    [org.bituf.clj-miscutil   :as mu]
+    [org.bituf.clj-dbspec     :as sp]
+    [org.bituf.fountain.test-util :as tu])
+  (:import
+    (org.springframework.batch.item.database JdbcCursorItemReader
+                                             JdbcPagingItemReader))
+  (:use clojure.test))
+
+
+(def ds (cp/h2-memory-datasource))
+
+
+(def dbspec (sp/make-dbspec ds))
+
+
+(defn do-with-dbspec
+  [f] {:post [(mu/not-fn? %)]
+       :pre  [(fn? f)]}
+  (let [g (sp/wrap-dbspec dbspec f)]
+    (g)))
+
+
+;(defn do-with-sjt
+;  "Execute f in the context of datasource and SimpleJdbcTemplate"
+;  [f] {:post [(mu/not-fn? %)]
+;       :pre  [(fn? f)]}
+;  (let [g (sp/wrap-dbspec dbspec
+;            (jd/wrap-sjt f))]
+;    (g)))
+
+
+(defn jdbc-cursor-reader-default-config-test
+  [sql & args]
+  (tu/sample-ba1-setup)
+  (is (= (into []
+               (ba/row-seq (ba/jdbc-cursor-reader (into [sql] args))))
+         [{:age 30}])))
+
+
+(deftest test-cursor-reader-works-with-default-config
+  (testing "With no params"
+           (do-with-dbspec
+             (partial jdbc-cursor-reader-default-config-test
+                      "SELECT age FROM sample WHERE age = 30")))
+  (testing "With positional params"
+           (do-with-dbspec
+             (partial jdbc-cursor-reader-default-config-test
+                      "SELECT age FROM sample WHERE age = ?" 30)))
+  (testing "With named params"
+           (do-with-dbspec
+             (partial jdbc-cursor-reader-default-config-test
+                      "SELECT age FROM sample WHERE age = :age" {:age 30}))))
+
+
+(defn jdbc-cursor-reader-custom-config-test
+  [sql & args]
+  (tu/sample-ba1-setup)
+  (with-open [r (JdbcCursorItemReader.)]
+    (is (= (into []
+                 (ba/row-seq (ba/jdbc-cursor-reader (into [sql] args)
+                                                    :jci-reader r
+                                                    :fetch-size 10)))
+           [{:age 30}]))))
+
+
+(deftest test-cursor-reader-works-with-custom-config
+  (testing "Custom config"
+           (do-with-dbspec
+             (partial jdbc-cursor-reader-custom-config-test
+                      "SELECT age FROM sample WHERE age = :age" {:age 30}))))
+
+
+(defn jdbc-paging-reader-helper
+  [criteria & args]
+  (tu/sample-ba1-setup)
+  (into []
+        (ba/row-seq (apply ba/jdbc-paging-reader criteria args))))
+
+
+(deftest test-paging-reader-works-with-default-config
+  (testing "With no params"
+           (is (= (do-with-dbspec
+                    (partial jdbc-paging-reader-helper
+                             (ba/make-criteia :select   "SELECT age"     :from     "FROM sample"
+                                              :where    "WHERE age = 30" :sort-key "age"
+                                              ;:sort-asc ;:args
+                                              )))
+                  [{:age 30}])))
+  (testing "With positional params"
+           (is (= (do-with-dbspec
+                    (partial jdbc-paging-reader-helper
+                             (ba/make-criteia :select   "SELECT age"    :from     "FROM sample"
+                                              :where    "WHERE age = ?" :sort-key "age"
+                                              ;:sort-asc
+                                              :args [30])))
+                  [{:age 30}])))
+  (testing "With named params"
+           (is (= (do-with-dbspec
+                    (partial jdbc-paging-reader-helper
+                             (ba/make-criteia :select   "SELECT age"       :from     "FROM sample"
+                                              :where    "WHERE age = :age" :sort-key "age"
+                                              ;:sort-asc
+                                              :args {:age 30})))
+                  [{:age 30}])))
+  (testing "With sort-asc false (i.e. Descending order)"
+           (is (= (do-with-dbspec
+                    (partial jdbc-paging-reader-helper
+                             (ba/make-criteia :select   "SELECT age" :from     "FROM sample"
+                                              :sort-key "age"        :sort-asc false)))
+                  [{:age 40} {:age 30}]))))
+
+
+(deftest test-paging-reader-works-with-custom-config
+  (testing "With custom config"
+           (is (= (do-with-dbspec
+                    (partial jdbc-paging-reader-helper
+                             (ba/make-criteia :select   "SELECT age" :from     "FROM sample"
+                                              :sort-key "age"        :sort-asc false)
+                             :fetch-size 100
+                             :save-state false
+                             :start-pos  1
+                             :max-count  100))
+                  [{:age 40} {:age 30}]))))
+
+
+(deftest test-paging-reader-can-paginate
+  (testing "With pagination"
+           (do-with-dbspec
+             #(let [_ (tu/sample-ba1-setup)
+                    jpi-reader (JdbcPagingItemReader.)
+                    reader-fn  (ba/jdbc-paging-reader
+                                 (ba/make-criteia :select   "SELECT age" :from     "FROM sample"
+                                                  :sort-key "age"        :sort-asc false)
+                                 :jpi-reader jpi-reader)]
+                (ba/set-page! jpi-reader 1 1)
+                (is (= (into [] (ba/row-seq reader-fn)) [{:age 40}]))
+                (ba/set-page! jpi-reader 1 2)
+                (is (= (into [] (ba/row-seq reader-fn)) [{:age 30}]))))))
+
+
+(defn test-ns-hook []
+  (test-cursor-reader-works-with-default-config)
+  (test-cursor-reader-works-with-custom-config)
+  (test-paging-reader-works-with-default-config)
+  (test-paging-reader-works-with-custom-config)
+  (test-paging-reader-can-paginate))

src/test/clj/org/bituf/fountain/test_jdbc.clj

       query-for-long-test)))
 
 
-(defn query-for-map-test
+(defn query-for-row-test
   []
   (tu/sample-setup)
   (is (= {:age 30}
-        (jd/query-for-map "SELECT age FROM sample WHERE age = 30")))
+        (jd/query-for-row "SELECT age FROM sample WHERE age = 30")))
   (is (= {:age 30}
-        (jd/query-for-map "SELECT age FROM sample WHERE age = ?" [30])))
+        (jd/query-for-row "SELECT age FROM sample WHERE age = ?" [30])))
   (is (= {:age 30}
-        (jd/query-for-map "SELECT age FROM sample WHERE age = :age"
+        (jd/query-for-row "SELECT age FROM sample WHERE age = :age"
           {:age 30}))))
 
-(deftest test-query-for-map
-  (testing "test-query-for-map"
+(deftest test-query-for-row
+  (testing "test-query-for-row"
     (do-with-sjt
-      query-for-map-test)))
+      query-for-row-test)))
 
 
 (defn query-for-list-test
 (defn test-ns-hook []
   (test-query-for-int)
   (test-query-for-long)
-  (test-query-for-map)
+  (test-query-for-row)
   (test-query-for-list)
   (test-update)
   (test-batch-update)

src/test/clj/org/bituf/fountain/test_util.clj

   (row-count :sample2))
 
 
+(defn sample-ba1-setup
+  []
+  (let [sql-coll (with-liquibase
+                   (lb/change-sql (ch/create-table-withid :sample
+                                    [[:name [:varchar 30] :null false]
+                                     [:age  :int]])))]
+    (with-stmt ^Statement st
+      (mu/maybe (.execute st "DROP TABLE sample"))
+      (doseq [each sql-coll]
+        (println "\n" each "\n")
+        (.execute st ^String each))
+      (.execute st "INSERT INTO sample (name, age)
+                    VALUES ('Harry', 30)")
+      (.execute st "INSERT INTO sample (name, age)
+                    VALUES ('Abdul', 40)")
+      )))
+
 (defn fail
   ([msg] (is false msg))
   ([] (is false)))
-
-

src/test/script/runtests.clj

 (ns runtests
   (:require
-    [org.bituf.fountain.test-jdbc :as jd]
-    [org.bituf.fountain.test-transaction :as tx])
+    [org.bituf.fountain.test-jdbc        :as jd]
+    [org.bituf.fountain.test-transaction :as tx]
+    [org.bituf.fountain.test-batch       :as ba])
   (:use clojure.test))
 
 
 
 (run-tests
   'org.bituf.fountain.test-jdbc
-  'org.bituf.fountain.test-transaction)
+  'org.bituf.fountain.test-transaction
+  'org.bituf.fountain.test-batch)