This commit is contained in:
Sean Corfield 2023-02-28 17:38:13 -08:00
parent a610f256dd
commit 0936095040
3 changed files with 127 additions and 51 deletions

View file

@ -1,6 +1,7 @@
# Changes # Changes
* 2.4.next in progress * 2.4.next in progress
* Address [#471](https://github.com/seancorfield/honeysql/issues/471) by supported interspersed SQL keywords in function calls. Documentation TBD!
* Fix [#467](https://github.com/seancorfield/honeysql/issues/467) by allowing single keywords (symbols) as a short hand for a single-element sequence in more constructs via PR [#470](https://github.com/seancorfield/honeysql/pull/470) [@p-himik](https://github.com/p-himik). * Fix [#467](https://github.com/seancorfield/honeysql/issues/467) by allowing single keywords (symbols) as a short hand for a single-element sequence in more constructs via PR [#470](https://github.com/seancorfield/honeysql/pull/470) [@p-himik](https://github.com/p-himik).
* Address [#466](https://github.com/seancorfield/honeysql/issues/466) by treating `[:and]` as `TRUE` and `[:or]` as `FALSE`. * Address [#466](https://github.com/seancorfield/honeysql/issues/466) by treating `[:and]` as `TRUE` and `[:or]` as `FALSE`.
* Fix [#465](https://github.com/seancorfield/honeysql/issues/465) to allow multiple columns in `:order-by` special syntax via PR [#468](https://github.com/seancorfield/honeysql/pull/468) [@p-himik](https://github.com/p-himik). * Fix [#465](https://github.com/seancorfield/honeysql/issues/465) to allow multiple columns in `:order-by` special syntax via PR [#468](https://github.com/seancorfield/honeysql/pull/468) [@p-himik](https://github.com/p-himik).

View file

@ -465,6 +465,50 @@
(let [[sqls params] (reduce-sql (map #(format-dsl %) xs))] (let [[sqls params] (reduce-sql (map #(format-dsl %) xs))]
(into [(str/join (str " " (sql-kw k) " ") sqls)] params))) (into [(str/join (str " " (sql-kw k) " ") sqls)] params)))
(defn- inline-kw?
"Return true if the expression should be treated as an inline SQL keeyword."
[expr]
(and (ident? expr)
(nil? (namespace expr))
(re-find #"^\*[a-zA-Z]" (name expr))))
(defn format-interspersed-expr-list
"If there are inline (SQL) keywords, use them to join the formatted
expressions together. Otherwise behaves like plain format-expr-list.
This allows for argument lists like:
* [:overlay :foo :*placing :?subs :*from 3 :*for 4]
* [:trim :*leading-from :bar]"
[args & [opts]]
(loop [exprs (map #(format-expr % opts) (remove inline-kw? args))
args args
prev-in false
result []]
(if (seq args)
(let [[arg & args'] args]
(if (inline-kw? arg)
(let [sql (sql-kw (keyword (subs (name arg) 1)))]
(if (seq result)
(let [[cur & params] (peek result)]
(recur exprs args' true (conj (pop result)
(into [(str cur " " sql)] params))))
(recur exprs args' true (conj result [sql]))))
(if prev-in
(let [[cur & params] (peek result)
[sql & params'] (first exprs)]
(recur (rest exprs) args' false (conj (pop result)
(-> [(str cur " " sql)]
(into params)
(into params')))))
(recur (rest exprs) args' false (conj result (first exprs))))))
(reduce-sql result))))
(comment
(format-interspersed-expr-list [:foo :*placing :?subs :*from 3 :*for 4]
{:params {:subs "bar"}})
(format-interspersed-expr-list [:*leading-from " foo "] {})
)
(defn format-expr-list (defn format-expr-list
"Given a sequence of expressions represented as data, return a pair "Given a sequence of expressions represented as data, return a pair
where the first element is a sequence of SQL fragments and the second where the first element is a sequence of SQL fragments and the second
@ -1551,6 +1595,63 @@
(raw-render xs)) (raw-render xs))
:within-group expr-clause-pairs})) :within-group expr-clause-pairs}))
(defn- format-equality-expr [op' op expr nested]
(let [[_ a b & y] expr
_ (when (seq y)
(throw (ex-info (str "only binary "
op'
" is supported")
{:expr expr})))
[s1 & p1] (format-expr a {:nested true})
[s2 & p2] (format-expr b {:nested true})]
(-> (if (or (nil? a) (nil? b))
(str (if (nil? a)
(if (nil? b) "NULL" s2)
s1)
(if (= := op) " IS NULL" " IS NOT NULL"))
(str s1 " " (sql-kw op) " " s2))
(cond-> nested
(as-> s (str "(" s ")")))
(vector)
(into p1)
(into p2))))
(defn- format-infix-expr [op' op expr nested]
(let [args (cond->> (rest expr)
(contains? @op-ignore-nil op)
(remove nil?))
args (cond (seq args)
args
(= :and op)
[true]
(= :or op)
[false]
:else ; args is empty and not a special case
[])
[sqls params]
(reduce-sql (map #(format-expr % {:nested true}) args))]
(when-not (pos? (count sqls))
(throw (ex-info (str "no operands found for " op')
{:expr expr})))
(into [(cond-> (str/join (str " " (sql-kw op) " ") sqls)
(and (contains? @op-can-be-unary op)
(= 1 (count sqls)))
(as-> s (str (sql-kw op) " " s))
nested
(as-> s (str "(" s ")")))]
params)))
(defn- format-fn-call-expr [op expr]
(let [args (rest expr)
[sqls params] (format-interspersed-expr-list args)]
(into [(str (sql-kw op)
(if (and (= 1 (count args))
(map? (first args))
(= 1 (count sqls)))
(str " " (first sqls))
(str "(" (str/join ", " sqls) ")")))]
params)))
(defn format-expr (defn format-expr
"Given a data structure that represents a SQL expression and a hash "Given a data structure that represents a SQL expression and a hash
map of options, return a vector containing a string -- the formatted map of options, return a vector containing a string -- the formatted
@ -1571,48 +1672,8 @@
(if (keyword? op') (if (keyword? op')
(cond (contains? @infix-ops op') (cond (contains? @infix-ops op')
(if (contains? #{:= :<>} op) (if (contains? #{:= :<>} op)
(let [[_ a b & y] expr (format-equality-expr op' op expr nested)
_ (when (seq y) (format-infix-expr op' op expr nested))
(throw (ex-info (str "only binary "
op'
" is supported")
{:expr expr})))
[s1 & p1] (format-expr a {:nested true})
[s2 & p2] (format-expr b {:nested true})]
(-> (if (or (nil? a) (nil? b))
(str (if (nil? a)
(if (nil? b) "NULL" s2)
s1)
(if (= := op) " IS NULL" " IS NOT NULL"))
(str s1 " " (sql-kw op) " " s2))
(cond-> nested
(as-> s (str "(" s ")")))
(vector)
(into p1)
(into p2)))
(let [args (cond->> (rest expr)
(contains? @op-ignore-nil op)
(remove nil?))
args (cond (seq args)
args
(= :and op)
[true]
(= :or op)
[false]
:else ; args is empty and not a special case
[])
[sqls params]
(reduce-sql (map #(format-expr % {:nested true}) args))]
(when-not (pos? (count sqls))
(throw (ex-info (str "no operands found for " op')
{:expr expr})))
(into [(cond-> (str/join (str " " (sql-kw op) " ") sqls)
(and (contains? @op-can-be-unary op)
(= 1 (count sqls)))
(as-> s (str (sql-kw op) " " s))
nested
(as-> s (str "(" s ")")))]
params)))
(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))
@ -1620,15 +1681,7 @@
(let [formatter (get @special-syntax op)] (let [formatter (get @special-syntax op)]
(formatter op (rest expr))) (formatter op (rest expr)))
:else :else
(let [args (rest expr) (format-fn-call-expr op expr))
[sqls params] (format-expr-list args)]
(into [(str (sql-kw op)
(if (and (= 1 (count args))
(map? (first args))
(= 1 (count sqls)))
(str " " (first sqls))
(str "(" (str/join ", " sqls) ")")))]
params)))
(let [[sqls params] (format-expr-list expr)] (let [[sqls params] (format-expr-list expr)]
(into [(str "(" (str/join ", " sqls) ")")] params)))) (into [(str "(" (str/join ", " sqls) ")")] params))))

View file

@ -1108,3 +1108,25 @@ ORDER BY id = ? DESC
(sut/format {:select [[[:- 1 2]]]}))) (sut/format {:select [[[:- 1 2]]]})))
(is (= ["SELECT ? ~ ?" "a" "b"] ; regex op (is (= ["SELECT ? ~ ?" "a" "b"] ; regex op
(sut/format {:select [[[(keyword "~") "a" "b"]]]})))) (sut/format {:select [[[(keyword "~") "a" "b"]]]}))))
(deftest issue-471-interspersed-kws
(testing "overlay"
(is (= ["SELECT OVERLAY(foo PLACING ? FROM ? FOR ?)"
"bar" 3 4]
(sut/format {:select [[[:overlay :foo :*placing "bar" :*from 3 :*for 4]]]}))))
(testing "position"
(is (= ["SELECT POSITION(? IN bar)" "foo"]
(sut/format {:select [[[:position "foo" :*in :bar]]]}))))
(testing "trim"
(is (= ["SELECT TRIM(LEADING FROM bar)"]
(sut/format {:select [[[:trim :*leading :*from :bar]]]})))
(is (= ["SELECT TRIM(LEADING FROM bar)"]
(sut/format {:select [[[:trim :*leading-from :bar]]]}))))
(testing "extract"
(is (= ["SELECT EXTRACT(CENTURY FROM TIMESTAMP '2000-12-16 12:21:13')"]
(sut/format {:select [[[:extract :*century :*from
:*timestamp [:inline "2000-12-16 12:21:13"]]]]}))))
(testing "xmlelement"
(is (= ["SELECT XMLELEMENT(NAME \"foo$bar\", XMLATTRIBUTES('xyz' AS \"a&b\"))"]
(sut/format {:select [[[:xmlelement :*name :foo$bar
[:xmlattributes [:inline "xyz"] :*as :a&b]]]]})))))