diff --git a/CHANGELOG.md b/CHANGELOG.md index 1556eff..8e7907d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,11 @@ # Changes -* 2.0.next (gold) in progress - * Address #332 by improving `:cross-join` documentation. - * Fix `fetch` helper. - -* 2.0.0-rc4 (for testing; 2021-07-17) - * Fix #338 by adding `ONLY` to `:fetch`. +* 2.0.0-rc5 in progress + * Fix #338 by producing `OFFSET n ROWS` (or `ROW` if `n` is 1) if `:fetch` is present or `:sqlserver` dialect is specified; and by producing `FETCH NEXT n ROWS ONLY` (or `ROW` is `n` is 1; or `FIRST` instead of `NEXT` if `:offset` is not present). * Fix #337 by switching to `clojure.test` even for ClojureScript. + * Address #332 by improving `:cross-join` documentation. * Address #330 by improving exception when a non-entity is encountered where an entity is expected. + * Fix `fetch` helper (it previously returned an `:offset` clause). * Fix bug in unrolling nested argument to `with-columns` helper. * 2.0.0-rc3 (for testing; 2021-06-16) diff --git a/pom.xml b/pom.xml index 5f5571f..f25bd30 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.github.seancorfield honeysql - 2.0.0-rc4 + 2.0.0-rc5 honeysql SQL as Clojure data structures. https://github.com/seancorfield/honeysql @@ -25,7 +25,7 @@ https://github.com/seancorfield/honeysql scm:git:git://github.com/seancorfield/honeysql.git scm:git:ssh://git@github.com/seancorfield/honeysql.git - v2.0.0-rc4 + v2.0.0-rc5 diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 66b2e2b..d912375 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -86,11 +86,14 @@ (conj order clause)))) (def ^:private dialects - {:ansi {:quote #(str \" % \")} - :sqlserver {:quote #(str \[ % \])} - :mysql {:quote #(str \` % \`) - :clause-order-fn #(add-clause-before % :set :where)} - :oracle {:quote #(str \" % \") :as false}}) + (reduce-kv (fn [m k v] + (assoc m k (assoc v :dialect k))) + {} + {:ansi {:quote #(str \" % \")} + :sqlserver {:quote #(str \[ % \])} + :mysql {:quote #(str \` % \`) + :clause-order-fn #(add-clause-before % :set :where)} + :oracle {:quote #(str \" % \") :as false}})) ; should become defonce (def ^:private default-dialect (atom (:ansi dialects))) @@ -109,9 +112,26 @@ (def ^:private ^:dynamic *allow-suspicious-entities* false) ;; "linting" mode (:none, :basic, :strict): (def ^:private ^:dynamic *checking* :none) +;; the current DSL hash map being formatted (for contains-clause?): +(def ^:private ^:dynamic *dsl* nil) ;; clause helpers +(defn contains-clause? + "Returns true if the current DSL expression being formatted + contains the specified clause (as a keyword or symbol)." + [clause] + (or (contains? *dsl* clause) + (contains? *dsl* + (if (keyword? clause) + (symbol (name clause)) + (keyword (name clause)))))) + +(defn- sql-server? + "Helper to detect if SQL Server is the current dialect." + [] + (= :sqlserver (:dialect *dialect*))) + ;; String.toUpperCase() or `str/upper-case` for that matter converts the ;; string to uppercase for the DEFAULT LOCALE. Normally this does what you'd ;; expect but things like `inner join` get converted to `İNNER JOİN` (dot over @@ -869,10 +889,18 @@ :partition-by #'format-selects :order-by #'format-order-by :limit #'format-on-expr - :offset #'format-on-expr + :offset (fn [_ x] + (if (or (contains-clause? :fetch) (sql-server?)) + (let [[sql & params] (format-on-expr :offset x) + rows (if (and (number? x) (== 1 x)) :row :rows)] + (into [(str sql " " (sql-kw rows))] params)) + ;; format in the old style: + (format-on-expr :offset x))) :fetch (fn [_ x] - (let [[sql & params] (format-on-expr :fetch x)] - (into [(str sql " " (sql-kw :only))] params))) + (let [which (if (contains-clause? :offset) :fetch-next :fetch-first) + rows (if (and (number? x) (== 1 x)) :row-only :rows-only) + [sql & params] (format-on-expr which x)] + (into [(str sql " " (sql-kw rows))] params))) :for #'format-lock-strength :lock #'format-lock-strength :values #'format-values @@ -907,29 +935,30 @@ This is intended to be used when writing your own formatters to extend the DSL supported by HoneySQL." [statement-map & [{:keys [aliased nested pretty]}]] - (let [[sqls params leftover] - (reduce (fn [[sql params leftover] k] - (if-some [xs (if-some [xs (k leftover)] - xs - (let [s (kw->sym k)] - (get leftover s)))] - (let [formatter (k @clause-format) - [sql' & params'] (formatter k xs)] - [(conj sql sql') - (if params' (into params params') params) - (dissoc leftover k (kw->sym k))]) - [sql params leftover])) - [[] [] statement-map] - *clause-order*)] - (if (seq leftover) - (throw (ex-info (str "These SQL clauses are unknown or have nil values: " - (str/join ", " (keys leftover))) - leftover)) - (into [(cond-> (str/join (if pretty "\n" " ") (filter seq sqls)) - pretty - (as-> s (str "\n" s "\n")) - (and nested (not aliased)) - (as-> s (str "(" s ")")))] params)))) + (binding [*dsl* statement-map] + (let [[sqls params leftover] + (reduce (fn [[sql params leftover] k] + (if-some [xs (if-some [xs (k leftover)] + xs + (let [s (kw->sym k)] + (get leftover s)))] + (let [formatter (k @clause-format) + [sql' & params'] (formatter k xs)] + [(conj sql sql') + (if params' (into params params') params) + (dissoc leftover k (kw->sym k))]) + [sql params leftover])) + [[] [] statement-map] + *clause-order*)] + (if (seq leftover) + (throw (ex-info (str "These SQL clauses are unknown or have nil values: " + (str/join ", " (keys leftover))) + leftover)) + (into [(cond-> (str/join (if pretty "\n" " ") (filter seq sqls)) + pretty + (as-> s (str "\n" s "\n")) + (and nested (not aliased)) + (as-> s (str "(" s ")")))] params))))) (def ^:private infix-aliases "Provided for backward compatibility with earlier HoneySQL versions." diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 75f2197..6fc16c9 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -783,7 +783,30 @@ ORDER BY id = ? DESC :join [[{:select :a :from :b :where [:= :id 123]} :x] :y] :where [:= :id 456]}))))) -(deftest fetch-offset-issue338 - (is (= ["SELECT foo FROM bar OFFSET ? FETCH ? ONLY" 20 10] - (format {:select :foo :from :bar - :fetch 10 :offset 20})))) +(deftest fetch-offset-issue-338 + (testing "default offset (with and without limit)" + (is (= ["SELECT foo FROM bar LIMIT ? OFFSET ?" 10 20] + (format {:select :foo :from :bar + :limit 10 :offset 20}))) + (is (= ["SELECT foo FROM bar OFFSET ?" 20] + (format {:select :foo :from :bar + :offset 20})))) + (testing "default offset / fetch" + (is (= ["SELECT foo FROM bar OFFSET ? ROWS FETCH NEXT ? ROWS ONLY" 20 10] + (format {:select :foo :from :bar + :fetch 10 :offset 20}))) + (is (= ["SELECT foo FROM bar OFFSET ? ROW FETCH NEXT ? ROW ONLY" 1 1] + (format {:select :foo :from :bar + :fetch 1 :offset 1}))) + (is (= ["SELECT foo FROM bar FETCH FIRST ? ROWS ONLY" 2] + (format {:select :foo :from :bar + :fetch 2})))) + (testing "SQL Server offset" + (is (= ["SELECT [foo] FROM [bar] OFFSET ? ROWS FETCH NEXT ? ROWS ONLY" 20 10] + (format {:select :foo :from :bar + :fetch 10 :offset 20} + {:dialect :sqlserver}))) + (is (= ["SELECT [foo] FROM [bar] OFFSET ? ROWS" 20] + (format {:select :foo :from :bar + :offset 20} + {:dialect :sqlserver})))))