Skip to content

Commit c49ed6d

Browse files
authored
Merge branch 'groue:master' into sqlcipher
2 parents 463f5df + 04e73c2 commit c49ed6d

13 files changed

+264
-41
lines changed

CHANGELOG.md

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

88
#### 7.x Releases
99

10+
- `7.4.x` Releases - [7.4.0](#740) - [7.4.1](#741)
1011
- `7.3.x` Releases - [7.3.0](#730)
1112
- `7.2.x` Releases - [7.2.0](#720)
1213
- `7.1.x` Releases - [7.1.0](#710)
@@ -135,6 +136,19 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
135136

136137
---
137138

139+
## 7.4.1
140+
141+
Released March 28, 2025
142+
143+
- Removed a compiler warning
144+
145+
## 7.4.0
146+
147+
Released March 22, 2025
148+
149+
- **New**: Add the MIN and MAX multi-argument SQL functions to the query interface by [@groue](https://github.com/groue) in [#1745](https://github.com/groue/GRDB.swift/pull/1745)
150+
- **Fixed**: Transaction observers are not impacted by Task Cancellation by [@groue](https://github.com/groue) in [#1747](https://github.com/groue/GRDB.swift/pull/1747). This fixes unexpected `ValueObservation` failures in applications that perform asynchronous writes.
151+
138152
## 7.3.0
139153

140154
Released February 23, 2025

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 = '7.3.0'
3+
s.version = '7.4.1'
44

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

GRDB/Core/Database+Statements.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -494,11 +494,9 @@ extension Database {
494494

495495
switch ResultCode(rawValue: resultCode) {
496496
case .SQLITE_INTERRUPT, .SQLITE_ABORT:
497-
if suspensionMutex.load().isCancelled {
498-
// The only error that a user sees when a Task is cancelled
499-
// is CancellationError.
500-
throw CancellationError()
501-
}
497+
// The only error that a user sees when a Task is cancelled
498+
// is CancellationError.
499+
try suspensionMutex.load().checkCancellation()
502500
default:
503501
break
504502
}

GRDB/Core/Database.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,10 +341,22 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
341341

342342
/// If true, the database access has been cancelled.
343343
var isCancelled: Bool
344+
345+
/// If true, the database throws an error when it is cancelled.
346+
var interruptsWhenCancelled: Bool
347+
348+
func checkCancellation() throws {
349+
if isCancelled, interruptsWhenCancelled {
350+
throw CancellationError()
351+
}
352+
}
344353
}
345354

346355
/// Support for `checkForSuspensionViolation(from:)`
347-
let suspensionMutex = Mutex(Suspension(isSuspended: false, isCancelled: false))
356+
let suspensionMutex = Mutex(Suspension(
357+
isSuspended: false,
358+
isCancelled: false,
359+
interruptsWhenCancelled: true))
348360

349361
// MARK: - Transaction Date
350362

@@ -1222,7 +1234,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
12221234
}
12231235

12241236
suspension.isCancelled = true
1225-
return true
1237+
return suspension.interruptsWhenCancelled
12261238
}
12271239

12281240
if needsInterrupt {
@@ -1237,6 +1249,24 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
12371249
}
12381250
}
12391251

1252+
/// Within the given closure, Task cancellation does not interrupt
1253+
/// database accesses.
1254+
func ignoringCancellation<T>(_ value: () throws -> T) rethrows -> T {
1255+
let previous = suspensionMutex.withLock {
1256+
let previous = $0.interruptsWhenCancelled
1257+
$0.interruptsWhenCancelled = false
1258+
return previous
1259+
}
1260+
1261+
defer {
1262+
suspensionMutex.withLock {
1263+
$0.interruptsWhenCancelled = previous
1264+
}
1265+
}
1266+
1267+
return try value()
1268+
}
1269+
12401270
/// Support for `checkForSuspensionViolation(from:)`
12411271
private func journalMode() throws -> String {
12421272
if let journalMode = journalModeCache {
@@ -1303,7 +1333,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
13031333
let interrupt: Interrupt? = try suspensionMutex.withLock { suspension in
13041334
// Check for cancellation first, so that the only error that
13051335
// a user sees when a Task is cancelled is CancellationError.
1306-
if suspension.isCancelled {
1336+
if suspension.isCancelled, suspension.interruptsWhenCancelled {
13071337
return .cancel
13081338
}
13091339

GRDB/Core/TransactionObserver.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,8 +549,12 @@ class DatabaseObservationBroker {
549549
savepointStack.clear()
550550

551551
if !database.isReadOnly {
552-
for observation in transactionObservations {
553-
observation.databaseDidCommit(database)
552+
// Observers must be able to access the database, even if the
553+
// task that has performed the commit is cancelled.
554+
database.ignoringCancellation {
555+
for observation in transactionObservations {
556+
observation.databaseDidCommit(database)
557+
}
554558
}
555559
}
556560

@@ -613,8 +617,13 @@ class DatabaseObservationBroker {
613617

614618
if notifyTransactionObservers {
615619
assert(!database.isReadOnly, "Read-only transactions are not notified")
616-
for observation in transactionObservations {
617-
observation.databaseDidRollback(database)
620+
621+
// Observers must be able to access the database, even if the
622+
// task that has performed the commit is cancelled.
623+
database.ignoringCancellation {
624+
for observation in transactionObservations {
625+
observation.databaseDidRollback(database)
626+
}
618627
}
619628
}
620629
databaseDidEndTransaction()

GRDB/Documentation.docc/Migrations.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,26 @@ migrator.registerMigration("Create authors") { db in
9494
}
9595
```
9696

97+
#### How to Rename a Foreign Key
98+
99+
When a migration **renames a foreign key**, make sure the migration runs with `.immediate` foreign key checks, in order to avoid database integrity problems:
100+
101+
```swift
102+
// IMPORTANT: rename foreign keys with immediate foreign key checks.
103+
migrator.registerMigration("Guilds", foreignKeyChecks: .immediate) { db in
104+
try db.rename(table: "team", to: "guild")
105+
106+
try db.alter(table: "player") { t in
107+
// Rename a foreign key
108+
t.rename(column: "teamId", to: "guildId")
109+
}
110+
}
111+
```
112+
113+
Note: migrations that run with `.immediate` foreign key checks can not be used to recreated database tables, as described below. When needed, define two migrations instead of one.
114+
115+
#### How to Recreate a Database Table
116+
97117
When you need to modify a table in a way that is not directly supported by SQLite, or not available on your target operating system, you will need to recreate the database table.
98118

99119
For example:
@@ -113,19 +133,39 @@ migrator.registerMigration("Add NOT NULL check on author.name") { db in
113133

114134
The detailed sequence of operations for recreating a database table from a migration is:
115135

116-
1. When relevant, remember the format of all indexes, triggers, and views associated with table X. This information will be needed in steps 6 and 7 below. One way to do this is to run a query like the following: `SELECT type, sql FROM sqlite_schema WHERE tbl_name='X'`.
136+
1. When relevant, remember the format of all indexes, triggers, and views associated with table `X`. This information will be needed in steps 6 below. One way to do this is to run the following statement and examine the output in the console:
117137

118-
2. Use `CREATE TABLE` to construct a new table "new_X" that is in the desired revised format of table X. Make sure that the name "new_X" does not collide with any existing table name, of course.
138+
```swift
139+
try db.dumpSQL("SELECT type, sql FROM sqlite_schema WHERE tbl_name='X'")
140+
```
119141

120-
3. Transfer content from X into new_X using a statement like: `INSERT INTO new_X SELECT ... FROM X`.
142+
2. Construct a new table `new_X` that is in the desired revised format of table `X`. Make sure that the name `new_X` does not collide with any existing table name, of course.
121143

122-
4. Drop the old table X: `DROP TABLE X`.
144+
```swift
145+
try db.create(table: "new_X") { t in ... }
146+
```
147+
148+
3. Transfer content from `X` into `new_X` using a statement like:
149+
150+
```swift
151+
try db.execute(sql: "INSERT INTO new_X SELECT ... FROM X")
152+
```
153+
154+
4. Drop the old table `X`:
155+
156+
```swift
157+
try db.drop(table: "X")
158+
```
123159

124-
5. Change the name of new_X to X using: `ALTER TABLE new_X RENAME TO X`.
160+
5. Change the name of `new_X` to `X` using:
161+
162+
```swift
163+
try db.rename(table: "new_X", to: "X")
164+
```
125165

126-
6. When relevant, use `CREATE INDEX`, `CREATE TRIGGER`, and `CREATE VIEW` to reconstruct indexes, triggers, and views associated with table X. Perhaps use the old format of the triggers, indexes, and views saved from step 3 above as a guide, making changes as appropriate for the alteration.
166+
6. When relevant, reconstruct indexes, triggers, and views associated with table `X`.
127167

128-
7. If any views refer to table X in a way that is affected by the schema change, then drop those views using `DROP VIEW` and recreate them with whatever changes are necessary to accommodate the schema change using `CREATE VIEW`.
168+
7. If any views refer to table `X` in a way that is affected by the schema change, then drop those views using `DROP VIEW` and recreate them with whatever changes are necessary to accommodate the schema change using `CREATE VIEW`.
129169

130170
> Important: When recreating a table, be sure to follow the above procedure exactly, in the given order, or you might corrupt triggers, views, and foreign key constraints.
131171
>

GRDB/QueryInterface/SQL/SQLExpression.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2209,8 +2209,10 @@ extension SQLExpressible where Self == Column {
22092209
/// - ``localizedLowercased``
22102210
/// - ``localizedUppercased``
22112211
/// - ``lowercased``
2212+
/// - ``min(_:_:_:)``
22122213
/// - ``min(_:)``
22132214
/// - ``min(_:filter:)``
2215+
/// - ``max(_:_:_:)``
22142216
/// - ``max(_:)``
22152217
/// - ``max(_:filter:)``
22162218
/// - ``sum(_:)``

0 commit comments

Comments
 (0)