From f7d5e3a4cf0526a22611085c09319229200fdd03 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Wed, 23 Sep 2020 18:15:20 -0700 Subject: [PATCH] Down to just 8 failures now! Mising: array, inline, parameterizer. --- src/honey/sql.cljc | 240 +++++++++++++++++++++++++++------------ test/honey/sql_test.cljc | 58 ++++++---- 2 files changed, 203 insertions(+), 95 deletions(-) diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 965977f..9940e15 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -9,12 +9,13 @@ (declare format-dsl) (declare format-expr) +(declare format-expr-list) ;; dynamic dialect handling for formatting (def ^:private default-clause-order "The (default) order for known clauses. Can have items added and removed." - [:intersect :union :union-all :except + [:with :with-recursive :intersect :union :union-all :except :select :insert-into :update :delete :delete-from :truncate :columns :set :from :join :left-join :right-join :inner-join :outer-join :full-join @@ -50,7 +51,9 @@ (def ^:private default-dialect (atom (:ansi dialects))) (def ^:private ^:dynamic *dialect* nil) -(def ^:private ^:dynamic *clause-order* nil) +;; nil would be a better default but that makes testing individual +;; functions harder than necessary: +(def ^:private ^:dynamic *clause-order* default-clause-order) (def ^:private ^:dynamic *quoted* nil) ;; clause helpers @@ -72,41 +75,66 @@ (defn- sql-kw [k] (-> k (name) (upper-case) (str/replace "-" " "))) -(defn- format-entity [x] - (let [q (if *quoted* (:quote *dialect*) identity) - [t c] (if-let [n (namespace x)] - [n (name x)] - (let [[t c] (str/split (name x) #"\.")] - (if c [t c] [nil t])))] +(defn- format-entity [x & [{:keys [aliased? drop-ns?]}]] + (let [q (if *quoted* (:quote *dialect*) identity) + call (fn [f x] (str f "(" x ")")) + [f t c] (if-let [n (when-not (or drop-ns? (string? x)) + (namespace x))] + [nil n (name x)] + (let [[t c] (if aliased? + [(name x)] + (str/split (name x) #"\."))] + ;; I really dislike like %func.arg shorthand syntax! + (cond (= \% (first t)) + [(subs t 1) nil c] + c + [nil t c] + :else + [nil nil t])))] (cond->> c (not= "*" c) (q) t - (str (q t) ".")))) + (str (q t) ".") + f + (call f)))) -(defn- format-selectable [x] +(defn- format-entity-alias [x] (cond (sequential? x) (str (let [s (first x)] (if (map? s) - (format-dsl s true) + (throw (ex-info "selectable cannot be statement!" + {:selectable s})) (format-entity s))) #_" AS " " " - (format-entity (second x))) + (format-entity (second x) {:aliased? true})) :else (format-entity x))) -(defn- format-selectable-dsl [x] +(defn- format-selectable-dsl [x & [{:keys [as? aliased?] :as opts}]] (cond (map? x) - (format-dsl x true) + (format-dsl x {:nested? true}) (sequential? x) (let [s (first x) - [sql & params] (if (map? s) (format-dsl s true) [(format-entity s)])] - (into [(str sql #_" AS " " " (format-entity (second x)))] params)) + a (second x) + [sql & params] (if (map? s) + (format-dsl s {:nested? true}) + (format-expr s)) + [sql' & params'] (if (sequential? a) + (let [[sql params] (format-expr-list a {:aliased? true})] + (into [(str/join " " sql)] params)) + (format-selectable-dsl a {:aliased? true}))] + (-> [(str sql (if as? " AS " " ") sql')] + (into params) + (into params'))) - (keyword? x) - [(format-entity x)] + (or (keyword? x) (symbol? x)) + [(format-entity x opts)] + + (and aliased? (string? x)) + [(format-entity x opts)] :else (format-expr x))) @@ -121,17 +149,55 @@ (map #'format-dsl xs))] (into [(str/join (str " " (sql-kw k) " ") sqls)] params))) -(defn- format-selector [k xs] +(defn- format-expr-list [xs & [opts]] + (reduce (fn [[sql params] [sql' & params']] + [(conj sql sql') (if params' (into params params') params)]) + [[] []] + (map #(format-expr % opts) xs))) + +(defn- format-columns [_ xs] + (let [[sqls params] (format-expr-list xs {:drop-ns? true})] + (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)]) [[] []] - (map #'format-selectable-dsl xs))] + (map #(format-selectable-dsl % {:as? (= k :select)}) xs))] (into [(str (sql-kw k) " " (str/join ", " sqls))] params)) - (let [[sql & params] (format-selectable-dsl xs)] + (let [[sql & params] (format-selectable-dsl xs {:as? (= k :select)})] (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)] + (cond-> [(str sql " AS " + (if (seq params') + (str "(" sql' ")") + 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] ;; table can be just a table, a pair of table and statement, or a ;; pair of a pair of table and columns and a statement (yikes!) @@ -139,24 +205,24 @@ (if (sequential? (first table)) (let [[[table cols] statement] table [sql & params] (format-dsl statement)] - (into [(str (sql-kw k) " " (format-selectable table) + (into [(str (sql-kw k) " " (format-entity-alias table) " (" - (str/join ", " (map #'format-selectable cols)) + (str/join ", " (map #'format-entity-alias cols)) ") " sql)] params)) (let [[table statement] table [sql & params] (format-dsl statement)] - (into [(str (sql-kw k) " " (format-selectable table) + (into [(str (sql-kw k) " " (format-entity-alias table) " " sql)] params))) - [(str (sql-kw k) " " (format-selectable table))])) + [(str (sql-kw k) " " (format-entity-alias table))])) (defn- format-join [k [j e]] (let [[sql & params] (format-expr e)] ;; for backward compatibility, treat plain JOIN as INNER JOIN: (into [(str (sql-kw (if (= :join k) :inner-join k)) " " - (format-selectable j) " ON " + (format-entity-alias j) " ON " sql)] params))) @@ -164,12 +230,6 @@ (let [[sql & params] (format-expr e)] (into [(str (sql-kw k) " " sql)] params))) -(defn- format-expr-list [xs] - (reduce (fn [[sql params] [sql' & params']] - [(conj sql sql') (if params' (into params params') params)]) - [[] []] - (map #'format-expr xs))) - (defn- format-group-by [k xs] (let [[sqls params] (format-expr-list xs)] (into [(str (sql-kw k) " " (str/join ", " sqls))] params))) @@ -184,22 +244,47 @@ dirs)))] params))) (defn- format-values [k xs] - (if (sequential? (first xs)) - ;; [[1 2 3] [4 5 6]] - (let [[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)) - ;; [1 2 3] - (let [[sqls params] (format-expr-list xs)] - (into [(str (sql-kw k) " (" (str/join ", " sqls) ")")] params)))) + (cond (sequential? (first xs)) + ;; [[1 2 3] [4 5 6]] + (let [[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 (keys (first xs)) + [sqls params] + (reduce (fn [[sql params] [sqls' params']] + [(conj sql (str/join ", " sqls')) + (if params' (into params params') params')]) + [[] []] + (map (fn [m] + (format-expr-list (map #(get m %) cols))) + xs))] + (into [(str "(" + (str/join ", " + (map #(format-entity % {:drop-ns? true}) cols)) + ") " + (sql-kw k) " (" (str/join ", " sqls) ")")] + params)) + + :else + (throw (ex-info ":values expects sequences or maps" + {:first (first xs)})))) (defn- format-set-exprs [k xs] - ;; TODO: !!! - ["SET a = ?, b = ?" 42 13]) + (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))) (def ^:private current-clause-order "The (current) order for known clauses. Can have items added and removed." @@ -208,26 +293,28 @@ (def ^:private clause-format "The (default) behavior for each known clause. Can also have items added and removed." - (atom {:intersect #'format-on-set-op + (atom {: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 - :select #'format-selector + :select #'format-selects :insert-into #'format-insert :update #'format-selector - :delete #'format-selector + :delete #'format-selects :delete-from #'format-selector :truncate #'format-selector - :columns #'format-selector + :columns #'format-columns :set #'format-set-exprs - :from #'format-selector + :from #'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-selector + :cross-join #'format-selects :where #'format-on-expr :group-by #'format-group-by :having #'format-on-expr @@ -239,8 +326,8 @@ (assert (= (set @current-clause-order) (set (keys @clause-format)))) (comment :target - {:with 20 - :with-recursive 30 + {;:with 20 + ;:with-recursive 30 ;:intersect 35 ;:union 40 ;:union-all 45 @@ -253,15 +340,15 @@ ;:truncate 85 ;:columns 90 :composite 95 - :set0 100 ; low-priority set clause + ;; no longer needed/supported :set0 100 ; low-priority set clause ;:from 110 ;:join 120 ;:left-join 130 ;:right-join 140 ;:full-join 150 ;:cross-join 152 ; doesn't have on clauses - :set 155 - :set1 156 ; high-priority set clause (synonym for :set) + ;:set 155 + ;; no longer needed/supported :set1 156 ; high-priority set clause (synonym for :set) ;:where 160 ;:group-by 170 ;:having 180 @@ -269,10 +356,10 @@ ;:limit 200 ;:offset 210 :lock 215 - :values 220 + ;:values 220 :query-values 230}) -(defn- format-dsl [x & [nested?]] +(defn- format-dsl [x & [{:keys [aliased? nested?]}]] (let [[sqls params leftover] (reduce (fn [[sql params leftover] k] (if-let [xs (k x)] @@ -294,7 +381,8 @@ leftover)) [(str "")]) (into [(cond-> (str/join " " sqls) - nested? (as-> s (str "(" s ")")))] params)))) + (and nested? (not aliased?)) + (as-> s (str "(" s ")")))] params)))) (def ^:private infix-aliases "Provided for backward compatibility with earlier HoneySQL versions." @@ -316,9 +404,9 @@ (def ^:private special-syntax {:between (fn [[x a b]] - (let [[sql-x & params-x] (format-expr x true) - [sql-a & params-a] (format-expr a true) - [sql-b & params-b] (format-expr b true)] + (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) @@ -332,20 +420,20 @@ (let [[sql & params] (format-expr n)] (into [(str "INTERVAL " sql " " (sql-kw units))] params)))}) -(defn format-expr [x & [nested?]] - (cond (keyword? x) - [(format-entity x)] +(defn format-expr [x & [{:keys [nested?] :as opts}]] + (cond (or (keyword? x) (symbol? x)) + [(format-entity x opts)] (map? x) - (format-dsl x true) + (format-dsl x (assoc opts :nested? true)) (sequential? x) (let [op (first x)] (if (keyword? op) (cond (infix-ops op) (let [[_ a b] x - [s1 & p1] (format-expr a true) - [s2 & p2] (format-expr b true)] + [s1 & p1] (format-expr a {:nested? true}) + [s2 & p2] (format-expr b {:nested? true})] (-> (str s1 " " (sql-kw (get infix-aliases op op)) " " s2) @@ -358,14 +446,22 @@ (let [formatter (special-syntax op)] (formatter (rest x))) :else - (let [[sqls params] (format-expr-list (rest x))] + (let [args (rest x) + [sqls params] (format-expr-list args)] (into [(str (sql-kw op) - "(" (str/join ", " sqls) ")")] + (if (and (= 1 (count args)) + (map? (first args)) + (= 1 (count sqls))) + (str " " (first sqls)) + (str "(" (str/join ", " sqls) ")")))] params))) - (into [(str "(" (str/join "," + (into [(str "(" (str/join ", " (repeat (count x) "?")) ")")] x))) + (boolean? x) + [(upper-case (str x))] + :else ["?" x])) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index d45bbf6..33ef52c 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -8,8 +8,8 @@ (deftest mysql-tests (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] - (#'sut/format {:select [:*] :from [:table] :where [:= :id 1]} - {:dialect :mysql})))) + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} + {:dialect :mysql})))) (deftest expr-tests (is (= ["id = ?" 1] @@ -33,24 +33,24 @@ (deftest general-tests (is (= ["SELECT * FROM \"table\" WHERE \"id\" = ?" 1] - (#'sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:quoted true}))) + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:quoted true}))) ;; temporarily remove AS from alias here (is (= ["SELECT \"t\".* FROM \"table\" \"t\" WHERE \"id\" = ?" 1] - (#'sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} {:quoted true}))) + (sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} {:quoted true}))) (is (= ["SELECT * FROM \"table\" GROUP BY \"foo\", \"bar\""] - (#'sut/format {:select [:*] :from [:table] :group-by [:foo :bar]} {:quoted true}))) + (sut/format {:select [:*] :from [:table] :group-by [:foo :bar]} {:quoted true}))) (is (= ["SELECT * FROM \"table\" GROUP BY DATE(\"bar\")"] - (#'sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]} {:quoted true}))) + (sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]} {:quoted true}))) (is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" DESC, \"bar\" ASC"] - (#'sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} {:quoted true}))) + (sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} {:quoted true}))) (is (= ["SELECT * FROM \"table\" ORDER BY DATE(\"expiry\") DESC, \"bar\" ASC"] - (#'sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {:quoted true}))) + (sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {:quoted true}))) (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL ? DAYS) < NOW()" 30] - (#'sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true}))) + (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true}))) (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] - (#'sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}))) - (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?,?,?,?)" 1 2 3 4] - (#'sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {:quoted true})))) + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}))) + (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?, ?, ?, ?)" 1 2 3 4] + (sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {:quoted true})))) ;; tests lifted from HoneySQL v1 to check for compatibility @@ -105,8 +105,12 @@ ["INSERT INTO foo (id) VALUES (?)" 2]))) (deftest exists-test - (is (= (format {:exists {:select [:a] :from [:foo]}}) - ["EXISTS (SELECT a FROM foo)"])) + ;; EXISTS should never have been implemented as SQL syntax: it's an operator! + #_(is (= (format {:exists {:select [:a] :from [:foo]}}) + ["EXISTS (SELECT a FROM foo)"])) + ;; ugly because it's hard to select just a function call without an alias: + (is (= (format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]}) + ["SELECT EXISTS (SELECT a FROM foo) AS x"])) (is (= (format {:select [:id] :from [:foo] :where [:exists {:select [1] @@ -115,12 +119,12 @@ ["SELECT id FROM foo WHERE EXISTS (SELECT ? FROM bar WHERE deleted)" 1]))) (deftest array-test - (println 'sql-array :unimplemented) + (is nil "sql-array unimplemented") #_(is (= (format {:insert-into :foo :columns [:baz] :values [[(sql/array [1 2 3 4])]]}) ["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?, ?])" 1 2 3 4])) - (println 'sql-array :unimplemented) + (is nil "sql-array unimplemented") #_(is (= (format {:insert-into :foo :columns [:baz] :values [[(sql/array ["one" "two" "three"])]]}) @@ -135,7 +139,15 @@ ;; ORDER BY foo ASC (is (= (format {:union [{:select [:foo] :from [:bar1]} {:select [:foo] :from [:bar2]}]}) - ["SELECT foo FROM bar1 UNION SELECT foo FROM bar2"]))) + ["SELECT foo FROM bar1 UNION SELECT foo FROM bar2"])) + + (testing "union complex values" + (is (= (format {:union [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}] + :with [[[:bar {:columns [:spam :eggs]}] + {:values [[1 2] [3 4] [5 6]]}]]}) + ["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) SELECT foo FROM bar1 UNION SELECT foo FROM bar2" + 1 2 3 4 5 6])))) (deftest union-all-test (is (= (format {:union-all [{:select [:foo] :from [:bar1]} @@ -168,7 +180,7 @@ (deftest compare-expressions-test (testing "Sequences should be fns when in value/comparison spots" - (is (= ["SELECT foo FROM bar WHERE (col1 mod ?) = (col2 + ?)" 4 4] + (is (= ["SELECT foo FROM bar WHERE (col1 MOD ?) = (col2 + ?)" 4 4] (format {:select [:foo] :from [:bar] :where [:= [:mod :col1 4] [:+ :col2 4]]})))) @@ -177,11 +189,11 @@ (let [sub {:select [:%sum.amount] :from [:bar] :where [:in :id ["id-1" "id-2"]]}] - (is (= ["SELECT total FROM foo WHERE (SELECT sum(amount) FROM bar WHERE (id in (?, ?))) = total" "id-1" "id-2"] + (is (= ["SELECT total FROM foo WHERE (SELECT sum(amount) FROM bar WHERE id IN (?, ?)) = total" "id-1" "id-2"] (format {:select [:total] :from [:foo] :where [:= sub :total]}))) - (is (= ["WITH t AS (SELECT sum(amount) FROM bar WHERE (id in (?, ?))) SELECT total FROM foo WHERE total = t" "id-1" "id-2"] + (is (= ["WITH t AS (SELECT sum(amount) FROM bar WHERE id IN (?, ?)) SELECT total FROM foo WHERE total = t" "id-1" "id-2"] (format {:with [[:t sub]] :select [:total] :from [:foo] @@ -204,7 +216,7 @@ (deftest parameterizer-none (testing "array parameter" - (println 'sql-array :unimplemented) + (is nil "sql-array unimplemented") #_(is (= (format {:insert-into :foo :columns [:baz] :values [[(sql/array [1 2 3 4])]]} @@ -250,7 +262,7 @@ :from [[:bar :b]] :where [:= :b.id 1]} :c]] :where [:= :f.kind "drama"]} - (format))))) + (format {:quoted true}))))) (deftest set-after-join (is (= @@ -282,7 +294,7 @@ (format {:dialect :mysql}))))) (deftest inlined-values-are-stringified-correctly - (println 'inline :unimplemented) + (is nil "inline unimplemented") #_(is (= ["SELECT foo, bar, NULL"] (format {:select [(honeysql.core/inline "foo") (honeysql.core/inline :bar)