diff --git a/doc/clause-reference.md b/doc/clause-reference.md index a32a243..3a8f3e4 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -700,7 +700,7 @@ one or more SQL entities. Each entity can either be a simple table name (keyword or symbol) or a pair of a table name and an alias. -`:using` is intended to be used as a simple join with a `:delete-from` +`:using` is intended to be used as a simple join, for example with a `:delete-from` clause (see [PostgreSQL DELETE statement](https://www.postgresql.org/docs/12/sql-delete.html) for more detail). @@ -778,6 +778,9 @@ user=> (sql/format {:select [:t.ref :pp.code] ["SELECT t.ref, pp.code FROM transaction AS t LEFT JOIN paypal_tx AS pp USING (id) WHERE ? = pp.status" "settled"] ``` +See also the [`:join` special syntax](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-special-syntax-#join) +for nested `JOIN` expressions. + ## cross-join `:cross-join` accepts a single sequence argument that lists diff --git a/doc/special-syntax.md b/doc/special-syntax.md index 3163133..a24f1f9 100644 --- a/doc/special-syntax.md +++ b/doc/special-syntax.md @@ -220,6 +220,17 @@ that represents a time unit. Produces an `INTERVAL` expression: > Note: PostgreSQL has an `INTERVAL` data type which is unrelated to this syntax. In PostgreSQL, the closet equivalent would be `[:cast "30 days" :interval]` which will lift `"30 days"` out as a parameter. In DDL, for PostgreSQL, you can use `:interval` to produce the `INTERVAL` data type (without wrapping it in a vector). +## join + +Accepts a table name (or expression) followed by one or more join clauses. +Produces a nested `JOIN` expression, typically used as the table expression of +a `JOIN` clause. + +```clojure +(sql/format {:join [[[:join :tbl1 {:left-join [:tbl2 [:using :id]]}]]]}) +;;=> ["INNER JOIN (tbl1 LEFT JOIN tbl2 USING (id))"] +``` + ## lateral Accepts a single argument that can be a (`SELECT`) clause or diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 5e7c76a..a0b3d16 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -1170,3 +1170,28 @@ ORDER BY id = ? DESC (sut/format [:raw "@foo := " [42]]))) (is (= ["@foo := MYFUNC(?)" 42] (sut/format [:raw "@foo := " [:myfunc 42]]))))) + +(deftest issue-483-join + (testing "single nested join" + (is (= ["SELECT * FROM tbl1 LEFT JOIN (tbl2 INNER JOIN tbl3 USING (common_column)) ON (tbl2.col2 = tbl1.col2) AND (tbl3.col3 = tbl1.col3)"] + (-> {:select :* + :from :tbl1 + :left-join [[[:join :tbl2 {:join [:tbl3 [:using [:common_column]]]}]] + [:and + [:= :tbl2.col2 :tbl1.col2] + [:= :tbl3.col3 :tbl1.col3]]]} + (sut/format))))) + (testing "multiple nested join" + (is (= ["SELECT * FROM tbl1 LEFT JOIN (tbl2 INNER JOIN tbl3 USING (common_column) RIGHT JOIN tbl4 USING (id)) ON (tbl2.col2 = tbl1.col2) AND (tbl3.col3 = tbl1.col3)"] + (-> {:select :* + :from :tbl1 + :left-join [[[:join :tbl2 + {:join [:tbl3 [:using [:common_column]]]} + {:right-join [:tbl4 [:using :id]]}]] + [:and + [:= :tbl2.col2 :tbl1.col2] + [:= :tbl3.col3 :tbl1.col3]]]} + (sut/format))))) + (testing "special syntax example" + (is (= ["INNER JOIN (tbl1 LEFT JOIN tbl2 USING (id))"] + (sut/format {:join [[[:join :tbl1 {:left-join [:tbl2 [:using :id]]}]]]})))))