Skip to content

Commit 985794e

Browse files
authored
Merge pull request groue#670 from groue/development
GRDB 4.7.0
2 parents 2b9932c + 5d0e37e commit 985794e

File tree

85 files changed

+3602
-855
lines changed

Some content is hidden

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

85 files changed

+3602
-855
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: [groue]

CHANGELOG.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
1313

1414
#### 4.x Releases
1515

16+
- `4.7.x` Releases - [4.7.0](#470)
1617
- `4.6.x` Releases - [4.6.0](#460) | [4.6.1](#461) | [4.6.2](#462)
1718
- `4.5.x` Releases - [4.5.0](#450)
1819
- `4.4.x` Releases - [4.4.0](#440)
@@ -64,6 +65,67 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
6465
-->
6566

6667

68+
## 4.7.0
69+
70+
Released December 18, 2019 • [diff](https://github.com/groue/GRDB.swift/compare/v4.6.2...v4.7.0)
71+
72+
**New**
73+
74+
- [#656](https://github.com/groue/GRDB.swift/pull/656): Type inference of selected columns
75+
- [#659](https://github.com/groue/GRDB.swift/pull/659): Database interruption
76+
- [#660](https://github.com/groue/GRDB.swift/pull/660): Database Lock Prevention
77+
- [#662](https://github.com/groue/GRDB.swift/pull/662): Upgrade custom SQLite builds to version 3.30.1 (thanks to [@swiftlyfalling](https://github.com/swiftlyfalling/SQLiteLib))
78+
- [#668](https://github.com/groue/GRDB.swift/pull/668): Database Suspension
79+
80+
### Breaking Changes
81+
82+
[Custom SQLite builds](Documentation/CustomSQLiteBuilds.md) now disable by default the support for the [Double-quoted String Literals misfeature](https://sqlite.org/quirks.html#dblquote). This is a technically a breaking change, but it fixes an SQLite bug. You can restore the previous behavior if your application relies on it:
83+
84+
```swift
85+
// Enable support for the Double-quoted String Literals misfeature
86+
var configuration = Configuration()
87+
configuration.acceptsDoubleQuotedStringLiterals = true
88+
let dbQueue = try DatabaseQueue(path: ..., configuration: configuration)
89+
```
90+
91+
92+
### Documentation Diff
93+
94+
The new [Interrupt a Database](README.md#interrupt-a-database) chapter documents the new `interrupt()` method.
95+
96+
The new [Sharing a Datatase in an App Group Container](Documentation/AppGroupContainers.md) guide explains how to setup GRDB when you share a database in an iOS App Group container.
97+
98+
99+
### API Diff
100+
101+
```diff
102+
struct Configuration {
103+
+ var observesSuspensionNotifications: Bool // Experimental
104+
+ var acceptsDoubleQuotedStringLiterals: Bool
105+
}
106+
107+
class Database {
108+
+ static let suspendNotification: Notification.Name // Experimental
109+
+ static let resumeNotification: Notification.Name // Experimental
110+
}
111+
112+
extension DatabaseError {
113+
+ var isInterruptionError: Bool { get }
114+
}
115+
116+
protocol DatabaseReader {
117+
+ func interrupt()
118+
}
119+
120+
extension SQLSpecificExpressible {
121+
+ #if GRDBCUSTOMSQLITE
122+
+ var ascNullsLast: SQLOrderingTerm { get }
123+
+ var descNullsFirst: SQLOrderingTerm { get }
124+
+ #endif
125+
}
126+
```
127+
128+
67129
## 4.6.2
68130

69131
Released November 20, 2019 • [diff](https://github.com/groue/GRDB.swift/compare/v4.6.1...v4.6.2)

Documentation/AppGroupContainers.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
Sharing a Datatase in an App Group Container
2+
============================================
3+
4+
On iOS, you can share database files between multiple processes by storing them in an [App Group Container](https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati).
5+
6+
A shared database is accessed from several SQLite connections, from several processes. This creates challenges at various levels:
7+
8+
1. **Database setup** may be attempted by multiple processes, concurrently.
9+
2. **SQLite** may throw `SQLITE_BUSY` errors, code 5, "database is locked".
10+
3. **iOS** may kill your application with a `0xDEAD10CC` exception.
11+
4. **GRDB** database observation misses changes performed by external processes.
12+
13+
We'll address all of those challenges below.
14+
15+
- [Use a Database Pool]
16+
- [How to limit the `SQLITE_BUSY` error]
17+
- [How to limit the `0xDEAD10CC` exception]
18+
- [How to perform cross-process database observation]
19+
20+
21+
## Use a Database Pool
22+
23+
In order to access a shared database, use a [Database Pool]. It opens the database in the [WAL mode](https://www.sqlite.org/wal.html), which helps sharing a database.
24+
25+
Since several processes may open the database at the same time, protect the creation of the database pool with an [NSFileCoordinator].
26+
27+
- In a process that can create and write in the database, use this sample code:
28+
29+
```swift
30+
/// Returns an initialized database pool at the shared location databaseURL
31+
func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool {
32+
let coordinator = NSFileCoordinator(filePresenter: nil)
33+
var coordinatorError: NSError?
34+
var dbPool: DatabasePool?
35+
var dbError: Error?
36+
coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { url in
37+
do {
38+
dbPool = try openDatabase(at: url)
39+
} catch {
40+
dbError = error
41+
}
42+
})
43+
if let error = dbError ?? coordinatorError {
44+
throw error
45+
}
46+
return dbPool!
47+
}
48+
49+
private func openDatabase(at databaseURL: URL) throws -> DatabasePool {
50+
let dbPool = try DatabasePool(path: databaseURL.path)
51+
// Perform here other database setups, such as defining
52+
// the database schema with a DatabaseMigrator.
53+
return dbPool
54+
}
55+
```
56+
57+
- In a process that only reads in the database, use this sample code:
58+
59+
```swift
60+
/// Returns an initialized database pool at the shared location databaseURL,
61+
/// or nil if the database was not created yet.
62+
func openSharedReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? {
63+
let coordinator = NSFileCoordinator(filePresenter: nil)
64+
var coordinatorError: NSError?
65+
var dbPool: DatabasePool?
66+
var dbError: Error?
67+
coordinator.coordinate(readingItemAt: databaseURL, options: .withoutChanges, error: &coordinatorError, byAccessor: { url in
68+
do {
69+
dbPool = try openReadOnlyDatabase(at: url)
70+
} catch {
71+
dbError = error
72+
}
73+
})
74+
if let error = dbError ?? coordinatorError {
75+
throw error
76+
}
77+
return dbPool
78+
}
79+
80+
private func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? {
81+
do {
82+
var configuration = Configuration()
83+
configuration.readonly = true
84+
return try DatabasePool(path: databaseURL.path, configuration: configuration)
85+
} catch {
86+
if FileManager.default.fileExists(atPath: databaseURL.path) {
87+
throw error
88+
} else {
89+
return nil
90+
}
91+
}
92+
}
93+
```
94+
95+
96+
## How to limit the `SQLITE_BUSY` error
97+
98+
> The SQLITE_BUSY result code indicates that the database file could not be written (or in some cases read) because of concurrent activity by some other database connection, usually a database connection in a separate process.
99+
100+
See https://www.sqlite.org/rescode.html#busy for more information about this error.
101+
102+
If several processes want to write in the database, configure the database pool of each process that wants to write:
103+
104+
```swift
105+
var configuration = Configuration()
106+
configuration.busyMode = .timeout(/* a TimeInterval */)
107+
configuration.defaultTransactionKind = .immediate
108+
let dbPool = try DatabasePool(path: ..., configuration: configuration)
109+
```
110+
111+
With such a setup, you may still get `SQLITE_BUSY` (5, "database is locked") errors from all write operations. They will occur if the database remains locked by another process for longer than the specified timeout.
112+
113+
```swift
114+
do {
115+
try dbPool.write { db in ... }
116+
} catch let error as DatabaseError where error.resultCode == .SQLITE_BUSY {
117+
// Another process won't let you write. Deal with it.
118+
}
119+
```
120+
121+
> :bulb: **Tip**: In order to be nice to other processes, measure the duration of your longest writes, and attempt at optimizing the ones that last for too long.
122+
123+
124+
## How to limit the `0xDEAD10CC` exception
125+
126+
> The exception code 0xDEAD10CC indicates that an application has been terminated by the OS because it held on to a file lock or sqlite database lock during suspension.
127+
128+
See https://developer.apple.com/library/archive/technotes/tn2151/_index.html for more information about this exception.
129+
130+
1. If you use SQLCipher, use SQLCipher 4+, and call the `cipher_plaintext_header_size` pragma from your database preparation function:
131+
132+
```swift
133+
var configuration = Configuration()
134+
configuration.prepareDatabase = { (db: Database) in
135+
try db.usePassphrase("secret")
136+
try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
137+
}
138+
let dbPool = try DatabasePool(path: ..., configuration: configuration)
139+
```
140+
141+
This will avoid https://github.com/sqlcipher/sqlcipher/issues/255.
142+
143+
2. [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features) In each process that wants to write in the database:
144+
145+
Set the `observesSuspensionNotifications` configuration flag:
146+
147+
```swift
148+
var configuration = Configuration()
149+
configuration.suspendsOnBackgroundTimeExpiration = true
150+
let dbPool = try DatabasePool(path: ..., configuration: configuration)
151+
```
152+
153+
Post `Database.suspendNotification` when the application is about to be [suspended](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle). You can for example post this notification from `UIApplicationDelegate.applicationDidEnterBackground(_:)`, or in the expiration handler of a [background task](https://forums.developer.apple.com/thread/85066).
154+
155+
```swift
156+
@UIApplicationMain
157+
class AppDelegate: UIResponder, UIApplicationDelegate {
158+
func applicationDidEnterBackground(_ application: UIApplication) {
159+
// Suspend databases
160+
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
161+
}
162+
}
163+
```
164+
165+
Post `Database.resumeNotification` from `UIApplicationDelegate.applicationWillEnterForeground(_:)` (or `SceneDelegate.sceneWillEnterForeground(_:)` for scene-based applications):
166+
167+
```swift
168+
@UIApplicationMain
169+
class AppDelegate: UIResponder, UIApplicationDelegate {
170+
func applicationWillEnterForeground(_ application: UIApplication) {
171+
// Resume databases
172+
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
173+
}
174+
}
175+
```
176+
177+
If the application uses the background modes supported by iOS, post `Database.resumeNotification` method from each and every background mode callback that may use the database. For example, if your application supports background fetches:
178+
179+
```swift
180+
@UIApplicationMain
181+
class AppDelegate: UIResponder, UIApplicationDelegate {
182+
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
183+
// Resume databases
184+
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
185+
// Proceed with background fetch
186+
...
187+
}
188+
}
189+
```
190+
191+
Suspended databases greatly reduce the odds of `0xDEAD10CC` exception are greatly reduced. If you see one in your crash logs, please open an issue!
192+
193+
In exchange, you will get `SQLITE_INTERRUPT` (9) or `SQLITE_ABORT` (4) errors, with messages "Database is suspended", "Transaction was aborted", or "interrupted", for any attempt at writing in the database when it is suspended.
194+
195+
You can catch those errors:
196+
197+
```swift
198+
do {
199+
try dbPool.write { db in ... }
200+
} catch let error as DatabaseError where error.isInterruptionError {
201+
// Oops, the database is suspended.
202+
// Maybe try again after database is resumed?
203+
}
204+
```
205+
206+
207+
## How to perform cross-process database observation
208+
209+
GRDB [Database Observation] features, as well as [GRDBCombine] and [RxGRDB], are not able to notify database changes performed by other processes.
210+
211+
Whenever you need to notify other processes that the database has been changed, you will have to use a cross-process notification mechanism such as [NSFileCoordinator] or [CFNotificationCenterGetDarwinNotifyCenter].
212+
213+
You can trigger those notifications automatically with [DatabaseRegionObservation]:
214+
215+
```swift
216+
// Notify all changes made to the "player" and "team" database tables
217+
let observation = DatabaseRegionObservation(tracking: Player.all(), Team.all())
218+
let observer = try observation.start(in: dbPool) { (db: Database) in
219+
// Notify other processes
220+
}
221+
222+
// Notify all changes made to the database
223+
let observation = DatabaseRegionObservation(tracking: DatabaseRegion.fullDatabase)
224+
let observer = try observation.start(in: dbPool) { (db: Database) in
225+
// Notify other processes
226+
}
227+
```
228+
229+
[Use a Database Pool]: #use-a-database-pool
230+
[How to limit the `SQLITE_BUSY` error]: #how-to-limit-the-sqlite_busy-error
231+
[How to limit the `0xDEAD10CC` exception]: #how-to-limit-the-0xdead10cc-exception
232+
[How to perform cross-process database observation]: #how-to-perform-cross-process-database-observation
233+
[Database Pool]: ../README.md#database-pools
234+
[Database Observation]: ../README.md#database-changes-observation
235+
[GRDBCombine]: http://github.com/groue/GRDBCombine
236+
[RxGRDB]: https://github.com/RxSwiftCommunity/RxGRDB
237+
[NSFileCoordinator]: https://developer.apple.com/documentation/foundation/nsfilecoordinator
238+
[CFNotificationCenterGetDarwinNotifyCenter]: https://developer.apple.com/documentation/corefoundation/1542572-cfnotificationcentergetdarwinnot
239+
[DatabaseRegionObservation]: ../README.md#databaseregionobservation

Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1030"
3+
LastUpgradeVersion = "1130"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"
@@ -55,8 +55,6 @@
5555
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
5656
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
5757
shouldUseLaunchSchemeArgsEnv = "YES">
58-
<Testables>
59-
</Testables>
6058
<MacroExpansion>
6159
<BuildableReference
6260
BuildableIdentifier = "primary"
@@ -66,8 +64,8 @@
6664
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
6765
</BuildableReference>
6866
</MacroExpansion>
69-
<AdditionalOptions>
70-
</AdditionalOptions>
67+
<Testables>
68+
</Testables>
7169
</TestAction>
7270
<LaunchAction
7371
buildConfiguration = "Debug"
@@ -91,17 +89,6 @@
9189
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
9290
</BuildableReference>
9391
</RemoteRunnable>
94-
<MacroExpansion>
95-
<BuildableReference
96-
BuildableIdentifier = "primary"
97-
BlueprintIdentifier = "568E5FBE1E926430002582E0"
98-
BuildableName = "GRDBDemoWatchOS.app"
99-
BlueprintName = "GRDBDemoWatchOS"
100-
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
101-
</BuildableReference>
102-
</MacroExpansion>
103-
<AdditionalOptions>
104-
</AdditionalOptions>
10592
</LaunchAction>
10693
<ProfileAction
10794
buildConfiguration = "Release"

Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoiOS.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1030"
3+
LastUpgradeVersion = "1130"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

GRDB.swift.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'GRDB.swift'
3-
s.version = '4.6.2'
3+
s.version = '4.7.0'
44

55
s.license = { :type => 'MIT', :file => 'LICENSE' }
66
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'

0 commit comments

Comments
 (0)