diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d9372..a5ab5aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changes * 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). * 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). diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 3f559b1..7986516 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -465,6 +465,50 @@ (let [[sqls params] (reduce-sql (map #(format-dsl %) xs))] (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 "Given a sequence of expressions represented as data, return a pair where the first element is a sequence of SQL fragments and the second @@ -1551,6 +1595,63 @@ (raw-render xs)) :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 "Given a data structure that represents a SQL expression and a hash map of options, return a vector containing a string -- the formatted @@ -1571,48 +1672,8 @@ (if (keyword? op') (cond (contains? @infix-ops op') (if (contains? #{:= :<>} op) - (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))) - (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))) + (format-equality-expr op' op expr nested) + (format-infix-expr op' op expr nested)) (contains? #{:in :not-in} op) (let [[sql & params] (format-in op (rest expr))] (into [(if nested (str "(" sql ")") sql)] params)) @@ -1620,15 +1681,7 @@ (let [formatter (get @special-syntax op)] (formatter op (rest expr))) :else - (let [args (rest 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))) + (format-fn-call-expr op expr)) (let [[sqls params] (format-expr-list expr)] (into [(str "(" (str/join ", " sqls) ")")] params)))) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index aa0c77e..e26c887 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -1108,3 +1108,25 @@ ORDER BY id = ? DESC (sut/format {:select [[[:- 1 2]]]}))) (is (= ["SELECT ? ~ ?" "a" "b"] ; regex op (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]]]]})))))