Skip to content

Commit 752415b

Browse files
authored
Merge pull request groue#1213 from groue/dev/fix-1209
Fix crash when the number of active ValueObservations is high
2 parents f0fe7e8 + f17a786 commit 752415b

File tree

7 files changed

+237
-29
lines changed

7 files changed

+237
-29
lines changed

CHANGELOG.md

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

9191
---
9292

93+
## Next Release
94+
95+
- **Fixed**: [#1213](https://github.com/groue/GRDB.swift/pull/1213) by [@groue](https://github.com/groue): Fix crash when the number of active ValueObservations is high
96+
- **Breaking Change**: Transactions performed during a read-only database access are no longer notified to transaction observers. This is, strictly speaking, a breaking change. However it should have no impact since read-only transactions have very little interest.
97+
9398
## 5.23.0
9499

95100
Released April 16, 2022 • [diff](https://github.com/groue/GRDB.swift/compare/v5.22.2...v5.23.0)

GRDB/Core/Database.swift

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
233233
setupDefaultFunctions()
234234
setupDefaultCollations()
235235
setupAuthorizer()
236-
observationBroker.installCommitAndRollbackHooks()
236+
if !configuration.readonly {
237+
observationBroker.installCommitAndRollbackHooks()
238+
}
237239
try activateExtendedCodes()
238240

239241
#if SQLITE_HAS_CODEC
@@ -546,6 +548,11 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
546548
finally: endReadOnly)
547549
}
548550

551+
/// Returns whether database connection is read-only.
552+
var isReadOnly: Bool {
553+
_readOnlyDepth > 0 || configuration.readonly
554+
}
555+
549556
// MARK: - Snapshots
550557

551558
#if SQLITE_ENABLE_SNAPSHOT
@@ -939,10 +946,16 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
939946
/// This method is not reentrant: you can't nest transactions.
940947
///
941948
/// - parameters:
942-
/// - kind: The transaction type (default nil). If nil, the transaction
943-
/// type is configuration.defaultTransactionKind, which itself
944-
/// defaults to .deferred. See <https://www.sqlite.org/lang_transaction.html>
945-
/// for more information.
949+
/// - kind: The transaction type (default nil).
950+
///
951+
/// If nil, and the database connection is read-only, the transaction
952+
/// kind is `.deferred`.
953+
///
954+
/// If nil, and the database connection is not read-only, the
955+
/// transaction kind is `configuration.defaultTransactionKind`.
956+
///
957+
/// See <https://www.sqlite.org/lang_transaction.html> for
958+
/// more information.
946959
/// - block: A block that executes SQL statements and return either
947960
/// .commit or .rollback.
948961
/// - throws: The error thrown by the block.
@@ -1005,13 +1018,20 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
10051018
/// - parameter readOnly: If true, writes are forbidden.
10061019
func isolated<T>(readOnly: Bool = false, _ block: () throws -> T) throws -> T {
10071020
var result: T?
1008-
try inSavepoint {
1009-
if readOnly {
1010-
result = try self.readOnly(block)
1011-
} else {
1021+
if readOnly {
1022+
// Enter read-only mode before starting a transaction, so that the
1023+
// transaction commit does not trigger database observation.
1024+
try self.readOnly {
1025+
try inSavepoint {
1026+
result = try block()
1027+
return .commit
1028+
}
1029+
}
1030+
} else {
1031+
try inSavepoint {
10121032
result = try block()
1033+
return .commit
10131034
}
1014-
return .commit
10151035
}
10161036
return result!
10171037
}
@@ -1055,7 +1075,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
10551075
//
10561076
// For those two reasons, we open a transaction instead of a
10571077
// top-level savepoint.
1058-
try inTransaction(configuration.defaultTransactionKind, block)
1078+
try inTransaction { try block() }
10591079
return
10601080
}
10611081

@@ -1123,13 +1143,20 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
11231143

11241144
/// Begins a database transaction.
11251145
///
1126-
/// - parameter kind: The transaction type (default nil). If nil, the
1127-
/// transaction type is configuration.defaultTransactionKind, which itself
1128-
/// defaults to .deferred. See <https://www.sqlite.org/lang_transaction.html>
1129-
/// for more information.
1130-
/// - throws: The error thrown by the block.
1146+
/// - parameter kind: The transaction type (default nil).
1147+
///
1148+
/// If nil, and the database connection is read-only, the transaction kind
1149+
/// is `.deferred`.
1150+
///
1151+
/// If nil, and the database connection is not read-only, the transaction
1152+
/// kind is `configuration.defaultTransactionKind`.
1153+
///
1154+
/// See <https://www.sqlite.org/lang_transaction.html> for
1155+
/// more information.
1156+
/// - throws: A DatabaseError whenever an SQLite error occurs.
11311157
public func beginTransaction(_ kind: TransactionKind? = nil) throws {
1132-
let kind = kind ?? configuration.defaultTransactionKind
1158+
// SQLite throws an error for non-deferred transactions when read-only.
1159+
let kind = kind ?? (isReadOnly ? .deferred : configuration.defaultTransactionKind)
11331160
try execute(sql: "BEGIN \(kind.rawValue) TRANSACTION")
11341161
assert(sqlite3_get_autocommit(sqliteConnection) == 0)
11351162
}

GRDB/Core/TransactionObserver.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,7 @@ class DatabaseObservationBroker {
220220

221221
/// Prepares observation of changes that are about to be performed by the statement.
222222
func statementWillExecute(_ statement: Statement) {
223-
// If any observer observes row deletions, we'll have to disable
224-
if transactionObservations.isEmpty == false {
223+
if !database.isReadOnly && !transactionObservations.isEmpty {
225224
// As statement executes, it may trigger database changes that will
226225
// be notified to transaction observers. As a consequence, observers
227226
// may disable themselves with stopObservingDatabaseChangesUntilNextTransaction()
@@ -310,7 +309,7 @@ class DatabaseObservationBroker {
310309
// SQLITE_CONSTRAINT error)
311310
databaseDidRollback(notifyTransactionObservers: false)
312311
case .cancelledCommit(let error):
313-
databaseDidRollback(notifyTransactionObservers: true)
312+
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
314313
throw error
315314
default:
316315
break
@@ -382,7 +381,7 @@ class DatabaseObservationBroker {
382381
case .commit:
383382
databaseDidCommit()
384383
case .rollback:
385-
databaseDidRollback(notifyTransactionObservers: true)
384+
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
386385
default:
387386
break
388387
}
@@ -391,6 +390,8 @@ class DatabaseObservationBroker {
391390
#if SQLITE_ENABLE_PREUPDATE_HOOK
392391
// Called from sqlite3_preupdate_hook
393392
private func databaseWillChange(with event: DatabasePreUpdateEvent) {
393+
assert(!database.isReadOnly, "Read-only transactions are not notified")
394+
394395
if savepointStack.isEmpty {
395396
// Notify now
396397
for (observation, predicate) in statementObservations where predicate.evaluate(event) {
@@ -405,6 +406,8 @@ class DatabaseObservationBroker {
405406

406407
// Called from sqlite3_update_hook
407408
private func databaseDidChange(with event: DatabaseEvent) {
409+
assert(!database.isReadOnly, "Read-only transactions are not notified")
410+
408411
// We're about to call the databaseDidChange(with:) method of
409412
// transaction observers. In this method, observers may disable
410413
// themselves with stopObservingDatabaseChangesUntilNextTransaction()
@@ -430,18 +433,23 @@ class DatabaseObservationBroker {
430433
// Called from sqlite3_commit_hook and databaseDidCommitEmptyDeferredTransaction()
431434
private func databaseWillCommit() throws {
432435
notifyBufferedEvents()
433-
for observation in transactionObservations {
434-
try observation.databaseWillCommit()
436+
if !database.isReadOnly {
437+
for observation in transactionObservations {
438+
try observation.databaseWillCommit()
439+
}
435440
}
436441
}
437442

438443
// Called from statementDidExecute
439444
private func databaseDidCommit() {
440445
savepointStack.clear()
441446

442-
for observation in transactionObservations {
443-
observation.databaseDidCommit(database)
447+
if !database.isReadOnly {
448+
for observation in transactionObservations {
449+
observation.databaseDidCommit(database)
450+
}
444451
}
452+
445453
databaseDidEndTransaction()
446454
}
447455

@@ -485,7 +493,7 @@ class DatabaseObservationBroker {
485493
try databaseWillCommit()
486494
databaseDidCommit()
487495
} catch {
488-
databaseDidRollback(notifyTransactionObservers: true)
496+
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
489497
throw error
490498
}
491499
}
@@ -495,6 +503,7 @@ class DatabaseObservationBroker {
495503
savepointStack.clear()
496504

497505
if notifyTransactionObservers {
506+
assert(!database.isReadOnly, "Read-only transactions are not notified")
498507
for observation in transactionObservations {
499508
observation.databaseDidRollback(database)
500509
}
@@ -566,6 +575,7 @@ class DatabaseObservationBroker {
566575
savepointStack.clear()
567576

568577
for (event, statementObservations) in eventsBuffer {
578+
assert(statementObservations.isEmpty || !database.isReadOnly, "Read-only transactions are not notified")
569579
for (observation, predicate) in statementObservations where predicate.evaluate(event) {
570580
event.send(to: observation)
571581
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6683,6 +6683,8 @@ By default, database holds weak references to its transaction observers: they ar
66836683
**A transaction observer is notified of all database changes**: inserts, updates and deletes. This includes indirect changes triggered by ON DELETE and ON UPDATE actions associated to [foreign keys](https://www.sqlite.org/foreignkeys.html#fk_actions), and [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
66846684
66856685
> :point_up: **Note**: Some changes are not notified: changes to internal system tables (such as `sqlite_master`), changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables, and the deletion of duplicate rows triggered by [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) clauses (this last exception might change in a future release of SQLite).
6686+
>
6687+
> :point_up: **Note**: Transactions performed during read-only database accesses are not notified.
66866688
66876689
Notified changes are not actually written to disk until the [transaction](#transactions-and-savepoints) commits, and the `databaseDidCommit` callback is called. On the other side, `databaseDidRollback` confirms their invalidation:
66886690
@@ -6794,7 +6796,7 @@ class PlayerScoreObserver: TransactionObserver {
67946796
}
67956797
```
67966798
6797-
When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of commits and rollbacks:
6799+
When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of commits and rollbacks (except during read-only database accesses):
67986800
67996801
```swift
68006802
class PureTransactionObserver: TransactionObserver {

Tests/GRDBTests/DatabaseTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,36 @@ class DatabaseTests : GRDBTestCase {
566566
}
567567
}
568568

569+
func testReadOnlyTransaction() throws {
570+
// query_only pragma was added in SQLite 3.8.0 http://www.sqlite.org/changes.html#version_3_8_0
571+
guard sqlite3_libversion_number() >= 3008000 else {
572+
return
573+
}
574+
575+
dbConfiguration.defaultTransactionKind = .immediate
576+
let dbQueue = try makeDatabaseQueue()
577+
try dbQueue.inDatabase { db in
578+
do {
579+
sqlQueries.removeAll()
580+
try db.inSavepoint { .commit }
581+
try db.inTransaction { .commit }
582+
try db.inTransaction(.immediate) { .commit }
583+
XCTAssertEqual(Set(sqlQueries), ["BEGIN IMMEDIATE TRANSACTION", "COMMIT TRANSACTION"])
584+
}
585+
586+
try db.readOnly {
587+
sqlQueries.removeAll()
588+
try db.inSavepoint { .commit }
589+
try db.inTransaction { .commit }
590+
XCTAssertEqual(Set(sqlQueries), ["BEGIN DEFERRED TRANSACTION", "COMMIT TRANSACTION"])
591+
do {
592+
try db.inTransaction(.immediate) { .commit }
593+
XCTFail("Expected error")
594+
} catch DatabaseError.SQLITE_READONLY { }
595+
}
596+
}
597+
}
598+
569599
func testCheckpoint() throws {
570600
do {
571601
// Not a WAL database

Tests/GRDBTests/TransactionObserverTests.swift

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import XCTest
2-
import GRDB
2+
@testable import GRDB
33

44
private class Observer : TransactionObserver {
55
var lastCommittedEvents: [DatabaseEvent] = []
@@ -1784,4 +1784,89 @@ class TransactionObserverTests: GRDBTestCase {
17841784
}
17851785
}
17861786
}
1787+
1788+
// MARK: - Read-Only Connection
1789+
1790+
func testReadOnlyConnection() throws {
1791+
let dbQueue = try makeDatabaseQueue(filename: "database.sqlite")
1792+
try setupArtistDatabase(in: dbQueue)
1793+
1794+
dbConfiguration.readonly = true
1795+
let readOnlyQueue = try makeDatabaseQueue(filename: "database.sqlite")
1796+
1797+
let observer = Observer()
1798+
readOnlyQueue.add(transactionObserver: observer, extent: .databaseLifetime)
1799+
1800+
try readOnlyQueue.inDatabase { db in
1801+
do {
1802+
try db.execute(sql: """
1803+
BEGIN;
1804+
COMMIT;
1805+
""")
1806+
XCTAssertEqual(observer.didChangeCount, 0)
1807+
XCTAssertEqual(observer.willCommitCount, 0)
1808+
XCTAssertEqual(observer.didCommitCount, 0)
1809+
XCTAssertEqual(observer.didRollbackCount, 0)
1810+
#if SQLITE_ENABLE_PREUPDATE_HOOK
1811+
XCTAssertEqual(observer.willChangeCount, 0)
1812+
#endif
1813+
}
1814+
1815+
do {
1816+
try db.execute(sql: """
1817+
BEGIN;
1818+
SELECT * FROM artists;
1819+
COMMIT;
1820+
""")
1821+
XCTAssertEqual(observer.didChangeCount, 0)
1822+
XCTAssertEqual(observer.willCommitCount, 0)
1823+
XCTAssertEqual(observer.didCommitCount, 0)
1824+
XCTAssertEqual(observer.didRollbackCount, 0)
1825+
#if SQLITE_ENABLE_PREUPDATE_HOOK
1826+
XCTAssertEqual(observer.willChangeCount, 0)
1827+
#endif
1828+
}
1829+
}
1830+
}
1831+
1832+
func testReadOnlyBlock() throws {
1833+
let dbQueue = try makeDatabaseQueue()
1834+
try setupArtistDatabase(in: dbQueue)
1835+
1836+
let observer = Observer()
1837+
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
1838+
1839+
try dbQueue.inDatabase { db in
1840+
try db.readOnly {
1841+
do {
1842+
try db.execute(sql: """
1843+
BEGIN;
1844+
COMMIT;
1845+
""")
1846+
XCTAssertEqual(observer.didChangeCount, 0)
1847+
XCTAssertEqual(observer.willCommitCount, 0)
1848+
XCTAssertEqual(observer.didCommitCount, 0)
1849+
XCTAssertEqual(observer.didRollbackCount, 0)
1850+
#if SQLITE_ENABLE_PREUPDATE_HOOK
1851+
XCTAssertEqual(observer.willChangeCount, 0)
1852+
#endif
1853+
}
1854+
1855+
do {
1856+
try db.execute(sql: """
1857+
BEGIN;
1858+
SELECT * FROM artists;
1859+
COMMIT;
1860+
""")
1861+
XCTAssertEqual(observer.didChangeCount, 0)
1862+
XCTAssertEqual(observer.willCommitCount, 0)
1863+
XCTAssertEqual(observer.didCommitCount, 0)
1864+
XCTAssertEqual(observer.didRollbackCount, 0)
1865+
#if SQLITE_ENABLE_PREUPDATE_HOOK
1866+
XCTAssertEqual(observer.willChangeCount, 0)
1867+
#endif
1868+
}
1869+
}
1870+
}
1871+
}
17871872
}

0 commit comments

Comments
 (0)