Skip to content

Commit ed37586

Browse files
committed
Updated interfaces for working with results from async query execution so that we no longer offer an operation closure to execute inside the actor's domain. It is now the responsibility of the caller to remain inside that domain.
1 parent 1b0bc69 commit ed37586

File tree

5 files changed

+148
-249
lines changed

5 files changed

+148
-249
lines changed

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,19 @@ let jill = Person
216216
Where SwiftQuery really shines is it's automatic support for performing queries
217217
in a concurrency environment. The current isolation context is passed in to each function
218218
that performs a query, so if you have a custom model actor, you can freely perform
219-
queries inside the actor:
219+
queries and operate on the results inside the actor:
220220

221221
```swift
222222
@ModelActor
223223
actor MyActor {
224224
func promoteJill() throws {
225-
if let jill = Person
225+
let jill = Person
226226
.include(#Predicate { $0.name == "Jill" })
227-
.first()
228-
{
229-
jill.isPromoted = true
230-
try modelContext.save()
231-
}
227+
.findOrCreate {
228+
Person(name: "Jill")
229+
}
230+
jill.isPromoted = true
231+
try modelContext.save()
232232
}
233233
}
234234
```
@@ -249,7 +249,10 @@ await modelContainer.createQueryActor().perform { _ in
249249
}
250250
```
251251

252-
Or, to return a value:
252+
The results remain inside the actor's isolation
253+
domain so can be safely used within the closure.
254+
255+
If we need to produce a side effect for the query, we can return a value:
253256

254257
```swift
255258
let count = await modelContainer.createQueryActor().perform { _ in
@@ -259,7 +262,7 @@ let count = await modelContainer.createQueryActor().perform { _ in
259262
}
260263
```
261264

262-
Note that models cannot be returned out of the actor's isolation context using this function;
265+
> Note: Models cannot be returned out of the actor's isolation context using this function;
263266
only `Sendable` values can be transported across the boundary. This means the compiler
264267
effectively makes it impossible to use the models returned from a query incorrectly in
265268
a multi-context environment, thus guaranteeing the SwiftData concurrency contract at

Sources/SwiftQuery/Fetch+Concurrency.swift

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -80,42 +80,6 @@ public extension Query {
8080
try isolation.modelContext.fetch(fetchDescriptor)
8181
}
8282

83-
/// Provides access to a lazily-evaluated collection of objects matching the
84-
/// query via a closure from within a model actor's isolation context.
85-
///
86-
/// - Parameters:
87-
/// - batchSize: Number of objects to fetch per batch. Defaults to 20.
88-
/// - isolation: The model actor to execute the query within. Defaults to `#isolation`
89-
/// which infers the current actor context.
90-
/// - operation: A closure that receives the `FetchResultsCollection` for processing within
91-
/// the actor's isolation domain.
92-
/// - Throws: any SwiftData errors thrown during query execution
93-
///
94-
/// ## Example
95-
/// ```swift
96-
/// // Process large result set in batches concurrently
97-
/// await container.createQueryActor().perform { _ in
98-
/// try Person.sortBy(\.name).fetchedResults(batchSize: 50) { results in
99-
/// for person in results.prefix(100) {
100-
/// // Process first 100 results efficiently
101-
/// print(person.name)
102-
/// }
103-
/// }
104-
/// }
105-
/// ```
106-
///
107-
/// - SeeAlso: `fetchedResults(in:batchSize:)`
108-
func fetchedResults(
109-
batchSize: Int = 20,
110-
isolation: isolated (any ModelActor) = #isolation,
111-
operation: @Sendable (FetchResultsCollection<T>) -> Void
112-
) throws {
113-
var descriptor = fetchDescriptor
114-
descriptor.includePendingChanges = false
115-
let results = try isolation.modelContext.fetch(descriptor, batchSize: batchSize)
116-
operation(results)
117-
}
118-
11983
/// Returns a value computed from a lazily-evaluated collection of objects
12084
/// matching the query from within a model actor's isolation context.
12185
///
@@ -136,15 +100,14 @@ public extension Query {
136100
/// }
137101
/// }
138102
/// ```
139-
func fetchedResults<Value>(
103+
func fetchedResults(
140104
batchSize: Int = 20,
141-
isolation: isolated (any ModelActor) = #isolation,
142-
operation: @Sendable (FetchResultsCollection<T>) -> Value
143-
) throws -> Value where Value: Sendable {
105+
isolation: isolated (any ModelActor) = #isolation
106+
) throws -> FetchResultsCollection<T> {
144107
var descriptor = fetchDescriptor
145108
descriptor.includePendingChanges = false
146109
let results = try isolation.modelContext.fetch(descriptor, batchSize: batchSize)
147-
return operation(results)
110+
return results
148111
}
149112

150113
/// Returns the number of objects matching the query from within a model actor's
@@ -189,46 +152,45 @@ public extension Query {
189152
try count(isolation: isolation) < 1
190153
}
191154

192-
/// Finds an existing object matching the query, or creates a new one if none exists,
193-
/// then operates on it from within a model actor's isolation context.
155+
/// Finds an existing object matching the query, or creates a new one if none exists.
156+
///
157+
/// Can only be called from within the model actor itself because the returned results
158+
/// must remain isolated.
159+
///
194160
///
195161
/// - Parameters:
196162
/// - isolation: The model actor to execute the query within. Defaults to `#isolation`
197163
/// which infers the current actor context.
198164
/// - body: Closure that creates a new object if none is found
199-
/// - operation: Closure that operates on the found or created object
200165
/// - Throws: `Error.missingPredicate` if the query has no predicate, or SwiftData errors
201166
///
202167
/// ## Example
203168
/// ```swift
204169
/// // Find or create admin user concurrently
205170
/// try await container.createQueryActor().perform { actor in
206-
/// try Person
171+
/// let admin = try Person
207172
/// .include(#Predicate { $0.role == "admin" })
208173
/// .findOrCreate(
209-
/// body: { Person(name: "Administrator", role: "admin") },
210-
/// operation: { admin in
211-
/// print("Admin user: \(admin.name)")
212-
/// }
174+
/// body: { Person(name: "Administrator", role: "admin") }
213175
/// )
176+
/// print("Admin user: \(admin.name)")
214177
/// }
215178
/// ```
216179
///
217180
/// - SeeAlso: `findOrCreate(in:body:)`
218181
func findOrCreate(
219182
isolation: isolated (any ModelActor) = #isolation,
220-
body: () -> T,
221-
operation: (T) -> Void
222-
) throws {
183+
body: () -> T
184+
) throws -> T {
223185
guard predicate != nil else {
224186
throw Error.missingPredicate
225187
}
226188
if let found = try first() {
227-
operation(found)
189+
return found
228190
} else {
229191
let created = body()
230192
isolation.modelContext.insert(created)
231-
operation(created)
193+
return created
232194
}
233195
}
234196

Sources/SwiftQuery/PersistentModel+Fetch.swift

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,38 +42,19 @@ extension PersistentModel {
4242
try query().results()
4343
}
4444

45-
/// Builds a query over this model type and invokes ``Query/fetchedResults(isolation:operation:)`` on that query.
45+
/// Builds a query over this model type and invokes ``Query/fetchedResults(isolation:)`` on that query.
4646
static func fetchedResults(
47-
isolation: isolated (any ModelActor) = #isolation,
48-
operation: @Sendable (FetchResultsCollection<Self>) -> Void
49-
) throws {
50-
try query().fetchedResults(operation: operation)
47+
isolation: isolated (any ModelActor) = #isolation
48+
) throws -> FetchResultsCollection<Self> {
49+
try query().fetchedResults()
5150
}
5251

53-
/// Builds a query over this model type and invokes ``Query/fetchedResults(batchSize:isolation:operation:)`` on that query.
52+
/// Builds a query over this model type and invokes ``Query/fetchedResults(batchSize:isolation:)`` on that query.
5453
static func fetchedResults(
5554
batchSize: Int,
56-
isolation: isolated (any ModelActor) = #isolation,
57-
operation: @Sendable (FetchResultsCollection<Self>) -> Void
58-
) throws {
59-
try query().fetchedResults(batchSize: batchSize, operation: operation)
60-
}
61-
62-
/// Builds a query over this model type and invokes ``Query/fetchedResults(isolation:operation:)`` on that query.
63-
static func fetchedResults<Value>(
64-
isolation: isolated (any ModelActor) = #isolation,
65-
operation: @Sendable (FetchResultsCollection<Self>) -> Value
66-
) throws -> Value where Value: Sendable {
67-
try query().fetchedResults(operation: operation)
68-
}
69-
70-
/// Builds a query over this model type and invokes ``Query/fetchedResults(batchSize:isolation:operation:)`` on that query.
71-
static func fetchedResults<Value>(
72-
batchSize: Int,
73-
isolation: isolated (any ModelActor) = #isolation,
74-
operation: @Sendable (FetchResultsCollection<Self>) -> Value
75-
) throws -> Value where Value: Sendable {
76-
try query().fetchedResults(batchSize: batchSize, operation: operation)
55+
isolation: isolated (any ModelActor) = #isolation
56+
) throws -> FetchResultsCollection<Self> {
57+
try query().fetchedResults(batchSize: batchSize)
7758
}
7859

7960
/// Builds a query over this model type and invokes ``Query/count(isolation:)`` on that query.
@@ -89,9 +70,8 @@ extension PersistentModel {
8970
/// Builds a query over this model type and invokes ``Query/findOrCreate(isolation:body:operation:)`` on that query.
9071
static func findOrCreate(
9172
isolation: isolated (any ModelActor) = #isolation,
92-
body: () -> Self,
93-
operation: (Self) -> Void
94-
) throws {
95-
try query().findOrCreate(body: body, operation: operation)
73+
body: () -> Self
74+
) throws -> Self {
75+
try query().findOrCreate(body: body)
9676
}
9777
}

0 commit comments

Comments
 (0)