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
* 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.
* 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
(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"]
;; you can declare that an operator is variadic:
(sql/register-op! :<=> :variadic true)
(-> (select :a) (where [:<=> "food" :a "fool"]) sql/format)
=> ["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.
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.

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 (`?`).
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
described in the [Special Syntax](special-syntax.md) section.

View file

@ -34,7 +34,7 @@ can simply evaluate to `nil` instead).
## in
Binary predicate for checking an expression is
Predicate for checking an expression is
is a member of a specified set of values.
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.
## = <> < > <= >=
## = <>
Binary comparison operators. These expect exactly
two arguments.
`not=` and `!=` are accepted as aliases for `<>`.
## < > <= >=
Comparison operators. These expect exactly
two arguments.
## is, is-not
Binary predicates for `NULL` and Boolean values:
Predicates for `NULL` and Boolean values:
```clojure
{...
@ -106,16 +111,15 @@ Binary predicates for `NULL` and Boolean values:
## mod, xor, + - * / % | & ^
Mathematical and bitwise operators. `+` and `*` are
variadic; the rest are strictly binary operators.
Mathematical and bitwise operators.
## 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`.
`similar-to` and `not-similar-to` are also supported.
## ||
Variadic string concatenation operator.
String concatenation operator.

View file

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

View file

@ -52,7 +52,7 @@
(def !regex !tilde)
(def !iregex !tilde*)
(sql/register-op! :-> :variadic true)
(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
(is (= ["`x` + ?" 1]
(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]]]}))))