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})))))