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)
|
2020-09-24 01:15:20 +00:00
|
|
|
(declare format-expr-list)
|
2020-09-21 01:48:07 +00:00
|
|
|
|
|
|
|
|
;; dynamic dialect handling for formatting
|
|
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
(def ^:private default-clause-order
|
|
|
|
|
"The (default) order for known clauses. Can have items added and removed."
|
2020-09-24 01:15:20 +00:00
|
|
|
[:with :with-recursive :intersect :union :union-all :except
|
2020-09-23 19:55:02 +00:00
|
|
|
: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
|
2020-09-25 02:07:32 +00:00
|
|
|
{:ansi {:quote #(str \" % \")}
|
|
|
|
|
:sqlserver {:quote #(str \[ % \])}
|
|
|
|
|
:mysql {:quote #(str \` % \`)
|
|
|
|
|
:clause-order-fn #(add-clause-before
|
|
|
|
|
(filterv (complement #{:set}) %)
|
|
|
|
|
:set
|
|
|
|
|
:where)}
|
|
|
|
|
:oracle {:quote #(str \" % \")}})
|
2020-09-21 01:48:07 +00:00
|
|
|
|
|
|
|
|
; should become defonce
|
|
|
|
|
(def ^:private default-dialect (atom (:ansi dialects)))
|
|
|
|
|
|
|
|
|
|
(def ^:private ^:dynamic *dialect* nil)
|
2020-09-24 01:15:20 +00:00
|
|
|
;; 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
|
|
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
;; 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]
|
2020-09-23 19:55:02 +00:00
|
|
|
(-> k (name) (upper-case) (str/replace "-" " ")))
|
2020-09-21 01:48:07 +00:00
|
|
|
|
2020-09-24 01:15:20 +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
|
2020-09-24 01:15:20 +00:00
|
|
|
(str (q t) ".")
|
|
|
|
|
f
|
|
|
|
|
(call f))))
|
2020-09-21 01:48:07 +00:00
|
|
|
|
2020-09-24 01:15:20 +00:00
|
|
|
(defn- format-entity-alias [x]
|
2020-09-23 07:14:25 +00:00
|
|
|
(cond (sequential? x)
|
2020-09-24 05:52:57 +00:00
|
|
|
(let [s (first x)
|
|
|
|
|
pair? (< 1 (count x))]
|
|
|
|
|
(when (map? s)
|
|
|
|
|
(throw (ex-info "selectable cannot be statement!"
|
|
|
|
|
{:selectable s})))
|
|
|
|
|
(cond-> (format-entity s)
|
|
|
|
|
pair?
|
|
|
|
|
(str #_" AS " " "
|
|
|
|
|
(format-entity (second x) {:aliased? true}))))
|
2020-09-23 07:14:25 +00:00
|
|
|
|
|
|
|
|
:else
|
|
|
|
|
(format-entity x)))
|
|
|
|
|
|
2020-09-24 01:15:20 +00:00
|
|
|
(defn- format-selectable-dsl [x & [{:keys [as? aliased?] :as opts}]]
|
2020-09-23 07:14:25 +00:00
|
|
|
(cond (map? x)
|
2020-09-24 01:15:20 +00:00
|
|
|
(format-dsl x {:nested? true})
|
2020-09-23 07:14:25 +00:00
|
|
|
|
|
|
|
|
(sequential? x)
|
2020-09-24 05:52:57 +00:00
|
|
|
(let [s (first x)
|
|
|
|
|
pair? (< 1 (count x))
|
|
|
|
|
a (second x)
|
2020-09-24 01:15:20 +00:00
|
|
|
[sql & params] (if (map? s)
|
|
|
|
|
(format-dsl s {:nested? true})
|
|
|
|
|
(format-expr s))
|
2020-09-24 05:52:57 +00:00
|
|
|
[sql' & params'] (when pair?
|
|
|
|
|
(if (sequential? a)
|
|
|
|
|
(let [[sql params] (format-expr-list a {:aliased? true})]
|
|
|
|
|
(into [(str/join " " sql)] params))
|
|
|
|
|
(format-selectable-dsl a {:aliased? true})))]
|
|
|
|
|
(-> [(cond-> sql
|
|
|
|
|
pair?
|
|
|
|
|
(str (if as? " AS " " ") sql'))]
|
2020-09-24 01:15:20 +00:00
|
|
|
(into params)
|
|
|
|
|
(into params')))
|
|
|
|
|
|
|
|
|
|
(or (keyword? x) (symbol? x))
|
|
|
|
|
[(format-entity x opts)]
|
|
|
|
|
|
|
|
|
|
(and aliased? (string? x))
|
|
|
|
|
[(format-entity x opts)]
|
2020-09-23 19:55:02 +00:00
|
|
|
|
2020-09-23 07:14:25 +00:00
|
|
|
:else
|
2020-09-23 19:55:02 +00:00
|
|
|
(format-expr x)))
|
2020-09-21 01:48:07 +00:00
|
|
|
|
|
|
|
|
;; primary clauses
|
|
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
(defn- format-on-set-op [k xs]
|
2020-09-23 07:14:25 +00:00
|
|
|
(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)))
|
|
|
|
|
|
2020-09-24 01:15:20 +00:00
|
|
|
(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]
|
2020-09-21 02:17:37 +00:00
|
|
|
(if (sequential? xs)
|
2020-09-23 07:14:25 +00:00
|
|
|
(let [[sqls params]
|
|
|
|
|
(reduce (fn [[sql params] [sql' & params']]
|
|
|
|
|
[(conj sql sql') (if params' (into params params') params)])
|
|
|
|
|
[[] []]
|
2020-09-24 01:15:20 +00:00
|
|
|
(map #(format-selectable-dsl % {:as? (= k :select)}) xs))]
|
2020-09-23 07:14:25 +00:00
|
|
|
(into [(str (sql-kw k) " " (str/join ", " sqls))] params))
|
2020-09-24 01:15:20 +00:00
|
|
|
(let [[sql & params] (format-selectable-dsl xs {:as? (= k :select)})]
|
2020-09-23 07:14:25 +00:00
|
|
|
(into [(str (sql-kw k) " " sql)] params))))
|
|
|
|
|
|
2020-09-24 01:15:20 +00:00
|
|
|
(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)]
|
2020-09-25 23:38:38 +00:00
|
|
|
;; according to docs, CTE should _always_ be wrapped:
|
|
|
|
|
(cond-> [(str sql " AS " (str "(" sql' ")"))]
|
2020-09-24 01:15:20 +00:00
|
|
|
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]))
|
|
|
|
|
|
2020-09-23 07:14:25 +00:00
|
|
|
(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)]
|
2020-09-24 01:15:20 +00:00
|
|
|
(into [(str (sql-kw k) " " (format-entity-alias table)
|
2020-09-23 07:14:25 +00:00
|
|
|
" ("
|
2020-09-24 01:15:20 +00:00
|
|
|
(str/join ", " (map #'format-entity-alias cols))
|
2020-09-23 07:14:25 +00:00
|
|
|
") "
|
|
|
|
|
sql)]
|
|
|
|
|
params))
|
|
|
|
|
(let [[table statement] table
|
|
|
|
|
[sql & params] (format-dsl statement)]
|
2020-09-24 01:15:20 +00:00
|
|
|
(into [(str (sql-kw k) " " (format-entity-alias table)
|
2020-09-23 07:14:25 +00:00
|
|
|
" " sql)]
|
|
|
|
|
params)))
|
2020-09-24 01:15:20 +00:00
|
|
|
[(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)]
|
2020-09-23 19:55:02 +00:00
|
|
|
;; for backward compatibility, treat plain JOIN as INNER JOIN:
|
|
|
|
|
(into [(str (sql-kw (if (= :join k) :inner-join k)) " "
|
2020-09-24 01:15:20 +00:00
|
|
|
(format-entity-alias j) " ON "
|
2020-09-23 19:55:02 +00:00
|
|
|
sql)]
|
|
|
|
|
params)))
|
2020-09-21 01:48:07 +00:00
|
|
|
|
2020-09-23 07:14:25 +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]
|
2020-09-21 02:17:37 +00:00
|
|
|
(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)))
|
|
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
(defn- format-values [k xs]
|
2020-09-24 01:15:20 +00:00
|
|
|
(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)}))))
|
2020-09-23 19:55:02 +00:00
|
|
|
|
|
|
|
|
(defn- format-set-exprs [k xs]
|
2020-09-24 01:15:20 +00:00
|
|
|
(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)))
|
2020-09-23 19:55:02 +00:00
|
|
|
|
2020-09-24 18:13:08 +00:00
|
|
|
(def ^:private base-clause-order
|
|
|
|
|
"The (base) order for known clauses. Can have items added and removed.
|
|
|
|
|
|
|
|
|
|
This is the 'pre-dialect' ordering."
|
|
|
|
|
(atom default-clause-order))
|
|
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
(def ^:private current-clause-order
|
2020-09-24 18:13:08 +00:00
|
|
|
"The (current) order for known clauses. Can have items added and removed.
|
|
|
|
|
|
|
|
|
|
This is the 'post-dialect` ordering when a new default dialect is set."
|
2020-09-23 19:55:02 +00:00
|
|
|
(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."
|
2020-09-24 01:15:20 +00:00
|
|
|
(atom {:with #'format-with
|
|
|
|
|
:with-recursive #'format-with
|
|
|
|
|
:intersect #'format-on-set-op
|
2020-09-23 19:55:02 +00:00
|
|
|
:union #'format-on-set-op
|
|
|
|
|
:union-all #'format-on-set-op
|
|
|
|
|
:except #'format-on-set-op
|
2020-09-24 01:15:20 +00:00
|
|
|
:select #'format-selects
|
2020-09-23 07:14:25 +00:00
|
|
|
:insert-into #'format-insert
|
2020-09-21 02:17:37 +00:00
|
|
|
:update #'format-selector
|
2020-09-24 01:15:20 +00:00
|
|
|
:delete #'format-selects
|
2020-09-21 02:17:37 +00:00
|
|
|
:delete-from #'format-selector
|
|
|
|
|
:truncate #'format-selector
|
2020-09-24 01:15:20 +00:00
|
|
|
:columns #'format-columns
|
2020-09-23 19:55:02 +00:00
|
|
|
:set #'format-set-exprs
|
2020-09-24 01:15:20 +00:00
|
|
|
:from #'format-selects
|
2020-09-21 02:17:37 +00:00
|
|
|
:join #'format-join
|
|
|
|
|
:left-join #'format-join
|
|
|
|
|
:right-join #'format-join
|
|
|
|
|
:inner-join #'format-join
|
|
|
|
|
:outer-join #'format-join
|
|
|
|
|
:full-join #'format-join
|
2020-09-24 01:15:20 +00:00
|
|
|
:cross-join #'format-selects
|
2020-09-23 07:14:25 +00:00
|
|
|
:where #'format-on-expr
|
2020-09-21 02:17:37 +00:00
|
|
|
:group-by #'format-group-by
|
2020-09-23 07:14:25 +00:00
|
|
|
:having #'format-on-expr
|
|
|
|
|
:order-by #'format-order-by
|
|
|
|
|
:limit #'format-on-expr
|
2020-09-23 19:55:02 +00:00
|
|
|
:offset #'format-on-expr
|
|
|
|
|
:values #'format-values}))
|
2020-09-23 07:14:25 +00:00
|
|
|
|
2020-09-24 18:13:08 +00:00
|
|
|
(assert (= (set @base-clause-order)
|
|
|
|
|
(set @current-clause-order)
|
|
|
|
|
(set (keys @clause-format))))
|
2020-09-21 02:17:37 +00:00
|
|
|
|
|
|
|
|
(comment :target
|
2020-09-24 01:15:20 +00:00
|
|
|
{;:with 20
|
|
|
|
|
;:with-recursive 30
|
2020-09-23 19:55:02 +00:00
|
|
|
;:intersect 35
|
2020-09-23 07:14:25 +00:00
|
|
|
;:union 40
|
|
|
|
|
;:union-all 45
|
2020-09-23 19:55:02 +00:00
|
|
|
;:except 47
|
2020-09-21 02:17:37 +00:00
|
|
|
;:select 50
|
|
|
|
|
;:insert-into 60
|
|
|
|
|
;:update 70
|
|
|
|
|
;:delete 75
|
|
|
|
|
;:delete-from 80
|
|
|
|
|
;:truncate 85
|
2020-09-23 19:55:02 +00:00
|
|
|
;:columns 90
|
2020-09-21 02:17:37 +00:00
|
|
|
:composite 95
|
2020-09-24 01:15:20 +00:00
|
|
|
;; no longer needed/supported :set0 100 ; low-priority set clause
|
2020-09-21 02:17:37 +00:00
|
|
|
;:from 110
|
|
|
|
|
;:join 120
|
|
|
|
|
;:left-join 130
|
|
|
|
|
;:right-join 140
|
|
|
|
|
;:full-join 150
|
2020-09-23 19:55:02 +00:00
|
|
|
;:cross-join 152 ; doesn't have on clauses
|
2020-09-24 01:15:20 +00:00
|
|
|
;:set 155
|
|
|
|
|
;; no longer needed/supported :set1 156 ; high-priority set clause (synonym for :set)
|
2020-09-21 02:17:37 +00:00
|
|
|
;:where 160
|
|
|
|
|
;:group-by 170
|
|
|
|
|
;:having 180
|
|
|
|
|
;:order-by 190
|
2020-09-23 07:14:25 +00:00
|
|
|
;:limit 200
|
|
|
|
|
;:offset 210
|
2020-09-21 02:17:37 +00:00
|
|
|
:lock 215
|
2020-09-24 01:15:20 +00:00
|
|
|
;:values 220
|
2020-09-21 02:17:37 +00:00
|
|
|
:query-values 230})
|
2020-09-21 01:48:07 +00:00
|
|
|
|
2020-09-24 01:15:20 +00:00
|
|
|
(defn- format-dsl [x & [{:keys [aliased? nested?]}]]
|
2020-09-23 07:14:25 +00:00
|
|
|
(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)]
|
2020-09-23 07:14:25 +00:00
|
|
|
[(conj sql sql')
|
|
|
|
|
(if params' (into params params') params)
|
|
|
|
|
(dissoc leftover k)])
|
|
|
|
|
[sql params leftover]))
|
|
|
|
|
[[] [] x]
|
2020-09-23 19:55:02 +00:00
|
|
|
*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)
|
2020-09-24 01:15:20 +00:00
|
|
|
(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)))))
|
|
|
|
|
|
2020-09-24 05:52:57 +00:00
|
|
|
(defn- sqlize-value [x]
|
|
|
|
|
(cond
|
|
|
|
|
(nil? x) "NULL"
|
2020-09-25 23:38:11 +00:00
|
|
|
(string? x) (str \' (str/replace x "'" "''") \')
|
2020-09-24 05:52:57 +00:00
|
|
|
(symbol? x) (name x)
|
|
|
|
|
(keyword? x) (name x)
|
|
|
|
|
:else (str x)))
|
|
|
|
|
|
2020-09-21 01:48:07 +00:00
|
|
|
(def ^:private special-syntax
|
2020-09-24 05:25:13 +00:00
|
|
|
{:array
|
|
|
|
|
(fn [[arr]]
|
|
|
|
|
(let [[sqls params] (format-expr-list arr)]
|
|
|
|
|
(into [(str "ARRAY[" (str/join ", " sqls) "]")] params)))
|
|
|
|
|
:between
|
2020-09-21 02:17:37 +00:00
|
|
|
(fn [[x a b]]
|
2020-09-24 01:15:20 +00:00
|
|
|
(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})]
|
2020-09-21 02:17:37 +00:00
|
|
|
(-> [(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)))
|
2020-09-24 05:52:57 +00:00
|
|
|
:inline
|
|
|
|
|
(fn [[x]]
|
|
|
|
|
[(sqlize-value x)])
|
2020-09-21 01:48:07 +00:00
|
|
|
:interval
|
|
|
|
|
(fn [[n units]]
|
|
|
|
|
(let [[sql & params] (format-expr n)]
|
|
|
|
|
(into [(str "INTERVAL " sql " " (sql-kw units))] params)))})
|
|
|
|
|
|
2020-09-24 01:15:20 +00:00
|
|
|
(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
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
(map? x)
|
2020-09-24 01:15:20 +00:00
|
|
|
(format-dsl x (assoc opts :nested? true))
|
2020-09-23 19:55:02 +00:00
|
|
|
|
2020-09-21 02:17:37 +00:00
|
|
|
(sequential? x)
|
2020-09-21 01:48:07 +00:00
|
|
|
(let [op (first x)]
|
|
|
|
|
(if (keyword? op)
|
|
|
|
|
(cond (infix-ops op)
|
2020-09-25 22:31:11 +00:00
|
|
|
(let [[_ a b] x
|
2020-09-24 01:15:20 +00:00
|
|
|
[s1 & p1] (format-expr a {:nested? true})
|
2020-09-25 22:31:11 +00:00
|
|
|
[s2 & p2] (format-expr b {:nested? true})
|
|
|
|
|
op (get infix-aliases op op)]
|
|
|
|
|
(if (and (#{:= :<>} op) (or (nil? a) (nil? b)))
|
|
|
|
|
(-> (str (if (nil? a)
|
|
|
|
|
(if (nil? b) "NULL" s2)
|
|
|
|
|
s1)
|
|
|
|
|
(if (= := op) " IS NULL" " IS NOT NULL"))
|
|
|
|
|
(cond-> nested?
|
|
|
|
|
(as-> s (str "(" s ")")))
|
|
|
|
|
(vector))
|
|
|
|
|
(-> (str s1 " " (sql-kw op) " " s2)
|
|
|
|
|
(cond-> nested?
|
|
|
|
|
(as-> s (str "(" s ")")))
|
|
|
|
|
(vector)
|
|
|
|
|
(into p1)
|
|
|
|
|
(into p2))))
|
2020-09-21 01:48:07 +00:00
|
|
|
(special-syntax op)
|
|
|
|
|
(let [formatter (special-syntax op)]
|
|
|
|
|
(formatter (rest x)))
|
|
|
|
|
:else
|
2020-09-24 01:15:20 +00:00
|
|
|
(let [args (rest x)
|
|
|
|
|
[sqls params] (format-expr-list args)]
|
2020-09-21 01:48:07 +00:00
|
|
|
(into [(str (sql-kw op)
|
2020-09-24 01:15:20 +00:00
|
|
|
(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)))
|
2020-09-24 01:15:20 +00:00
|
|
|
(into [(str "(" (str/join ", "
|
2020-09-21 01:48:07 +00:00
|
|
|
(repeat (count x) "?")) ")")]
|
|
|
|
|
x)))
|
|
|
|
|
|
2020-09-24 18:04:18 +00:00
|
|
|
(or (true? x) (false? x)) ; because (boolean? x) requires Clojure 1.9+
|
2020-09-24 01:15:20 +00:00
|
|
|
[(upper-case (str x))]
|
|
|
|
|
|
2020-09-21 01:48:07 +00:00
|
|
|
:else
|
|
|
|
|
["?" x]))
|
|
|
|
|
|
2020-09-23 19:55:02 +00:00
|
|
|
(defn- check-dialect [dialect]
|
|
|
|
|
(when-not (contains? dialects dialect)
|
|
|
|
|
(throw (ex-info (str "Invalid dialect: " dialect)
|
|
|
|
|
{:valid-dialects (vec (sort (keys dialects)))})))
|
|
|
|
|
dialect)
|
|
|
|
|
|
2020-09-21 02:17:37 +00:00
|
|
|
(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."
|
2020-09-21 17:56:05 +00:00
|
|
|
([data] (format data {}))
|
|
|
|
|
([data opts]
|
2020-09-23 19:55:02 +00:00
|
|
|
(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)]
|
2020-09-24 18:13:08 +00:00
|
|
|
(f @base-clause-order)
|
2020-09-23 19:55:02 +00:00
|
|
|
@current-clause-order)
|
|
|
|
|
@current-clause-order)
|
2020-09-23 07:14:25 +00:00
|
|
|
*quoted* (if (contains? opts :quoted)
|
|
|
|
|
(:quoted opts)
|
2020-09-23 19:55:02 +00:00
|
|
|
dialect?)]
|
2020-09-21 17:56:05 +00:00
|
|
|
(format-dsl data)))))
|
2020-09-21 01:48:07 +00:00
|
|
|
|
|
|
|
|
(defn set-dialect!
|
|
|
|
|
"Set the default dialect for formatting.
|
|
|
|
|
|
2020-09-25 02:07:32 +00:00
|
|
|
Can be: `:ansi` (the default), `:mysql`, `:oracle`, or `:sqlserver`.
|
2020-09-24 18:13:08 +00:00
|
|
|
|
|
|
|
|
Dialects are always applied to the base order to create the current order."
|
2020-09-21 01:48:07 +00:00
|
|
|
[dialect]
|
2020-09-23 19:55:02 +00:00
|
|
|
(reset! default-dialect (get dialects (check-dialect dialect)))
|
|
|
|
|
(when-let [f (:clause-order-fn @default-dialect)]
|
2020-09-24 18:13:08 +00:00
|
|
|
(reset! current-clause-order (f @base-clause-order))))
|
2020-09-23 19:55:02 +00:00
|
|
|
|
|
|
|
|
(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
|
2020-09-24 18:13:08 +00:00
|
|
|
immediately prior to that clause.
|
|
|
|
|
|
|
|
|
|
New clauses are registered in the base order and the current order so
|
|
|
|
|
that any dialect selections are able to include them while still working
|
|
|
|
|
predictably from the base order. Caveat: that means if you are a new
|
|
|
|
|
clause `before` a clause that is ordered differently in different
|
|
|
|
|
dialects, your new clause may also end up in a different place. The
|
|
|
|
|
only clause so far where that would matter is `:set` which differs in
|
|
|
|
|
MySQL..."
|
2020-09-23 19:55:02 +00:00
|
|
|
[clause formatter before]
|
2020-09-25 02:07:32 +00:00
|
|
|
(let [f (if (keyword? formatter)
|
|
|
|
|
(get @clause-format formatter)
|
|
|
|
|
formatter)]
|
|
|
|
|
(when-not (and f (fn? f))
|
|
|
|
|
(throw (ex-info "The formatter must be a function or existing clause"
|
|
|
|
|
{:type (type formatter)})))
|
|
|
|
|
(swap! base-clause-order add-clause-before clause before)
|
|
|
|
|
(swap! current-clause-order add-clause-before clause before)
|
|
|
|
|
(swap! clause-format assoc clause f)))
|
2020-09-21 01:48:07 +00:00
|
|
|
|
|
|
|
|
(comment
|
2020-09-23 07:14:25 +00:00
|
|
|
(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)
|
2020-09-25 02:07:32 +00:00
|
|
|
(format {:select [:a [:b :c] [[:d :e]] [[:f :g] :h]]})
|
2020-09-23 07:14:25 +00:00
|
|
|
(format-on-expr :where [:= :id 1])
|
2020-09-21 01:48:07 +00:00
|
|
|
(format-dsl {:select [:*] :from [:table] :where [:= :id 1]})
|
2020-09-21 17:56:05 +00:00
|
|
|
(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])
|
2020-09-25 22:31:11 +00:00
|
|
|
(format {:select [:*] :from [:table] :where [:= :id (int 1)]} {:dialect :mysql})
|
|
|
|
|
(map fn? (format {:select [:*] :from [:table] :where [:= :id (with-meta (constantly 42) {:foo true})]} {:dialect :mysql}))
|
2020-09-21 17:56:05 +00:00
|
|
|
(format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {})
|
2020-09-21 01:48:07 +00:00
|
|
|
,)
|