This commit is contained in:
Michael Blume 2015-03-26 05:09:58 +00:00
commit d2e511a9ba
2 changed files with 237 additions and 143 deletions

View file

@ -8,15 +8,6 @@
;;;; ;;;;
(defn comma-join [s]
(string/join ", " s))
(defn space-join [s]
(string/join " " s))
(defn paren-wrap [x]
(str "(" x ")"))
(def ^:dynamic *clause* (def ^:dynamic *clause*
"During formatting, *clause* is bound to :select, :from, :where, etc." "During formatting, *clause* is bound to :select, :from, :where, etc."
nil) nil)
@ -38,6 +29,8 @@
(def ^:dynamic *subquery?* false) (def ^:dynamic *subquery?* false)
(def ^:dynamic *builder* nil)
(def ^:private quote-fns (def ^:private quote-fns
{:ansi #(str \" % \") {:ansi #(str \" % \")
:mysql #(str \` % \`) :mysql #(str \` % \`)
@ -54,6 +47,30 @@
(defn- undasherize [s] (defn- undasherize [s]
(string/replace s "-" "_")) (string/replace s "-" "_"))
(defn append-str [^String s]
(.append ^StringBuilder *builder* s))
(defn append-obj [o]
(.append ^StringBuilder *builder* o))
(defn append-char [c]
(.append ^StringBuilder *builder* (char c)))
(defn do-join [sep appender xs]
(when (seq xs)
(appender (first xs))
(doseq [x (rest xs)]
(append-str sep)
(appender x))))
(defn close-paren []
(.append ^StringBuilder *builder* \)))
(defn do-paren-join [sep appender xs]
(append-char \()
(do-join sep appender xs)
(close-paren))
(defn quote-identifier [x & {:keys [style split] :or {split true}}] (defn quote-identifier [x & {:keys [style split] :or {split true}}]
(let [qf (if style (let [qf (if style
(quote-fns style) (quote-fns style)
@ -63,12 +80,14 @@
(string? x) (if qf x (undasherize x)) (string? x) (if qf x (undasherize x))
:else (str x))] :else (str x))]
(if-not qf (if-not qf
s (append-str s)
(let [qf* #(if (= "*" %) % (qf %))] (let [qf* #(if (= "*" %) % (qf %))]
(if-not split (if-not split
(qf* s) (append-str (qf* s))
(let [parts (string/split s #"\.")] (do-join
(string/join "." (map qf* parts)))))))) "."
append-str
(map qf* (string/split s #"\."))))))))
(def infix-fns (def infix-fns
#{"+" "-" "*" "/" "%" "mod" "|" "&" "^" #{"+" "-" "*" "/" "%" "mod" "|" "&" "^"
@ -89,83 +108,97 @@
(defmulti fn-handler (fn [op & args] op)) (defmulti fn-handler (fn [op & args] op))
(defn expand-binary-ops [op & args] (defn expand-binary-ops [op & args]
(str "(" (do-paren-join
(string/join " AND " " AND "
(for [[a b] (partition 2 1 args)] (fn [[a b]] (fn-handler op a b))
(fn-handler op a b))) (partition 2 1 args)))
")"))
(defmethod fn-handler :default [op & args] (defmethod fn-handler :default [op & args]
(let [args (map to-sql args)] (if (infix-fns op)
(if (infix-fns op) (do-paren-join
(paren-wrap (string/join (str " " op " ") args)) (str " " op " ")
(str op (paren-wrap (comma-join args)))))) to-sql
args)
(do
(append-str op)
(do-paren-join ", " to-sql args))))
(defmethod fn-handler "count-distinct" [_ & args] (defmethod fn-handler "count-distinct" [_ & args]
(str "COUNT(DISTINCT " (comma-join (map to-sql args)) ")")) (append-str "COUNT(DISTINCT")
(do-join ", " to-sql args)
(close-paren)
)
(defmethod fn-handler "distinct-on" [_ & args] (defmethod fn-handler "distinct-on" [_ & args]
(str "DISTINCT ON (" (comma-join (map to-sql args)) ")")) (append-str "DISTINCT ON (")
(do-join ", " to-sql args)
(close-paren)
)
(defmethod fn-handler "cast" [_ field cast-to-type] (defmethod fn-handler "cast" [_ field cast-to-type]
(str "CAST" (paren-wrap (str (to-sql field) (append-str "CAST(")
" AS " (to-sql field)
(to-sql cast-to-type))))) (append-str " AS ")
(to-sql cast-to-type)
(close-paren))
(defmethod fn-handler "=" [_ a b & more] (defmethod fn-handler "=" [_ a b & more]
(if (seq more) (if (seq more)
(apply expand-binary-ops "=" a b more) (apply expand-binary-ops "=" a b more)
(cond (cond
(nil? a) (str (to-sql b) " IS NULL") (nil? a) (do (to-sql b) (append-str " IS NULL"))
(nil? b) (str (to-sql a) " IS NULL") (nil? b) (do (to-sql a) (append-str " IS NULL"))
:else (str (to-sql a) " = " (to-sql b))))) :else (do (to-sql a) (append-str " = ") (to-sql b)))))
(defmethod fn-handler "<>" [_ a b & more] (defmethod fn-handler "<>" [_ a b & more]
(if (seq more) (if (seq more)
(apply expand-binary-ops "<>" a b more) (apply expand-binary-ops "<>" a b more)
(cond (cond
(nil? a) (str (to-sql b) " IS NOT NULL") (nil? a) (do (to-sql b) (append-str " IS NOT NULL"))
(nil? b) (str (to-sql a) " IS NOT NULL") (nil? b) (do (to-sql a) (append-str " IS NOT NULL"))
:else (str (to-sql a) " <> " (to-sql b))))) :else (do (to-sql a) (append-str " <> ") (to-sql b)))))
(defmethod fn-handler "<" [_ a b & more] (defmethod fn-handler "<" [_ a b & more]
(if (seq more) (if (seq more)
(apply expand-binary-ops "<" a b more) (apply expand-binary-ops "<" a b more)
(str (to-sql a) " < " (to-sql b)))) (do (to-sql a) (append-str " < ") (to-sql b))))
(defmethod fn-handler "<=" [_ a b & more] (defmethod fn-handler "<=" [_ a b & more]
(if (seq more) (if (seq more)
(apply expand-binary-ops "<=" a b more) (apply expand-binary-ops "<=" a b more)
(str (to-sql a) " <= " (to-sql b)))) (do (to-sql a) (append-str " <= ") (to-sql b))))
(defmethod fn-handler ">" [_ a b & more] (defmethod fn-handler ">" [_ a b & more]
(if (seq more) (if (seq more)
(apply expand-binary-ops ">" a b more) (apply expand-binary-ops ">" a b more)
(str (to-sql a) " > " (to-sql b)))) (do (to-sql a) (append-str " > ") (to-sql b))))
(defmethod fn-handler ">=" [_ a b & more] (defmethod fn-handler ">=" [_ a b & more]
(if (seq more) (if (seq more)
(apply expand-binary-ops ">=" a b more) (apply expand-binary-ops ">=" a b more)
(str (to-sql a) " >= " (to-sql b)))) (do (to-sql a) (append-str " >= ") (to-sql b))))
(defmethod fn-handler "between" [_ field lower upper] (defmethod fn-handler "between" [_ field lower upper]
(str (to-sql field) " BETWEEN " (to-sql lower) " AND " (to-sql upper))) (to-sql field)
(append-str " BETWEEN ")
(to-sql lower)
(append-str " AND ")
(to-sql upper))
;; Handles MySql's MATCH (field) AGAINST (pattern). The third argument ;; Handles MySql's MATCH (field) AGAINST (pattern). The third argument
;; can be a set containing one or more of :boolean, :natural, or :expand. ;; can be a set containing one or more of :boolean, :natural, or :expand.
(defmethod fn-handler "match" [_ fields pattern & [opts]] (defmethod fn-handler "match" [_ fields pattern & [opts]]
(str "MATCH (" (append-str "MATCH (")
(comma-join (do-join ", " to-sql (if (coll? fields) fields [fields]))
(map to-sql (if (coll? fields) fields [fields]))) (append-str ") AGAINST (")
") AGAINST (" (to-sql pattern)
(to-sql pattern) (doseq [opt opts]
(when (seq opts) (append-str
(str " " (space-join (for [opt opts] (case opt
(case opt :boolean " IN BOOLEAN MODE"
:boolean "IN BOOLEAN MODE" :natural " IN NATURAL LANGUAGE MODE"
:natural "IN NATURAL LANGUAGE MODE" :expand " WITH QUERY EXPANSION")))
:expand "WITH QUERY EXPANSION"))))) (close-paren))
")"))
(def default-clause-priorities (def default-clause-priorities
"Determines the order that clauses will be placed within generated SQL" "Determines the order that clauses will be placed within generated SQL"
@ -226,8 +259,10 @@
*param-names* (atom []) *param-names* (atom [])
*input-params* (atom params) *input-params* (atom params)
*quote-identifier-fn* (quote-fns (:quoting opts)) *quote-identifier-fn* (quote-fns (:quoting opts))
*parameterizer* (parameterizers (or (:parameterizer opts) :jdbc))] *parameterizer* (parameterizers (or (:parameterizer opts) :jdbc))
(let [sql-str (to-sql sql-map)] *builder* (StringBuilder.)]
(to-sql sql-map)
(let [sql-str (str *builder*)]
(if (seq @*params*) (if (seq @*params*)
(if (:return-param-names opts) (if (:return-param-names opts)
[sql-str @*params* @*param-names*] [sql-str @*params* @*param-names*]
@ -241,8 +276,10 @@
*param-counter* (atom 0) *param-counter* (atom 0)
*param-names* (atom []) *param-names* (atom [])
*quote-identifier-fn* (or (quote-fns quoting) *quote-identifier-fn* (or (quote-fns quoting)
*quote-identifier-fn*)] *quote-identifier-fn*)
(let [sql-str (format-predicate* pred)] *builder* (StringBuilder.)]
(format-predicate* pred)
(let [sql-str (str *builder*)]
(if (seq @*params*) (if (seq @*params*)
(into [sql-str] @*params*) (into [sql-str] @*params*)
[sql-str])))) [sql-str]))))
@ -264,24 +301,26 @@
clojure.lang.Symbol clojure.lang.Symbol
(-to-sql [x] (quote-identifier x)) (-to-sql [x] (quote-identifier x))
java.lang.Number java.lang.Number
(-to-sql [x] (str x)) (-to-sql [x] (append-obj x))
java.lang.Boolean java.lang.Boolean
(-to-sql [x] (-to-sql [x]
(if x "TRUE" "FALSE")) (append-str (if x "TRUE" "FALSE")))
clojure.lang.Sequential clojure.lang.Sequential
(-to-sql [x] (-to-sql [x]
(if *fn-context?* (if *fn-context?*
;; list argument in fn call ;; list argument in fn call
(paren-wrap (comma-join (map to-sql x))) (do-paren-join ", " to-sql x)
;; alias ;; alias
(str (to-sql (first x)) (do
; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it (to-sql (first x))
(if (= :select *clause*) ; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it
" AS " (append-str
" ") (if (= :select *clause*)
(if (string? (second x)) " AS "
(quote-identifier (second x)) " "))
(to-sql (second x)))))) (if (string? (second x))
(quote-identifier (second x))
(to-sql (second x))))))
SqlCall SqlCall
(-to-sql [x] (-to-sql [x]
(binding [*fn-context?* true] (binding [*fn-context?* true]
@ -289,20 +328,21 @@
fn-name (fn-aliases fn-name fn-name)] fn-name (fn-aliases fn-name fn-name)]
(apply fn-handler fn-name (.args x))))) (apply fn-handler fn-name (.args x)))))
SqlRaw SqlRaw
(-to-sql [x] (.s x)) (-to-sql [x] (append-str (.s x)))
clojure.lang.IPersistentMap clojure.lang.IPersistentMap
(-to-sql [x] (-to-sql [x]
(let [clause-ops (sort-clauses (keys x)) (when *subquery?*
sql-str (binding [*subquery?* true (append-char \())
*fn-context?* false] (binding [*subquery?* true
(space-join *fn-context?* false]
(map (comp #(-format-clause % x) #(find x %)) (do-join
clause-ops)))] " "
(if *subquery?* (comp #(-format-clause % x) #(find x %))
(paren-wrap sql-str) (sort-clauses (keys x))))
sql-str))) (when *subquery?*
(close-paren)))
nil nil
(-to-sql [x] "NULL") (-to-sql [x] (append-str "NULL"))
Object Object
(-to-sql [x] (-to-sql [x]
(let [[x pname] (if (instance? SqlParam x) (let [[x pname] (if (instance? SqlParam x)
@ -316,7 +356,7 @@
[x (keyword (str "_" (swap! *param-counter* inc)))])] [x (keyword (str "_" (swap! *param-counter* inc)))])]
(swap! *param-names* conj pname) (swap! *param-names* conj pname)
(swap! *params* conj x) (swap! *params* conj x)
(*parameterizer*)))) (append-obj (*parameterizer*)))))
(defn sqlable? [x] (defn sqlable? [x]
(satisfies? ToSql x)) (satisfies? ToSql x))
@ -332,11 +372,12 @@
(let [[op & args] pred (let [[op & args] pred
op-name (name op)] op-name (name op)]
(if (= "not" op-name) (if (= "not" op-name)
(str "NOT " (format-predicate* (first args))) (do (append-str "NOT ") (format-predicate* (first args)))
(if (#{"and" "or" "xor"} op-name) (if (#{"and" "or" "xor"} op-name)
(paren-wrap (do-paren-join
(string/join (str " " (string/upper-case op-name) " ") (str " " (string/upper-case op-name) " ")
(map format-predicate* args))) format-predicate*
args)
(to-sql (apply call pred))))))) (to-sql (apply call pred)))))))
(defmulti format-clause (defmulti format-clause
@ -348,111 +389,155 @@
(binding [*clause* (key clause)] (binding [*clause* (key clause)]
(format-clause clause _))) (format-clause clause _)))
(defmethod format-clause :default [& _] (defmethod format-clause :default [& _])
"")
(defmethod format-clause :select [[_ fields] sql-map] (defmethod format-clause :select [[_ fields] sql-map]
(str "SELECT " (append-str "SELECT ")
(when (:modifiers sql-map) (doseq [m (:modifiers sql-map)]
(str (space-join (map (comp string/upper-case name) (append-str (string/upper-case (name m)))
(:modifiers sql-map))) (append-char \space))
" ")) (do-join ", " to-sql fields))
(comma-join (map to-sql fields))))
(defmethod format-clause :from [[_ tables] _] (defmethod format-clause :from [[_ tables] _]
(str "FROM " (comma-join (map to-sql tables)))) (append-str "FROM ")
(do-join ", " to-sql tables))
(defmethod format-clause :where [[_ pred] _] (defmethod format-clause :where [[_ pred] _]
(str "WHERE " (format-predicate* pred))) (append-str "WHERE ")
(format-predicate* pred))
(defn format-join [type table pred] (defn format-join [type table pred]
(str (when type (when type
(str (string/upper-case (name type)) " ")) (append-str (string/upper-case (name type)))
"JOIN " (to-sql table) (append-char \space))
" ON " (format-predicate* pred))) (append-str "JOIN ")
(to-sql table)
(append-str " ON ")
(format-predicate* pred))
(defmethod format-clause :join [[_ join-groups] _] (defmethod format-clause :join [[_ join-groups] _]
(space-join (map #(apply format-join :inner %) (do-join
(partition 2 join-groups)))) " "
#(apply format-join :inner %)
(partition 2 join-groups)))
(defmethod format-clause :left-join [[_ join-groups] _] (defmethod format-clause :left-join [[_ join-groups] _]
(space-join (map #(apply format-join :left %) (do-join
(partition 2 join-groups)))) " "
#(apply format-join :left %)
(partition 2 join-groups)))
(defmethod format-clause :right-join [[_ join-groups] _] (defmethod format-clause :right-join [[_ join-groups] _]
(space-join (map #(apply format-join :right %) (do-join
(partition 2 join-groups)))) " "
#(apply format-join :right %)
(partition 2 join-groups)))
(defmethod format-clause :full-join [[_ join-groups] _] (defmethod format-clause :full-join [[_ join-groups] _]
(space-join (map #(apply format-join :full %) (do-join
(partition 2 join-groups)))) " "
#(apply format-join :full %)
(partition 2 join-groups)))
(defmethod format-clause :group-by [[_ fields] _] (defmethod format-clause :group-by [[_ fields] _]
(str "GROUP BY " (comma-join (map to-sql fields)))) (append-str "GROUP BY ")
(do-join ", " to-sql fields))
(defmethod format-clause :having [[_ pred] _] (defmethod format-clause :having [[_ pred] _]
(str "HAVING " (format-predicate* pred))) (append-str "HAVING ")
(format-predicate* pred))
(defmethod format-clause :order-by [[_ fields] _] (defmethod format-clause :order-by [[_ fields] _]
(str "ORDER BY " (append-str "ORDER BY ")
(comma-join (for [field fields] (do-join
(if (sequential? field) ", "
(let [[field order] field] (fn [field]
(str (to-sql field) " " (if (= :desc order) (if (sequential? field)
"DESC" "ASC"))) (let [[field order] field]
(to-sql field)))))) (to-sql field)
(append-str
(if (= :desc order) " DESC" " ASC")))
(to-sql field)))
fields))
(defmethod format-clause :limit [[_ limit] _] (defmethod format-clause :limit [[_ limit] _]
(str "LIMIT " (to-sql limit))) (append-str "LIMIT ")
(to-sql limit))
(defmethod format-clause :offset [[_ offset] _] (defmethod format-clause :offset [[_ offset] _]
(str "OFFSET " (to-sql offset))) (append-str "OFFSET ")
(to-sql offset))
(defmethod format-clause :insert-into [[_ table] _] (defmethod format-clause :insert-into [[_ table] _]
(append-str "INSERT INTO ")
(if (and (sequential? table) (sequential? (first table))) (if (and (sequential? table) (sequential? (first table)))
(str "INSERT INTO " (do
(to-sql (ffirst table)) (to-sql (ffirst table))
" (" (comma-join (map to-sql (second (first table)))) ") " (append-char \space)
(to-sql (second table))) (do-paren-join
(str "INSERT INTO " (to-sql table)))) ", "
to-sql
(second (first table)))
(append-char \space)
(to-sql (second table)))
(to-sql table)))
(defmethod format-clause :columns [[_ fields] _] (defmethod format-clause :columns [[_ fields] _]
(str "(" (comma-join (map to-sql fields)) ")")) (do-paren-join ", " to-sql fields))
(defmethod format-clause :values [[_ values] _] (defmethod format-clause :values [[_ values] _]
(if (sequential? (first values)) (if (sequential? (first values))
(str "VALUES " (comma-join (for [x values] (do
(str "(" (comma-join (map to-sql x)) ")")))) (append-str "VALUES ")
(str (do-join
"(" (comma-join (map to-sql (keys (first values)))) ") VALUES " ", "
(comma-join (for [x values] #(do-paren-join to-sql %)
(str "(" (comma-join (map to-sql (vals x))) ")")))))) values))
(do
(do-paren-join ", " to-sql (keys (first values)))
(append-str " VALUES ")
(do-join
", "
#(do-paren-join ", " to-sql (vals %))
values))))
(defmethod format-clause :query-values [[_ query-values] _] (defmethod format-clause :query-values [[_ query-values] _]
(to-sql query-values)) (to-sql query-values))
(defmethod format-clause :update [[_ table] _] (defmethod format-clause :update [[_ table] _]
(str "UPDATE " (to-sql table))) (append-str "UPDATE ")
(to-sql table))
(defmethod format-clause :set [[_ values] _] (defmethod format-clause :set [[_ values] _]
(str "SET " (comma-join (for [[k v] values] (append-str "SET ")
(str (to-sql k) " = " (to-sql v)))))) (do-join
", "
(fn [[k v]]
(to-sql k)
(append-str " = ")
(to-sql v))
values))
(defmethod format-clause :delete-from [[_ table] _] (defmethod format-clause :delete-from [[_ table] _]
(str "DELETE FROM " (to-sql table))) (append-str "DELETE FROM ")
(to-sql table))
(defn cte->sql (defn cte->sql
[[cte-name query]] [[cte-name query]]
(str (to-sql cte-name) " AS " (to-sql query))) (to-sql cte-name)
(append-str " AS ")
(to-sql query))
(defmethod format-clause :with [[_ ctes] _] (defmethod format-clause :with [[_ ctes] _]
(str "WITH " (comma-join (map cte->sql ctes)))) (append-str "WITH ")
(do-join ", " cte->sql ctes))
(defmethod format-clause :with-recursive [[_ ctes] _] (defmethod format-clause :with-recursive [[_ ctes] _]
(str "WITH RECURSIVE " (comma-join (map cte->sql ctes)))) (append-str "WITH RECURSIVE ")
(do-join ", " cte->sql ctes))
(defmethod format-clause :union [[_ maps] _] (defmethod format-clause :union [[_ maps] _]
(string/join " UNION " (map to-sql maps))) (do-join " UNION " to-sql maps))
(defmethod format-clause :union-all [[_ maps] _] (defmethod format-clause :union-all [[_ maps] _]
(string/join " UNION ALL " (map to-sql maps))) (do-join " UNION ALL " to-sql maps))

View file

@ -3,33 +3,42 @@
(:require [clojure.test :refer [deftest testing is are]] (:require [clojure.test :refer [deftest testing is are]]
[honeysql.format :refer :all])) [honeysql.format :refer :all]))
(defmacro with-builder [& forms]
`(binding [*builder* (StringBuilder.)]
~@forms
(str *builder*)))
(deftest test-quote (deftest test-quote
(are (are
[qx res] [qx res]
(= (apply quote-identifier "foo.bar.baz" qx) res) (= (with-builder (apply quote-identifier "foo.bar.baz" qx)) res)
[] "foo.bar.baz" [] "foo.bar.baz"
[:style :mysql] "`foo`.`bar`.`baz`" [:style :mysql] "`foo`.`bar`.`baz`"
[:style :mysql :split false] "`foo.bar.baz`") [:style :mysql :split false] "`foo.bar.baz`")
(are (are
[x res] [x res]
(= (quote-identifier x) res) (= (with-builder (quote-identifier x)) res)
3 "3" 3 "3"
'foo "foo" 'foo "foo"
:foo-bar "foo_bar") :foo-bar "foo_bar")
(is (= (quote-identifier "*" :style :ansi) "*"))) (is (= (with-builder (quote-identifier "*" :style :ansi)) "*")))
(defn make-clause [& args]
(with-builder
(apply format-clause args)))
(deftest test-cte (deftest test-cte
(is (= (format-clause (is (= (make-clause
(first {:with [[:query {:select [:foo] :from [:bar]}]]}) nil) (first {:with [[:query {:select [:foo] :from [:bar]}]]}) nil)
"WITH query AS SELECT foo FROM bar")) "WITH query AS SELECT foo FROM bar"))
(is (= (format-clause (is (= (make-clause
(first {:with-recursive [[:query {:select [:foo] :from [:bar]}]]}) nil) (first {:with-recursive [[:query {:select [:foo] :from [:bar]}]]}) nil)
"WITH RECURSIVE query AS SELECT foo FROM bar"))) "WITH RECURSIVE query AS SELECT foo FROM bar")))
(deftest insert-into (deftest insert-into
(is (= (format-clause (first {:insert-into :foo}) nil) (is (= (make-clause (first {:insert-into :foo}) nil)
"INSERT INTO foo")) "INSERT INTO foo"))
(is (= (format-clause (first {:insert-into [:foo {:select [:bar] :from [:baz]}]}) nil) (is (= (make-clause (first {:insert-into [:foo {:select [:bar] :from [:baz]}]}) nil)
"INSERT INTO foo SELECT bar FROM baz")) "INSERT INTO foo SELECT bar FROM baz"))
(is (= (format-clause (first {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]}) nil) (is (= (make-clause (first {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]}) nil)
"INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"))) "INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz")))