diff --git a/README.md b/README.md index 9c9d347..e9126cf 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ check, because there may be a newer version available): |Oracle|`[com.oracle/ojdbc14 "10.2.0.4.0"]`| |SQLite|`[org.xerial/sqlite-jdbc "3.7.2"]`| |Derby|`[org.apache.derby/derby "10.11.1.1"]`| +|h2|`[com.h2database/h2 "1.4.191"]`| (Any database with a JDBC driver should work. If you know of a driver that's not listed here, please open a pull request to update this @@ -65,7 +66,7 @@ query: -- name: users-by-country SELECT * FROM users -WHERE country_code = :country +WHERE country_code = :country_code ``` ...and then read that file to turn it into a regular Clojure function: @@ -76,8 +77,8 @@ WHERE country_code = :country ;;; A function with the name `users-by-country` has been created. ;;; Let's use it: -(users-by-country {:country "GB"}) -;=> ({:name "Kris" :country "GB" ...} ...) +(users-by-country {:country_code "GB"}) +;=> ({:name "Kris" :country_code "GB" ...} ...) ``` By keeping the SQL and Clojure separate you get: @@ -141,7 +142,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. @@ -151,15 +152,15 @@ Now we can use it: ```clojure ; Use it standalone. -(users-by-country {:country "GB"}) +(users-by-country {:country_code "GB"}) ;=> ({:count 58}) ; Use it in a clojure.java.jdbc transaction. (require '[clojure.java.jdbc :as jdbc]) (jdbc/with-db-transaction [tx db-spec] - {:limeys (users-by-country {:country "GB"} {:connection tx}) - :yanks (users-by-country {:country "US"} {:connection tx})}) + {:limeys (users-by-country {:country_code "GB"} {:connection tx}) + :yanks (users-by-country {:country_code "US"} {:connection tx})}) ``` ### One File, Many Queries @@ -209,14 +210,14 @@ WHERE ( OR country_code = ? ) -AND age < :maxage +AND age < :max_age ``` Supply the `?` parameters as a vector under the `:?` key, like so: ```clojure (young-users-by-country {:? ["GB" "US"] - :maxage 18}) + :max_age 18}) ``` #### Selectively import queries @@ -262,7 +263,7 @@ And then supply the `IN`-list as a vector, like so: {:connection db-spec}) (find-users {:id [1001 1003 1005] - :maxage 18}) + :min_age 18}) ``` The query will be automatically expanded to `... IN (1001, 1003, 1005) @@ -383,6 +384,7 @@ Yesql has inspired ports to other languages: |Ruby|[yayql](https://github.com/gnarmis/yayql)| |Erlang|[eql](https://github.com/artemeff/eql)| |Clojure|[YeSPARQL](https://github.com/joelkuiper/yesparql)| +|PHP|[YepSQL](https://github.com/LionsHead/YepSQL)| ## Status diff --git a/project.clj b/project.clj index e9cb7cb..729872f 100644 --- a/project.clj +++ b/project.clj @@ -1,18 +1,18 @@ -(defproject yesql "0.5.2" +(defproject yesql "0.5.3" :description "A Clojure library for using SQL" :url "https://github.com/krisajenkins/yesql" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/java.jdbc "0.4.2"] + :dependencies [[org.clojure/clojure "1.8.0"] + [org.clojure/java.jdbc "0.5.8"] [instaparse "1.4.1" :exclusions [org.clojure/clojure]]] :pedantic? :abort :scm {:name "git" :url "https://github.com/krisajenkins/yesql"} :profiles {:dev {:dependencies [[expectations "2.1.3" :exclusions [org.clojure/clojure]] - [org.apache.derby/derby "10.11.1.1"]] - :plugins [[lein-autoexpect "1.4.0"] - [lein-expectations "0.0.8"]]} + [org.apache.derby/derby "10.12.1.1"]] + :plugins [[lein-autoexpect "1.4.0" :exclusions [org.clojure/tools.namespace]] + [lein-expectations "0.0.8" :exclusions [org.clojure/clojure]]]} :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} diff --git a/src/yesql/generate.clj b/src/yesql/generate.clj index 9ff489a..bfdf2b5 100644 --- a/src/yesql/generate.clj +++ b/src/yesql/generate.clj @@ -5,7 +5,8 @@ [yesql.util :refer [create-root-var]] [yesql.types :refer [map->Query]] [yesql.statement-parser :refer [tokenize]]) - (:import [yesql.types Query])) + (:import [yesql.types Query]) + (import java.lang.IllegalArgumentException)) (def in-list-parameter? "Check if a type triggers IN-list expansion." @@ -35,37 +36,47 @@ (defn rewrite-query-for-jdbc [tokens initial-args] (let [{:keys [expected-keys expected-positional-count]} (analyse-statement-tokens tokens) - actual-keys (set (keys (dissoc initial-args :?))) + actual-keys (set (keys (dissoc (if (or (vector? initial-args) (list? initial-args)) (apply merge initial-args) initial-args) :?))) actual-positional-count (count (:? initial-args)) missing-keys (set/difference expected-keys actual-keys)] - (assert (empty? missing-keys) - (format "Query argument mismatch.\nExpected keys: %s\nActual keys: %s\nMissing keys: %s" + (if-not (empty? missing-keys) + (throw (IllegalArgumentException. (format "Query argument mismatch.\nExpected keys: %s\nActual keys: %s\nMissing keys: %s" (str (seq expected-keys)) (str (seq actual-keys)) - (str (seq missing-keys)))) - (assert (= expected-positional-count actual-positional-count) - (format (join "\n" + (str (seq missing-keys)))))) + (if-not (= expected-positional-count actual-positional-count) + (throw (IllegalArgumentException. (format (join "\n" ["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)))) + (if (or (vector? initial-args) (list? 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) [(str query (args-to-placeholders "")) + (conj parameters (keyword token)) + args])) ["" [] initial-args] tokens)] (concat [final-query] (mapv (apply juxt final-parameters) 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 @@ -76,8 +87,10 @@ (first (jdbc/execute! db sql-and-params))) (defn insert-handler - [db [statement & params] call-options] - (jdbc/db-do-prepared-return-keys db statement params)) + [db statement-and-params call-options] + (if (vector? (second statement-and-params)) + (apply jdbc/db-do-prepared db statement-and-params) + (jdbc/db-do-prepared-return-keys db statement-and-params))) (defn query-handler [db sql-and-params @@ -87,9 +100,9 @@ result-set-fn doall} :as call-options}] (jdbc/query db sql-and-params - :identifiers identifiers - :row-fn row-fn - :result-set-fn result-set-fn)) + {:identifiers identifiers + :row-fn row-fn + :result-set-fn result-set-fn})) (defn generate-query-fn "Generate a function to run a query. diff --git a/src/yesql/queryfile_parser.clj b/src/yesql/queryfile_parser.clj index 55e0856..9834d90 100644 --- a/src/yesql/queryfile_parser.clj +++ b/src/yesql/queryfile_parser.clj @@ -1,6 +1,6 @@ (ns yesql.queryfile-parser (:require [clojure.java.io :as io] - [clojure.string :refer [join trim]] + [clojure.string :as str :refer [join trim]] [instaparse.core :as instaparse] [yesql.types :refer [map->Query]] [yesql.util :refer [str-non-nil]] @@ -11,6 +11,9 @@ (assert url) (instaparse/parser url))) +(defn- rm-semicolon [s] + (str/replace s #";$" "")) + (def parser-transforms {:whitespace str-non-nil :non-whitespace str-non-nil @@ -22,7 +25,7 @@ :docstring (fn [& comments] [:docstring (trim (join (map second comments)))]) :statement (fn [& lines] - [:statement (trim (join lines))]) + [:statement (rm-semicolon (trim (join lines)))]) :query (fn [& args] (map->Query (into {} args))) :queries list}) diff --git a/test/yesql/generate_test.clj b/test/yesql/generate_test.clj index 3b0e4f5..cba9077 100644 --- a/test/yesql/generate_test.clj +++ b/test/yesql/generate_test.clj @@ -2,7 +2,8 @@ (:require [expectations :refer :all] [clojure.template :refer [do-template]] [yesql.statement-parser :refer [tokenize]] - [yesql.generate :refer :all])) + [yesql.generate :refer :all]) + (:import [java.lang IllegalArgumentException])) (do-template [statement _ expected-parameters] (expect expected-parameters @@ -82,13 +83,13 @@ "SELECT * FROM users WHERE group_ids IN(:group_ids) AND parent_id = :parent_id" {:group_ids [1 2] :parent_id 3} - => ["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 +(expect IllegalArgumentException (rewrite-query-for-jdbc (tokenize "SELECT age FROM users WHERE country = :country AND name = :name") {:country "gb"})) -(expect AssertionError +(expect IllegalArgumentException (rewrite-query-for-jdbc (tokenize "SELECT age FROM users WHERE country = ? AND name = ?") {}))