From 24030c982a11644050ea9a5e5fb815dd64bfe0a0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 18 Aug 2025 23:03:34 -0700 Subject: [PATCH 1/3] Add join overloads for `Joins: Table` Parameter packs break down when a pack generic is a single value (instead of a tuple) and has a conformance constraint. These overloads are to work around it. Before 1.0 we should probably come up with a reasonable limitation of how joins work. Some ideas to reduce the overloads: - Joins require no selected columns on either side. While it'd be nice to splat them, it just doesn't work well with parameter packs today. - Joins require no joined tables on the righthand side. Again: it'd be nice to support this, but it doesn't work well in the language today. This maybe even means joins take `Table.Type` instead of `Select`. I think if these two limitations were known and documented we could eliminate the many of the overloads we've defined today. --- .../Statements/Select.swift | 105 ++++++++++++++++++ .../StructuredQueriesTests/SelectTests.swift | 16 +++ 2 files changed, 121 insertions(+) diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index b3396422..bfcfecc5 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -710,6 +710,18 @@ extension Select { ) } + @_disfavoredOverload + @_documentation(visibility: private) + public func join( + // TODO: Report issue to Swift team. Using 'some' crashes the compiler. + _ other: any SelectStatementOf, + on constraint: ( + (From.TableColumns, Joins.TableColumns, F.TableColumns) + ) -> some QueryExpression + ) -> Select<(), From, (Joins, F)> where Joins: Table { + fatalError() + } + /// Creates a new select statement from this one by left-joining another table. /// /// - Parameters: @@ -849,6 +861,37 @@ extension Select { ) } + @_disfavoredOverload + @_documentation(visibility: private) + public func leftJoin( + // TODO: Report issue to Swift team. Using 'some' crashes the compiler. + _ other: any SelectStatementOf, + on constraint: ( + (From.TableColumns, Joins.TableColumns, F.TableColumns) + ) -> some QueryExpression + ) -> Select<(), From, (Joins, F._Optionalized)> + where Joins: Table { + let other = other.asSelect() + let join = _JoinClause( + operator: .left, + table: F.self, + constraint: constraint( + (From.columns, Joins.columns, F.columns) + ) + ) + return Select<(), From, (Joins, F._Optionalized)>( + isEmpty: isEmpty || other.isEmpty, + distinct: distinct || other.distinct, + columns: columns + other.columns, + joins: joins + [join] + other.joins, + where: `where` + other.where, + group: group + other.group, + having: having + other.having, + order: order + other.order, + limit: other.limit ?? limit + ) + } + /// Creates a new select statement from this one by right-joining another table. /// /// - Parameters: @@ -988,6 +1031,37 @@ extension Select { ) } + @_disfavoredOverload + @_documentation(visibility: private) + public func rightJoin( + // TODO: Report issue to Swift team. Using 'some' crashes the compiler. + _ other: any SelectStatementOf, + on constraint: ( + (From.TableColumns, Joins.TableColumns, F.TableColumns) + ) -> some QueryExpression + ) -> Select<(), From._Optionalized, (Joins._Optionalized, F)> + where Joins: Table { + let other = other.asSelect() + let join = _JoinClause( + operator: .right, + table: F.self, + constraint: constraint( + (From.columns, Joins.columns, F.columns,) + ) + ) + return Select<(), From._Optionalized, (Joins._Optionalized, F)>( + isEmpty: isEmpty || other.isEmpty, + distinct: distinct || other.distinct, + columns: columns + other.columns, + joins: joins + [join] + other.joins, + where: `where` + other.where, + group: group + other.group, + having: having + other.having, + order: order + other.order, + limit: other.limit ?? limit + ) + } + /// Creates a new select statement from this one by full-joining another table. /// /// - Parameters: @@ -1127,6 +1201,37 @@ extension Select { ) } + @_disfavoredOverload + @_documentation(visibility: private) + public func fullJoin( + // TODO: Report issue to Swift team. Using 'some' crashes the compiler. + _ other: any SelectStatementOf, + on constraint: ( + (From.TableColumns, Joins.TableColumns, F.TableColumns) + ) -> some QueryExpression + ) -> Select<(), From._Optionalized, (Joins._Optionalized, F._Optionalized)> + where Joins: Table { + let other = other.asSelect() + let join = _JoinClause( + operator: .full, + table: F.self, + constraint: constraint( + (From.columns, Joins.columns, F.columns) + ) + ) + return Select<(), From._Optionalized, (Joins._Optionalized, F._Optionalized)>( + isEmpty: isEmpty || other.isEmpty, + distinct: distinct || other.distinct, + columns: columns + other.columns, + joins: joins + [join] + other.joins, + where: `where` + other.where, + group: group + other.group, + having: having + other.having, + order: order + other.order, + limit: other.limit ?? limit + ) + } + /// Creates a new select statement from this one by appending a predicate to its `WHERE` clause. /// /// - Parameter keyPath: A key path from this select's table to a Boolean expression to filter by. diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index 3039785e..a4a4c817 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -1406,6 +1406,22 @@ extension SnapshotTests { @Test func singleJoinChaining() { let base = Reminder.group(by: \.id).join(ReminderTag.all) { $0.id.eq($1.reminderID) } _ = base.select { r, _ in r.isCompleted } + _ = base.join(RemindersList.all) { _, _, _ in true } + _ = base.leftJoin(RemindersList.all) { _, _, _ in true } + _ = base.rightJoin(RemindersList.all) { _, _, _ in true } + _ = base.fullJoin(RemindersList.all) { _, _, _ in true } + _ = base + .join(RemindersList.all) { _, _, _ in true } + .join(RemindersList.all) { _, _, _, _ in true } + _ = base + .leftJoin(RemindersList.all) { _, _, _ in true } + .leftJoin(RemindersList.all) { _, _, _, _ in true } + _ = base + .rightJoin(RemindersList.all) { _, _, _ in true } + .rightJoin(RemindersList.all) { _, _, _, _ in true } + _ = base + .fullJoin(RemindersList.all) { _, _, _ in true } + .fullJoin(RemindersList.all) { _, _, _, _ in true } _ = base.where { r, _ in r.isCompleted } _ = base.group { r, _ in r.isCompleted } _ = base.having { r, _ in r.isCompleted } From 136967379c77767f598a180e246e288ff5782d2b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 19 Aug 2025 09:09:54 -0700 Subject: [PATCH 2/3] wip --- .../Statements/Select.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index bfcfecc5..a8000860 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -719,7 +719,25 @@ extension Select { (From.TableColumns, Joins.TableColumns, F.TableColumns) ) -> some QueryExpression ) -> Select<(), From, (Joins, F)> where Joins: Table { - fatalError() + let other = other.asSelect() + let join = _JoinClause( + operator: .inner, + table: F.self, + constraint: constraint( + (From.columns, Joins.columns, F.columns) + ) + ) + return Select<(), From, (Joins, F)>( + isEmpty: isEmpty || other.isEmpty, + distinct: distinct || other.distinct, + columns: columns + other.columns, + joins: joins + [join] + other.joins, + where: `where` + other.where, + group: group + other.group, + having: having + other.having, + order: order + other.order, + limit: other.limit ?? limit + ) } /// Creates a new select statement from this one by left-joining another table. From cc1b70ad0659376ee1afe2c227b5557ebce65602 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 19 Aug 2025 10:43:07 -0700 Subject: [PATCH 3/3] fix --- Sources/StructuredQueriesCore/Statements/Select.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index a8000860..d1704888 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -1064,7 +1064,7 @@ extension Select { operator: .right, table: F.self, constraint: constraint( - (From.columns, Joins.columns, F.columns,) + (From.columns, Joins.columns, F.columns) ) ) return Select<(), From._Optionalized, (Joins._Optionalized, F)>(