diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index b67fd85..7030653 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -31,7 +31,7 @@ (:require [clojure.string :as str] #?(:clj [clojure.template]) [honey.sql.protocols :as p] - [honey.sql.util :refer [str join]])) + [honey.sql.util :refer [str join split-by-separator into*]])) ;; default formatting for known clauses @@ -316,7 +316,7 @@ [n %] (if aliased [%] - (str/split % #"\.")))) + (split-by-separator % ".")))) parts (parts-fn col-e) entity (join "." (map #(cond-> % (not= "*" %) (quote-fn))) parts)] (suspicious-entity-check entity) @@ -457,7 +457,7 @@ :default (subs (str x) 1)) (str x))] (cond (str/starts-with? c "%") - (let [[f & args] (str/split (subs c 1) #"\.")] + (let [[f & args] (split-by-separator (subs c 1) ".")] [(str (format-fn-name f) "(" (join ", " (map #(format-entity (keyword %) opts)) args) ")")]) @@ -525,14 +525,10 @@ :else (throw (ex-info "bigquery * only supports except and replace" {:clause k :arg arg})))] - (-> [(cond->> sql' sql (str sql " "))] - (into params) - (into params')))) + (into* [(cond->> sql' sql (str sql " "))] params params'))) [] (partition-all 2 x))] - (-> [(str sql " " sql')] - (into params) - (into params')))) + (into* [(str sql " " sql')] params params'))) (comment (bigquery-*-except-replace? [:* :except [:a :b :c]]) @@ -676,9 +672,7 @@ sql')) (when hints (str " WITH (" hints ")")))] - (into params) - (into params') - (into params''))))) + (into* params params' params''))))) (defn- format-selectable-dsl ([x] (format-selectable-dsl x {})) @@ -752,9 +746,8 @@ (let [[cur & params] (peek result) [sql & params'] (first exprs)] (recur (rest exprs) args' false (conj (pop result) - (-> [(str cur " " sql)] - (into params) - (into params'))))) + (into* [(str cur " " sql)] + params params')))) (recur (rest exprs) args' false (conj result (first exprs)))))) (reduce-sql result))))) @@ -836,7 +829,7 @@ (str (sql-kw :select) " " sql) true cols)] - (-> [sql'] (into params) (into params')))) + (into* [sql'] params params'))) (defn- format-select-top [k xs] (let [[top & cols] xs @@ -864,7 +857,7 @@ (join " " (map sql-kw) parts)) true cols)] - (-> [sql'] (into params) (into params')))) + (into* [sql'] params params'))) (defn- format-select-into [k xs] (let [[v e] (ensure-sequential xs) @@ -956,19 +949,14 @@ (format-dsl expr)) [sql'' & params'' :as sql-params''] (if non-query-expr? - (cond-> [(str sql' " AS " sql)] - params' (into params') - params (into params)) + (into* [(str sql' " AS " sql)] params' params) ;; according to docs, CTE should _always_ be wrapped: - (cond-> [(str sql " " (as-fn with) " " (str "(" sql' ")"))] - params (into params) - params' (into params'))) + (into* [(str sql " " (as-fn with) " " (str "(" sql' ")"))] + params params')) [tail-sql & tail-params] (format-with-query-tail tail)] (if (seq tail-sql) - (cond-> [(str sql'' " " tail-sql)] - params'' (into params'') - tail-params (into tail-params)) + (into* [(str sql'' " " tail-sql)] params'' tail-params) sql-params'')))) xs)] (into [(str (sql-kw k) " " (join ", " sqls))] params))) @@ -1012,10 +1000,7 @@ (str cols-sql' " ")) overriding sql)] - (into t-params) - (into c-params) - (into cols-params') - (into params))) + (into* t-params c-params cols-params' params))) (sequential? (second table)) (let [[table cols] table [t-sql & t-params] (format-entity-alias table) @@ -1025,23 +1010,20 @@ (join ", " c-sqls) ")" overriding)] - (into t-params) - (into c-params))) + (into* t-params c-params))) :else (let [[sql & params] (format-entity-alias table)] (-> [(str (sql-kw k) " " sql (when (seq cols') (str " " cols-sql')) overriding)] - (into cols-params') - (into params)))) + (into* cols-params' params)))) (let [[sql & params] (format-entity-alias table)] (-> [(str (sql-kw k) " " sql (when (seq cols') (str " " cols-sql')) overriding)] - (into cols-params') - (into params)))))) + (into* cols-params' params)))))) (comment (format-insert :insert-into [[[:raw ":foo"]] {:select :bar}]) @@ -1069,12 +1051,10 @@ (str "(" (join ", " u-sqls) ")")) - (-> params (into params-j) (into u-params))]) + (into* params params-j u-params)]) (let [[sql & params'] (when e (format-expr e))] [(cond-> sqls e (conj "ON" sql)) - (-> params - (into params-j) - (into params'))]))))) + (into* params params-j params')]))))) [[] []] clauses)] (into [(join " " sqls)] params))) @@ -1282,8 +1262,7 @@ (str " (" (join ", " sqls) ")")) (when sql (str " " sql)))] - (into expr-params) - (into clause-params))) + (into* expr-params clause-params))) (format-on-conflict k [x]))) (defn- format-do-update-set [k x] @@ -1302,8 +1281,7 @@ where (or (:where x) ('where x)) [sql & params] (when where (format-dsl {:where where}))] (-> [(str sets (when sql (str " " sql)))] - (into set-params) - (into params))) + (into* set-params params))) (format-set-exprs k x)) (sequential? x) (let [[cols clauses] (split-with (complement map?) x)] @@ -1753,7 +1731,10 @@ (if (keyword? k) (if-let [n (namespace k)] (symbol n (name k)) - (symbol (name k))) + ;; In CLJ runtime, reuse symbol that's already present in the keyword. + #?(:bb (symbol (name k)) + :clj (.sym ^clojure.lang.Keyword k) + :default (symbol (name k)))) k)) (defn format-dsl @@ -1849,23 +1830,18 @@ (= 1 (count params-y)) (coll? v1)) (let [sql (str "(" (join ", " (repeat (count v1) "?")) ")")] - (-> [(str sql-x " " (sql-kw in) " " sql)] - (into params-x) - (into v1))) + (into* [(str sql-x " " (sql-kw in) " " sql)] params-x v1)) (and *numbered* (= (str "$" (count @*numbered*)) sql-y) (= 1 (count params-y)) (coll? v1)) (let [vs (for [v v1] (->numbered v)) sql (str "(" (join ", " (map first) vs) ")")] - (-> [(str sql-x " " (sql-kw in) " " sql)] - (into params-x) - (conj nil) - (into (map second vs)))) + (into* [(str sql-x " " (sql-kw in) " " sql)] + params-x [nil] (map second vs))) :else - (-> [(str sql-x " " (sql-kw in) " " sql-y)] - (into params-x) - (into (if *numbered* values params-y)))))) + (into* [(str sql-x " " (sql-kw in) " " sql-y)] + params-x (if *numbered* values params-y))))) (defn- function-0 [k xs] [(str (sql-kw k) @@ -1909,7 +1885,7 @@ (let [[sql-e & params-e] (format-expr e) [sql-c & params-c] (format-dsl c {:nested true})] [(conj sqls (str sql-e " " (sql-kw k) " " sql-c)) - (-> params (into params-e) (into params-c))])) + (into* params params-e params-c)])) [[] []] (partition 2 pairs))] (into [(join ", " sqls)] params))) @@ -1928,7 +1904,7 @@ (= 'else condition)) (conj sqls (sql-kw :else) sqlv) (conj sqls (sql-kw :when) sqlc (sql-kw :then) sqlv)) - (-> params (into paramsc) (into paramsv))])) + (into* params paramsc paramsv)])) [[] []] (partition 2 (if case-expr? (rest clauses) clauses)))] (-> [(str (sql-kw :case) " " @@ -1936,8 +1912,7 @@ (str sqlx " ")) (join " " sqls) " " (sql-kw :end))] - (into paramsx) - (into params)))) + (into* paramsx params)))) (defn- between-fn "For both :between and :not-between" @@ -1945,10 +1920,8 @@ (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 " " (sql-kw k) " " sql-a " AND " sql-b)] - (into params-x) - (into params-a) - (into params-b)))) + (into* [(str sql-x " " (sql-kw k) " " sql-a " AND " sql-b)] + params-x params-a params-b))) (defn- object-record-literal [k [x]] @@ -1967,9 +1940,7 @@ (let [[sql' & params'] (format-expr %)] (cons (str "[" sql' "]") params'))) kix))] - (-> [(str "(" sql ")" (join "" sqls))] - (into params) - (into params')))) + (into* [(str "(" sql ")" (join "" sqls))] params params'))) (defn ignore-respect-nulls [k [x]] (let [[sql & params] (format-expr x)] @@ -2042,9 +2013,7 @@ [sql' & params'] (if (ident? type) [(sql-kw type)] (format-expr type))] - (-> [(str "CAST(" sql " AS " sql' ")")] - (into params) - (into params')))) + (into* [(str "CAST(" sql " AS " sql' ")")] params params'))) :composite (fn [_ [& args]] (let [[sqls params] (format-expr-list args)] @@ -2057,9 +2026,7 @@ (fn [_ [pattern escape-chars]] (let [[sql-p & params-p] (format-expr pattern) [sql-e & params-e] (format-expr escape-chars)] - (-> [(str sql-p " " (sql-kw :escape) " " sql-e)] - (into params-p) - (into params-e)))) + (into* [(str sql-p " " (sql-kw :escape) " " sql-e)] params-p params-e))) :filter expr-clause-pairs :get-in #'get-in-navigation :ignore-nulls ignore-respect-nulls @@ -2106,9 +2073,7 @@ (fn [k [e & qs]] (let [[sql-e & params-e] (format-expr e) [sql-q & params-q] (format-dsl {k qs})] - (-> [(str sql-e " " sql-q)] - (into params-e) - (into params-q)))) + (into* [(str sql-e " " sql-q)] params-e params-q))) :over (fn [_ [& args]] (let [[sqls params] @@ -2119,7 +2084,7 @@ [(format-entity p)])] [(conj sqls (str sql-e " OVER " sql-p (when a (str " AS " (format-entity a))))) - (-> params (into params-e) (into params-p))])) + (into* params params-e params-p)])) [[] []] args)] (into [(join ", " sqls)] params))) @@ -2160,8 +2125,7 @@ (cond-> nested (as-> s (str "(" s ")"))) (vector) - (into p1) - (into p2)))) + (into* p1 p2)))) (defn- format-infix-expr [op' op expr nested] (let [args (cond->> (rest expr) diff --git a/src/honey/sql/util.cljc b/src/honey/sql/util.cljc index 1e621ad..3d01c25 100644 --- a/src/honey/sql/util.cljc +++ b/src/honey/sql/util.cljc @@ -75,3 +75,32 @@ :default (clojure.string/join separator (transduce xform conj [] coll))))) + +(defn split-by-separator + "More efficient implementation of `clojure.string/split` for cases when a + literal string (not regex) is used as a separator, and for cases where the + separator is not present in the haystack at all." + [s sep] + (loop [start 0, res []] + (if-let [sep-idx (clojure.string/index-of s sep start)] + (recur (inc sep-idx) (conj res (subs s start sep-idx))) + (if (= start 0) + ;; Fastpath - zero separators in s + [s] + (conj res (subs s start)))))) + +(defn into* + "An extension of `clojure.core/into` that accepts multiple \"from\" arguments. + Doesn't support `xform`." + ([to from1] (into* to from1 nil nil nil)) + ([to from1 from2] (into* to from1 from2 nil nil)) + ([to from1 from2 from3] (into* to from1 from2 from3 nil)) + ([to from1 from2 from3 from4] + (if (or from1 from2 from3 from4) + (as-> (transient to) to' + (reduce conj! to' from1) + (reduce conj! to' from2) + (reduce conj! to' from3) + (reduce conj! to' from4) + (persistent! to')) + to))) diff --git a/test/honey/util_test.cljc b/test/honey/util_test.cljc index 7ee6f6c..ef76459 100644 --- a/test/honey/util_test.cljc +++ b/test/honey/util_test.cljc @@ -43,3 +43,20 @@ (is (= "1, 2, 3, 4" (sut/join ", " (remove nil?) [1 nil 2 nil 3 nil nil nil 4]))) (is (= "" (sut/join ", " (remove nil?) [nil nil nil nil])))) + +(deftest split-by-separator-test + (is (= [""] (sut/split-by-separator "" "."))) + (is (= ["" ""] (sut/split-by-separator "." "."))) + (is (= ["hello"] (sut/split-by-separator "hello" "."))) + (is (= ["h" "e" "l" "l" "o"] (sut/split-by-separator "h.e.l.l.o" "."))) + (is (= ["" "h" "e" "" "" "l" "" "l" "o" ""] + (sut/split-by-separator ".h.e...l..l.o." ".")))) + +(deftest into*-test + (is (= [1] (sut/into* [1] nil))) + (is (= [1] (sut/into* [1] []))) + (is (= [1] (sut/into* [1] nil [] nil []))) + (is (= [1 2 3] (sut/into* [1] [2 3]))) + (is (= [1 2 3 4 5 6] (sut/into* [1] [2 3] [4 5 6]))) + (is (= [1 2 3 4 5 6 7] (sut/into* [1] [2 3] [4 5 6] [7]))) + (is (= [1 2 3 4 5 6 7 8 9] (sut/into* [1] [2 3] [4 5 6] [7] [8 9]))))