diff --git a/CHANGELOG.md b/CHANGELOG.md index 9633787..b0f0e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 0a64359..fe84434 100644 --- a/README.md +++ b/README.md @@ -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"] ``` diff --git a/doc/differences-from-1-x.md b/doc/differences-from-1-x.md index 9819620..d5962fe 100644 --- a/doc/differences-from-1-x.md +++ b/doc/differences-from-1-x.md @@ -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. diff --git a/doc/getting-started.md b/doc/getting-started.md index 490a5e6..e8b9924 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -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. diff --git a/doc/operator-reference.md b/doc/operator-reference.md index 383e996..dcbc8a6 100644 --- a/doc/operator-reference.md +++ b/doc/operator-reference.md @@ -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. diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 3cabe55..19e9c45 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -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)])) diff --git a/src/honey/sql/pg_ops.cljc b/src/honey/sql/pg_ops.cljc index fd3076f..ef66509 100644 --- a/src/honey/sql/pg_ops.cljc +++ b/src/honey/sql/pg_ops.cljc @@ -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! :#>>) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 39e6426..4894d98 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -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]]]}))))