diff --git a/CHANGELOG.md b/CHANGELOG.md index be822e7..5a6daaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changes * 2.4.next in progress - * Address [#504](https://github.com/seancorfield/honeysql/issues/504) by adding special syntax for ignore/respect nulls. More work will be needed to support distinct/order by/limit in BigQuery array aggregation. + * Address [#504](https://github.com/seancorfield/honeysql/issues/504) for BigQuery support, by adding special syntax for ignore/respect nulls, as well as new `:distinct` and `:expr` clauses to allow expressions to be qualified with SQL clauses. The latter will probably be useful for other dialects too. * 2.4.1066 -- 2023-08-27 * Add `:select` with function call and alias example to README (PR [#502](https://github.com/seancorfield/honeysql/pull/502) [@markbastian](https://github.com/markbastian)). diff --git a/README.md b/README.md index 19c459d..d701018 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,11 @@ section of the documentation before trying to use HoneySQL to build your own que From Clojure: ```clojure -(refer-clojure :exclude '[filter for group-by into partition-by set update]) +(refer-clojure :exclude '[distinct filter for group-by into partition-by set update]) (require '[honey.sql :as sql] ;; CAUTION: this overwrites several clojure.core fns: ;; - ;; filter, for, group-by, into, partition-by, set, and update + ;; distinct, filter, for, group-by, into, partition-by, set, and update ;; ;; you should generally only refer in the specific ;; helpers that you want to use! diff --git a/doc/clause-reference.md b/doc/clause-reference.md index 6fbf53e..f5fafea 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -1002,6 +1002,19 @@ user=> (sql/format (-> (select :id ["SELECT id, AVG(salary) OVER () AS Average, MAX(salary) OVER () AS MaxSalary FROM employee"] ``` +## distinct, expr + +Related to the windowing clauses above, `:distinct` and `:expr` are +intended to let you mix clauses with expressions, such as in BigQuery's +`ARRAY_AGG` function: + +```clojure +user=> (sql/format {:select [[[:over + [[:array_agg {:distinct [:ignore-nulls :col] :order-by :x}] + {:partition-by :y}]]]]}) +["SELECT ARRAY_AGG (DISTINCT col IGNORE NULLS ORDER BY x ASC) OVER (PARTITION BY y)"] +``` + ## order-by `:order-by` accepts a sequence of one or more ordering diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 4926b7a..0ecf3ca 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -55,6 +55,7 @@ :raw :nest :with :with-recursive :intersect :union :union-all :except :except-all :table :select :select-distinct :select-distinct-on :select-top :select-distinct-top + :distinct :expr :into :bulk-collect-into :insert-into :replace-into :update :delete :delete-from :truncate :columns :set :from :using @@ -288,13 +289,14 @@ Any ? is escaped to ??." [k] - (let [n (str/replace (name k) "?" "??")] - (if (= \' (first n)) - (let [ident (subs n 1 (count n)) - ident-l (str/lower-case ident)] - (binding [*quoted* (when-not (contains? #{"array"} ident-l) *quoted*)] - (format-entity (keyword ident)))) - (-> n (dehyphen) (upper-case))))) + (when k + (let [n (str/replace (name k) "?" "??")] + (if (= \' (first n)) + (let [ident (subs n 1 (count n)) + ident-l (str/lower-case ident)] + (binding [*quoted* (when-not (contains? #{"array"} ident-l) *quoted*)] + (format-entity (keyword ident)))) + (-> n (dehyphen) (upper-case)))))) (defn- sym->kw "Given a symbol, produce a keyword, retaining the namespace @@ -1361,6 +1363,8 @@ :select-distinct-on #'format-selects-on :select-top #'format-select-top :select-distinct-top #'format-select-top + :distinct (fn [k xs] (format-selects k [[xs]])) + :expr (fn [_ xs] (format-selects nil [[xs]])) :into #'format-select-into :bulk-collect-into #'format-select-into :insert-into #'format-insert @@ -2269,4 +2273,8 @@ [:transport :t] [:id :name]] :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]} {:pretty true}) + (sql/format-expr [:array_agg {:distinct [:ignore-nulls :a] :order-by :a}]) + (sql/format-expr [:over [[:array_agg {:expr [:respect-nulls :a] :order-by :a + :limit 10}] + {:partition-by :something}]]) ) diff --git a/src/honey/sql/helpers.cljc b/src/honey/sql/helpers.cljc index 2922b34..ab1ce34 100644 --- a/src/honey/sql/helpers.cljc +++ b/src/honey/sql/helpers.cljc @@ -48,7 +48,7 @@ bulk-collect-info [& args] (as they are for all helper functions)." - (:refer-clojure :exclude [filter for group-by into partition-by set update]) + (:refer-clojure :exclude [distinct filter for group-by into partition-by set update]) (:require [clojure.core :as c] [honey.sql])) @@ -468,6 +468,16 @@ [& args] (generic :select-distinct-top args)) +(defn distinct + "Like `select-distinct` but produces DISTINCT..." + [& args] + (generic-1 :distinct args)) + +(defn expr + "Like `distinct` but produces ... (i.e., just the expression that follows)." + [& args] + (generic-1 :expr args)) + (defn into "Accepts table name, optionally followed a database name." {:arglists '([table] [table dbname])}