improve performance of optional arguments

Signed-off-by: Sean Corfield <sean@corfield.org>
This commit is contained in:
Sean Corfield 2024-10-11 09:12:07 -07:00
parent 203e923f99
commit a187ba98f1
No known key found for this signature in database
2 changed files with 216 additions and 203 deletions

View file

@ -2,6 +2,7 @@
* 2.6.next in progress * 2.6.next in progress
* Fix [#548](https://github.com/seancorfield/honeysql/issues/548) which was a regression introduced in [#526](https://github.com/seancorfield/honeysql/issues/526). * Fix [#548](https://github.com/seancorfield/honeysql/issues/548) which was a regression introduced in [#526](https://github.com/seancorfield/honeysql/issues/526).
* Replace all optional argument destructuring with multiple arities to improve performance.
* 2.6.1196 -- 2024-10-06 * 2.6.1196 -- 2024-10-06
* Address [#547](https://github.com/seancorfield/honeysql/issues/547) by adding examples of conditional SQL building with the helpers to the README and the `honey.sql.helpers` ns docstring. * Address [#547](https://github.com/seancorfield/honeysql/issues/547) by adding examples of conditional SQL building with the helpers to the README and the `honey.sql.helpers` ns docstring.

View file

@ -279,48 +279,49 @@
return the equivalent SQL fragment (as a string -- no parameters). return the equivalent SQL fragment (as a string -- no parameters).
Handles quoting, splitting at / or ., replacing - with _ etc." Handles quoting, splitting at / or ., replacing - with _ etc."
[e & [{:keys [aliased drop-ns]}]] ([e] (format-entity e {}))
(let [e (if (and aliased (keyword? e) (str/starts-with? (name e) "'")) ([e {:keys [aliased drop-ns]}]
(let [e (if (and aliased (keyword? e) (str/starts-with? (name e) "'"))
;; #497 quoted alias support (should behave like string) ;; #497 quoted alias support (should behave like string)
(subs (name e) 1) (subs (name e) 1)
e) e)
col-fn (or (:col-fn *dialect*) col-fn (or (:col-fn *dialect*)
(if (or *quoted* (string? e)) (if (or *quoted* (string? e))
(if *quoted-snake* name-_ name) (if *quoted-snake* name-_ name)
name-_)) name-_))
col-e (col-fn e) col-e (col-fn e)
dialect-q (:quote *dialect* identity) dialect-q (:quote *dialect* identity)
quote-fn (cond (or *quoted* (string? e)) quote-fn (cond (or *quoted* (string? e))
dialect-q dialect-q
;; #422: if default quoting and "unusual" ;; #422: if default quoting and "unusual"
;; characters in entity, then quote it: ;; characters in entity, then quote it:
(nil? *quoted*) (nil? *quoted*)
(fn opt-quote [part] (fn opt-quote [part]
(cond (and *quoted-always* (cond (and *quoted-always*
(re-find *quoted-always* part)) (re-find *quoted-always* part))
(dialect-q part) (dialect-q part)
(re-find alphanumeric part) (re-find alphanumeric part)
part part
:else :else
(dialect-q part))) (dialect-q part)))
*quoted-always* *quoted-always*
(fn always-quote [part] (fn always-quote [part]
(if (re-find *quoted-always* part) (if (re-find *quoted-always* part)
(dialect-q part) (dialect-q part)
part)) part))
:else :else
identity) identity)
parts-fn (or (:parts-fn *dialect*) parts-fn (or (:parts-fn *dialect*)
#(if-let [n (when-not (or drop-ns (string? e)) #(if-let [n (when-not (or drop-ns (string? e))
(namespace-_ e))] (namespace-_ e))]
[n %] [n %]
(if aliased (if aliased
[%] [%]
(str/split % #"\.")))) (str/split % #"\."))))
parts (parts-fn col-e) parts (parts-fn col-e)
entity (join "." (map #(cond-> % (not= "*" %) (quote-fn))) parts)] entity (join "." (map #(cond-> % (not= "*" %) (quote-fn))) parts)]
(suspicious-entity-check entity) (suspicious-entity-check entity)
entity)) entity)))
(comment (comment
(for [v [:foo-bar "foo-bar" ; symbol is the same as keyword (for [v [:foo-bar "foo-bar" ; symbol is the same as keyword
@ -415,41 +416,45 @@
[x] [x]
(upper-case (str/replace (name x) "-" "_"))) (upper-case (str/replace (name x) "-" "_")))
(defn- format-simple-var [x & [c opts]] (defn- format-simple-var
(let [c (or c ([x]
(if (keyword? x) (let [c (if (keyword? x)
#?(:clj (str (.sym ^clojure.lang.Keyword x)) ;; Omits leading colon #?(:clj (str (.sym ^clojure.lang.Keyword x)) ;; Omits leading colon
:default (subs (str x) 1)) :default (subs (str x) 1))
(str x)))] (str x))]
(if (str/starts-with? c "'") (format-simple-var x c {})))
(do ([x c opts]
(reset! *formatted-column* true) (if (str/starts-with? c "'")
[(subs c 1)]) (do
[(format-entity x opts)]))) (reset! *formatted-column* true)
[(subs c 1)])
[(format-entity x opts)])))
(defn- format-var [x & [opts]] (defn- format-var
;; rather than name/namespace, we want to allow ([x] (format-var x {}))
;; for multiple / in the %fun.call case so that ([x opts]
;; qualified column names can be used: ;; rather than name/namespace, we want to allow
(let [c (if (keyword? x) ;; for multiple / in the %fun.call case so that
#?(:clj (str (.sym ^clojure.lang.Keyword x)) ;; Omits leading colon ;; qualified column names can be used:
:default (subs (str x) 1)) (let [c (if (keyword? x)
(str x))] #?(:clj (str (.sym ^clojure.lang.Keyword x)) ;; Omits leading colon
(cond (str/starts-with? c "%") :default (subs (str x) 1))
(let [[f & args] (str/split (subs c 1) #"\.")] (str x))]
[(str (format-fn-name f) "(" (cond (str/starts-with? c "%")
(join ", " (map #(format-entity (keyword %) opts)) args) (let [[f & args] (str/split (subs c 1) #"\.")]
")")]) [(str (format-fn-name f) "("
(str/starts-with? c "?") (join ", " (map #(format-entity (keyword %) opts)) args)
(let [k (keyword (subs c 1))] ")")])
(cond *inline* (str/starts-with? c "?")
[(sqlize-value (param-value k))] (let [k (keyword (subs c 1))]
*numbered* (cond *inline*
(->numbered-param k) [(sqlize-value (param-value k))]
:else *numbered*
["?" (->param k)])) (->numbered-param k)
:else :else
(format-simple-var x c opts)))) ["?" (->param k)]))
:else
(format-simple-var x c opts)))))
(defn- format-entity-alias [x] (defn- format-entity-alias [x]
(cond (sequential? x) (cond (sequential? x)
@ -577,30 +582,31 @@
"If the expression has metadata, format it as a sequence of keywords, "If the expression has metadata, format it as a sequence of keywords,
treating `:foo true` as `FOO` and `:foo :bar` as `FOO BAR`. treating `:foo true` as `FOO` and `:foo :bar` as `FOO BAR`.
Return nil if there is no metadata." Return nil if there is no metadata."
[x & [sep]] ([x] (format-meta x nil))
(when-let [data (meta x)] ([x sep]
(let [items (reduce-kv (fn [acc k v] (when-let [data (meta x)]
(cond (number? v) (let [items (reduce-kv (fn [acc k v]
(conj acc (str v)) (cond (number? v)
(true? v) (conj acc (str v))
(conj acc k) (true? v)
(ident? v) (conj acc k)
(conj acc k v) (ident? v)
(string? v) (conj acc k v)
(do (string? v)
(suspicious-entity-check v) (do
(conj acc k v)) (suspicious-entity-check v)
:else ; quietly ignore other metadata (conj acc k v))
acc)) :else ; quietly ignore other metadata
[] acc))
(reduce dissoc []
data (reduce dissoc
(into [; remove the somewhat "standard" metadata: data
:line :column :file (into [; remove the somewhat "standard" metadata:
:end-line :end-column] :line :column :file
*ignored-metadata*)))] :end-line :end-column]
(when (seq items) *ignored-metadata*)))]
(join (str sep " ") (map sql-kw) items))))) (when (seq items)
(join (str sep " ") (map sql-kw) items))))))
(comment (comment
(format-meta ^{:foo true :bar :baz :original {:line 1} :top 10} []) (format-meta ^{:foo true :bar :baz :original {:line 1} :top 10} [])
@ -650,23 +656,25 @@
(into params') (into params')
(into params''))))) (into params'')))))
(defn- format-selectable-dsl [x & [{:keys [as aliased] :as opts}]] (defn- format-selectable-dsl
(cond (map? x) ([x] (format-selectable-dsl x {}))
(format-dsl x {:nested true}) ([x {:keys [as aliased] :as opts}]
(cond (map? x)
(format-dsl x {:nested true})
(sequential? x) (sequential? x)
(format-item-selection x as) (format-item-selection x as)
(ident? x) (ident? x)
(if aliased (if aliased
[(format-entity x opts)] [(format-entity x opts)]
(format-var x opts)) (format-var x opts))
(and aliased (string? x)) (and aliased (string? x))
[(format-entity x opts)] [(format-entity x opts)]
:else :else
(format-expr x))) (format-expr x))))
(defn- reduce-sql (defn- reduce-sql
([xs] (reduce-sql identity xs)) ([xs] (reduce-sql identity xs))
@ -699,31 +707,32 @@
This allows for argument lists like: This allows for argument lists like:
* [:overlay :foo :*placing :?subs :*from 3 :*for 4] * [:overlay :foo :*placing :?subs :*from 3 :*for 4]
* [:trim :*leading-from :bar]" * [:trim :*leading-from :bar]"
[args & [opts]] ([args] (format-interspersed-expr-list args {}))
(loop [exprs (keep #(when-not (inline-kw? %) ([args opts]
(format-expr % opts)) (loop [exprs (keep #(when-not (inline-kw? %)
args) (format-expr % opts))
args args args)
prev-in false args args
result []] prev-in false
(if (seq args) result []]
(let [[arg & args'] args] (if (seq args)
(if (inline-kw? arg) (let [[arg & args'] args]
(let [sql (sql-kw (keyword (subs (name arg) 1)))] (if (inline-kw? arg)
(if (seq result) (let [sql (sql-kw (keyword (subs (name arg) 1)))]
(let [[cur & params] (peek result)] (if (seq result)
(recur exprs args' true (conj (pop result) (let [[cur & params] (peek result)]
(into [(str cur " " sql)] params)))) (recur exprs args' true (conj (pop result)
(recur exprs args' true (conj result [sql])))) (into [(str cur " " sql)] params))))
(if prev-in (recur exprs args' true (conj result [sql]))))
(let [[cur & params] (peek result) (if prev-in
[sql & params'] (first exprs)] (let [[cur & params] (peek result)
(recur (rest exprs) args' false (conj (pop result) [sql & params'] (first exprs)]
(-> [(str cur " " sql)] (recur (rest exprs) args' false (conj (pop result)
(into params) (-> [(str cur " " sql)]
(into params'))))) (into params)
(recur (rest exprs) args' false (conj result (first exprs)))))) (into params')))))
(reduce-sql result)))) (recur (rest exprs) args' false (conj result (first exprs))))))
(reduce-sql result)))))
(comment (comment
(format-interspersed-expr-list [:foo :*placing :?subs :*from 3 :*for 4] (format-interspersed-expr-list [:foo :*placing :?subs :*from 3 :*for 4]
@ -744,12 +753,13 @@
This is intended to be used when writing your own formatters to This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL." extend the DSL supported by HoneySQL."
[exprs & [opts]] ([exprs] (format-expr-list exprs {}))
(when-not (sequential? exprs) ([exprs opts]
(throw (ex-info (str "format-expr-list expects a sequence of expressions, found: " (when-not (sequential? exprs)
(type exprs)) (throw (ex-info (str "format-expr-list expects a sequence of expressions, found: "
{:exprs exprs}))) (type exprs))
(reduce-sql (map #(format-expr % opts)) exprs)) {:exprs exprs})))
(reduce-sql (map #(format-expr % opts)) exprs)))
(comment (comment
(format-expr-list :?tags) (format-expr-list :?tags)
@ -1617,34 +1627,35 @@
This is intended to be used when writing your own formatters to This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL." extend the DSL supported by HoneySQL."
[statement-map & [{:keys [aliased nested pretty]}]] ([statement-map] (format-dsl statement-map {}))
(binding [*dsl* statement-map] ([statement-map {:keys [aliased nested pretty]}]
(let [[sqls params leftover] (binding [*dsl* statement-map]
(reduce (fn [[sql params leftover] k] (let [[sqls params leftover]
(if-some [xs (if-some [xs (k leftover)] (reduce (fn [[sql params leftover] k]
xs (if-some [xs (if-some [xs (k leftover)]
(let [s (kw->sym k)] xs
(get leftover s)))] (let [s (kw->sym k)]
(let [formatter (k @clause-format) (get leftover s)))]
[sql' & params'] (formatter k xs)] (let [formatter (k @clause-format)
[(conj sql sql') [sql' & params'] (formatter k xs)]
(if params' (into params params') params) [(conj sql sql')
(dissoc leftover k (kw->sym k))]) (if params' (into params params') params)
[sql params leftover])) (dissoc leftover k (kw->sym k))])
[[] [] statement-map] [sql params leftover]))
*clause-order*)] [[] [] statement-map]
(if (seq leftover) *clause-order*)]
(throw (ex-info (str "These SQL clauses are unknown or have nil values: " (if (seq leftover)
(join ", " (keys leftover)) (throw (ex-info (str "These SQL clauses are unknown or have nil values: "
"(perhaps you need [:lift {" (join ", " (keys leftover))
(first (keys leftover)) "(perhaps you need [:lift {"
" ...}] here?)") (first (keys leftover))
leftover)) " ...}] here?)")
(into [(cond-> (join (if pretty "\n" " ") (remove empty?) sqls) leftover))
pretty (into [(cond-> (join (if pretty "\n" " ") (remove empty?) sqls)
(as-> s (str "\n" s "\n")) pretty
(and nested (not aliased)) (as-> s (str "\n" s "\n"))
(as-> s (str "(" s ")")))] params))))) (and nested (not aliased))
(as-> s (str "(" s ")")))] params))))))
(def ^:private infix-aliases (def ^:private infix-aliases
"Provided for backward compatibility with earlier HoneySQL versions." "Provided for backward compatibility with earlier HoneySQL versions."
@ -2031,45 +2042,46 @@
This is intended to be used when writing your own formatters to This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL." extend the DSL supported by HoneySQL."
[expr & [{:keys [nested] :as opts}]] ([expr] (format-expr expr {}))
(cond (ident? expr) ([expr {:keys [nested] :as opts}]
(format-var expr opts) (cond (ident? expr)
(format-var expr opts)
(map? expr) (map? expr)
(format-dsl expr (assoc opts :nested true)) (format-dsl expr (assoc opts :nested true))
(sequential? expr) (sequential? expr)
(let [op' (sym->kw (first expr)) (let [op' (sym->kw (first expr))
op (get infix-aliases op' op')] op (get infix-aliases op' op')]
(if (keyword? op') (if (keyword? op')
(cond (contains? @infix-ops op') (cond (contains? @infix-ops op')
(if (contains? #{:= :<>} op) (if (contains? #{:= :<>} op)
(format-equality-expr op' op expr nested) (format-equality-expr op' op expr nested)
(format-infix-expr op' op expr nested)) (format-infix-expr op' op expr nested))
(contains? #{:in :not-in} op) (contains? #{:in :not-in} op)
(let [[sql & params] (format-in op (rest expr))] (let [[sql & params] (format-in op (rest expr))]
(into [(if nested (str "(" sql ")") sql)] params)) (into [(if nested (str "(" sql ")") sql)] params))
(contains? @special-syntax op) (contains? @special-syntax op)
(let [formatter (get @special-syntax op)] (let [formatter (get @special-syntax op)]
(formatter op (rest expr))) (formatter op (rest expr)))
:else :else
(format-fn-call-expr op expr)) (format-fn-call-expr op expr))
(let [[sqls params] (format-expr-list expr)] (let [[sqls params] (format-expr-list expr)]
(into [(str "(" (join ", " sqls) ")")] params)))) (into [(str "(" (join ", " sqls) ")")] params))))
(boolean? expr) (boolean? expr)
[(upper-case (str expr))] [(upper-case (str expr))]
(nil? expr) (nil? expr)
["NULL"] ["NULL"]
:else :else
(cond *inline* (cond *inline*
[(sqlize-value expr)] [(sqlize-value expr)]
*numbered* *numbered*
(->numbered expr) (->numbered expr)
:else :else
["?" expr]))) ["?" expr]))))
(defn- check-dialect [dialect] (defn- check-dialect [dialect]
(when-not (contains? @dialects dialect) (when-not (contains? @dialects dialect)