From 0531ae02687dc41b1d73bbd7650c73b6b5638a3f Mon Sep 17 00:00:00 2001 From: Ryan Schmukler Date: Fri, 20 May 2022 12:40:32 -0500 Subject: [PATCH 1/2] feat: insert hashmaps with `sql.insert-multi!` Updates the API of `sql/insert-multi!` to support sequences of hash maps which will automatically be converted into rows and columns. --- src/next/jdbc/specs.clj | 30 ++++++++++++++++++++---------- src/next/jdbc/sql.clj | 18 ++++++++++++++++-- test/next/jdbc/sql_test.clj | 30 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/next/jdbc/specs.clj b/src/next/jdbc/specs.clj index de9fd19..beea4c7 100644 --- a/src/next/jdbc/specs.clj +++ b/src/next/jdbc/specs.clj @@ -180,16 +180,26 @@ :opts (s/? ::opts-map))) (s/fdef sql/insert-multi! - :args (s/and (s/cat :connectable ::connectable - :table keyword? - :cols (s/coll-of keyword? - :kind sequential? - :min-count 1) - :rows (s/coll-of (s/coll-of any? :kind sequential?) - :kind sequential?) - :opts (s/? ::opts-map)) - #(apply = (count (:cols %)) - (map count (:rows %))))) + :args + (s/or + :with-rows-and-columns + (s/and (s/cat :connectable ::connectable + :table keyword? + :cols (s/coll-of keyword? + :kind sequential? + :min-count 1) + :rows (s/coll-of (s/coll-of any? :kind sequential?) + :kind sequential?) + :opts (s/? ::opts-map)) + #(apply = (count (:cols %)) + (map count (:rows %)))) + :with-hash-maps + (s/cat :connectable ::connectable + :table keyword? + :hash-maps (s/coll-of map? + :kind sequential? + :min-count 1) + :opts (s/? ::opts-map)))) (s/fdef sql/query :args (s/cat :connectable ::connectable diff --git a/src/next/jdbc/sql.clj b/src/next/jdbc/sql.clj index 4867b26..0c19ab1 100644 --- a/src/next/jdbc/sql.clj +++ b/src/next/jdbc/sql.clj @@ -50,12 +50,26 @@ multiple rows in the database and attempts to return a vector of maps of generated keys. + Also supports a sequence of hash maps with keys corresponding to column + names. + Note: this expands to a single SQL statement with placeholders for every value being inserted -- for large sets of rows, this may exceed the limits on SQL string size and/or number of parameters for your JDBC driver or your database!" - ([connectable table cols rows] - (insert-multi! connectable table cols rows {})) + {:arglists '([connectable table hash-maps] + [connectable table hash-maps opts] + [connectable table cols rows] + [connectable table cols rows opts])} + ([connectable table hash-maps] + (insert-multi! connectable table hash-maps {})) + ([connectable table hash-maps-or-cols opts-or-rows] + (if-not (-> hash-maps-or-cols first map?) + (insert-multi! connectable table hash-maps-or-cols opts-or-rows {}) + (let [cols (keys (first hash-maps-or-cols)) + ->row (fn ->row [m] + (map (partial get m) cols))] + (insert-multi! connectable table cols (map ->row hash-maps-or-cols) opts-or-rows)))) ([connectable table cols rows opts] (if (seq rows) (let [opts (merge (:options connectable) opts)] diff --git a/test/next/jdbc/sql_test.clj b/test/next/jdbc/sql_test.clj index 117ad6b..c2859a5 100644 --- a/test/next/jdbc/sql_test.clj +++ b/test/next/jdbc/sql_test.clj @@ -152,6 +152,36 @@ (is (= {:next.jdbc/update-count 2} (sql/delete! (ds) :fruit ["id > ?" 4]))) (is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) + (testing "multiple insert/delete with maps" + (is (= (cond (derby?) + [nil] ; WTF Apache Derby? + (mssql?) + [14M] + (sqlite?) + [14] + :else + [12 13 14]) + (mapv new-key + (sql/insert-multi! (ds) :fruit + [{:name "Kiwi" + :appearance "green & fuzzy" + :cost 100 + :grade 99.9} + {:name "Grape" + :appearance "black" + :cost 10 + :grade 50} + {:name "Lemon" + :appearance "yellow" + :cost 20 + :grade 9.9}])))) + (is (= 7 (count (sql/query (ds) ["select * from fruit"])))) + (is (= {:next.jdbc/update-count 1} + (sql/delete! (ds) :fruit {:id 12}))) + (is (= 6 (count (sql/query (ds) ["select * from fruit"])))) + (is (= {:next.jdbc/update-count 2} + (sql/delete! (ds) :fruit ["id > ?" 10]))) + (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] From 003d47ea5e0365d80b7535c5d0df34c589fce145 Mon Sep 17 00:00:00 2001 From: Ryan Schmukler Date: Fri, 20 May 2022 12:53:23 -0500 Subject: [PATCH 2/2] feat: insert-multi! :batch option Adds support for using a `:batch` option to make `insert-multi!` use `execute-batch!` instead of `execute!`. --- src/next/jdbc/sql.clj | 23 ++++++++++++++++------- src/next/jdbc/sql/builder.clj | 9 +++++++-- test/next/jdbc/sql/builder_test.clj | 12 ++++++++++-- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/next/jdbc/sql.clj b/src/next/jdbc/sql.clj index 0c19ab1..c207fc6 100644 --- a/src/next/jdbc/sql.clj +++ b/src/next/jdbc/sql.clj @@ -21,7 +21,7 @@ In addition, `find-by-keys` supports `:order-by` to add an `ORDER BY` clause to the generated SQL." - (:require [next.jdbc :refer [execute! execute-one!]] + (:require [next.jdbc :refer [execute! execute-one! execute-batch!]] [next.jdbc.sql.builder :refer [for-delete for-insert for-insert-multi for-query for-update]])) @@ -53,8 +53,12 @@ Also supports a sequence of hash maps with keys corresponding to column names. - Note: this expands to a single SQL statement with placeholders for every - value being inserted -- for large sets of rows, this may exceed the limits + If called with `:batch` true will call `execute-batch!` - see its documentation + for situations in which the generated keys may or may not be returned as well as + additional options that can be passed. + + Note: without `:batch` this expands to a single SQL statement with placeholders for + every value being inserted -- for large sets of rows, this may exceed the limits on SQL string size and/or number of parameters for your JDBC driver or your database!" {:arglists '([connectable table hash-maps] @@ -72,10 +76,15 @@ (insert-multi! connectable table cols (map ->row hash-maps-or-cols) opts-or-rows)))) ([connectable table cols rows opts] (if (seq rows) - (let [opts (merge (:options connectable) opts)] - (execute! connectable - (for-insert-multi table cols rows opts) - (merge {:return-keys true} opts))) + (let [opts (merge (:options connectable) opts) + batch? (:batch opts)] + (if batch? + (let [[sql & param-groups] (for-insert-multi table cols rows opts)] + (execute-batch! connectable sql param-groups + (merge {:return-keys true :return-generated-keys true} opts))) + (execute! connectable + (for-insert-multi table cols rows opts) + (merge {:return-keys true} opts)))) []))) (defn query diff --git a/src/next/jdbc/sql/builder.clj b/src/next/jdbc/sql/builder.clj index 5df8c4b..c4cd813 100644 --- a/src/next/jdbc/sql/builder.clj +++ b/src/next/jdbc/sql/builder.clj @@ -137,6 +137,10 @@ Applies any `:table-fn` / `:column-fn` supplied in the options. + If `:batch` is set to `true` in `opts` the INSERT statement will be prepared + using a single set of placeholders and remaining parameters in the vector will + be grouped at the row level. + If `:suffix` is provided in `opts`, that string is appended to the `INSERT ...` statement." [table cols rows opts] @@ -147,15 +151,16 @@ (assert (seq rows) "rows may not be empty") (let [table-fn (:table-fn opts identity) column-fn (:column-fn opts identity) + batch? (:batch opts) params (str/join ", " (map (comp column-fn name) cols)) places (as-? (first rows) opts)] (into [(str "INSERT INTO " (table-fn (safe-name table)) " (" params ")" " VALUES " - (str/join ", " (repeat (count rows) (str "(" places ")"))) + (str/join ", " (repeat (if batch? 1 (count rows)) (str "(" places ")"))) (when-let [suffix (:suffix opts)] (str " " suffix)))] - cat + (if batch? identity cat) rows))) (defn for-order-col diff --git a/test/next/jdbc/sql/builder_test.clj b/test/next/jdbc/sql/builder_test.clj index d1a0b55..65c2657 100644 --- a/test/next/jdbc/sql/builder_test.clj +++ b/test/next/jdbc/sql/builder_test.clj @@ -146,11 +146,19 @@ {:id 9 :status 42 :opt nil} {:table-fn sql-server :column-fn mysql}) ["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil]))) - (testing "multi-row insert" + (testing "multi-row insert (normal mode)" (is (= (builder/for-insert-multi :user [:id :status] [[42 "hello"] [35 "world"] [64 "dollars"]] {:table-fn sql-server :column-fn mysql}) - ["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"])))) + ["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"]))) + (testing "multi-row insert (batch mode)" + (is (= (builder/for-insert-multi :user + [:id :status] + [[42 "hello"] + [35 "world"] + [64 "dollars"]] + {:table-fn sql-server :column-fn mysql :batch true}) + ["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]]))))