Skip to content

Commit e198472

Browse files
authored
Merge pull request #1079 from groue/dev/async
Async / Await
2 parents 89421c9 + 469898c commit e198472

File tree

88 files changed

+3685
-75
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+3685
-75
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,16 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
8585

8686
## Next Release
8787

88+
- **New**: :star: [#1079](https://github.com/groue/GRDB.swift/pull/1079) by [@groue](https://github.com/groue): Async / Await
8889
- **New**: `DatabaseRegion(table: tableName)` is deprecated. Use `Table(tableName)` instead.
8990
- **New**: The `ABS` and `LENGTH` SQL functions are now available on association aggregates, through `abs(aggregate)` and `length(aggregate)`.
9091
- **New**: Support for the [`TOTAL` aggregate function](https://www.sqlite.org/lang_aggfunc.html#sumunc). You can use `total` in Swift, at all places `sum` is available.
9192
- **New**: `ValueObservation.removeDuplicates(by:)` with a closure argument.
9293
- **New**: Testing for the existence of an associated record with `TableAlias.exists` now supports associated views, and associated tables WITHOUT ROWID that have a compound primary key.
9394
- **Fixed**: `Database.primaryKey(_:)` throws when given the name of a view.
95+
- **Documentation Update**: :star: The [Concurrency Guide](Documentation/Concurrency.md) was updated for the new support for asynchronous Swift.
96+
- **Documentation Update**: :star: [GRDBAsyncDemo](Documentation/DemoApps/GRDBAsyncDemo/README.md) is a new SwiftUI demo app that uses the new async apis.
97+
- **Documentation Update**: A new FAQ gives hints for avoiding a [Mutation of captured var in concurrently-executing code](README.md##mutation-of-captured-var-in-concurrently-executing-code) compiler error.
9498

9599
## 5.16.0
96100

Documentation/Concurrency.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,27 @@ try dbQueue.write { db in
132132
}
133133
```
134134

135-
:twisted_rightwards_arrows: **An async access does not block the current thread.** Instead, it notifies you when the database operations are completed. There are three ways to access the database asynchronously:
135+
:twisted_rightwards_arrows: **An async access does not block the current thread.** Instead, it notifies you when the database operations are completed. There are four ways to access the database asynchronously:
136+
137+
<details>
138+
<summary><b>Swift concurrency</b> (async/await)</summary>
139+
140+
[**:fire: EXPERIMENTAL**](../README.md#what-are-experimental-features) GRDB support for Swift concurrency requires Xcode 13.2+.
141+
142+
```swift
143+
let playerCount = try await dbQueue.read { db in
144+
try Player.fetchCount(db)
145+
}
146+
147+
let newPlayerCount = try await dbQueue.write { db -> Int in
148+
try Player(id: 12, name: "Arthur").insert(db)
149+
return try Player.fetchCount(db)
150+
}
151+
```
152+
153+
Note the identical method names: `read`, `write`. The async version is only available in async Swift functions.
154+
155+
</details>
136156

137157
<details>
138158
<summary><b>Combine publishers</b></summary>
@@ -205,7 +225,7 @@ During one async access, all individual database operations grouped inside (fetc
205225

206226
```swift
207227
// One asynchronous access...
208-
dbQueue.writePublisher { db in
228+
try await dbQueue.write { db in
209229
// ... always performs synchronous database operations:
210230
try Player(...).insert(db)
211231
try Player(...).insert(db)
@@ -237,7 +257,7 @@ Some applications need to relax this safety net, in order to achieve specific SQ
237257
try dbQueue.writeWithoutTransaction { db in ... }
238258
```
239259

240-
You can also use `asyncWriteWithoutTransaction`.
260+
`writeWithoutTransaction` is also available as an `async` function. You can also use `asyncWriteWithoutTransaction`.
241261

242262
- **Write outside of any transaction, and prevents concurrent reads**
243263
(Lifted guarantee: [Write Transactions])
@@ -252,7 +272,7 @@ Some applications need to relax this safety net, in order to achieve specific SQ
252272

253273
You will use this method, for example, when you [change the password](../README.md#changing-the-passphrase-of-an-encrypted-database) of an encrypted database.
254274

255-
You can also use `asyncBarrierWriteWithoutTransaction`.
275+
`barrierWriteWithoutTransaction` is also available as an `async` function. You can also use `asyncBarrierWriteWithoutTransaction`.
256276

257277
- **Reentrant write outside of any transaction**
258278
(Lifted guarantees: [Write Transactions], [Non-Reentrancy])
@@ -279,7 +299,7 @@ Some applications need to relax this safety net, in order to achieve specific SQ
279299
try dbQueue.unsafeRead { db in ... }
280300
```
281301

282-
`unsafeRead` has no async version.
302+
`unsafeRead` is also available as an `async` function.
283303

284304
- **Reentrant read, outside of any transaction**
285305
(Lifted guarantees: [Isolated Reads], [Forbidden Writes], [Non-Reentrancy])

Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.pbxproj

Lines changed: 727 additions & 0 deletions
Large diffs are not rendered by default.

Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1230"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
18+
BuildableName = "GRDBAsyncDemo.app"
19+
BlueprintName = "GRDBAsyncDemo"
20+
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Debug"
27+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
shouldUseLaunchSchemeArgsEnv = "YES">
30+
<Testables>
31+
<TestableReference
32+
skipped = "NO">
33+
<BuildableReference
34+
BuildableIdentifier = "primary"
35+
BlueprintIdentifier = "56026C9725B8A7D000D1DF3F"
36+
BuildableName = "GRDBAsyncDemoTests.xctest"
37+
BlueprintName = "GRDBAsyncDemoTests"
38+
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
39+
</BuildableReference>
40+
</TestableReference>
41+
<TestableReference
42+
skipped = "NO">
43+
<BuildableReference
44+
BuildableIdentifier = "primary"
45+
BlueprintIdentifier = "78E2F9E2261EEC22005D1F7F"
46+
BuildableName = "GRDBAsyncDemoUITests.xctest"
47+
BlueprintName = "GRDBAsyncDemoUITests"
48+
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
49+
</BuildableReference>
50+
</TestableReference>
51+
</Testables>
52+
</TestAction>
53+
<LaunchAction
54+
buildConfiguration = "Debug"
55+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
56+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
57+
launchStyle = "0"
58+
useCustomWorkingDirectory = "NO"
59+
ignoresPersistentStateOnLaunch = "NO"
60+
debugDocumentVersioning = "YES"
61+
debugServiceExtension = "internal"
62+
allowLocationSimulation = "YES">
63+
<BuildableProductRunnable
64+
runnableDebuggingMode = "0">
65+
<BuildableReference
66+
BuildableIdentifier = "primary"
67+
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
68+
BuildableName = "GRDBAsyncDemo.app"
69+
BlueprintName = "GRDBAsyncDemo"
70+
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
71+
</BuildableReference>
72+
</BuildableProductRunnable>
73+
</LaunchAction>
74+
<ProfileAction
75+
buildConfiguration = "Release"
76+
shouldUseLaunchSchemeArgsEnv = "YES"
77+
savedToolIdentifier = ""
78+
useCustomWorkingDirectory = "NO"
79+
debugDocumentVersioning = "YES">
80+
<BuildableProductRunnable
81+
runnableDebuggingMode = "0">
82+
<BuildableReference
83+
BuildableIdentifier = "primary"
84+
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
85+
BuildableName = "GRDBAsyncDemo.app"
86+
BlueprintName = "GRDBAsyncDemo"
87+
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
88+
</BuildableReference>
89+
</BuildableProductRunnable>
90+
</ProfileAction>
91+
<AnalyzeAction
92+
buildConfiguration = "Debug">
93+
</AnalyzeAction>
94+
<ArchiveAction
95+
buildConfiguration = "Release"
96+
revealArchiveInOrganizer = "YES">
97+
</ArchiveAction>
98+
</Scheme>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import Foundation
2+
import GRDB
3+
4+
/// AppDatabase lets the application access the database.
5+
///
6+
/// It applies the pratices recommended at
7+
/// <https://github.com/groue/GRDB.swift/blob/master/Documentation/GoodPracticesForDesigningRecordTypes.md>
8+
struct AppDatabase {
9+
/// Creates an `AppDatabase`, and make sure the database schema is ready.
10+
init(_ dbWriter: DatabaseWriter) throws {
11+
self.dbWriter = dbWriter
12+
try migrator.migrate(dbWriter)
13+
}
14+
15+
/// Provides access to the database.
16+
///
17+
/// Application can use a `DatabasePool`, while SwiftUI previews and tests
18+
/// can use a fast in-memory `DatabaseQueue`.
19+
///
20+
/// See <https://github.com/groue/GRDB.swift/blob/master/README.md#database-connections>
21+
private let dbWriter: DatabaseWriter
22+
23+
/// The DatabaseMigrator that defines the database schema.
24+
///
25+
/// See <https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md>
26+
private var migrator: DatabaseMigrator {
27+
var migrator = DatabaseMigrator()
28+
29+
#if DEBUG
30+
// Speed up development by nuking the database when migrations change
31+
// See https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md#the-erasedatabaseonschemachange-option
32+
migrator.eraseDatabaseOnSchemaChange = true
33+
#endif
34+
35+
migrator.registerMigration("createPlayer") { db in
36+
// Create a table
37+
// See https://github.com/groue/GRDB.swift#create-tables
38+
try db.create(table: "player") { t in
39+
t.autoIncrementedPrimaryKey("id")
40+
t.column("name", .text).notNull()
41+
t.column("score", .integer).notNull()
42+
}
43+
}
44+
45+
// Migrations for future application versions will be inserted here:
46+
// migrator.registerMigration(...) { db in
47+
// ...
48+
// }
49+
50+
return migrator
51+
}
52+
}
53+
54+
// MARK: - Database Access: Writes
55+
56+
extension AppDatabase {
57+
/// A validation error that prevents some players from being saved into
58+
/// the database.
59+
enum ValidationError: LocalizedError {
60+
case missingName
61+
62+
var errorDescription: String? {
63+
switch self {
64+
case .missingName:
65+
return "Please provide a name"
66+
}
67+
}
68+
}
69+
70+
/// Saves (inserts or updates) a player. When the method returns, the
71+
/// player is present in the database, and its id is not nil.
72+
func savePlayer(_ player: inout Player) async throws {
73+
if player.name.isEmpty {
74+
throw ValidationError.missingName
75+
}
76+
player = try await dbWriter.write { [player] db in
77+
try player.saved(db)
78+
}
79+
}
80+
81+
/// Delete the specified players
82+
func deletePlayers(ids: [Int64]) async throws {
83+
try await dbWriter.write { db in
84+
_ = try Player.deleteAll(db, ids: ids)
85+
}
86+
}
87+
88+
/// Delete all players
89+
func deleteAllPlayers() async throws {
90+
try await dbWriter.write { db in
91+
_ = try Player.deleteAll(db)
92+
}
93+
}
94+
95+
/// Refresh all players (by performing some random changes, for demo purpose).
96+
func refreshPlayers() async throws {
97+
try await dbWriter.write { db in
98+
if try Player.all().isEmpty(db) {
99+
// When database is empty, insert new random players
100+
try createRandomPlayers(db)
101+
} else {
102+
// Insert a player
103+
if Bool.random() {
104+
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
105+
}
106+
107+
// Delete a random player
108+
if Bool.random() {
109+
try Player.order(sql: "RANDOM()").limit(1).deleteAll(db)
110+
}
111+
112+
// Update some players
113+
for var player in try Player.fetchAll(db) where Bool.random() {
114+
try player.updateChanges(db) {
115+
$0.score = Player.randomScore()
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
/// Create random players if the database is empty.
123+
func createRandomPlayersIfEmpty() throws {
124+
try dbWriter.write { db in
125+
if try Player.all().isEmpty(db) {
126+
try createRandomPlayers(db)
127+
}
128+
}
129+
}
130+
131+
static let uiTestPlayers = [
132+
Player(id: nil, name: "Arthur", score: 5),
133+
Player(id: nil, name: "Barbara", score: 6),
134+
Player(id: nil, name: "Craig", score: 8),
135+
Player(id: nil, name: "David", score: 4),
136+
Player(id: nil, name: "Elena", score: 1),
137+
Player(id: nil, name: "Frederik", score: 2),
138+
Player(id: nil, name: "Gilbert", score: 7),
139+
Player(id: nil, name: "Henriette", score: 3)]
140+
141+
func createPlayersForUITests() throws {
142+
try dbWriter.write { db in
143+
try AppDatabase.uiTestPlayers.forEach { player in
144+
_ = try player.inserted(db) // insert but ignore inserted id
145+
}
146+
}
147+
}
148+
149+
/// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
150+
private func createRandomPlayers(_ db: Database) throws {
151+
for _ in 0..<8 {
152+
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
153+
}
154+
}
155+
}
156+
157+
// MARK: - Database Access: Reads
158+
159+
// This demo app does not provide any specific reading method, and instead
160+
// gives an unrestricted read-only access to the rest of the application.
161+
// In your app, you are free to choose another path, and define focused
162+
// reading methods.
163+
extension AppDatabase {
164+
/// Provides a read-only access to the database
165+
var databaseReader: DatabaseReader {
166+
dbWriter
167+
}
168+
}

0 commit comments

Comments
 (0)