Skip to content

Commit a28fe52

Browse files
committed
Merge remote-tracking branch 'origin/main' into temp-triggers
2 parents 7b81eb4 + 6bd8700 commit a28fe52

22 files changed

+828
-70
lines changed

Sources/StructuredQueriesCore/AggregateFunctions.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ where QueryValue: _OptionalPromotable, QueryValue._Optionalized.Wrapped == Strin
8484
}
8585
}
8686

87-
extension QueryExpression where QueryValue: QueryBindable {
87+
extension QueryExpression where QueryValue: QueryBindable & _OptionalPromotable {
8888
/// A maximum aggregate of this expression.
8989
///
9090
/// ```swift
@@ -96,7 +96,7 @@ extension QueryExpression where QueryValue: QueryBindable {
9696
/// - Returns: A maximum aggregate of this expression.
9797
public func max(
9898
filter: (some QueryExpression<Bool>)? = Bool?.none
99-
) -> some QueryExpression<Int?> {
99+
) -> some QueryExpression<QueryValue._Optionalized.Wrapped?> {
100100
AggregateFunction("max", [queryFragment], filter: filter?.queryFragment)
101101
}
102102

@@ -111,7 +111,7 @@ extension QueryExpression where QueryValue: QueryBindable {
111111
/// - Returns: A minimum aggregate of this expression.
112112
public func min(
113113
filter: (some QueryExpression<Bool>)? = Bool?.none
114-
) -> some QueryExpression<Int?> {
114+
) -> some QueryExpression<QueryValue._Optionalized.Wrapped?> {
115115
AggregateFunction("min", [queryFragment], filter: filter?.queryFragment)
116116
}
117117
}
@@ -170,7 +170,7 @@ where QueryValue: _OptionalPromotable, QueryValue._Optionalized.Wrapped: Numeric
170170
///
171171
/// ```swift
172172
/// Item.select { $0.price.total() }
173-
/// // SELECT sum("items"."price") FROM "items"
173+
/// // SELECT total("items"."price") FROM "items"
174174
/// ```
175175
///
176176
/// - Parameters:

Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ table. For example, querying for all reminders lists along with an array of the
349349
list. We'd like to be able to query for this data and decode it into a collection of values
350350
from the following data type:
351351

352-
```struct
352+
```swift
353353
struct Row {
354354
let remindersList: RemindersList
355355
let reminders: [Reminder]
@@ -395,7 +395,7 @@ Another way to do this is to use the `@Selection` macro described above
395395
(<doc:QueryCookbook#Custom-selections>), along with a ``Swift/Decodable/JSONRepresentation`` of the
396396
collection of reminders you want to load for each list:
397397

398-
```struct
398+
```swift
399399
@Selection
400400
struct Row {
401401
let remindersList: RemindersList
@@ -414,7 +414,7 @@ columns of [primary keyed tables](<doc:PrimaryKeyedTables>):
414414

415415
```swift
416416
RemindersList
417-
.join(Reminder.all) { $0.id.eq($1.remindersListID) }
417+
.leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) }
418418
.select {
419419
Row.Columns(
420420
remindersList: $0,
@@ -423,8 +423,63 @@ RemindersList
423423
}
424424
```
425425

426+
> Note: There are 2 important things to note about this query:
427+
>
428+
> * Since not every reminders list will have a reminder associated with it, we are using a
429+
> ``Select/leftJoin(_:on:)``. That will make sure to select all lists no matter what.
430+
> * The left join introduces _optional_ reminders, but we are using a special overload of
431+
> `jsonGroupArray` on optionals that automatically filters out `nil` reminders and unwraps them.
432+
426433
This allows you to fetch all of the data in a single SQLite query and decode the data into a
427434
collection of `Row` values. There is an extra cost associated with decoding the JSON object,
428435
but that cost may be smaller than executing multiple SQLite requests and transforming the data
429436
into `Row` manually, not to mention the additional code you need to write and maintain to process
430437
the data.
438+
439+
It is even possible to load multiple associations at once. For example, suppose that there is a
440+
`Milestone` table that is associated with a `RemindersList`:
441+
442+
```swift
443+
@Table
444+
struct Milestone: Identifiable, Codable {
445+
let id: Int
446+
var title = ""
447+
var remindersListID: RemindersList.ID
448+
}
449+
```
450+
451+
And suppose you would like to fetch all `RemindersList`s along with the collection of all milestones
452+
and reminders associated with the list:
453+
454+
```struct
455+
@Selection
456+
struct Row {
457+
let remindersList: RemindersList
458+
@Column(as: [Milestone].JSONRepresentation.self)
459+
let milestones: [Milestone]
460+
@Column(as: [Reminder].JSONRepresentation.self)
461+
let reminders: [Reminder]
462+
}
463+
```
464+
465+
It is possible to do this using two left joins and two `jsonGroupArray`s:
466+
467+
```swift
468+
RemindersList
469+
.leftJoin(Milestone.all) { $0.id.eq($1.remindersListID) }
470+
.leftJoin(Reminder.all) { $0.id.eq($2.remindersListID) }
471+
.select {
472+
Row.Columns(
473+
remindersList: $0,
474+
milestones: $1.jsonGroupArray(isDistinct: true),
475+
reminders: $2.jsonGroupArray(isDistinct: true)
476+
)
477+
}
478+
```
479+
480+
> Note: Because we are now joining two independent tables to `RemindersList`, we will get duplicate
481+
> entries for all pairs of reminders with milestones. To remove those duplicates we use the
482+
> `isDistinct` option for `jsonGroupArray`.
483+
484+
This will now load all reminders lists with all of their reminders and milestones in one single
485+
SQL query.

Sources/StructuredQueriesCore/Operators.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public func == <QueryValue: _OptionalProtocol>(
192192
lhs: any QueryExpression<QueryValue>,
193193
rhs: some QueryExpression<QueryValue.Wrapped>
194194
) -> some QueryExpression<Bool> {
195-
BinaryOperator(lhs: lhs, operator: isNull(lhs) ? "IS" : "=", rhs: rhs)
195+
BinaryOperator(lhs: lhs, operator: "IS", rhs: rhs)
196196
}
197197

198198
// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'.
@@ -202,7 +202,7 @@ public func != <QueryValue: _OptionalProtocol>(
202202
lhs: any QueryExpression<QueryValue>,
203203
rhs: some QueryExpression<QueryValue.Wrapped>
204204
) -> some QueryExpression<Bool> {
205-
BinaryOperator(lhs: lhs, operator: isNull(lhs) ? "IS NOT" : "<>", rhs: rhs)
205+
BinaryOperator(lhs: lhs, operator: "IS NOT", rhs: rhs)
206206
}
207207

208208
// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'.
@@ -211,7 +211,7 @@ public func == <QueryValue: _OptionalProtocol>(
211211
lhs: any QueryExpression<QueryValue>,
212212
rhs: some QueryExpression<QueryValue>
213213
) -> some QueryExpression<Bool> {
214-
BinaryOperator(lhs: lhs, operator: isNull(lhs) || isNull(rhs) ? "IS" : "=", rhs: rhs)
214+
BinaryOperator(lhs: lhs, operator: "IS", rhs: rhs)
215215
}
216216

217217
// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'.
@@ -220,7 +220,7 @@ public func != <QueryValue: _OptionalProtocol>(
220220
lhs: any QueryExpression<QueryValue>,
221221
rhs: some QueryExpression<QueryValue>
222222
) -> some QueryExpression<Bool> {
223-
BinaryOperator(lhs: lhs, operator: isNull(lhs) || isNull(rhs) ? "IS NOT" : "<>", rhs: rhs)
223+
BinaryOperator(lhs: lhs, operator: "IS NOT", rhs: rhs)
224224
}
225225

226226
// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'.

Sources/StructuredQueriesCore/PrimaryKeyed.swift

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ public protocol TableDraft: Table {
2222
init(_ primaryTable: PrimaryTable)
2323
}
2424

25+
extension TableDraft {
26+
public static subscript(
27+
dynamicMember keyPath: KeyPath<PrimaryTable.Type, some Statement<PrimaryTable>>
28+
) -> some Statement<Self> {
29+
SQLQueryExpression("\(PrimaryTable.self[keyPath: keyPath])")
30+
}
31+
32+
public static subscript(
33+
dynamicMember keyPath: KeyPath<PrimaryTable.Type, some SelectStatementOf<PrimaryTable>>
34+
) -> SelectOf<Self> {
35+
unsafeBitCast(PrimaryTable.self[keyPath: keyPath].asSelect(), to: SelectOf<Self>.self)
36+
}
37+
38+
public static var all: SelectOf<Self> {
39+
unsafeBitCast(PrimaryTable.all.asSelect(), to: SelectOf<Self>.self)
40+
}
41+
}
42+
2543
/// A type representing a database table's columns.
2644
///
2745
/// Don't conform to this protocol directly. Instead, use the `@Table` and `@Column` macros to
@@ -38,6 +56,14 @@ where QueryValue: PrimaryKeyedTable {
3856
var primaryKey: TableColumn<QueryValue, PrimaryKey> { get }
3957
}
4058

59+
extension TableDefinition where QueryValue: TableDraft {
60+
public subscript<Member>(
61+
dynamicMember keyPath: KeyPath<QueryValue.PrimaryTable.TableColumns, Member>
62+
) -> Member {
63+
QueryValue.PrimaryTable.columns[keyPath: keyPath]
64+
}
65+
}
66+
4167
extension PrimaryKeyedTableDefinition {
4268
/// A query expression representing the number of rows in this table.
4369
///
@@ -60,6 +86,22 @@ extension PrimaryKeyedTable {
6086
}
6187
}
6288

89+
extension TableDraft {
90+
/// A where clause filtered by a primary key.
91+
///
92+
/// - Parameter primaryKey: A primary key identifying a table row.
93+
/// - Returns: A `WHERE` clause.
94+
public static func find(
95+
_ primaryKey: PrimaryTable.TableColumns.PrimaryKey.QueryOutput
96+
) -> Where<Self> {
97+
Self.where { _ in
98+
PrimaryTable.columns.primaryKey.eq(
99+
PrimaryTable.TableColumns.PrimaryKey(queryOutput: primaryKey)
100+
)
101+
}
102+
}
103+
}
104+
63105
extension Where where From: PrimaryKeyedTable {
64106
/// Adds a primary key condition to a where clause.
65107
///
@@ -70,6 +112,40 @@ extension Where where From: PrimaryKeyedTable {
70112
}
71113
}
72114

115+
extension Where where From: TableDraft {
116+
/// Adds a primary key condition to a where clause.
117+
///
118+
/// - Parameter primaryKey: A primary key.
119+
/// - Returns: A where clause with the added primary key.
120+
public func find(_ primaryKey: From.PrimaryTable.TableColumns.PrimaryKey.QueryOutput) -> Self {
121+
self.where { _ in
122+
From.PrimaryTable.columns.primaryKey.eq(
123+
From.PrimaryTable.TableColumns.PrimaryKey(queryOutput: primaryKey)
124+
)
125+
}
126+
}
127+
}
128+
129+
extension Select where From: PrimaryKeyedTable {
130+
/// A select statement filtered by a primary key.
131+
///
132+
/// - Parameter primaryKey: A primary key identifying a table row.
133+
/// - Returns: A select statement filtered by the given key.
134+
public func find(_ primaryKey: From.TableColumns.PrimaryKey.QueryOutput) -> Self {
135+
self.and(From.find(primaryKey))
136+
}
137+
}
138+
139+
extension Select where From: TableDraft {
140+
/// A select statement filtered by a primary key.
141+
///
142+
/// - Parameter primaryKey: A primary key identifying a table row.
143+
/// - Returns: A select statement filtered by the given key.
144+
public func find(_ primaryKey: From.PrimaryTable.TableColumns.PrimaryKey.QueryOutput) -> Self {
145+
self.and(From.find(primaryKey))
146+
}
147+
}
148+
73149
extension Update where From: PrimaryKeyedTable {
74150
/// An update statement filtered by a primary key.
75151
///
@@ -80,6 +156,20 @@ extension Update where From: PrimaryKeyedTable {
80156
}
81157
}
82158

159+
extension Update where From: TableDraft {
160+
/// An update statement filtered by a primary key.
161+
///
162+
/// - Parameter primaryKey: A primary key identifying a table row.
163+
/// - Returns: An update statement filtered by the given key.
164+
public func find(_ primaryKey: From.PrimaryTable.TableColumns.PrimaryKey.QueryOutput) -> Self {
165+
self.where { _ in
166+
From.PrimaryTable.columns.primaryKey.eq(
167+
From.PrimaryTable.TableColumns.PrimaryKey(queryOutput: primaryKey)
168+
)
169+
}
170+
}
171+
}
172+
83173
extension Delete where From: PrimaryKeyedTable {
84174
/// A delete statement filtered by a primary key.
85175
///
@@ -90,12 +180,16 @@ extension Delete where From: PrimaryKeyedTable {
90180
}
91181
}
92182

93-
extension Select where From: PrimaryKeyedTable {
94-
/// A select statement filtered by a primary key.
183+
extension Delete where From: TableDraft {
184+
/// A delete statement filtered by a primary key.
95185
///
96186
/// - Parameter primaryKey: A primary key identifying a table row.
97-
/// - Returns: A select statement filtered by the given key.
98-
public func find(_ primaryKey: From.TableColumns.PrimaryKey.QueryOutput) -> Self {
99-
self.and(From.find(primaryKey))
187+
/// - Returns: A delete statement filtered by the given key.
188+
public func find(_ primaryKey: From.PrimaryTable.TableColumns.PrimaryKey.QueryOutput) -> Self {
189+
self.where { _ in
190+
From.PrimaryTable.columns.primaryKey.eq(
191+
From.PrimaryTable.TableColumns.PrimaryKey(queryOutput: primaryKey)
192+
)
193+
}
100194
}
101195
}

Sources/StructuredQueriesCore/QueryFragmentBuilder.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ extension QueryFragmentBuilder<Bool> {
2727
) -> [QueryFragment] {
2828
[expression.queryFragment]
2929
}
30+
31+
public static func buildExpression(
32+
_ expression: some QueryExpression<some _OptionalPromotable<Bool?>>
33+
) -> [QueryFragment] {
34+
[expression.queryFragment]
35+
}
3036
}
3137

3238
extension QueryFragmentBuilder<()> {

0 commit comments

Comments
 (0)