Fixes #322 by rewriting where/having merge

This commit is contained in:
Sean Corfield 2021-04-13 12:51:21 -07:00
parent 272b088918
commit dd52ebe7e8
3 changed files with 69 additions and 40 deletions

View file

@ -2,6 +2,7 @@
* 2.0.next in progress * 2.0.next in progress
* The documentation continues to be expanded and clarified in response to feedback! * The documentation continues to be expanded and clarified in response to feedback!
* Fix #322 by rewriting/simplifying `WHERE`/`HAVING` merge logic.
* Fix #310 by adding support for `FILTER`, `WITHIN GROUP`, and `ORDER BY` (as an expression), from [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) 0.4.112. These are [Special Syntax](doc/special-syntax.md) and there are also helpers for `filter` and `within-group` -- so **be careful about referring in all of `honey.sql.helpers`** since it will now shadow `clojure.core/filter` (it already shadows `for`, `group-by`, `into`, `partition-by`, `set`, and `update`). * Fix #310 by adding support for `FILTER`, `WITHIN GROUP`, and `ORDER BY` (as an expression), from [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) 0.4.112. These are [Special Syntax](doc/special-syntax.md) and there are also helpers for `filter` and `within-group` -- so **be careful about referring in all of `honey.sql.helpers`** since it will now shadow `clojure.core/filter` (it already shadows `for`, `group-by`, `into`, `partition-by`, `set`, and `update`).
* Fix #308 by supporting join clauses in `join-by` (and correcting the helper docstring). * Fix #308 by supporting join clauses in `join-by` (and correcting the helper docstring).

View file

@ -11,53 +11,67 @@
(defn- default-merge [current args] (defn- default-merge [current args]
(c/into (vec current) args)) (c/into (vec current) args))
(defn- and-merge (defn- sym->kw
"Merge a single conjunction expression into an existing one. "Given a symbol, produce a keyword, retaining the namespace
This merges `AND` to avoid nesting." qualifier, if any."
[current arg] [s]
(if-let [conj' (and (sequential? arg) (if (symbol? s)
(ident? (first arg)) (if-let [n (namespace s)]
(#{:and :or} (keyword (first arg))))] (keyword n (name s))
(cond (= conj' (first current)) (keyword (name s)))
(c/into (vec current) (rest arg)) s))
(seq current)
(c/into [conj' current] (rest arg))
:else
(c/into [conj'] (rest arg)))
(cond (#{:and 'and} (first current))
(conj (vec current) arg)
(seq current)
(conj [:and current] arg)
:else
(conj [:and] arg))))
(defn- and-merges (defn- conjunction?
"Merge multiple conjunction expressions into an existing, [e]
possibly empty, expression. This ensures AND expressions (and (ident? e)
are merged and that we do not end up with a single AND (contains? #{:and :or} (sym->kw e))))
or OR expression."
(defn- simplify-logic
"For Boolean expressions, simplify the logic to make
the output expression less nested. Finding :and or
:or with a single condition can be lifted. Finding
a conjunction inside the same conjunction can be
merged.
Always called on an expression that begins with a conjunction!"
[e]
(if (= 1 (count (rest e)))
(fnext e)
(let [conjunction (sym->kw (first e))]
(reduce (fn [acc e]
(if (and (sequential? e)
(conjunction? (first e))
(= conjunction (sym->kw (first e))))
(c/into acc (rest e))
(conj acc e)))
[conjunction]
(rest e)))))
(defn- conjunction-merge
"Merge for where/having. We ignore nil expressions.
By default, we combine with AND unless the new expression
begins with a conjunction, in which case use that to
combine the new expression. Then we perform some
simplifications to reduce nesting."
[current args] [current args]
(let [args (remove nil? args) (let [args (remove nil? args)
result [conjunction args]
(cond (ident? (first args)) (cond (conjunction? (first args))
(and-merges current [args]) [(first args) (rest args)]
(seq args) (ident? (first args))
(let [[arg & args] args] [:and [args]]
(and-merges (and-merge current arg) args))
:else :else
current)] [:and args])]
(case (count result) (if (seq args)
0 nil (-> [conjunction]
1 (if (sequential? (first result))(first result) result) (cond-> (seq current) (conj current))
2 (if (#{:and :or} (first result)) (c/into args)
(second result) (simplify-logic))
result) current)))
result)))
(def ^:private special-merges (def ^:private special-merges
"Identify the conjunction merge clauses." "Identify the conjunction merge clauses."
{:where #'and-merges {:where #'conjunction-merge
:having #'and-merges}) :having #'conjunction-merge})
(defn- helper-merge [data k args] (defn- helper-merge [data k args]
(if-let [merge-fn (special-merges k)] (if-let [merge-fn (special-merges k)]

View file

@ -843,3 +843,17 @@
" SUM(q) FILTER (WHERE x IS NULL) AS b," " SUM(q) FILTER (WHERE x IS NULL) AS b,"
" FOO(y) WITHIN GROUP (ORDER BY x ASC)") " FOO(y) WITHIN GROUP (ORDER BY x ASC)")
5 10])))) 5 10]))))
(deftest issue-322
(testing "Combining WHERE clauses with conditions"
(is (= {:where [:and [:= :a 1] [:or [:= :b 2] [:= :c 3]]]}
(where [:= :a 1] [:or [:= :b 2] [:= :c 3]])))
(is (= (-> (where :or [:= :b 2] [:= :c 3]) ; or first
(where := :a 1)) ; then implicit and
(-> (where := :b 2) ; implicit and
(where :or [:= :c 3]) ; then explicit or
(where := :a 1)))) ; then implicit and
(is (= {:where [:and [:or [:= :b 2] [:= :c 3]] [:= :a 1]]}
(where [:or [:= :b 2] [:= :c 3]] [:= :a 1])
(-> (where :or [:= :b 2] [:= :c 3]) ; explicit or
(where := :a 1)))))) ; then implicit and