diff --git a/README.md b/README.md index 9c9d347..822f773 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ in the REPL: ;=> ------------------------- ;=> user/users-by-country -;=> ([{:keys [country_code]}] +;=> ([{:keys [country_code]}] ;=> [{:keys [country_code]} {:keys [connection]}]) ;=> ;=> Counts the users in a given country. @@ -351,6 +351,13 @@ INSERT INTO person (name) VALUES (:name) ;=> {:name "Dave" :id 5} ``` +To batch insert, Yesql will take a vector of maps. + +```clojure +(create-person '({:name "Dave" :id 5} {:name "Jill" :id 6}) +``` + The exact return value will depend on your database driver. For example PostgreSQL returns the whole row, whereas Derby returns just `{:1 5M}`. diff --git a/project.clj b/project.clj index e9cb7cb..1dc4a42 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject yesql "0.5.2" +(defproject yesql "0.6.0" :description "A Clojure library for using SQL" :url "https://github.com/krisajenkins/yesql" :license {:name "Eclipse Public License" diff --git a/src/yesql/generate.clj b/src/yesql/generate.clj index 9ff489a..341b30b 100644 --- a/src/yesql/generate.clj +++ b/src/yesql/generate.clj @@ -4,7 +4,7 @@ [clojure.string :refer [join lower-case]] [yesql.util :refer [create-root-var]] [yesql.types :refer [map->Query]] - [yesql.statement-parser :refer [tokenize]]) + [yesql.statement-parser :refer [tokenize insert-table-name-regex]]) (:import [yesql.types Query])) (def in-list-parameter? @@ -32,10 +32,11 @@ expected-keys (conj expected-keys :?)))) -(defn rewrite-query-for-jdbc - [tokens initial-args] +(defn sane-query? [tokens initial-args] (let [{:keys [expected-keys expected-positional-count]} (analyse-statement-tokens tokens) - actual-keys (set (keys (dissoc initial-args :?))) + actual-keys (if (vector? initial-args) + (-> (mapcat keys initial-args) set (disj :?)) + (-> (keys initial-args) set (disj :?))) actual-positional-count (count (:? initial-args)) missing-keys (set/difference expected-keys actual-keys)] (assert (empty? missing-keys) @@ -48,45 +49,55 @@ ["Query argument mismatch." "Expected %d positional parameters. Got %d." "Supply positional parameters as {:? [...]}"]) - expected-positional-count actual-positional-count)) - (let [[final-query final-parameters consumed-args] - (reduce (fn [[query parameters args] token] - (cond - (string? token) [(str query token) - parameters - args] - (symbol? token) (let [[arg new-args] (if (= '? token) - [(first (:? args)) (update-in args [:?] rest)] - [(get args (keyword token)) args])] - [(str query (args-to-placeholders arg)) - (vec (if (in-list-parameter? arg) - (concat parameters arg) - (conj parameters arg))) - new-args]))) - ["" [] initial-args] - tokens)] - (concat [final-query] final-parameters)))) + expected-positional-count actual-positional-count)))) + +(defn rewrite-query-for-jdbc + [tokens initial-args] + (sane-query? tokens initial-args) + (let [[final-query final-parameters consumed-args] + (reduce (fn [[query parameters args] token] + (cond + (string? token) [(str query token) + parameters + args] + (symbol? token) (let [[arg new-args] (if (= '? token) + [(first (:? args)) (update-in args [:?] rest)] + [(get args (keyword token)) args])] + [(str query (args-to-placeholders arg)) + (vec (if (in-list-parameter? arg) + (concat parameters arg) + (conj parameters arg))) + new-args]))) + ["" [] initial-args] + tokens)] + (concat [final-query] final-parameters))) ;; Maintainer's note: clojure.java.jdbc.execute! returns a list of ;; rowcounts, because it takes a list of parameter groups. In our ;; case, we only ever use one group, so we'll unpack the ;; single-element list with `first`. (defn execute-handler - [db sql-and-params call-options] - (first (jdbc/execute! db sql-and-params))) + [db sql params call-options] + (first (jdbc/execute! db (rewrite-query-for-jdbc sql params)))) (defn insert-handler - [db [statement & params] call-options] - (jdbc/db-do-prepared-return-keys db statement params)) + [db sql params call-options] + (if (vector? params) + (let [full-query (apply str sql) + table-name (re-find insert-table-name-regex full-query)] + (sane-query? sql params) + (apply jdbc/insert! db table-name params)) + (let [[rewritten-sql & rewritten-params] (rewrite-query-for-jdbc sql params)] + (jdbc/db-do-prepared-return-keys db rewritten-sql rewritten-params)))) (defn query-handler - [db sql-and-params + [db sql params {:keys [row-fn result-set-fn identifiers] :or {identifiers lower-case row-fn identity result-set-fn doall} :as call-options}] - (jdbc/query db sql-and-params + (jdbc/query db (rewrite-query-for-jdbc sql params) :identifiers identifiers :row-fn row-fn :result-set-fn result-set-fn)) @@ -118,7 +129,8 @@ "Check the docs, and supply {:connection ...} as an option to the function call, or globally to the defquery declaration."]) name)) (jdbc-fn connection - (rewrite-query-for-jdbc tokens args) + tokens + args call-options))) [display-args generated-function] (let [named-args (if-let [as-vec (seq (mapv (comp symbol clojure.core/name) required-args))] diff --git a/src/yesql/statement_parser.clj b/src/yesql/statement_parser.clj index 7d8b798..91762a7 100644 --- a/src/yesql/statement_parser.clj +++ b/src/yesql/statement_parser.clj @@ -6,6 +6,8 @@ [yesql.instaparse-util :refer [process-instaparse-result]]) (:import [yesql.types Query])) +(def insert-table-name-regex #"(?i)(?<=INSERT INTO\s).*?(?=\sVALUES|\s\()") + (def parser (instaparse/parser (io/resource "yesql/statement.bnf"))) diff --git a/test/yesql/acceptance_test.clj b/test/yesql/acceptance_test.clj index 069f9ae..5e8f0bf 100644 --- a/test/yesql/acceptance_test.clj +++ b/test/yesql/acceptance_test.clj @@ -26,16 +26,14 @@ (expect (create-person-table!)) ;; Insert -> Select. -(expect {:1 1M} (insert-person Select. (expect 1 (delete-person! {:name "Alice"})) -(expect 2 (count (find-older-than {:age 10}))) -(expect 1 (count (find-older-than {:age 30}))) -(expect 0 (count (find-older-than {:age 50}))) +(expect 4 (count (find-older-than {:age 10}))) +(expect 3 (count (find-older-than {:age 30}))) +(expect 1 (count (find-older-than {:age 50}))) ;; Failing transaction: Insert with abort. ;; Insert two rows in a transaction. The second throws a deliberate error, meaning no new rows created. -(expect 2 (count (find-older-than {:age 10}))) +(expect 4 (count (find-older-than {:age 10}))) (expect SQLException (jdbc/with-db-transaction [connection derby-db] @@ -68,10 +66,9 @@ {:connection connection}) (insert-person ["SELECT * FROM users WHERE group_ids IN(?,?) AND parent_id = ?" 1 2 3]) + => ["SELECT * FROM users WHERE group_ids IN(?,?) AND parent_id = ?" 1 2 3]) ;;; Incorrect parameters. (expect AssertionError - (rewrite-query-for-jdbc (tokenize "SELECT age FROM users WHERE country = :country AND name = :name") - {:country "gb"})) + (sane-query? + (tokenize "SELECT age FROM users WHERE country = :country AND name = :name") + {:country "gb"})) (expect AssertionError - (rewrite-query-for-jdbc (tokenize "SELECT age FROM users WHERE country = ? AND name = ?") - {})) + (sane-query? + (tokenize "SELECT age FROM users WHERE country = ? AND name = ?") + {})) + +(expect AssertionError + (sane-query? + (tokenize "INSERT INTO users (country, name) VALUES (:country, :name)") + [{:country "gb"} {:country "us"}])) diff --git a/test/yesql/statement_parser_test.clj b/test/yesql/statement_parser_test.clj index 7cd62ff..f793d36 100644 --- a/test/yesql/statement_parser_test.clj +++ b/test/yesql/statement_parser_test.clj @@ -4,6 +4,16 @@ [yesql.types :refer [map->Query]] [yesql.statement-parser :refer :all])) +(do-template [statement _ table-name-result] + (do (expect (quote table-name-result) + (re-find insert-table-name-regex statement))) + + "INSERT INTO table (:column1, :column2) VALUES (a, b)" => "table" + "insert into table (:column1, :column2) VALUES (a, b)" => "table" + "INSERT INTO table VALUES (a, b)" => "table" + "insert into table VALUES (a, b)" => "table" + "insert into table values (a, b)" => "table") + (do-template [statement _ split-result] (do (expect (quote split-result) (tokenize statement))