Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 134 additions & 46 deletions Sources/Squeal/Api/DML/TSQL+JOIN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,83 +24,171 @@ public struct TypedJOINSQLQuery<T: Table, Row>: TableSQLQuery {

/// A query that can be followed by a join.
public protocol JoinableQuery: TableSQLQuery {

/// Joins another table: `JOIN table2 ON a = b`.
func JOIN<Y: Table, F>(_ table2: Y, ON: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row>
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table: `JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table: `INNER JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func INNER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table: `INNER JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func INNER_JOIN<Y: Table, F>(_ table2: Y, ON: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row>
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func INNER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table, keeping rows with no match: `LEFT JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func LEFT_JOIN<Y: Table, F>(_ table2: Y, ON: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row>
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func LEFT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table, keeping rows with no match: `LEFT JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func LEFT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table, keeping its rows with no match: `RIGHT JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func RIGHT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table, keeping its rows with no match: `RIGHT JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func RIGHT_JOIN<Y: Table, F>(_ table2: Y, ON: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row>
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func RIGHT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table, keeping unmatched rows from both: `FULL OUTER JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func FULL_OUTER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row>
/// Joins another table, keeping unmatched rows from both: `FULL OUTER JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func FULL_OUTER_JOIN<Y: Table, F>(_ table2: Y, ON: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row>
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func FULL_OUTER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row>
}

public extension JoinableQuery {

/// Joins another table: `JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
///
/// ```swift
/// SQL.SELECT(posts.title, users.name)
/// .FROM(posts)
/// .JOIN(users, ON: posts.author_id == users.id)
/// // SELECT posts.title, users.name FROM posts JOIN users ON posts.author_id = users.id
/// SQL.SELECT(users.name, posts.title, comments.body)
/// .FROM(users)
/// .JOIN(posts, ON: posts.author_id == users.id)
/// .JOIN(comments, ON: comments.post_id == posts.id)
/// // SELECT users.name, posts.title, comments.body FROM users
/// // JOIN posts ON posts.author_id = users.id JOIN comments ON comments.post_id = posts.id
/// ```
func JOIN<Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: "", table2, ON: predicate)
func JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: "", Y.schema, ON: predicate)
}

/// Joins another table, with the joined table's column on the left of the condition.
func JOIN<Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<Y, T, F>) -> TypedFromSQLQuery<T, Row> {
return joinReverse(word: "", table2, ON: predicate)

/// Joins another table: `JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: "", Y.schema, ON: predicate)
}

/// Joins another table: `INNER JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func INNER_JOIN<Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " INNER", table2, ON: predicate)
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func INNER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " INNER", Y.schema, ON: predicate)
}


/// Joins another table: `INNER JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func INNER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " INNER", Y.schema, ON: predicate)
}

/// Joins another table, keeping rows with no match: `LEFT JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func LEFT_JOIN<Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " LEFT", table2, ON: predicate)
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func LEFT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " LEFT", Y.schema, ON: predicate)
}


/// Joins another table, keeping rows with no match: `LEFT JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func LEFT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " LEFT", Y.schema, ON: predicate)
}

/// Joins another table, keeping its rows with no match: `RIGHT JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func RIGHT_JOIN<Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " RIGHT", table2, ON: predicate)
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func RIGHT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " RIGHT", Y.schema, ON: predicate)
}


/// Joins another table, keeping its rows with no match: `RIGHT JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func RIGHT_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " RIGHT", Y.schema, ON: predicate)
}

/// Joins another table, keeping unmatched rows from both: `FULL OUTER JOIN table2 ON a = b`.
///
/// Put the `FROM` table's column on the left of the condition.
func FULL_OUTER_JOIN<Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " FULL OUTER", table2, ON: predicate)
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func FULL_OUTER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<A, Y, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " FULL OUTER", Y.schema, ON: predicate)
}

private func join<Y: Table, F>(word: String, _ table2: Y, ON predicate: JOINPredicate<T, Y, F>) -> TypedFromSQLQuery<T, Row> {
let statement = "\(word) JOIN \(Y.schema) ON \(predicate.left.tableName).\(predicate.left.name) = \(predicate.right.tableName).\(predicate.right.name)"
return TypedFromSQLQuery(for: table,
query: query + statement,
parameters: parameters)

/// Joins another table, keeping unmatched rows from both: `FULL OUTER JOIN table2 ON a = b`.
///
/// The condition compares a column of the joined table with a column of the same type
/// from any table in the query: the `FROM` table or one joined earlier. The columns can be
/// written in either order.
func FULL_OUTER_JOIN<A: Table, Y: Table, F>(_ table2: Y, ON predicate: JOINPredicate<Y, A, F>) -> TypedFromSQLQuery<T, Row> {
return join(word: " FULL OUTER", Y.schema, ON: predicate)
}
private func joinReverse<Y: Table, F>(word: String, _ table2: Y, ON predicate: JOINPredicate<Y, T, F>) -> TypedFromSQLQuery<T, Row> {
let statement = "\(word) JOIN \(Y.schema) ON \(predicate.left.tableName).\(predicate.left.name) = \(predicate.right.tableName).\(predicate.right.name)"

private func join<A: Table, B: Table, F>(word: String, _ schema: String, ON predicate: JOINPredicate<A, B, F>) -> TypedFromSQLQuery<T, Row> {
let statement = "\(word) JOIN \(schema) ON \(predicate.left.tableName).\(predicate.left.name) = \(predicate.right.tableName).\(predicate.right.name)"
return TypedFromSQLQuery(for: table,
query: query + statement,
parameters: parameters)
Expand Down
21 changes: 14 additions & 7 deletions Sources/Squeal/Squeal.docc/Joins.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,35 @@ table and are never ambiguous.
.FULL_OUTER_JOIN(users, ON: …) // FULL OUTER JOIN
```

`JOIN` accepts the condition in either order. The other join kinds expect the `FROM`
table's column on the left:
Every join kind accepts the condition's columns in either order:

```swift
SQL.SELECT(users.name, COUNT(posts.id))
.FROM(users)
.LEFT_JOIN(posts, ON: users.id == posts.author_id)
.LEFT_JOIN(posts, ON: posts.author_id == users.id)
.GROUP_BY(\.name)
// SELECT users.name, COUNT(posts.id) FROM users LEFT JOIN posts ON users.id = posts.author_id GROUP BY name
// SELECT users.name, COUNT(posts.id) FROM users LEFT JOIN posts ON posts.author_id = users.id GROUP BY name
```

### Joining several tables

Joins chain, so you can follow a many-to-many relationship through a link table:
Joins chain, and each condition can refer to the `FROM` table or to any table joined before
it. For example, you can follow a many-to-many relationship through a link table:

```swift
SQL.SELECT(users.name, departments.name)
.FROM(users_departments)
.JOIN(users, ON: users.id == users_departments.user_id)
.FROM(users)
.JOIN(users_departments, ON: users_departments.user_id == users.id)
.JOIN(departments, ON: departments.id == users_departments.department_id)
// SELECT users.name, departments.name FROM users
// JOIN users_departments ON users_departments.user_id = users.id
// JOIN departments ON departments.id = users_departments.department_id
```

A join's condition must use a column of the table being joined, and both columns must have
the same type. Squeal doesn't check that the condition's other table is already part of
the query. If it isn't, Postgres reports a missing `FROM`-clause entry when the query runs.

### Filtering and sorting on joined tables

`WHERE`, `AND` and `ORDER_BY` accept columns from any table in the query. These columns
Expand Down
2 changes: 2 additions & 0 deletions Sources/Squeal/Squeal.docc/WhatSquealChecks.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ Squeal checks queries against your Swift description of the schema. It doesn't c
your ``Table`` definitions and your migrations disagree, queries fail at runtime.
- **How you decode rows.** With PostgresNIO, `rows.decode((UUID, String).self)` isn't
checked against the columns you selected.
- **That a join's other table is in the query.** A join condition must use the joined
table, but its other column can come from any table, even one you haven't added yet.
- **SQL semantics.** Squeal doesn't add parentheses around `AND` and `OR` conditions. Group
them yourself with an ``SQLExpr``. It lets you write `UPDATE` and `DELETE` without
`WHERE`, just like SQL. It can't know whether a subquery used as a value returns a single
Expand Down
10 changes: 5 additions & 5 deletions Tests/SquealTests/DocumentationExamplesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ struct DocumentationExamplesTests {
let query = SQL
.SELECT(users.name, COUNT(posts.id))
.FROM(users)
.LEFT_JOIN(posts, ON: users.id == posts.author_id)
.LEFT_JOIN(posts, ON: posts.author_id == users.id)
.GROUP_BY(\.name)
#expect(query.query == "SELECT users.name, COUNT(posts.id) FROM users LEFT JOIN posts ON users.id = posts.author_id GROUP BY name")
#expect(query.query == "SELECT users.name, COUNT(posts.id) FROM users LEFT JOIN posts ON posts.author_id = users.id GROUP BY name")
}

@Test
Expand All @@ -218,10 +218,10 @@ struct DocumentationExamplesTests {
func manyToManyJoin() {
let query = SQL
.SELECT(users.name, departments.name)
.FROM(users_departments)
.JOIN(users, ON: users.uuid == users_departments.user_id)
.FROM(users)
.JOIN(users_departments, ON: users_departments.user_id == users.uuid)
.JOIN(departments, ON: departments.id == users_departments.department_id)
#expect(query.query == "SELECT users.name, departments.name FROM users_departments JOIN users ON users.uuid = users_departments.user_id JOIN departments ON departments.id = users_departments.department_id")
#expect(query.query == "SELECT users.name, departments.name FROM users JOIN users_departments ON users_departments.user_id = users.uuid JOIN departments ON departments.id = users_departments.department_id")
}

// MARK: - Subqueries
Expand Down
29 changes: 29 additions & 0 deletions Tests/SquealTests/JOINTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,32 @@ struct JOIN_WHERE_qualifiedColumnsTests {
#expect(query.parameters.count == 5)
}
}

struct JOIN_conditionOrderTests {

@Test
func everyJoinKindAcceptsBothOrders() {
let base = SQL.SELECT(users.name).FROM(users)
let joined = "orders ON orders.user_id = users.uuid"
#expect(base.JOIN(orders, ON: orders.user_id == users.uuid).query == "SELECT users.name FROM users JOIN \(joined)")
#expect(base.INNER_JOIN(orders, ON: orders.user_id == users.uuid).query == "SELECT users.name FROM users INNER JOIN \(joined)")
#expect(base.LEFT_JOIN(orders, ON: orders.user_id == users.uuid).query == "SELECT users.name FROM users LEFT JOIN \(joined)")
#expect(base.RIGHT_JOIN(orders, ON: orders.user_id == users.uuid).query == "SELECT users.name FROM users RIGHT JOIN \(joined)")
#expect(base.FULL_OUTER_JOIN(orders, ON: orders.user_id == users.uuid).query == "SELECT users.name FROM users FULL OUTER JOIN \(joined)")
}

@Test
func chainThroughAnEarlierJoinedTable_eitherOrder() {
let userId = UUID()
let query = SQL
.SELECT(users.name, departments.name)
.FROM(users)
.JOIN(users_departments, ON: users_departments.user_id == users.uuid)
.LEFT_JOIN(departments, ON: departments.id == users_departments.department_id)
.WHERE(users_departments.user_id == userId)
.ORDER_BY(departments.name, .ASC)
.LIMIT(5)
#expect(query.query == "SELECT users.name, departments.name FROM users JOIN users_departments ON users_departments.user_id = users.uuid LEFT JOIN departments ON departments.id = users_departments.department_id WHERE users_departments.user_id = $1 ORDER BY departments.name ASC LIMIT $2")
#expect(query.parameters.count == 2)
}
}
Loading
Loading