diff --git a/CHANGELOG.md b/CHANGELOG.md index cfdf8bf..53b9a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/next/jdbc/specs.clj b/src/next/jdbc/specs.clj index 63cc8bc..eb73599 100644 --- a/src/next/jdbc/specs.clj +++ b/src/next/jdbc/specs.clj @@ -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?) diff --git a/src/next/jdbc/sql.clj b/src/next/jdbc/sql.clj index f976774..0f1f7dc 100644 --- a/src/next/jdbc/sql.clj +++ b/src/next/jdbc/sql.clj @@ -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. diff --git a/test/next/jdbc/sql_test.clj b/test/next/jdbc/sql_test.clj index 31d8f99..6f25152 100644 --- a/test/next/jdbc/sql_test.clj +++ b/test/next/jdbc/sql_test.clj @@ -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 []}))))