diff --git a/README.md b/README.md index 42e5372..0541838 100644 --- a/README.md +++ b/README.md @@ -559,7 +559,7 @@ big-complicated-map {:params {:param1 "gabba" :param2 2} :pretty true}) => [" -SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 +SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 35e7587..719f9aa 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -42,7 +42,8 @@ :create-table :with-columns :create-view :drop-table ;; then SQL clauses in priority order: :nest :with :with-recursive :intersect :union :union-all :except :except-all - :select :select-distinct :insert-into :update :delete :delete-from :truncate + :select :select-distinct :select-distinct-on + :insert-into :update :delete :delete-from :truncate :columns :set :from :using :join :left-join :right-join :inner-join :outer-join :full-join :cross-join @@ -272,16 +273,35 @@ (let [[sqls params] (format-expr-list xs {:drop-ns (= :columns k)})] (into [(str "(" (str/join ", " sqls) ")")] params))) -(defn- format-selects [k xs] +(defn- format-selects-common [prefix as 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 % {:as (#{:select :from :window} k)}) xs))] - (into [(str (sql-kw k) " " (str/join ", " sqls))] params)) - (let [[sql & params] (format-selectable-dsl xs {:as (#{:select :from} k)})] - (into [(str (sql-kw k) " " sql)] params)))) + (map #(format-selectable-dsl % {:as as}) xs))] + (into [(str prefix " " (str/join ", " sqls))] params)) + (let [[sql & params] (format-selectable-dsl xs {:as as})] + (into [(str prefix " " sql)] params)))) + +(defn- format-selects [k xs] + (format-selects-common + (sql-kw k) + (#{:select :select-distinct :from :window + 'select 'select-distinct 'from 'window} + k) + xs)) + +(defn- format-selects-on [k xs] + (let [[on & cols] xs + [sql & params] + (format-expr (into [:distinct-on] on)) + [sql' & params'] + (format-selects-common + (str (sql-kw :select) " " sql) + true + cols)] + (-> [sql'] (into params) (into params')))) (defn- format-with-part [x] (if (sequential? x) @@ -457,16 +477,46 @@ (into [(str (sql-kw k) " " (str/join ", " sqls))] params))) (defn- format-on-conflict [k x] - (if (or (keyword? x) (symbol? x)) - [(str (sql-kw k) " (" (format-entity x) ")")] - (let [[sql & params] (format-dsl x)] - (into [(str (sql-kw k) " " sql)] params)))) + (cond (or (keyword? x) (symbol? x)) + [(str (sql-kw k) " (" (format-entity x) ")")] + (map? x) + (let [[sql & params] (format-dsl x)] + (into [(str (sql-kw k) " " sql)] params)) + (and (sequential? x) + (or (keyword? (first x)) (symbol? (first x))) + (map? (second x))) + (let [[sql & params] (format-dsl (second x))] + (into [(str (sql-kw k) + " (" (format-entity (first x)) ") " + sql)] + params)) + :else + (throw (ex-info "unsupported :on-conflict format" + {:clause x})))) +(comment + keyword/symbol -> e = excluded.e + [k/s] -> join , e = excluded.e + {e v} -> join , e = v + {:fields f :where w} -> join , e = excluded.e (from f) where w + ,) (defn- format-do-update-set [k x] - (if (or (keyword? x) (symbol? x)) + (if (map? x) + (if (and (or (contains? x :fields) (contains? x 'fields)) + (or (contains? x :where) (contains? x 'where))) + (let [sets (str/join ", " + (map (fn [e] + (let [e (format-entity e {:drop-ns true})] + (str e " = EXCLUDED." e))) + (or (:fields x) + ('fields x)))) + [sql & params] (format-dsl {:where + (or (:where x) + ('where x))})] + (into [(str (sql-kw k) " " sets " " sql)] params)) + (format-set-exprs k x)) (let [e (format-entity x {:drop-ns true})] - [(str (sql-kw k) " " e " = EXCLUDED." e)]) - (format-set-exprs k x))) + [(str (sql-kw k) " " e " = EXCLUDED." e)]))) (defn- format-simple-clause [c] (binding [*inline* true] @@ -561,6 +611,7 @@ :except-all #'format-on-set-op :select #'format-selects :select-distinct #'format-selects + :select-distinct-on #'format-selects-on :insert-into #'format-insert :update #'format-selector :delete #'format-selects diff --git a/src/honey/sql/helpers.cljc b/src/honey/sql/helpers.cljc index 172681a..a852f39 100644 --- a/src/honey/sql/helpers.cljc +++ b/src/honey/sql/helpers.cljc @@ -76,6 +76,7 @@ (defn select [& args] (generic :select args)) (defn select-distinct [& args] (generic :select-distinct args)) +(defn select-distinct-on [& args] (generic :select-distinct-on args)) (defn insert-into [& args] (generic :insert-into args)) (defn update [& args] (generic :update args)) (defn delete [& args] (generic-1 :delete args)) diff --git a/test/honey/sql/helpers_test.cljc b/test/honey/sql/helpers_test.cljc index a5c251c..b1063d5 100644 --- a/test/honey/sql/helpers_test.cljc +++ b/test/honey/sql/helpers_test.cljc @@ -58,7 +58,7 @@ (testing "Various construction methods are consistent" (is (= m1 m2))) (testing "SQL data formats correctly" - (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?" + (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?" 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] (sql/format m1 {:params {:param1 "gabba" :param2 2}})))) #?(:clj (testing "SQL data prints and reads correctly" @@ -68,7 +68,7 @@ ["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2) AND (b.baz <> $3)) OR (($4 < $5) AND ($6 < $7)) OR (f.e IN ($8, $9, $10)) OR f.e BETWEEN $11 AND $12 GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT $14 OFFSET $15" 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]))) (testing "Locking" - (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE" + (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE" 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] (sql/format (assoc m1 :lock [:in-share-mode]) {:params {:param1 "gabba" :param2 2}