Fixes #277 by adding join-by
This commit is contained in:
parent
97c9236842
commit
e70985e93b
5 changed files with 104 additions and 1 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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]]]})))
|
||||
|
|
|
|||
Loading…
Reference in a new issue