Fixes #321 by adding :checking option

Initial linting is only for IN () and IN (NULL)
This commit is contained in:
Sean Corfield 2021-04-22 22:13:32 -07:00
parent f606dc6044
commit 20cba15da2
4 changed files with 63 additions and 4 deletions

View file

@ -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!

View file

@ -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

View file

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

View file

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