Fixes #277 by adding join-by

This commit is contained in:
Sean Corfield 2021-03-07 19:21:13 -08:00
parent 97c9236842
commit e70985e93b
5 changed files with 104 additions and 1 deletions

View file

@ -5,6 +5,7 @@
* Add tests to confirm #299 does not affect v2.
* Confirm the whole of the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) is implemented out-of-the-box (#293).
* Reconcile `where` behavior with recent 1.0 changes (porting #283 to v2).
* Fix #277 by adding `:join-by`/`join-by` so that you can have multiple `JOIN`'s in a specific order.
* 2.0.0-alpha2 (for early testing)
* Since Alpha 1, a lot more documentation has been written and docstrings have been added to most functions in `honey.sql.helpers`.

View file

@ -410,6 +410,8 @@ user=> (sql/format {:select [:u.username :s.name]
["SELECT u.username, s.name FROM user AS u INNER JOIN status AS s ON u.statusid = s.id WHERE s.id = ?" 2]
```
`:join` is shorthand for `:inner-join`.
An alternative to a join condition is a `USING` expression:
```clojure
@ -430,6 +432,28 @@ table name and an alias.
> Note: the actual formatting of a `:cross-join` clause is currently identical to the formatting of a `:select` clause.
## join-by
This is a convenience that allows for an arbitrary sequence of `JOIN`
operations to be performed in a specific order. It accepts a sequence
of join operation name (keyword or symbol) and the clause that join
would take:
```clojure
user=> (sql/format {:select [:t.ref :pp.code]
:from [[:transaction :t]]
:join-by [:left [[:paypal-tx :pp]
[:using :id]]
:join [[:logtransaction :log]
[:= :t.id :log.id]]]
:where [:= "settled" :pp.status]})
["SELECT t.ref, pp.code FROM transaction AS t LEFT JOIN paypal_tx AS pp USING (id) INNER JOIN logtransaction AS log ON t.id = log.id WHERE ? = pp.status" "settled"]
```
Without `:join-by`, a `:join` would normally be generated before a `:left-join`.
To avoid repetition, `:join-by` allows shorthand versions of the join clauses
using a keyword (or symbol) without the `-join` suffix, as shown in this example.
## set (MySQL)
This is the precedence of the `:set` clause for the MySQL dialect.

View file

@ -46,6 +46,7 @@
:select :select-distinct :select-distinct-on
:insert-into :update :delete :delete-from :truncate
:columns :set :from :using
:join-by
:join :left-join :right-join :inner-join :outer-join :full-join
:cross-join
:where :group-by :having
@ -381,6 +382,42 @@
(partition 2 clauses))]
(into [(str/join " " sqls)] params)))
(def ^:private join-by-aliases
"Map of shorthand to longhand join names."
{:join :inner-join
:left :left-join
:right :right-join
:inner :inner-join
:outer :outer-join
:full :full-join
:cross :cross-join})
(def ^:private valid-joins
(set (vals join-by-aliases)))
(defn- format-join-by
"Clauses should be a sequence of join types followed
by their table and condition, so that you can construct
a series of joins in a specific order."
[_ clauses]
(let [joins (partition-by ident? clauses)]
(when-not (even? (count joins))
(throw (ex-info ":join-by expects a sequence of join clauses"
{:clauses clauses})))
(let [[sqls params]
(reduce (fn [[sqls params] [[j] [clauses]]]
(let [j' (sym->kw j)
j' (sym->kw (join-by-aliases j' j'))]
(when-not (valid-joins j')
(throw (ex-info (str ":join-by found an invalid join type "
j)
{})))
(let [[sql' & params'] (format-dsl {j' clauses})]
[(conj sqls sql') (into params params')])))
[[] []]
(partition 2 joins))]
(into [(str/join " " sqls)] params))))
(defn- format-on-expr [k e]
(if (or (not (sequential? e)) (seq e))
(let [[sql & params] (format-expr e)]
@ -628,6 +665,7 @@
:set #'format-set-exprs
:from #'format-selects
:using #'format-selects
:join-by #'format-join-by
:join #'format-join
:left-join #'format-join
:right-join #'format-join

View file

@ -393,6 +393,22 @@
[& args]
(generic :using args))
(defn join-by
"Accepts a sequence of join clauses to be generated
in a specific order.
(-> (select :*)
(from :foo)
(join-by :left :bar [:= :foo.id :bar.id]
:join :quux [:= :bar.qid = :quux.id])
This produces a LEFT JOIN followed by an INNER JOIN
even though the 'natural' order for `left-join` and
`join` would be to generate the INNER JOIN first,
followed by the LEFT JOIN."
[& args]
(generic :join-by args))
(defn join
[& args]
(generic :join args))

View file

@ -9,7 +9,7 @@
:refer [add-column add-index alter-table columns create-table create-view
cross-join do-update-set drop-column drop-index drop-table from full-join
group-by having insert-into
join left-join limit offset on-conflict order-by
join-by join left-join limit offset on-conflict order-by
over partition-by
rename-column rename-table returning right-join
select select-distinct values where window with with-columns]]))
@ -75,6 +75,30 @@
;; to enable :lock
:dialect :mysql :quoted false}))))))
(deftest join-by-test
(testing "Natural JOIN orders"
(is (= ["SELECT * FROM foo INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y"]
(sql/format {:select [:*] :from [:foo]
:full-join [:beck [:= :beck.x :c.y]]
:right-join [:bock [:= :bock.z :c.e]]
:left-join [[:clod :c] [:= :f.a :c.d]]
:join [:draq [:= :f.b :draq.x]]}))))
(testing "Specific JOIN orders"
(is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"]
(sql/format {:select [:*] :from [:foo]
:join-by [:full [:beck [:= :beck.x :c.y]]
:right [:bock [:= :bock.z :c.e]]
:left [[:clod :c] [:= :f.a :c.d]]
:join [:draq [:= :f.b :draq.x]]]})))
(is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"]
(-> (select :*)
(from :foo)
(join-by :full-join [:beck [:= :beck.x :c.y]]
:right-join [:bock [:= :bock.z :c.e]]
:left-join [[:clod :c] [:= :f.a :c.d]]
:inner-join [:draq [:= :f.b :draq.x]])
(sql/format))))))
(deftest test-cast
(is (= ["SELECT foo, CAST(bar AS integer)"]
(sql/format {:select [:foo [[:cast :bar :integer]]]})))