From a653f9b15776bf5236d53b19c15b1099820f56bf Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Thu, 6 Jan 2022 23:02:20 -0800 Subject: [PATCH] address #281 add select * except / replace for BigQuery --- CHANGELOG.md | 1 + doc/clause-reference.md | 12 +++++-- src/honey/sql.cljc | 65 ++++++++++++++++++++++++++--------- test/honey/bigquery_test.cljc | 17 +++++++-- 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb25f1..231fe16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changes * 2.2.next in progress + * Address #281 by adding support for `SELECT * EXCEPT ..` and `SELECT * REPLACE ..` -- see [SQL Clause Reference - SELECT](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-clause-reference#select-select-distinct) for more details. * Update `build-clj` to v0.6.7. * 2.2.840 -- 2021-12-23 diff --git a/doc/clause-reference.md b/doc/clause-reference.md index 16fa67e..a8525dd 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -348,8 +348,16 @@ third is a simple column name and its alias. `:select-distinct` works the same way but produces `SELECT DISTINCT`. -HoneySQL does not yet support `SELECT .. INTO ..` -or `SELECT .. BULK COLLECT INTO ..`. +> Google BigQuery support: to provide `SELECT * EXCEPT ..` and `SELECT * REPLACE ..` syntax, HoneySQL supports a vector starting with `:*` or the symbol `*` followed by except columns and/or replace expressions as columns: + +```clojure +user=> (sql/format {:select [[:* :except [:a :b :c]]] :from [:table]}) +["SELECT * EXCEPT (a, b, c) FROM table"] +user=> (sql/format {:select [[:* :replace [[[:* :a [:inline 100]] :b] [[:inline 2] :c]]]] :from [:table]}) +["SELECT * REPLACE (a * 100 AS b, 2 AS c) FROM table"] +user=> (sql/format {:select [[:* :except [:a :b] :replace [[[:inline 2] :c]]]] :from [:table]}) +["SELECT * EXCEPT (a, b) REPLACE (2 AS c) FROM table"] +``` ## select-distinct-on diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index c3d1eb9..749a082 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -294,30 +294,63 @@ :else (format-entity x))) +(declare format-selects-common) + (defn- format-selectable-dsl [x & [{:keys [as aliased] :as opts}]] (cond (map? x) (format-dsl x {:nested true}) (sequential? x) (let [s (first x) - pair? (< 1 (count x)) a (second x) + pair? (= 2 (count x)) + big? (and (ident? s) (= "*" (name s)) + (ident? a) (#{"except" "replace"} (name a))) + more? (and (< 2 (count x)) (not big?)) [sql & params] (if (map? s) (format-dsl s {:nested true}) (format-expr s)) - [sql' & params'] (when pair? - (if (sequential? a) - (let [[sql params] (format-expr-list a {:aliased true})] - (into [(str/join " " sql)] params)) - (format-selectable-dsl a {:aliased true})))] - (-> [(cond-> sql - pair? - (str (if as - (if (and (contains? *dialect* :as) - (not (:as *dialect*))) - " " - " AS ") - " ") sql'))] + [sql' & params'] (when (or pair? big?) + (cond (sequential? a) + (let [[sqls params] (format-expr-list a {:aliased true})] + (into [(str/join " " sqls)] params)) + big? ; BigQuery support #281 + (reduce (fn [[sql & params] [k arg]] + (let [[sql' params'] + (cond (and (ident? k) (= "except" (name k)) arg) + (let [[sqls params] + (format-expr-list arg {:aliased true})] + [(str (sql-kw k) " (" (str/join ", " sqls) ")") + params]) + (and (ident? k) (= "replace" (name k)) arg) + (let [[sql & params] (format-selects-common nil true arg)] + [(str (sql-kw k) " (" sql ")") + params]) + :else + (throw (ex-info "bigquery * only supports except and replace" + {:clause k :arg arg})))] + (-> [(cond->> sql' sql (str sql " "))] + (into params) + (into params')))) + [] + (partition-all 2 (rest x))) + :else + (format-selectable-dsl a {:aliased true})))] + (-> [(cond pair? + (str sql + (if as + (if (and (contains? *dialect* :as) + (not (:as *dialect*))) + " " + " AS ") + " ") sql') + big? + (str sql " " sql') + more? + (throw (ex-info "illegal syntax in select expression" + {:symbol s :alias a :unexpected (nnext x)})) + :else + sql)] (into params) (into params'))) @@ -376,9 +409,9 @@ (when (empty? xs) (throw (ex-info (str prefix " empty column list is illegal") {:clause (into [prefix] xs)})))) - (into [(str prefix " " (str/join ", " sqls))] params)) + (into [(str (when prefix (str prefix " ")) (str/join ", " sqls))] params)) (let [[sql & params] (format-selectable-dsl xs {:as as})] - (into [(str prefix " " sql)] params)))) + (into [(str (when prefix (str prefix " ")) sql)] params)))) (defn- format-selects [k xs] (format-selects-common diff --git a/test/honey/bigquery_test.cljc b/test/honey/bigquery_test.cljc index ba77d5e..1ffe820 100644 --- a/test/honey/bigquery_test.cljc +++ b/test/honey/bigquery_test.cljc @@ -3,7 +3,8 @@ (ns honey.bigquery-test (:refer-clojure :exclude [format]) (:require [clojure.test :refer [deftest is]] - [honey.sql :as sut])) + [honey.sql :as sut]) + #?(:clj (:import (clojure.lang ExceptionInfo)))) (deftest except-replace-tests (is (= ["SELECT * FROM table WHERE id = ?" 1] @@ -13,4 +14,16 @@ (is (= ["SELECT * REPLACE (a * 100 AS b, 2 AS c) FROM table WHERE id = ?" 1] (sut/format {:select [[:* :replace [[[:* :a [:inline 100]] :b] [[:inline 2] :c]]]] :from [:table] :where [:= :id 1]}))) (is (= ["SELECT * EXCEPT (a, b) REPLACE (2 AS c) FROM table WHERE id = ?" 1] - (sut/format {:select [[:* :except [:a :b] :replace [[[:inline 2] :c]]]] :from [:table] :where [:= :id 1]})))) + (sut/format {:select [[:* :except [:a :b] :replace [[[:inline 2] :c]]]] :from [:table] :where [:= :id 1]}))) + (is (= ["SELECT * REPLACE (a * ? AS b, ? AS c) FROM table WHERE id = ?" 100 2 1] + (sut/format {:select [[:* :replace [[[:* :a 100] :b] [2 :c]]]] :from [:table] :where [:= :id 1]}))) + (is (= ["SELECT * EXCEPT (a, b) REPLACE (? AS c) FROM table WHERE id = ?" 2 1] + (sut/format {:select [[:* :except [:a :b] :replace [[2 :c]]]] :from [:table] :where [:= :id 1]})))) + +(deftest bad-select-tests + (is (thrown? ExceptionInfo + (sut/format {:select [[:* :except [:a] :bad]]}))) + (is (thrown? ExceptionInfo + (sut/format {:select [[:* :except]]}))) + (is (thrown? ExceptionInfo + (sut/format {:select [[:foo :bar :quux]]}))))