Also adds `assert`s in SQL-generating functions, instead of just 
producing illegal SQL.
This commit is contained in:
Sean Corfield 2019-07-11 12:11:32 -07:00
parent 2131136c0d
commit 821f9b1a5a
4 changed files with 40 additions and 13 deletions

View file

@ -7,9 +7,12 @@ Only accretive/fixative changes will be made from now on.
The following changes have been committed to the **master** branch since the 1.0.1 release:
* Fix #45 by adding TimesTen driver support.
* Fix #44 so that `insert-multi!` with an empty `rows` vector returns `[]`.
* Fix #42 by adding specs for `execute-batch!` and `set-parameters` in `next.jdbc.prepare`.
* Fix #41 by improving docstrings and documentation, especially around prepared statement handling.
* Fix #40 by adding `next.jdbc.prepare/execute-batch!`.
* Added `assert`s in `next.jdbc.sql` as more informative errors for cases that would generate SQL exceptions (from malformed SQL).
* Added spec for `:order-by` to reflect what is actually permitted.
* Expose `next.jdbc.connect/dbtypes` as a table of known database types and aliases, along with their class name(s), port, and other JDBC string components.
## Stable Builds

View file

@ -47,7 +47,13 @@
(s/def ::connectable any?)
(s/def ::key-map (s/map-of keyword? any?))
(s/def ::example-map (s/map-of keyword? any? :min-count 1))
(s/def ::opts-map (s/map-of keyword? any?))
(s/def ::order-by-col (s/or :col keyword?
:dir (s/cat :col keyword?
:dir #{:asc :desc})))
(s/def ::order-by (s/coll-of ::order-by-col :kind vector? :min-count 1))
(s/def ::opts-map (s/and (s/map-of keyword? any?)
(s/keys :opt-un [::order-by])))
(s/def ::transactable any?)

View file

@ -38,6 +38,7 @@
[(conj conds (str e " = ?")) (conj params v)])))
[[] []]
key-map)]
(assert (seq where) "key-map may not be empty")
(into [(str (str/upper-case (name clause)) " "
(str/join (if (= :where clause) " AND " ", ") where))]
params)))
@ -79,10 +80,11 @@
(defn- for-order
"Given an `:order-by` vector, return an `ORDER BY` clause."
[order-by opts]
(if (vector? order-by)
(str "ORDER BY "
(str/join ", " (map #(for-order-col % opts) order-by)))
(throw (IllegalArgumentException. ":order-by must be a vector"))))
(when-not (vector? order-by)
(throw (IllegalArgumentException. ":order-by must be a vector")))
(assert (seq order-by) ":order-by may not be empty")
(str "ORDER BY "
(str/join ", " (map #(for-order-col % opts) order-by))))
(defn- for-query
"Given a table name and either a hash map of column names and values or a
@ -147,6 +149,7 @@
(let [entity-fn (:table-fn opts identity)
params (as-keys key-map opts)
places (as-? key-map opts)]
(assert (seq key-map) "key-map may not be empty")
(into [(str "INSERT INTO " (entity-fn (name table))
" (" params ")"
" VALUES (" places ")")]
@ -159,7 +162,10 @@
Applies any `:table-fn` / `:column-fn` supplied in the options."
[table cols rows opts]
(assert (apply = (count cols) (map count rows)))
(assert (apply = (count cols) (map count rows))
"column counts are not consistent across cols and rows")
;; to avoid generating bad SQL
(assert (seq rows) "rows may not be empty")
(let [table-fn (:table-fn opts identity)
column-fn (:column-fn opts identity)
params (str/join ", " (map (comp column-fn name) cols))
@ -199,9 +205,11 @@
([connectable table cols rows]
(insert-multi! connectable table cols rows {}))
([connectable table cols rows opts]
(execute! connectable
(for-insert-multi table cols rows opts)
(merge {:return-keys true} opts))))
(if (seq rows)
(execute! connectable
(for-insert-multi table cols rows opts)
(merge {:return-keys true} opts))
[])))
(defn query
"Syntactic sugar over `execute!` to provide a query alias.

View file

@ -55,9 +55,9 @@
["DELETE FROM [user] WHERE id = ? and opt is null" 9]))))
(deftest test-for-update
(testing "empty example (SQL error)"
(is (= (#'sql/for-update :user {:status 42} {} {:table-fn sql-server :column-fn mysql})
["UPDATE [user] SET `status` = ? WHERE " 42])))
(testing "empty example (would be a SQL error)"
(is (thrown? AssertionError ; changed in #44
(#'sql/for-update :user {:status 42} {} {:table-fn sql-server :column-fn mysql}))))
(testing "by example"
(is (= (#'sql/for-update :user {:status 42} {:id 9} {:table-fn sql-server :column-fn mysql})
["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9])))
@ -154,7 +154,11 @@
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit ["id > ?" 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))))
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "empty insert-multi!" ; per #44
(is (= [] (sql/insert-multi! (ds) :fruit
[:name :appearance :cost :grade]
[])))))
(deftest no-empty-example-maps
(is (thrown? clojure.lang.ExceptionInfo
@ -163,3 +167,9 @@
(sql/update! (ds) :fruit {} {})))
(is (thrown? clojure.lang.ExceptionInfo
(sql/delete! (ds) :fruit {}))))
(deftest no-empty-order-by
(is (thrown? clojure.lang.ExceptionInfo
(sql/find-by-keys (ds) :fruit
{:name "Apple"}
{:order-by []}))))