honeysql/src/honey/sql.cljc

869 lines
34 KiB
Text
Raw Normal View History

2021-01-30 00:11:52 +00:00
;; copyright (c) 2020-2021 sean corfield, all rights reserved
2020-09-21 01:48:07 +00:00
(ns honey.sql
2021-02-01 20:00:42 +00:00
"Primary API for HoneySQL 2.x.
This includes the `format` function -- the primary entry point -- as well
as several public formatters that are intended to help users extend the
supported syntax.
In addition, functions to extend HoneySQL are also provided here:
* `sql-kw` -- turns a Clojure keyword into SQL code (makes it uppercase
and replaces - with space).
* `format-dsl` -- intended to format SQL statements; returns a vector
containing a SQL string followed by parameter values.
* `format-expr` -- intended to format SQL expressions; returns a vector
containing a SQL string followed by parameter values.
* `format-expr-list` -- intended to format a list of SQL expressions;
returns a pair comprising: a sequence of SQL expressions (to be
join with a delimiter) and a sequence of parameter values.
* `set-dialect!` -- set the default dialect to be used for formatting.
* `register-clause!` -- register a new statement/clause formatter.
* `register-fn!` -- register a new function call (or special syntax)
formatter.
* `register-op!` -- register a new operator formatter."
2020-09-21 01:48:07 +00:00
(: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
(declare clause-format)
(def ^:private default-clause-order
"The (default) order for known clauses. Can have items added and removed."
[:nest :with :with-recursive :intersect :union :union-all :except :except-all
:select :select-distinct :insert-into :update :delete :delete-from :truncate
2021-01-30 19:19:12 +00:00
:columns :set :from :using
:join :left-join :right-join :inner-join :outer-join :full-join
:cross-join
:where :group-by :having :order-by :limit :offset :for :values
2020-09-26 22:16:12 +00:00
:on-conflict :on-constraint :do-nothing :do-update-set
2020-09-26 07:39:54 +00:00
:returning])
(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 \" % \")}
:sqlserver {:quote #(str \[ % \])}
:mysql {:quote #(str \` % \`)
:clause-order-fn (fn [order]
;; :lock is like :for
(swap! clause-format assoc :lock
(get @clause-format :for))
;; MySQL :set has different priority
;; and :lock is between :for and :values
(-> (filterv (complement #{:set}) order)
(add-clause-before :set :where)
(add-clause-before :lock :values)))}
:oracle {:quote #(str \" % \") :as false}})
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)
(def ^:private ^:dynamic *inline* nil)
(def ^:private ^:dynamic *params* nil)
2020-09-21 01:48:07 +00:00
;; 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))
2021-02-01 20:00:42 +00:00
(defn sql-kw
"Given a keyword, return a SQL representation of it as a string.
A `:kebab-case` keyword becomes a `KEBAB CASE` (uppercase) string
with hyphens replaced by spaces, e.g., `:insert-into` => `INSERT INTO`."
[k]
(-> k (name) (upper-case)
(as-> s (if (= "-" s) s (str/replace s "-" " ")))))
2020-09-21 01:48:07 +00:00
2020-10-13 05:54:46 +00:00
(defn- namespace-_ [x] (some-> (namespace x) (str/replace "-" "_")))
(defn- name-_ [x] (str/replace (name x) "-" "_"))
2021-02-01 21:10:57 +00:00
(defn- format-entity [x & [{:keys [aliased drop-ns]}]]
2020-10-13 05:54:46 +00:00
(let [nn (if (or *quoted* (string? x)) name name-_)
q (if (or *quoted* (string? x)) (:quote *dialect*) identity)
2021-02-01 21:10:57 +00:00
[t c] (if-let [n (when-not (or drop-ns (string? x))
2020-10-13 05:54:46 +00:00
(namespace-_ x))]
[n (nn x)]
2021-02-01 21:10:57 +00:00
(if aliased
2020-10-13 05:54:46 +00:00
[nil (nn x)]
(let [[t c] (str/split (nn x) #"\.")]
(if c [t c] [nil t]))))]
2020-09-21 01:48:07 +00:00
(cond->> c
(not= "*" c)
(q)
t
(str (q t) "."))))
(defn- ->param [k]
(with-meta (constantly k)
{::wrapper
(fn [fk _]
(let [k (fk)]
(if (contains? *params* k)
(get *params* k)
(throw (ex-info (str "missing parameter value for " k)
{:params (keys *params*)})))))}))
(defn- format-var [x & [opts]]
2020-10-13 05:54:46 +00:00
(let [c (name-_ x)]
(cond (= \% (first c))
(let [[f & args] (str/split (subs c 1) #"\.")]
;; TODO: this does not quote arguments -- does that matter?
[(str f "(" (str/join "," args) ")")])
(= \? (first c))
["?" (->param (keyword (subs c 1)))]
:else
[(format-entity x opts)])))
2020-09-21 01:48:07 +00:00
(defn- format-entity-alias [x]
(cond (sequential? x)
(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 (if (and (contains? *dialect* :as) (not (:as *dialect*))) " " " AS ")
2021-02-01 21:10:57 +00:00
(format-entity (second x) {:aliased true}))))
:else
(format-entity x)))
2021-02-01 21:10:57 +00:00
(defn- format-selectable-dsl [x & [{:keys [as aliased] :as opts}]]
(cond (map? x)
2021-02-01 21:10:57 +00:00
(format-dsl x {:nested true})
(sequential? x)
(let [s (first x)
pair? (< 1 (count x))
a (second x)
[sql & params] (if (map? s)
2021-02-01 21:10:57 +00:00
(format-dsl s {:nested true})
(format-expr s))
[sql' & params'] (when pair?
(if (sequential? a)
2021-02-01 21:10:57 +00:00
(let [[sql params] (format-expr-list a {:aliased true})]
(into [(str/join " " sql)] params))
2021-02-01 21:10:57 +00:00
(format-selectable-dsl a {:aliased true})))]
(-> [(cond-> sql
pair?
2021-02-01 21:10:57 +00:00
(str (if as
(if (and (contains? *dialect* :as)
(not (:as *dialect*)))
" "
" AS ")
" ") sql'))]
(into params)
(into params')))
(or (keyword? x) (symbol? x))
2021-02-01 21:10:57 +00:00
(if aliased
[(format-entity x opts)]
(format-var x opts))
2021-02-01 21:10:57 +00:00
(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)])
[[] []]
2021-02-01 21:10:57 +00:00
(map #(format-dsl % {:nested true}) xs))]
(into [(str/join (str " " (sql-kw k) " ") sqls)] params)))
2021-02-01 20:00:42 +00:00
(defn format-expr-list
"Given a sequence of expressions represented as data, return a pair
where the first element is a sequence of SQL fragments and the second
element is a sequence of parameters. The caller should join the SQL
fragments with whatever appropriate delimiter is needed and then
return a vector whose first element is the complete SQL string and
whose subsequent elements are the parameters:
(let [[sqls params] (format-expr-list data opts)]
(into [(str/join delim sqls)] params))
This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL."
[exprs & [opts]]
(reduce (fn [[sql params] [sql' & params']]
[(conj sql sql') (if params' (into params params') params)])
[[] []]
2021-02-01 20:00:42 +00:00
(map #(format-expr % opts) exprs)))
2020-09-29 02:24:17 +00:00
(defn- format-columns [k xs]
2021-02-01 21:10:57 +00:00
(let [[sqls params] (format-expr-list xs {:drop-ns (= :columns k)})]
(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)])
[[] []]
2021-02-01 21:10:57 +00:00
(map #(format-selectable-dsl % {:as (#{:select :from} k)}) xs))]
(into [(str (sql-kw k) " " (str/join ", " sqls))] params))
2021-02-01 21:10:57 +00:00
(let [[sql & params] (format-selectable-dsl xs {:as (#{:select :from} k)})]
(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)]
;; according to docs, CTE should _always_ be wrapped:
(cond-> [(str sql " AS " (str "(" 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]
(if (sequential? table)
2020-09-28 19:12:40 +00:00
(cond (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))
(map? (second table))
(let [[table statement] table
[sql & params] (format-dsl statement)]
(into [(str (sql-kw k) " " (format-entity-alias table)
" " sql)]
params))
:else
[(str (sql-kw k) " " (format-entity-alias table))])
[(str (sql-kw k) " " (format-entity-alias table))]))
2020-09-21 01:48:07 +00:00
2020-10-13 05:54:59 +00:00
(defn- format-join [k clauses]
(let [[sqls params]
(reduce (fn [[sqls params] [j e]]
2021-01-30 19:00:34 +00:00
(let [sqls (conj sqls
2020-10-13 05:54:59 +00:00
(sql-kw (if (= :join k) :inner-join k))
2021-01-30 19:00:34 +00:00
(format-entity-alias j))]
(if (and (sequential? e) (= :using (first e)))
[(conj sqls
"USING"
(str "("
(str/join ", " (map #'format-entity-alias (rest e)))
")"))
params]
(let [[sql & params'] (when e (format-expr e))]
[(cond-> sqls e (conj "ON" sql))
(into params params')]))))
2020-10-13 05:54:59 +00:00
[[] []]
(partition 2 clauses))]
(into [(str/join " " sqls)] params)))
2020-09-21 01:48:07 +00:00
(defn- format-on-expr [k e]
(if (or (not (sequential? e)) (seq e))
(let [[sql & params] (format-expr e)]
(into [(str (sql-kw k) " " sql)] params))
[]))
2020-09-21 01:48:07 +00:00
(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-lock-strength [k xs]
(let [[strength tables nowait] (if (sequential? xs) xs [xs])]
[(str (sql-kw k) " " (sql-kw strength)
(when tables
(str
(cond (#{:nowait :skip-locked :wait} tables)
(str " " (sql-kw tables))
(sequential? tables)
(str " OF "
(str/join ", " (map #'format-entity tables)))
:else
(str " OF " (format-entity tables)))
(when nowait
(str " " (sql-kw nowait))))))]))
(defn- format-values [k xs]
(cond (sequential? (first xs))
;; [[1 2 3] [4 5 6]]
(let [n-1 (map count xs)
;; issue #291: ensure all value sequences are the same length
xs' (if (apply = n-1)
xs
(let [n-n (apply max n-1)]
(map (fn [x] (take n-n (concat x (repeat nil)))) xs)))
[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-1 (keys (first xs))
;; issue #291: check for all keys in all maps but still
;; use the keys from the first map if they match so that
;; users can rely on the key ordering if they want to,
;; e.g., see test that uses array-map for the first row
cols-n (into #{} (mapcat keys) xs)
cols (if (= (set cols-1) cols-n) cols-1 cols-n)
[sqls params]
(reduce (fn [[sql params] [sqls' params']]
2020-10-10 06:58:55 +00:00
[(conj sql (str "(" (str/join ", " sqls') ")"))
(if params' (into params params') params')])
[[] []]
(map (fn [m]
(format-expr-list (map #(get m %) cols)))
xs))]
(into [(str "("
(str/join ", "
2021-02-01 21:10:57 +00:00
(map #(format-entity % {:drop-ns true}) cols))
") "
2020-10-10 06:58:55 +00:00
(sql-kw k)
" "
(str/join ", " sqls))]
params))
:else
(throw (ex-info ":values expects sequences or maps"
{:first (first xs)}))))
(comment
(into #{} (mapcat keys) [{:a 1 :b 2} {:b 3 :c 4}])
,)
(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)))
2020-09-26 22:16:12 +00:00
(defn- format-on-conflict [k x]
(if (or (keyword? x) (symbol? x))
2020-09-26 22:16:12 +00:00
[(str (sql-kw k) " (" (format-entity x) ")")]
(let [[sql & params] (format-dsl x)]
(into [(str (sql-kw k) " " sql)] params))))
(defn- format-do-update-set [k x]
(if (or (keyword? x) (symbol? x))
2021-02-01 21:10:57 +00:00
(let [e (format-entity x {:drop-ns true})]
2020-09-26 22:16:12 +00:00
[(str (sql-kw k) " " e " = EXCLUDED." e)])
(format-set-exprs k x)))
2020-09-26 22:16:12 +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))
(def ^:private current-clause-order
"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."
(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 {:nest (fn [_ x] (format-expr x))
: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
:except-all #'format-on-set-op
:select #'format-selects
:select-distinct #'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
2021-01-30 19:19:12 +00:00
:using #'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
:for #'format-lock-strength
:values #'format-values
:on-conflict #'format-on-conflict
:on-constraint #'format-selector
:do-nothing (fn [k _] (vector (sql-kw k)))
:do-update-set #'format-do-update-set
:returning #'format-selects}))
(assert (= (set @base-clause-order)
(set @current-clause-order)
(set (keys @clause-format))))
2021-02-01 20:00:42 +00:00
(defn format-dsl
"Given a hash map representing a SQL statement and a hash map
of options, return a vector containing a string -- the formatted
SQL statement -- followed by any parameter values that SQL needs.
This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL."
2021-02-01 21:10:57 +00:00
[statement-map & [{:keys [aliased nested pretty]}]]
(let [[sqls params leftover]
(reduce (fn [[sql params leftover] k]
2021-02-01 20:00:42 +00:00
(if-let [xs (or (k statement-map)
(let [s (symbol (name k))]
(get statement-map s)))]
2020-09-21 01:48:07 +00:00
(let [formatter (k @clause-format)
[sql' & params'] (formatter k xs)]
[(conj sql sql')
(if params' (into params params') params)
(dissoc leftover k (symbol (name k)))])
[sql params leftover]))
2021-02-01 20:00:42 +00:00
[[] [] statement-map]
*clause-order*)]
(if (seq leftover)
2021-02-01 20:00:42 +00:00
(throw (ex-info (str "Unknown SQL clauses: "
(str/join ", " (keys leftover)))
leftover))
2021-02-01 21:10:57 +00:00
(into [(cond-> (str/join (if pretty "\n" " ") (filter seq sqls))
pretty
(as-> s (str "\n" s "\n"))
2021-02-01 21:10:57 +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
2020-09-28 18:49:13 +00:00
(-> #{"mod" "and" "or" "xor" "<>" "<=" ">=" "||"
"like" "not-like" "regexp"
"ilike" "not-ilike" "similar-to" "not-similar-to"
2020-09-21 01:48:07 +00:00
"is" "is-not" "not=" "!=" "regex"}
2021-01-31 01:27:00 +00:00
(into (map str "+-*%|&^=<>"))
2020-09-21 01:48:07 +00:00
(into (keys infix-aliases))
(into (vals infix-aliases))
(->> (into #{} (map keyword)))
2021-01-31 01:27:00 +00:00
(conj :/) ; because (keyword "/") does not work in cljs
(atom)))
(def ^:private op-ignore-nil (atom #{:and :or}))
(def ^:private op-variadic (atom #{:and :or :+ :* :||}))
2020-09-21 01:48:07 +00:00
(defn- sqlize-value [x]
(cond
(nil? x) "NULL"
2020-09-25 23:38:11 +00:00
(string? x) (str \' (str/replace x "'" "''") \')
(symbol? x) (sql-kw x)
(keyword? x) (sql-kw x)
:else (str x)))
(defn- unwrap [x opts]
(if-let [m (meta x)]
(if-let [f (::wrapper m)]
(f x opts)
x)
x))
(defn- format-in [in [x y]]
2021-02-01 21:10:57 +00:00
(let [[sql-x & params-x] (format-expr x {:nested true})
[sql-y & params-y] (format-expr y {:nested true})
values (unwrap (first params-y) {})]
(if (and (= "?" sql-y) (= 1 (count params-y)) (coll? values))
(let [sql (str "(" (str/join ", " (repeat (count values) "?")) ")")]
(-> [(str sql-x " " (sql-kw in) " " sql)]
(into params-x)
(into values)))
(-> [(str sql-x " " (sql-kw in) " " sql-y)]
(into params-x)
(into params-y)))))
2020-09-21 01:48:07 +00:00
(def ^:private special-syntax
(atom
{:array
(fn [_ [arr]]
(let [[sqls params] (format-expr-list arr)]
(into [(str "ARRAY[" (str/join ", " sqls) "]")] params)))
:between
(fn [_ [x a b]]
2021-02-01 21:10:57 +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})]
(-> [(str sql-x " BETWEEN " sql-a " AND " sql-b)]
(into params-x)
(into params-a)
(into params-b))))
2020-10-13 01:38:05 +00:00
:case
(fn [_ clauses]
(let [[sqls params]
(reduce (fn [[sqls params] [condition value]]
(let [[sqlc & paramsc] (when-not (= :else condition)
(format-expr condition))
[sqlv & paramsv] (format-expr value)]
[(if (= :else condition)
(conj sqls (sql-kw :else) sqlv)
(conj sqls (sql-kw :when) sqlc (sql-kw :then) sqlv))
(-> params (into paramsc) (into paramsv))]))
[[] []]
(partition 2 clauses))]
(into [(str (sql-kw :case) " "
(str/join " " sqls)
" " (sql-kw :end))]
params)))
:cast
(fn [_ [x type]]
(let [[sql & params] (format-expr x)
[sql' & params'] (format-expr type)]
(-> [(str "CAST(" sql " AS " sql' ")")]
(into params)
(into params'))))
:composite
(fn [_ [& args]]
(let [[sqls params] (format-expr-list args)]
(into [(str "(" (str/join ", " sqls) ")")] params)))
:default
(fn [_ []]
["DEFAULT"])
:inline
(fn [_ [x]]
(if (sequential? x)
[(str/join " " (map #'sqlize-value x))]
[(sqlize-value x)]))
:interval
(fn [_ [n units]]
(let [[sql & params] (format-expr n)]
(into [(str "INTERVAL " sql " " (sql-kw units))] params)))
2020-10-02 20:43:44 +00:00
:lift
(fn [_ [x]]
2021-01-31 13:31:10 +00:00
["?" (with-meta (constantly x)
{::wrapper (fn [fx _] (fx))})])
:nest
(fn [_ [x]]
2021-02-01 21:10:57 +00:00
(format-expr x {:nested true}))
:not
(fn [_ [x]]
(let [[sql & params] (format-expr x)]
(into [(str "NOT " sql)] params)))
:param
(fn [_ [k]]
["?" (->param k)])
:raw
(fn [_ [s]]
(if (sequential? s)
2021-01-30 20:35:51 +00:00
(let [[sqls params]
(reduce (fn [[sqls params] s]
(if (vector? s)
(let [[sql & params'] (format-expr s)]
[(conj sqls sql)
(into params params')])
[(conj sqls s) params]))
[[] []]
s)]
(into [(str/join sqls)] params))
[s]))}))
2020-09-21 01:48:07 +00:00
2021-02-01 20:00:42 +00:00
(defn format-expr
"Given a data structure that represents a SQL expression and a hash
map of options, return a vector containing a string -- the formatted
SQL statement -- followed by any parameter values that SQL needs.
2020-09-21 01:48:07 +00:00
2021-02-01 20:00:42 +00:00
This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL."
2021-02-01 21:10:57 +00:00
[expr & [{:keys [nested] :as opts}]]
2021-02-01 20:00:42 +00:00
(cond (or (keyword? expr) (symbol? expr))
(format-var expr opts)
2021-02-01 20:00:42 +00:00
(map? expr)
2021-02-01 21:10:57 +00:00
(format-dsl expr (assoc opts :nested true))
2021-02-01 20:00:42 +00:00
(sequential? expr)
(let [op (first expr)
;; normalize symbols to keywords here -- makes the subsequent
;; logic easier since we use op to lookup things in hash maps:
op (if (symbol? op) (keyword (name op)) op)]
2020-09-21 01:48:07 +00:00
(if (keyword? op)
(cond (contains? @infix-ops op)
(if (contains? @op-variadic op) ; no aliases here, no special semantics
2021-02-01 20:00:42 +00:00
(let [x (if (contains? @op-ignore-nil op)
(remove nil? expr)
expr)
2020-09-25 23:40:15 +00:00
[sqls params]
(reduce (fn [[sql params] [sql' & params']]
[(conj sql sql')
(if params' (into params params') params)])
[[] []]
2021-02-01 21:10:57 +00:00
(map #(format-expr % {:nested true})
2020-09-25 23:40:15 +00:00
(rest x)))]
(into [(cond-> (str/join (str " " (sql-kw op) " ") sqls)
2021-02-01 21:10:57 +00:00
nested
2020-09-25 23:40:15 +00:00
(as-> s (str "(" s ")")))]
params))
2021-02-01 20:00:42 +00:00
(let [[_ a b & y] expr
2020-09-25 23:40:15 +00:00
_ (when (seq y)
(throw (ex-info (str "only binary "
op
2020-10-10 06:59:43 +00:00
" is supported")
2021-02-01 20:00:42 +00:00
{:expr expr})))
2021-02-01 21:10:57 +00:00
[s1 & p1] (format-expr a {:nested true})
[s2 & p2] (format-expr b {:nested true})
2020-09-25 23:40:15 +00:00
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"))
2021-02-01 21:10:57 +00:00
(cond-> nested
2020-09-25 23:40:15 +00:00
(as-> s (str "(" s ")")))
(vector))
(-> (str s1 " " (sql-kw op) " " s2)
2021-02-01 21:10:57 +00:00
(cond-> nested
2020-09-25 23:40:15 +00:00
(as-> s (str "(" s ")")))
(vector)
(into p1)
(into p2)))))
(contains? #{:in :not-in} op)
2021-02-01 20:00:42 +00:00
(let [[sql & params] (format-in op (rest expr))]
2021-02-01 21:10:57 +00:00
(into [(if nested (str "(" sql ")") sql)] params))
(contains? @special-syntax op)
(let [formatter (get @special-syntax op)]
2021-02-01 20:00:42 +00:00
(formatter op (rest expr)))
2020-09-21 01:48:07 +00:00
:else
2021-02-01 20:00:42 +00:00
(let [args (rest expr)
[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)))
2021-02-01 20:00:42 +00:00
(let [[sqls params] (format-expr-list expr)]
(into [(str "(" (str/join ", " sqls) ")")] params))))
2020-09-21 01:48:07 +00:00
2021-02-01 20:00:42 +00:00
(boolean? expr)
[(upper-case (str expr))]
2021-02-01 20:00:42 +00:00
(nil? expr)
2020-09-28 19:12:10 +00:00
["NULL"]
2020-09-21 01:48:07 +00:00
:else
(if *inline*
2021-02-01 20:00:42 +00:00
[(sqlize-value expr)]
["?" expr])))
2020-09-21 01:48:07 +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)
(defn format
2020-09-21 01:48:07 +00:00
"Turn the data DSL into a vector containing a SQL string followed by
2021-02-01 20:00:42 +00:00
any parameter values that were encountered in the DSL structure.
This is the primary API for HoneySQL and handles dialects, quoting,
and named parameters."
([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 @base-clause-order)
@current-clause-order)
@current-clause-order)
*inline* (when (contains? opts :inline)
(:inline opts))
*quoted* (if (contains? opts :quoted)
(:quoted opts)
dialect?)
*params* (:params opts)]
(mapv #(unwrap % opts) (format-dsl data opts))))))
2020-09-21 01:48:07 +00:00
(defn set-dialect!
"Set the default dialect for formatting.
Can be: `:ansi` (the default), `:mysql`, `:oracle`, or `:sqlserver`.
Dialects are always applied to the base order to create the current order."
2020-09-21 01:48:07 +00:00
[dialect]
(reset! default-dialect (get dialects (check-dialect dialect)))
(when-let [f (:clause-order-fn @default-dialect)]
(reset! current-clause-order (f @base-clause-order))))
(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.
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 register 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."
[clause formatter before]
(assert (keyword? clause))
(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
(defn register-fn!
"Register a new function (as special syntax). The `formatter` is either
a keyword, meaning that this new function should use the same syntax as
an existing function, or a function of two arguments that generates a
SQL string and parameters (as a vector). The two arguments are the name
of the function (as a keyword) and a sequence of the arguments from the
DSL."
[function formatter]
(assert (keyword? function))
(let [f (if (keyword? formatter)
(get @special-syntax formatter)
formatter)]
(when-not (and f (fn? f))
(throw (ex-info "The formatter must be a function or existing fn name"
{:type (type formatter)})))
(swap! special-syntax assoc function f)))
(defn register-op!
"Register a new infix operator. Operators can be defined to be variadic (the
default is that they are binary) and may choose to ignore `nil` arguments
(this can make it easier to programmatically construct the DSL)."
[op & {:keys [variadic? ignore-nil?]}]
(assert (keyword? op))
(swap! infix-ops conj op)
(when variadic?
(swap! op-variadic conj op))
(when ignore-nil?
(swap! op-ignore-nil conj op)))
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 {:select [:a [:b :c] [[:d :e]] [[:f :g] :h]]})
(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]} {})
(println (format {:select [:*] :from [:table]
2021-02-01 21:10:57 +00:00
:order-by [[[:date :expiry] :desc] :bar]} {:pretty true}))
(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 (int 1)]} {:dialect :mysql})
(map fn? (format {:select [:*] :from [:table]
:where [:= :id (with-meta (constantly 42) {:foo true})]}
{:dialect :mysql}))
(println (format {:select [:*] :from [:table]
2021-02-01 21:10:57 +00:00
:where [:in :id [1 2 3 4]]} {:pretty true}))
(println (format {:select [:*] :from [:table]
:where [:and [:in :id [1 [:param :foo]]]
[:= :bar [:param :quux]]]}
{:params {:foo 42 :quux 13}
2021-02-01 21:10:57 +00:00
:pretty true}))
2020-09-21 01:48:07 +00:00
,)