Skip to content

Commit fe94201

Browse files
authored
Merge branch 'main' into tagged-trait
2 parents 3a5a0f0 + 4d854b9 commit fe94201

36 files changed

+2431
-764
lines changed

.github/workflows/format.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
NB: Compatible swift-format requires Xcode 16.3, not yet available on GitHub
21
name: Format
32

43
on:

.spi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ builder:
66
- StructuredQueries
77
custom_documentation_parameters:
88
- '--enable-experimental-overloaded-symbol-presentation'
9-
- '--enable-experimental-combined-documentation'
9+
# - '--enable-experimental-combined-documentation'

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ for index in package.targets.indices {
127127
package.targets[index].swiftSettings = swiftSettings
128128
}
129129

130-
#if !os(Darwin)
130+
#if !canImport(Darwin)
131131
package.targets.append(
132132
.systemLibrary(
133133
name: "StructuredQueriesSQLite3",

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# StructuredQueries
22

3-
[![CI](https://github.com/pointfreeco/swift-structured-queries/workflows/CI/badge.svg)](https://github.com/pointfreeco/swift-structured-queries/actions?query=workflow%3ACI)
3+
[![CI](https://github.com/pointfreeco/swift-structured-queries/actions/workflows/ci.yml/badge.svg)](https://github.com/pointfreeco/swift-structured-queries/actions/workflows/ci.yml)
44
[![Slack](https://img.shields.io/badge/slack-chat-informational.svg?label=Slack&logo=slack)](https://www.pointfree.co/slack-invite)
55
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswift-structured-queries%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/swift-structured-queries)
66
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswift-structured-queries%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/swift-structured-queries)

Sources/StructuredQueriesCore/CaseExpression.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ public struct Cases<Base, QueryValue: _OptionalProtocol>: QueryExpression {
9797
return cases
9898
}
9999

100+
/// Adds a `WHEN` clause to a `CASE` expression.
101+
///
102+
/// - Parameters:
103+
/// - condition: A condition to test.
104+
/// - expression: A return value should the condition pass.
105+
/// - Returns: A `CASE` expression builder.
106+
public func when(
107+
_ condition: some QueryExpression<Base>,
108+
then expression: some QueryExpression<QueryValue.Wrapped>
109+
) -> Cases {
110+
var cases = self
111+
cases.cases.append(
112+
When(predicate: condition.queryFragment, expression: expression.queryFragment).queryFragment
113+
)
114+
return cases
115+
}
116+
100117
/// Terminates a `CASE` expression with an `ELSE` clause.
101118
///
102119
/// - Parameter expression: A return value should every `WHEN` condition fail.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,7 @@ clause:
447447
### Statement types
448448

449449
- ``Insert``
450+
451+
### Seeding a database
452+
453+
- ``Seeds``

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ into the database by providing only a draft:
6161
@Row {
6262
@Column {
6363
```swift
64-
Reminder.insert(Reminder.Draft(title: "Get groceries"))
64+
Reminder
65+
.insert(Reminder.Draft(title: "Get groceries"))
6566
```
6667
}
6768
@Column {
@@ -122,8 +123,6 @@ Or even get back the entire newly inserted row:
122123
```
123124
}
124125
}
125-
```swift
126-
```
127126

128127
At times your application may want to provide the same business logic for creating a new record and
129128
editing an existing one. Your primary keyed table's `Draft` type can be used for these kinds of

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ scopes, and decoding into custom data types.
88
The library comes with a variety of tools that allow you to define helpers for composing together
99
large and complex queries.
1010

11+
* [Reusable table queries](#Reusable-table-queries)
12+
* [Reusable column queries](#Reusable-column-queries)
13+
* [Default scopes](#Default-scopes)
14+
* [Custom selections](#Custom-selections)
15+
* [Pre-loading associations with JSON](#Pre-loading-associations-with-json)
16+
1117
### Reusable table queries
1218

1319
One can define query helpers as statics on their tables in order to facilitate using those
@@ -339,3 +345,89 @@ And a query that selects into this type can be defined like so:
339345
```
340346
}
341347
}
348+
349+
### Pre-loading associations with JSON
350+
351+
There are times you may want to load rows from a table along with the data from some associated
352+
table. For example, querying for all reminders lists along with an array of the reminders in each
353+
list. We'd like to be able to query for this data and decode it into a collection of values
354+
from the following data type:
355+
356+
```struct
357+
struct Row {
358+
let remindersList: RemindersList
359+
let reminders: [Reminder]
360+
}
361+
```
362+
363+
However, typically this requires one to make multiple SQL queries. First a query to selects all
364+
of the reminders lists:
365+
366+
```swift
367+
let remindersLists = try RemindersLists.all.execute(db)
368+
```
369+
370+
Then you execute another query to fetch all of the reminders associated with the lists just
371+
fetched:
372+
373+
```swift
374+
let reminders = try Reminder
375+
.where { $0.id.in(remindersLists.map(\.id)) }
376+
.execute(db))
377+
```
378+
379+
And then finally you need to transform the `remindersLists` and `reminders` into a single collection
380+
of `Row` values:
381+
382+
```swift
383+
let rows = remindersLists.map { remindersList in
384+
Row(
385+
remindersList: remindersList,
386+
reminders: reminders.filter { reminder in
387+
reminder.remindersListID == remindersList.id
388+
}
389+
)
390+
}
391+
```
392+
393+
This can work, but it's incredibly inefficient, a lot of boilerplate, and prone to mistakes. And
394+
this is doing work that SQL actually excels at. In fact, the condition inside the `filter` looks
395+
suspiciously like a join constraint, which should give us a hint that what we are doing is not
396+
quite right.
397+
398+
Another way to do this is to use the `@Selection` macro described above
399+
(<doc:QueryCookbook#Custom-selections>), along with a ``JSONRepresentation`` of the collection
400+
of reminders you want to load for each list:
401+
402+
```struct
403+
@Selection
404+
struct Row {
405+
let remindersList: RemindersList
406+
@Column(as: JSONRepresentation<[Reminder]>.self)
407+
let reminders: [Reminder]
408+
}
409+
```
410+
411+
> Note: `Reminder` must conform to `Codable` to be able to use ``JSONRepresentation``.
412+
413+
This allows the query to serialize the associated rows into JSON, which are then deserialized into
414+
a `Row` type. To construct such a query you can use the
415+
``PrimaryKeyedTableDefinition/jsonGroupArray(order:filter:)`` property that is defined on the
416+
columns of [primary keyed tables](<doc:PrimaryKeyedTables>):
417+
418+
```swift
419+
RemindersList
420+
.join(Reminder.all) { $0.id.eq($1.remindersListID) }
421+
.select {
422+
Row.Columns(
423+
remindersList: $0,
424+
reminders: $1.jsonGroupArray()
425+
)
426+
}
427+
```
428+
429+
This allows you to fetch all of the data in a single SQLite query and decode the data into a
430+
collection of `Row` values. There is an extra cost associated with decoding the JSON object,
431+
but that cost may be smaller than executing multiple SQLite requests and transforming the data
432+
into `Row` manually, not to mention the additional code you need to write and maintain to process
433+
the data.

0 commit comments

Comments
 (0)