diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index ea4fd40..b397024 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -315,17 +315,29 @@ (defn- format-values [k xs] (cond (sequential? (first xs)) ;; [[1 2 3] [4 5 6]] - (let [[sqls params] + (let [n-1 (map count xs) + ;; issue #291: ensure all value sequences are the same length + xs' (if (apply = n-1) + xs + (let [n-n (apply max n-1)] + (map (fn [x] (take n-n (concat x (repeat nil)))) xs))) + [sqls params] (reduce (fn [[sql params] [sqls' params']] [(conj sql (str "(" (str/join ", " sqls') ")")) (into params params')]) [[] []] - (map #'format-expr-list xs))] + (map #'format-expr-list xs'))] (into [(str (sql-kw k) " " (str/join ", " sqls))] params)) (map? (first xs)) ;; [{:a 1 :b 2 :c 3}] - (let [cols (keys (first xs)) + (let [cols-1 (keys (first xs)) + ;; issue #291: check for all keys in all maps but still + ;; use the keys from the first map if they match so that + ;; users can rely on the key ordering if they want to, + ;; e.g., see test that uses array-map for the first row + cols-n (into #{} (mapcat keys) xs) + cols (if (= (set cols-1) cols-n) cols-1 cols-n) [sqls params] (reduce (fn [[sql params] [sqls' params']] [(conj sql (str "(" (str/join ", " sqls') ")")) @@ -347,6 +359,10 @@ (throw (ex-info ":values expects sequences or maps" {:first (first xs)})))) +(comment + (into #{} (mapcat keys) [{:a 1 :b 2} {:b 3 :c 4}]) + ,) + (defn- format-set-exprs [k xs] (let [[sqls params] (reduce-kv (fn [[sql params] v e] diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 9af6179..8655bbd 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -102,14 +102,14 @@ ["WITH query AS (SELECT foo FROM bar)"])) (is (= (format {:with-recursive [[:query {:select [:foo] :from [:bar]}]]}) ["WITH RECURSIVE query AS (SELECT foo FROM bar)"])) - (is (= (format {:with [[[:static {:columns [:a :b :c]}] {:values [[1 2 3] [4 5 6]]}]]}) - ["WITH static (a, b, c) AS (VALUES (?, ?, ?), (?, ?, ?))" 1 2 3 4 5 6])) + (is (= (format {:with [[[:static {:columns [:a :b :c]}] {:values [[1 2 3] [4 5]]}]]}) + ["WITH static (a, b, c) AS (VALUES (?, ?, ?), (?, ?, NULL))" 1 2 3 4 5])) (is (= (format {:with [[[:static {:columns [:a :b :c]}] - {:values [[1 2 3] [4 5 6]]}]] + {:values [[1 2] [4 5 6]]}]] :select [:*] :from [:static]}) - ["WITH static (a, b, c) AS (VALUES (?, ?, ?), (?, ?, ?)) SELECT * FROM static" 1 2 3 4 5 6]))) + ["WITH static (a, b, c) AS (VALUES (?, ?, NULL), (?, ?, ?)) SELECT * FROM static" 1 2 4 5 6]))) (deftest insert-into (is (= (format {:insert-into :foo}) @@ -134,6 +134,15 @@ {:namespace-as-table? true}) ["INSERT INTO foo (id) VALUES (?)" 2]))) +(deftest insert-into-uneven-maps + ;; we can't rely on ordering when the set of keys differs between maps: + (let [res (format {:insert-into :foo :values [{:id 1} {:id 2, :bar "quux"}]})] + (is (or (= res ["INSERT INTO foo (id, bar) VALUES (?, NULL), (?, ?)" 1 2 "quux"]) + (= res ["INSERT INTO foo (bar, id) VALUES (NULL, ?), (?, ?)" 1 "quux" 2])))) + (let [res (format {:insert-into :foo :values [{:id 1, :bar "quux"} {:id 2}]})] + (is (or (= res ["INSERT INTO foo (id, bar) VALUES (?, ?), (?, NULL)" 1 "quux" 2]) + (= res ["INSERT INTO foo (bar, id) VALUES (?, ?), (NULL, ?)" "quux" 1 2]))))) + (deftest exists-test ;; EXISTS should never have been implemented as SQL syntax: it's an operator! #_(is (= (format {:exists {:select [:a] :from [:foo]}})