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:
parent
6324eca4fc
commit
762252b660
8 changed files with 55 additions and 40 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)]))
|
||||||
|
|
|
||||||
|
|
@ -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! :#>>)
|
||||||
|
|
|
||||||
|
|
@ -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]]]}))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue