Fixes #321 by adding :checking option
Initial linting is only for IN () and IN (NULL)
This commit is contained in:
parent
f606dc6044
commit
20cba15da2
4 changed files with 63 additions and 4 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
* 2.0.next in progress
|
* 2.0.next in progress
|
||||||
* Fix #323 by supporting more than one SQL entity in `:on-conflict`.
|
* Fix #323 by supporting more than one SQL entity in `:on-conflict`.
|
||||||
|
* Fix #321 by adding `:checking` mode. Currently only detects potential problems with `IN` clauses.
|
||||||
|
|
||||||
* 2.0.0-beta2 (for testing; 2021-04-13)
|
* 2.0.0-beta2 (for testing; 2021-04-13)
|
||||||
* The documentation continues to be expanded and clarified in response to feedback!
|
* The documentation continues to be expanded and clarified in response to feedback!
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md).
|
||||||
## Format Options
|
## Format Options
|
||||||
|
|
||||||
In addition to the `:quoted` and `:dialect` options described above,
|
In addition to the `:quoted` and `:dialect` options described above,
|
||||||
`format` also accepts `:inline` and `:params`.
|
`format` also accepts `:checking`, `:inline`, and `:params`.
|
||||||
|
|
||||||
The `:params` option was mentioned above and is used to specify
|
The `:params` option was mentioned above and is used to specify
|
||||||
the values of named parameters in the DSL.
|
the values of named parameters in the DSL.
|
||||||
|
|
@ -312,6 +312,14 @@ was wrapped in `[:inline `..`]`:
|
||||||
* keywords and symbols become SQL keywords (uppercase, with `-` replaced by a space),
|
* keywords and symbols become SQL keywords (uppercase, with `-` replaced by a space),
|
||||||
* everything else is just turned into a string (by calling `str`) and added to the SQL string.
|
* everything else is just turned into a string (by calling `str`) and added to the SQL string.
|
||||||
|
|
||||||
|
The `:checking` option defaults to `:none`. If `:checking :basic` is
|
||||||
|
specified, certain obvious errors -- such as `IN` with an empty collection --
|
||||||
|
is treated as an error and an exception is thrown. If `:checking :strict`
|
||||||
|
is specified, certain dubious constructs -- such as `IN` with a collection
|
||||||
|
containing `NULL` values -- is also treated as an error and an exception is
|
||||||
|
thrown. It is expected that this feature will be expanded over time
|
||||||
|
to help avoid generating illegal SQL. _[New in version 2.0.next]_
|
||||||
|
|
||||||
`format` accepts options as either a single hash map argument or
|
`format` accepts options as either a single hash map argument or
|
||||||
as named arguments (alternating keys and values). If you are using
|
as named arguments (alternating keys and values). If you are using
|
||||||
Clojure 1.11 (or later) you can mix'n'match, providing some options
|
Clojure 1.11 (or later) you can mix'n'match, providing some options
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,8 @@
|
||||||
;; in entities; if someone complains about this check, an option
|
;; in entities; if someone complains about this check, an option
|
||||||
;; can be added to format to turn this on:
|
;; can be added to format to turn this on:
|
||||||
(def ^:private ^:dynamic *allow-suspicious-entities* false)
|
(def ^:private ^:dynamic *allow-suspicious-entities* false)
|
||||||
|
;; "linting" mode (:none, :basic, :strict):
|
||||||
|
(def ^:private ^:dynamic *checking* :none)
|
||||||
|
|
||||||
;; clause helpers
|
;; clause helpers
|
||||||
|
|
||||||
|
|
@ -929,6 +931,16 @@
|
||||||
(let [[sql-x & params-x] (format-expr x {:nested true})
|
(let [[sql-x & params-x] (format-expr x {:nested true})
|
||||||
[sql-y & params-y] (format-expr y {:nested true})
|
[sql-y & params-y] (format-expr y {:nested true})
|
||||||
values (unwrap (first params-y) {})]
|
values (unwrap (first params-y) {})]
|
||||||
|
(when-not (= :none *checking*)
|
||||||
|
(when (or (and (sequential? y) (empty? y))
|
||||||
|
(and (sequential? values) (empty? values)))
|
||||||
|
(throw (ex-info "IN () empty collection is illegal"
|
||||||
|
{:clause [in x y]})))
|
||||||
|
(when (and (= :strict *checking*)
|
||||||
|
(or (and (sequential? y) (some nil? y))
|
||||||
|
(and (sequential? values) (some nil? values))))
|
||||||
|
(throw (ex-info "IN (NULL) does not match"
|
||||||
|
{:clause [in x y]}))))
|
||||||
(if (and (= "?" sql-y) (= 1 (count params-y)) (coll? values))
|
(if (and (= "?" sql-y) (= 1 (count params-y)) (coll? values))
|
||||||
(let [sql (str "(" (str/join ", " (repeat (count values) "?")) ")")]
|
(let [sql (str "(" (str/join ", " (repeat (count values) "?")) ")")]
|
||||||
(-> [(str sql-x " " (sql-kw in) " " sql)]
|
(-> [(str sql-x " " (sql-kw in) " " sql)]
|
||||||
|
|
@ -1228,6 +1240,9 @@
|
||||||
(let [dialect? (contains? opts :dialect)
|
(let [dialect? (contains? opts :dialect)
|
||||||
dialect (when dialect? (get dialects (check-dialect (:dialect opts))))]
|
dialect (when dialect? (get dialects (check-dialect (:dialect opts))))]
|
||||||
(binding [*dialect* (if dialect? dialect @default-dialect)
|
(binding [*dialect* (if dialect? dialect @default-dialect)
|
||||||
|
*checking* (if (contains? opts :checking)
|
||||||
|
(:checking opts)
|
||||||
|
:none)
|
||||||
*clause-order* (if dialect?
|
*clause-order* (if dialect?
|
||||||
(if-let [f (:clause-order-fn dialect)]
|
(if-let [f (:clause-order-fn dialect)]
|
||||||
(f @base-clause-order)
|
(f @base-clause-order)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
#?(:clj [clojure.test :refer [deftest is testing]]
|
#?(:clj [clojure.test :refer [deftest is testing]]
|
||||||
:cljs [cljs.test :refer-macros [deftest is testing]])
|
:cljs [cljs.test :refer-macros [deftest is testing]])
|
||||||
[honey.sql :as sut :refer [format]]
|
[honey.sql :as sut :refer [format]]
|
||||||
[honey.sql.helpers :as h]))
|
[honey.sql.helpers :as h])
|
||||||
|
#?(:clj (:import (clojure.lang ExceptionInfo))))
|
||||||
|
|
||||||
(deftest mysql-tests
|
(deftest mysql-tests
|
||||||
(is (= ["SELECT * FROM `table` WHERE `id` = ?" 1]
|
(is (= ["SELECT * FROM `table` WHERE `id` = ?" 1]
|
||||||
|
|
@ -685,9 +686,9 @@ ORDER BY id = ? DESC
|
||||||
(format))
|
(format))
|
||||||
(is false "; not detected in entity!")
|
(is false "; not detected in entity!")
|
||||||
(catch #?(:clj Throwable :cljs :default) e
|
(catch #?(:clj Throwable :cljs :default) e
|
||||||
(is (:disallowed (ex-data e))))))
|
(is (:disallowed (ex-data e))))))))
|
||||||
;; should not produce: ["SELECT foo, bar FROM mytable ORDER BY foo; select * from users"]
|
;; should not produce: ["SELECT foo, bar FROM mytable ORDER BY foo; select * from users"]
|
||||||
))
|
|
||||||
|
|
||||||
(deftest issue-319-test
|
(deftest issue-319-test
|
||||||
(testing "that registering a clause is idempotent"
|
(testing "that registering a clause is idempotent"
|
||||||
|
|
@ -696,3 +697,37 @@ ORDER BY id = ? DESC
|
||||||
(sut/register-clause! :foo (constantly ["FOO"]) nil)
|
(sut/register-clause! :foo (constantly ["FOO"]) nil)
|
||||||
(sut/register-clause! :foo (constantly ["FOO"]) nil)
|
(sut/register-clause! :foo (constantly ["FOO"]) nil)
|
||||||
(format {:foo []}))))))
|
(format {:foo []}))))))
|
||||||
|
|
||||||
|
(deftest issue-321-linting
|
||||||
|
(testing "empty IN is ignored by default"
|
||||||
|
(is (= ["WHERE x IN ()"]
|
||||||
|
(format {:where [:in :x []]})))
|
||||||
|
(is (= ["WHERE x IN ()"]
|
||||||
|
(format {:where [:in :x :?y]}
|
||||||
|
{:params {:y []}}))))
|
||||||
|
(testing "empty IN is flagged in basic mode"
|
||||||
|
(is (thrown-with-msg? ExceptionInfo #"empty collection"
|
||||||
|
(format {:where [:in :x []]}
|
||||||
|
{:checking :basic})))
|
||||||
|
(is (thrown-with-msg? ExceptionInfo #"empty collection"
|
||||||
|
(format {:where [:in :x :?y]}
|
||||||
|
{:params {:y []} :checking :basic}))))
|
||||||
|
(testing "IN NULL is ignored by default and basic"
|
||||||
|
(is (= ["WHERE x IN (NULL)"]
|
||||||
|
(format {:where [:in :x [nil]]})))
|
||||||
|
(is (= ["WHERE x IN (NULL)"]
|
||||||
|
(format {:where [:in :x [nil]]}
|
||||||
|
{:checking :basic})))
|
||||||
|
(is (= ["WHERE x IN (?)" nil]
|
||||||
|
(format {:where [:in :x :?y]}
|
||||||
|
{:params {:y [nil]}})))
|
||||||
|
(is (= ["WHERE x IN (?)" nil]
|
||||||
|
(format {:where [:in :x :?y]}
|
||||||
|
{:params {:y [nil]} :checking :basic}))))
|
||||||
|
(testing "IN NULL is flagged in strict mode"
|
||||||
|
(is (thrown-with-msg? ExceptionInfo #"does not match"
|
||||||
|
(format {:where [:in :x [nil]]}
|
||||||
|
{:checking :strict})))
|
||||||
|
(is (thrown-with-msg? ExceptionInfo #"does not match"
|
||||||
|
(format {:where [:in :x :?y]}
|
||||||
|
{:params {:y [nil]} :checking :strict})))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue