fixes #459 by making all operators variadic

except for := and the various :<> variants

some operators only make sense in binary usage and will produce invalid
SQL if used in a non-binary manner
This commit is contained in:
Sean Corfield 2023-02-11 13:35:55 -08:00
parent 6324eca4fc
commit 762252b660
8 changed files with 55 additions and 40 deletions

View file

@ -1,6 +1,7 @@
# Changes # Changes
* 2.4.next in progress * 2.4.next in progress
* Address [#459](https://github.com/seancorfield/honeysql/issues/459) by making all operators variadic (except `:=` and `:<>`).
* Address [#458](https://github.com/seancorfield/honeysql/issues/458) by adding `registered-*?` predicates. * Address [#458](https://github.com/seancorfield/honeysql/issues/458) by adding `registered-*?` predicates.
* 2.4.972 -- 2023-02-02 * 2.4.972 -- 2023-02-02

View file

@ -893,11 +893,9 @@ If your database supports `<=>` as an operator, you can tell HoneySQL about it u
```clojure ```clojure
(sql/register-op! :<=>) (sql/register-op! :<=>)
;; default is a binary operator: ;; all operators are assumed to be variadic:
(-> (select :a) (where [:<=> :a "foo"]) sql/format) (-> (select :a) (where [:<=> :a "foo"]) sql/format)
=> ["SELECT a WHERE a <=> ?" "foo"] => ["SELECT a WHERE a <=> ?" "foo"]
;; you can declare that an operator is variadic:
(sql/register-op! :<=> :variadic true)
(-> (select :a) (where [:<=> "food" :a "fool"]) sql/format) (-> (select :a) (where [:<=> "food" :a "fool"]) sql/format)
=> ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"] => ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"]
``` ```

View file

@ -215,7 +215,7 @@ The protocols and multimethods in 1.x have all gone away. The primary extension
You can also register new "functions" that can implement special syntax (such as `:array`, `:inline`, `:raw` etc above) via `honey.sql/register-fn!`. This accepts a "function" name as a keyword and a formatter which will generally be a function of two arguments: the function name (so formatters can be reused across different names) and a vector of the arguments the function should accept. You can also register new "functions" that can implement special syntax (such as `:array`, `:inline`, `:raw` etc above) via `honey.sql/register-fn!`. This accepts a "function" name as a keyword and a formatter which will generally be a function of two arguments: the function name (so formatters can be reused across different names) and a vector of the arguments the function should accept.
And, finally, you can register new operators that will be recognized in expressions via `honey.sql/register-op!`. This accepts an operator name as a keyword and optional named parameters to indicate whether the operator is `:variadic` (the default is strictly binary) and whether it should ignore operands that evaluate to `nil` (via `:ignore-nil`). The latter can make it easier to construct complex expressions programmatically without having to worry about conditionally removing "optional" (`nil`) values. And, finally, you can register new operators that will be recognized in expressions via `honey.sql/register-op!`. This accepts an operator name as a keyword and an optional named parameter to indicate whether it should ignore operands that evaluate to `nil` (via `:ignore-nil`). That can make it easier to construct complex expressions programmatically without having to worry about conditionally removing "optional" (`nil`) values.
> Note: because of the changes in the extension machinery between 1.x and 2.x, it is not possible to use the [nilenso/honeysql-postgress](https://github.com/nilenso/honeysql-postgres) library with HoneySQL 2.x but the goal is to incorporate all of the syntax from that library into the core of HoneySQL. > Note: because of the changes in the extension machinery between 1.x and 2.x, it is not possible to use the [nilenso/honeysql-postgress](https://github.com/nilenso/honeysql-postgres) library with HoneySQL 2.x but the goal is to incorporate all of the syntax from that library into the core of HoneySQL.

View file

@ -112,7 +112,8 @@ Some "functions" are considered to be operators. In general,
> Note: you can use the `:numbered true` option to `format` to produce SQL containing numbered placeholders, like `FOO(a, $1, $2)`, instead of positional placeholders (`?`). > Note: you can use the `:numbered true` option to `format` to produce SQL containing numbered placeholders, like `FOO(a, $1, $2)`, instead of positional placeholders (`?`).
Operators can be strictly binary or variadic (most are strictly binary). Operators are all treated as variadic (except for `:=` and
`:<>` / `:!=` / `:not=` which are binary and require exactly two operands).
Special syntax can have zero or more arguments and each form is Special syntax can have zero or more arguments and each form is
described in the [Special Syntax](special-syntax.md) section. described in the [Special Syntax](special-syntax.md) section.

View file

@ -34,7 +34,7 @@ can simply evaluate to `nil` instead).
## in ## in
Binary predicate for checking an expression is Predicate for checking an expression is
is a member of a specified set of values. is a member of a specified set of values.
The two most common forms are: The two most common forms are:
@ -73,16 +73,21 @@ This produces `(col1, col2) IN ...`
> Note: This is a change from HoneySQL 1.x which accepted a sequence of column names but required more work for arbitrary expressions. > Note: This is a change from HoneySQL 1.x which accepted a sequence of column names but required more work for arbitrary expressions.
## = <> < > <= >= ## = <>
Binary comparison operators. These expect exactly Binary comparison operators. These expect exactly
two arguments. two arguments.
`not=` and `!=` are accepted as aliases for `<>`. `not=` and `!=` are accepted as aliases for `<>`.
## < > <= >=
Comparison operators. These expect exactly
two arguments.
## is, is-not ## is, is-not
Binary predicates for `NULL` and Boolean values: Predicates for `NULL` and Boolean values:
```clojure ```clojure
{... {...
@ -106,16 +111,15 @@ Binary predicates for `NULL` and Boolean values:
## mod, xor, + - * / % | & ^ ## mod, xor, + - * / % | & ^
Mathematical and bitwise operators. `+` and `*` are Mathematical and bitwise operators.
variadic; the rest are strictly binary operators.
## like, not like, ilike, not ilike, regexp ## like, not like, ilike, not ilike, regexp
Pattern matching binary operators. `regex` is accepted Pattern matching operators. `regex` is accepted
as an alias for `regexp`. as an alias for `regexp`.
`similar-to` and `not-similar-to` are also supported. `similar-to` and `not-similar-to` are also supported.
## || ## ||
Variadic string concatenation operator. String concatenation operator.

View file

@ -1279,7 +1279,6 @@
(atom))) (atom)))
(def ^:private op-ignore-nil (atom #{:and :or})) (def ^:private op-ignore-nil (atom #{:and :or}))
(def ^:private op-variadic (atom #{:and :or :+ :* :- :|| :&&}))
(defn- unwrap [x opts] (defn- unwrap [x opts]
(if-let [m (meta x)] (if-let [m (meta x)]
@ -1558,30 +1557,20 @@
(format-dsl expr (assoc opts :nested true)) (format-dsl expr (assoc opts :nested true))
(sequential? expr) (sequential? expr)
(let [op (sym->kw (first expr))] (let [op' (sym->kw (first expr))
(if (keyword? op) op (get infix-aliases op' op')]
(cond (contains? @infix-ops op) (if (keyword? op')
(if (contains? @op-variadic op) ; no aliases here, no special semantics (cond (contains? @infix-ops op')
(let [x (if (contains? @op-ignore-nil op) (if (contains? #{:= :<>} op)
(remove nil? expr)
expr)
[sqls params]
(reduce-sql (map #(format-expr % {:nested true})
(rest x)))]
(into [(cond-> (str/join (str " " (sql-kw op) " ") sqls)
nested
(as-> s (str "(" s ")")))]
params))
(let [[_ a b & y] expr (let [[_ a b & y] expr
_ (when (seq y) _ (when (seq y)
(throw (ex-info (str "only binary " (throw (ex-info (str "only binary "
op op'
" is supported") " is supported")
{:expr expr}))) {:expr expr})))
[s1 & p1] (format-expr a {:nested true}) [s1 & p1] (format-expr a {:nested true})
[s2 & p2] (format-expr b {:nested true}) [s2 & p2] (format-expr b {:nested true})]
op (get infix-aliases op op)] (-> (if (or (nil? a) (nil? b))
(-> (if (and (#{:= :<>} op) (or (nil? a) (nil? b)))
(str (if (nil? a) (str (if (nil? a)
(if (nil? b) "NULL" s2) (if (nil? b) "NULL" s2)
s1) s1)
@ -1591,7 +1580,22 @@
(as-> s (str "(" s ")"))) (as-> s (str "(" s ")")))
(vector) (vector)
(into p1) (into p1)
(into p2)))) (into p2)))
(let [x (if (contains? @op-ignore-nil op)
(remove nil? expr)
expr)
[sqls params]
(reduce-sql (map #(format-expr % {:nested true})
(rest x)))]
(when-not (pos? (count sqls))
(throw (ex-info (str "no operands found for " op')
{:expr expr})))
(into [(cond-> (str/join (str " " (sql-kw op) " ") sqls)
(= 1 (count sqls))
(as-> s (str (sql-kw op) " " s))
nested
(as-> s (str "(" s ")")))]
params)))
(contains? #{:in :not-in} op) (contains? #{:in :not-in} op)
(let [[sql & params] (format-in op (rest expr))] (let [[sql & params] (format-in op (rest expr))]
(into [(if nested (str "(" sql ")") sql)] params)) (into [(if nested (str "(" sql ")") sql)] params))
@ -1855,15 +1859,13 @@
(contains? @special-syntax (sym->kw function))) (contains? @special-syntax (sym->kw function)))
(defn register-op! (defn register-op!
"Register a new infix operator. Operators can be defined to be variadic (the "Register a new infix operator. All operators are variadic and may choose
default is that they are binary) and may choose to ignore `nil` arguments to ignore `nil` arguments (this can make it easier to programmatically
(this can make it easier to programmatically construct the DSL)." construct the DSL)."
[op & {:keys [variadic ignore-nil]}] [op & {:keys [ignore-nil]}]
(let [op (sym->kw op)] (let [op (sym->kw op)]
(assert (keyword? op)) (assert (keyword? op))
(swap! infix-ops conj op) (swap! infix-ops conj op)
(when variadic
(swap! op-variadic conj op))
(when ignore-nil (when ignore-nil
(swap! op-ignore-nil conj op)))) (swap! op-ignore-nil conj op))))
@ -1955,7 +1957,7 @@
(sql/format-expr [:primary-key]) (sql/format-expr [:primary-key])
(sql/register-op! 'y) (sql/register-op! 'y)
(sql/format {:where '[y 2 3]}) (sql/format {:where '[y 2 3]})
(sql/register-op! :<=> :variadic true :ignore-nil true) (sql/register-op! :<=> :ignore-nil true)
;; and then use the new operator: ;; and then use the new operator:
(sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]}) (sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]})
(sql/register-fn! :foo (fn [f args] ["FOO(?)" (first args)])) (sql/register-fn! :foo (fn [f args] ["FOO(?)" (first args)]))

View file

@ -52,7 +52,7 @@
(def !regex !tilde) (def !regex !tilde)
(def !iregex !tilde*) (def !iregex !tilde*)
(sql/register-op! :-> :variadic true) (sql/register-op! :->)
(sql/register-op! :->>) (sql/register-op! :->>)
(sql/register-op! :#>) (sql/register-op! :#>)
(sql/register-op! :#>>) (sql/register-op! :#>>)

View file

@ -1062,3 +1062,12 @@ ORDER BY id = ? DESC
(deftest issue-456-format-expr (deftest issue-456-format-expr
(is (= ["`x` + ?" 1] (is (= ["`x` + ?" 1]
(sut/format [:+ :x 1] {:dialect :mysql})))) (sut/format [:+ :x 1] {:dialect :mysql}))))
(deftest issue-459-variadic-ops
(sut/register-op! :op)
(is (= ["SELECT OP a"]
(sut/format {:select [[[:op :a]]]})))
(is (= ["SELECT a OP b"]
(sut/format {:select [[[:op :a :b]]]})))
(is (= ["SELECT a OP b OP c"]
(sut/format {:select [[[:op :a :b :c]]]}))))