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.
|
* 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).
|
* 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).
|
* 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)
|
* 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`.
|
* 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]
|
["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:
|
An alternative to a join condition is a `USING` expression:
|
||||||
|
|
||||||
```clojure
|
```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.
|
> 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)
|
## set (MySQL)
|
||||||
|
|
||||||
This is the precedence of the `:set` clause for the MySQL dialect.
|
This is the precedence of the `:set` clause for the MySQL dialect.
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
:select :select-distinct :select-distinct-on
|
:select :select-distinct :select-distinct-on
|
||||||
:insert-into :update :delete :delete-from :truncate
|
:insert-into :update :delete :delete-from :truncate
|
||||||
:columns :set :from :using
|
:columns :set :from :using
|
||||||
|
:join-by
|
||||||
:join :left-join :right-join :inner-join :outer-join :full-join
|
:join :left-join :right-join :inner-join :outer-join :full-join
|
||||||
:cross-join
|
:cross-join
|
||||||
:where :group-by :having
|
:where :group-by :having
|
||||||
|
|
@ -381,6 +382,42 @@
|
||||||
(partition 2 clauses))]
|
(partition 2 clauses))]
|
||||||
(into [(str/join " " sqls)] params)))
|
(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]
|
(defn- format-on-expr [k e]
|
||||||
(if (or (not (sequential? e)) (seq e))
|
(if (or (not (sequential? e)) (seq e))
|
||||||
(let [[sql & params] (format-expr e)]
|
(let [[sql & params] (format-expr e)]
|
||||||
|
|
@ -628,6 +665,7 @@
|
||||||
:set #'format-set-exprs
|
:set #'format-set-exprs
|
||||||
:from #'format-selects
|
:from #'format-selects
|
||||||
:using #'format-selects
|
:using #'format-selects
|
||||||
|
:join-by #'format-join-by
|
||||||
:join #'format-join
|
:join #'format-join
|
||||||
:left-join #'format-join
|
:left-join #'format-join
|
||||||
:right-join #'format-join
|
:right-join #'format-join
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,22 @@
|
||||||
[& args]
|
[& args]
|
||||||
(generic :using 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
|
(defn join
|
||||||
[& args]
|
[& args]
|
||||||
(generic :join args))
|
(generic :join args))
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
:refer [add-column add-index alter-table columns create-table create-view
|
: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
|
cross-join do-update-set drop-column drop-index drop-table from full-join
|
||||||
group-by having insert-into
|
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
|
over partition-by
|
||||||
rename-column rename-table returning right-join
|
rename-column rename-table returning right-join
|
||||||
select select-distinct values where window with with-columns]]))
|
select select-distinct values where window with with-columns]]))
|
||||||
|
|
@ -75,6 +75,30 @@
|
||||||
;; to enable :lock
|
;; to enable :lock
|
||||||
:dialect :mysql :quoted false}))))))
|
: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
|
(deftest test-cast
|
||||||
(is (= ["SELECT foo, CAST(bar AS integer)"]
|
(is (= ["SELECT foo, CAST(bar AS integer)"]
|
||||||
(sql/format {:select [:foo [[:cast :bar :integer]]]})))
|
(sql/format {:select [:foo [[:cast :bar :integer]]]})))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue