honeysql/src/honey/sql.cljc

533 lines
19 KiB
Text
Raw Normal View History

2020-09-21 01:48:07 +00:00
;; copyright (c) 2020 sean corfield, all rights reserved
(ns honey.sql
"Primary API for HoneySQL 2.x."
(:refer-clojure :exclude [format])
(:require [clojure.string :as str]))
;; default formatting for known clauses
(declare format-dsl)
(declare format-expr)
(declare format-expr-list)
2020-09-21 01:48:07 +00:00
;; dynamic dialect handling for formatting
(def ^:private default-clause-order
"The (default) order for known clauses. Can have items added and removed."
[:with :with-recursive :intersect :union :union-all :except
:select :insert-into :update :delete :delete-from :truncate
:columns :set :from
:join :left-join :right-join :inner-join :outer-join :full-join
:cross-join
:where :group-by :having :order-by :limit :offset :values])
(defn- add-clause-before
"Low-level helper just to insert a new clause."
[order clause before]
(if before
(do
(when-not (contains? (set order) before)
(throw (ex-info (str "Unrecognized clause: " before)
{:known-clauses order})))
(reduce (fn [v k]
(if (= k before)
(conj v clause k)
(conj v k)))
[]
order))
(conj order clause)))
2020-09-21 01:48:07 +00:00
(def ^:private dialects
{:ansi {:quote #(str \" % \")}
:mssql {:quote #(str \[ % \])}
:mysql {:quote #(str \` % \`)
:clause-order-fn #(add-clause-before
(filterv (complement #{:set}) %)
:set
:where)}})
2020-09-21 01:48:07 +00:00
; should become defonce
(def ^:private default-dialect (atom (:ansi dialects)))
(def ^:private ^:dynamic *dialect* nil)
;; nil would be a better default but that makes testing individual
;; functions harder than necessary:
(def ^:private ^:dynamic *clause-order* default-clause-order)
2020-09-21 01:48:07 +00:00
(def ^:private ^:dynamic *quoted* nil)
;; clause helpers
;; String.toUpperCase() or `str/upper-case` for that matter converts the
;; string to uppercase for the DEFAULT LOCALE. Normally this does what you'd
;; expect but things like `inner join` get converted to `İNNER JOİN` (dot over
;; the I) when user locale is Turkish. This predictably has bad consequences
;; for people who like their SQL queries to work. The fix here is to use
;; String.toUpperCase(Locale/US) instead which always converts things the
;; way we'd expect.
;;
;; Use this instead of `str/upper-case` as it will always use Locale/US.
(def ^:private ^{:arglists '([s])} upper-case
;; TODO - not sure if there's a JavaScript equivalent here we should be using as well
#?(:clj (fn [^String s] (.. s toString (toUpperCase (java.util.Locale/US))))
:cljs str/upper-case))
2020-09-21 01:48:07 +00:00
(defn- sql-kw [k]
(-> k (name) (upper-case) (str/replace "-" " ")))
2020-09-21 01:48:07 +00:00
(defn- format-entity [x & [{:keys [aliased? drop-ns?]}]]
(let [q (if *quoted* (:quote *dialect*) identity)
call (fn [f x] (str f "(" x ")"))
[f t c] (if-let [n (when-not (or drop-ns? (string? x))
(namespace x))]
[nil n (name x)]
(let [[t c] (if aliased?
[(name x)]
(str/split (name x) #"\."))]
;; I really dislike like %func.arg shorthand syntax!
(cond (= \% (first t))
[(subs t 1) nil c]
c
[nil t c]
:else
[nil nil t])))]
2020-09-21 01:48:07 +00:00
(cond->> c
(not= "*" c)
(q)
t
(str (q t) ".")
f
(call f))))
2020-09-21 01:48:07 +00:00
(defn- format-entity-alias [x]
(cond (sequential? x)
(str (let [s (first x)]
(if (map? s)
(throw (ex-info "selectable cannot be statement!"
{:selectable s}))
(format-entity s)))
#_" AS " " "
(format-entity (second x) {:aliased? true}))
:else
(format-entity x)))
(defn- format-selectable-dsl [x & [{:keys [as? aliased?] :as opts}]]
(cond (map? x)
(format-dsl x {:nested? true})
(sequential? x)
(let [s (first x)
a (second x)
[sql & params] (if (map? s)
(format-dsl s {:nested? true})
(format-expr s))
[sql' & params'] (if (sequential? a)
(let [[sql params] (format-expr-list a {:aliased? true})]
(into [(str/join " " sql)] params))
(format-selectable-dsl a {:aliased? true}))]
(-> [(str sql (if as? " AS " " ") sql')]
(into params)
(into params')))
(or (keyword? x) (symbol? x))
[(format-entity x opts)]
(and aliased? (string? x))
[(format-entity x opts)]
:else
(format-expr x)))
2020-09-21 01:48:07 +00:00
;; primary clauses
(defn- format-on-set-op [k xs]
(let [[sqls params]
(reduce (fn [[sql params] [sql' & params']]
[(conj sql sql') (if params' (into params params') params)])
[[] []]
(map #'format-dsl xs))]
(into [(str/join (str " " (sql-kw k) " ") sqls)] params)))
(defn- format-expr-list [xs & [opts]]
(reduce (fn [[sql params] [sql' & params']]
[(conj sql sql') (if params' (into params params') params)])
[[] []]
(map #(format-expr % opts) xs)))
(defn- format-columns [_ xs]
(let [[sqls params] (format-expr-list xs {:drop-ns? true})]
(into [(str "(" (str/join ", " sqls) ")")] params)))
(defn- format-selects [k xs]
(if (sequential? xs)
(let [[sqls params]
(reduce (fn [[sql params] [sql' & params']]
[(conj sql sql') (if params' (into params params') params)])
[[] []]
(map #(format-selectable-dsl % {:as? (= k :select)}) xs))]
(into [(str (sql-kw k) " " (str/join ", " sqls))] params))
(let [[sql & params] (format-selectable-dsl xs {:as? (= k :select)})]
(into [(str (sql-kw k) " " sql)] params))))
(defn- format-with-part [x]
(if (sequential? x)
(let [[sql & params] (format-dsl (second x))]
(into [(str (format-entity (first x)) " " sql)] params))
[(format-entity x)]))
(defn- format-with [k xs]
;; TODO: a sequence of pairs -- X AS expr -- where X is either [entity expr]
;; or just entity, as far as I can tell...
(let [[sqls params]
(reduce (fn [[sql params] [sql' & params']]
[(conj sql sql') (if params' (into params params') params)])
[[] []]
(map (fn [[x expr]]
(let [[sql & params] (format-with-part x)
[sql' & params'] (format-dsl expr)]
(cond-> [(str sql " AS "
(if (seq params')
(str "(" sql' ")")
sql'))]
params (into params)
params' (into params'))))
xs))]
(into [(str (sql-kw k) " " (str/join ", " sqls))] params)))
(defn- format-selector [k xs]
(format-selects k [xs]))
(defn- format-insert [k table]
;; table can be just a table, a pair of table and statement, or a
;; pair of a pair of table and columns and a statement (yikes!)
(if (sequential? table)
(if (sequential? (first table))
(let [[[table cols] statement] table
[sql & params] (format-dsl statement)]
(into [(str (sql-kw k) " " (format-entity-alias table)
" ("
(str/join ", " (map #'format-entity-alias cols))
") "
sql)]
params))
(let [[table statement] table
[sql & params] (format-dsl statement)]
(into [(str (sql-kw k) " " (format-entity-alias table)
" " sql)]
params)))
[(str (sql-kw k) " " (format-entity-alias table))]))
2020-09-21 01:48:07 +00:00
(defn- format-join [k [j e]]
(let [[sql & params] (format-expr e)]
;; for backward compatibility, treat plain JOIN as INNER JOIN:
(into [(str (sql-kw (if (= :join k) :inner-join k)) " "
(format-entity-alias j) " ON "
sql)]
params)))
2020-09-21 01:48:07 +00:00
(defn- format-on-expr [k e]
2020-09-21 01:48:07 +00:00
(let [[sql & params] (format-expr e)]
(into [(str (sql-kw k) " " sql)] params)))
(defn- format-group-by [k xs]
(let [[sqls params] (format-expr-list xs)]
(into [(str (sql-kw k) " " (str/join ", " sqls))] params)))
(defn- format-order-by [k xs]
(let [dirs (map #(if (sequential? %) (second %) :asc) xs)
[sqls params]
(format-expr-list (map #(if (sequential? %) (first %) %) xs))]
2020-09-21 01:48:07 +00:00
(into [(str (sql-kw k) " "
(str/join ", " (map (fn [sql dir] (str sql " " (sql-kw dir)))
sqls
dirs)))] params)))
(defn- format-values [k xs]
(cond (sequential? (first xs))
;; [[1 2 3] [4 5 6]]
(let [[sqls params]
(reduce (fn [[sql params] [sqls' params']]
[(conj sql (str "(" (str/join ", " sqls') ")"))
(into params params')])
[[] []]
(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))
[sqls params]
(reduce (fn [[sql params] [sqls' params']]
[(conj sql (str/join ", " sqls'))
(if params' (into params params') params')])
[[] []]
(map (fn [m]
(format-expr-list (map #(get m %) cols)))
xs))]
(into [(str "("
(str/join ", "
(map #(format-entity % {:drop-ns? true}) cols))
") "
(sql-kw k) " (" (str/join ", " sqls) ")")]
params))
:else
(throw (ex-info ":values expects sequences or maps"
{:first (first xs)}))))
(defn- format-set-exprs [k xs]
(let [[sqls params]
(reduce-kv (fn [[sql params] v e]
(let [[sql' & params'] (format-expr e)]
[(conj sql (str (format-entity v) " = " sql'))
(if params' (into params params') params)]))
[[] []]
xs)]
(into [(str (sql-kw k) " " (str/join ", " sqls))] params)))
(def ^:private current-clause-order
"The (current) order for known clauses. Can have items added and removed."
(atom default-clause-order))
2020-09-21 01:48:07 +00:00
(def ^:private clause-format
"The (default) behavior for each known clause. Can also have items added
and removed."
(atom {:with #'format-with
:with-recursive #'format-with
:intersect #'format-on-set-op
:union #'format-on-set-op
:union-all #'format-on-set-op
:except #'format-on-set-op
:select #'format-selects
:insert-into #'format-insert
:update #'format-selector
:delete #'format-selects
:delete-from #'format-selector
:truncate #'format-selector
:columns #'format-columns
:set #'format-set-exprs
:from #'format-selects
:join #'format-join
:left-join #'format-join
:right-join #'format-join
:inner-join #'format-join
:outer-join #'format-join
:full-join #'format-join
:cross-join #'format-selects
:where #'format-on-expr
:group-by #'format-group-by
:having #'format-on-expr
:order-by #'format-order-by
:limit #'format-on-expr
:offset #'format-on-expr
:values #'format-values}))
(assert (= (set @current-clause-order) (set (keys @clause-format))))
(comment :target
{;:with 20
;:with-recursive 30
;:intersect 35
;:union 40
;:union-all 45
;:except 47
;:select 50
;:insert-into 60
;:update 70
;:delete 75
;:delete-from 80
;:truncate 85
;:columns 90
:composite 95
;; no longer needed/supported :set0 100 ; low-priority set clause
;:from 110
;:join 120
;:left-join 130
;:right-join 140
;:full-join 150
;:cross-join 152 ; doesn't have on clauses
;:set 155
;; no longer needed/supported :set1 156 ; high-priority set clause (synonym for :set)
;:where 160
;:group-by 170
;:having 180
;:order-by 190
;:limit 200
;:offset 210
:lock 215
;:values 220
:query-values 230})
2020-09-21 01:48:07 +00:00
(defn- format-dsl [x & [{:keys [aliased? nested?]}]]
(let [[sqls params leftover]
(reduce (fn [[sql params leftover] k]
2020-09-21 01:48:07 +00:00
(if-let [xs (k x)]
(let [formatter (k @clause-format)
[sql' & params'] (formatter k xs)]
[(conj sql sql')
(if params' (into params params') params)
(dissoc leftover k)])
[sql params leftover]))
[[] [] x]
*clause-order*)]
(if (seq leftover)
(do
;; TODO: for testing purposes, make this less noisy
(println (str "\n-------------------\nUnknown SQL clauses: "
(str/join ", " (keys leftover))))
#_(throw (ex-info (str "Unknown SQL clauses: "
(str/join ", " (keys leftover)))
leftover))
[(str "<unknown" (str/join (keys leftover)) ">")])
(into [(cond-> (str/join " " sqls)
(and nested? (not aliased?))
(as-> s (str "(" s ")")))] params))))
2020-09-21 01:48:07 +00:00
(def ^:private infix-aliases
"Provided for backward compatibility with earlier HoneySQL versions."
{:is :=
:is-not :<>
:not= :<>
:!= :<>
:regex :regexp})
(def ^:private infix-ops
(-> #{"mod" "and" "or" "xor" "<>" "<=" ">="
"in" "not-in" "like" "not-like" "regexp"
"is" "is-not" "not=" "!=" "regex"}
(into (map str "+-*/%|&^=<>"))
(into (keys infix-aliases))
(into (vals infix-aliases))
(->> (into #{} (map keyword)))))
(def ^:private special-syntax
{:array
(fn [[arr]]
(let [[sqls params] (format-expr-list arr)]
(into [(str "ARRAY[" (str/join ", " sqls) "]")] params)))
:between
(fn [[x a b]]
(let [[sql-x & params-x] (format-expr x {:nested? true})
[sql-a & params-a] (format-expr a {:nested? true})
[sql-b & params-b] (format-expr b {:nested? true})]
(-> [(str sql-x " BETWEEN " sql-a " AND " sql-b)]
(into params-x)
(into params-a)
(into params-b))))
:cast
2020-09-21 01:48:07 +00:00
(fn [[x type]]
(let [[sql & params] (format-expr x)]
(into [(str "CAST(" sql " AS " (sql-kw type) ")")] params)))
:interval
(fn [[n units]]
(let [[sql & params] (format-expr n)]
(into [(str "INTERVAL " sql " " (sql-kw units))] params)))})
(defn format-expr [x & [{:keys [nested?] :as opts}]]
(cond (or (keyword? x) (symbol? x))
[(format-entity x opts)]
2020-09-21 01:48:07 +00:00
(map? x)
(format-dsl x (assoc opts :nested? true))
(sequential? x)
2020-09-21 01:48:07 +00:00
(let [op (first x)]
(if (keyword? op)
(cond (infix-ops op)
(let [[_ a b] x
[s1 & p1] (format-expr a {:nested? true})
[s2 & p2] (format-expr b {:nested? true})]
2020-09-21 01:48:07 +00:00
(-> (str s1 " "
(sql-kw (get infix-aliases op op))
" " s2)
(cond-> nested?
(as-> s (str "(" s ")")))
(vector)
(into p1)
(into p2)))
(special-syntax op)
(let [formatter (special-syntax op)]
(formatter (rest x)))
:else
(let [args (rest x)
[sqls params] (format-expr-list args)]
2020-09-21 01:48:07 +00:00
(into [(str (sql-kw op)
(if (and (= 1 (count args))
(map? (first args))
(= 1 (count sqls)))
(str " " (first sqls))
(str "(" (str/join ", " sqls) ")")))]
2020-09-21 01:48:07 +00:00
params)))
(into [(str "(" (str/join ", "
2020-09-21 01:48:07 +00:00
(repeat (count x) "?")) ")")]
x)))
(boolean? x)
[(upper-case (str x))]
2020-09-21 01:48:07 +00:00
:else
["?" x]))
(defn- check-dialect [dialect]
(when-not (contains? dialects dialect)
(throw (ex-info (str "Invalid dialect: " dialect)
{:valid-dialects (vec (sort (keys dialects)))})))
dialect)
(defn format
2020-09-21 01:48:07 +00:00
"Turn the data DSL into a vector containing a SQL string followed by
any parameter values that were encountered in the DSL structure."
([data] (format data {}))
([data opts]
(let [dialect? (contains? opts :dialect)
dialect (when dialect? (get dialects (check-dialect (:dialect opts))))]
(binding [*dialect* (if dialect? dialect @default-dialect)
*clause-order* (if dialect?
(if-let [f (:clause-order-fn dialect)]
(f @current-clause-order)
@current-clause-order)
@current-clause-order)
*quoted* (if (contains? opts :quoted)
(:quoted opts)
dialect?)]
(format-dsl data)))))
2020-09-21 01:48:07 +00:00
(defn set-dialect!
"Set the default dialect for formatting.
Can be: `:ansi` (the default), `:mssql`, `:mysql`."
[dialect]
(reset! default-dialect (get dialects (check-dialect dialect)))
(when-let [f (:clause-order-fn @default-dialect)]
(swap! current-clause-order f)))
(defn register-clause!
"Register a new clause formatter. If `before` is `nil`, the clause is
added to the end of the list of known clauses, otherwise it is inserted
immediately prior to that clause."
[clause formatter before]
(swap! current-clause-order add-clause-before clause before)
(swap! clause-format assoc clause formatter))
2020-09-21 01:48:07 +00:00
(comment
(format {:truncate :foo})
2020-09-21 01:48:07 +00:00
(format-expr [:= :id 1])
(format-expr [:+ :id 1])
(format-expr [:+ 1 [:+ 1 :quux]])
(format-expr [:foo [:bar [:+ 2 [:g :abc]]] [:f 1 :quux]])
(format-expr :id)
(format-expr 1)
(format-on-expr :where [:= :id 1])
2020-09-21 01:48:07 +00:00
(format-dsl {:select [:*] :from [:table] :where [:= :id 1]})
(format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} {})
(format {:select [:*] :from [:table] :group-by [:foo :bar]} {})
(format {:select [:*] :from [:table] :group-by [[:date :bar]]} {})
(format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} {})
(format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {})
(format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {})
2020-09-21 01:48:07 +00:00
(format-expr [:interval 30 :days])
(format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql})
(format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {})
2020-09-21 01:48:07 +00:00
,)