Skip to content

Commit 27c7bda

Browse files
committed
Refactor with DatabaseEventObservationStrategy
The issue with `observesAllDatabaseChanges` is that this was a misleading name: transaction observers are not notified of changes that are performed in a save point that is eventually rollbacked. By exposing the DatabaseEventObservationStrategy type, we allow for a future extra flag that ignores save points, trading observation precision for a reduced memory footprint.
1 parent 253cdfe commit 27c7bda

5 files changed

Lines changed: 102 additions & 34 deletions

File tree

GRDB/Core/TransactionObserver.swift

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ class DatabaseObservationBroker {
297297
}
298298
}
299299

300+
/// Notifies that some changes were performed in the provided
301+
/// database region.
302+
///
303+
/// Support for the public ``Database/notifyChanges(in:)`` method.
300304
func notifyChanges(withEventsOfKind eventKinds: [DatabaseEventKind]) throws {
301305
// Support for stopObservingDatabaseChangesUntilNextTransaction()
302306
SchedulingWatchdog.current!.databaseObservationBroker = self
@@ -338,7 +342,7 @@ class DatabaseObservationBroker {
338342
// Those statementObservations will be notified of individual changes
339343
// in databaseWillChange() and databaseDidChange().
340344
statementObservations = transactionObservations.compactMap { observation in
341-
observation.observations(for: statement)
345+
observation.statementObservation(for: statement)
342346
}
343347
}
344348

@@ -747,16 +751,78 @@ class DatabaseObservationBroker {
747751
}
748752
}
749753

754+
// MARK: - DatabaseEventObservationStrategy
755+
756+
/// Controls which database changes are notified to a `TransactionObserver`.
757+
public struct DatabaseEventObservationStrategy: Sendable {
758+
/// A boolean value indicating whether a ``TransactionObserver`` focuses
759+
/// on database changes filtered by its
760+
/// ``TransactionObserver/observes(eventsOfKind:)`` method.
761+
///
762+
/// When this flag is true (the default), the only notified changes are
763+
/// those performed by `DELETE`, `INSERT` and `UPDATE` statements that
764+
/// are compiled and executed by GRDB. Other changes are not: changes
765+
/// performed by `SELECT` statements (through a database function),
766+
/// changes performed by statements compiled or executed with the SQLite
767+
/// C API.
768+
///
769+
/// Set this flag to false in order to be notified of all database events.
770+
///
771+
/// When this flag is false, an observer prevents the
772+
/// [truncate optimization](https://www.sqlite.org/lang_delete.html#the_truncate_optimization)
773+
/// from being applied on all database tables.
774+
public var requiresDatabaseEventKind: Bool
775+
776+
/// The default strategy for observing database change events.
777+
///
778+
/// In this default strategy, only the `DELETE`, `INSERT` and `UPDATE`
779+
/// statements that are compiled and executed by GRDB are observed.
780+
public static let `default` = DatabaseEventObservationStrategy(requiresDatabaseEventKind: true)
781+
}
782+
750783
// MARK: - TransactionObserver
751784

752785
public protocol TransactionObserver: AnyObject {
753786

787+
/// Controls which database changes should be notified to the observer.
788+
///
789+
/// The default value is ``DatabaseEventObservationStrategy/default``,
790+
/// which only detects changes performed by `DELETE`, `INSERT` and
791+
/// `UPDATE` statements that are compiled and executed by GRDB.
792+
///
793+
/// You can define a universal observer that observes changes performed
794+
/// by `SELECT` statements (through a database function), changes
795+
/// performed by statements compiled or executed with the SQLite C API,
796+
/// as below:
797+
///
798+
/// ```swift
799+
/// // An observer that observes all database changes.
800+
/// class UniversalObserver: TransactionObserver {
801+
/// var databaseEventObservationStrategy: DatabaseEventObservationStrategy {
802+
/// var strategy = DatabaseEventObservationStrategy.default
803+
/// // Don't filter on database event kind, so that we are
804+
/// // notified of changes performed through the SQLite C API:
805+
/// strategy.requiresDatabaseEventKind = false
806+
/// return strategy
807+
/// }
808+
///
809+
/// // You still have to provide an implementation for this method,
810+
/// // but it will never be called since `requiresDatabaseEventKind`
811+
/// // is false.
812+
/// func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
813+
/// false // ignored
814+
/// }
815+
///
816+
/// func databaseDidChange(with event: DatabaseEvent) {
817+
/// // Handle the change
818+
/// }
819+
/// }
820+
/// ```
821+
var databaseEventObservationStrategy: DatabaseEventObservationStrategy { get }
822+
754823
/// Returns whether specific kinds of database changes should be notified
755824
/// to the observer.
756825
///
757-
/// When this method and ``observes(statement:)`` return false, database
758-
/// events are not notified to the ``databaseDidChange(with:)`` method.
759-
///
760826
/// For example:
761827
///
762828
/// ```swift
@@ -772,16 +838,12 @@ public protocol TransactionObserver: AnyObject {
772838
/// prevents the
773839
/// [truncate optimization](https://www.sqlite.org/lang_delete.html#the_truncate_optimization)
774840
/// from being applied on the observed tables.
775-
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool
776-
777-
/// Returns whether this observer should unconditionally be notified for all database changes, regardless of
778-
/// ``observes(eventsOfKind:)``.
779841
///
780-
/// This method can be used to observe indirect writes from statements (e.g. a statement invoking
781-
/// a custom SQL function which internally runs its own statements). ``observes(eventsOfKind:)``
782-
/// would not be called for such statements because potential writes can't be inferred from the syntax of
783-
/// these statements.
784-
var observesAllDatabaseChanges: Bool { get }
842+
/// - Note: This method is not called when the result of
843+
/// ``databaseEventObservationStrategy`` has the
844+
/// ``DatabaseEventObservationStrategy/requiresDatabaseEventKind``
845+
/// flag set to false.
846+
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool
785847

786848
/// Called when the database was modified in some unspecified way.
787849
///
@@ -796,8 +858,12 @@ public protocol TransactionObserver: AnyObject {
796858
/// Called when the database is changed by an insert, update, or
797859
/// delete event.
798860
///
799-
/// The change is pending until the current transaction ends. See
800-
/// ``databaseWillCommit()-7mksu``, ``databaseDidCommit(_:)`` and
861+
/// Whether this method is called or not for any given change is
862+
/// controlled by ``databaseEventObservationStrategy`` and
863+
/// ``observes(eventsOfKind:)``.
864+
///
865+
/// The notified change is pending until the current transaction ends.
866+
/// See ``databaseWillCommit()-7mksu``, ``databaseDidCommit(_:)`` and
801867
/// ``databaseDidRollback(_:)``.
802868
///
803869
/// The observer has an opportunity to stop receiving further change events
@@ -861,10 +927,10 @@ public protocol TransactionObserver: AnyObject {
861927

862928
extension TransactionObserver {
863929
/// The default implementation does not observe statements.
864-
public var observesAllDatabaseChanges: Bool {
865-
false
930+
public var databaseEventObservationStrategy: DatabaseEventObservationStrategy {
931+
.default
866932
}
867-
933+
868934
/// The default implementation does nothing.
869935
public func databaseWillCommit() throws { }
870936

@@ -962,16 +1028,15 @@ final class TransactionObservation {
9621028
guard let observer else {
9631029
return false
9641030
}
965-
return observer.observesAllDatabaseChanges || observer
966-
.observes(eventsOfKind: eventKind)
1031+
return !observer.databaseEventObservationStrategy.requiresDatabaseEventKind || observer.observes(eventsOfKind: eventKind)
9671032
}
9681033

969-
func observations(for statement: Statement) -> StatementObservation? {
1034+
func statementObservation(for statement: Statement) -> StatementObservation? {
9701035
guard let observer else {
9711036
return nil
9721037
}
9731038

974-
if observer.observesAllDatabaseChanges {
1039+
if !observer.databaseEventObservationStrategy.requiresDatabaseEventKind {
9751040
return StatementObservation(
9761041
transactionObservation: self,
9771042
trackingEvents: .all)

GRDB/Documentation.docc/Extension/DatabaseRegionObservation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ let observation = DatabaseRegionObservation(
7979
`DatabaseRegionObservation` will not notify impactful transactions whenever the database is modified in an undetectable way:
8080

8181
- Changes performed by external database connections.
82-
- Changes performed by SQLite statements that are not compiled and executed by GRDB, including statements ran from a custom SQL function that is itself invoked with GRDB APIs.
82+
- Changes performed by SQLite statements that are not a `DELETE`, `INSERT` or `UPDATE` statement compiled and executed by GRDB.
8383
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
8484
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
8585

GRDB/Documentation.docc/Extension/TransactionObserver.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ do {
9797

9898
**Transaction observers can choose the database changes they are interested in.**
9999

100-
The ``observes(eventsOfKind:)`` method filters events that are notified to ``databaseDidChange(with:)``. It is the most efficient and recommended change filtering technique, because it is only called once before a database query is executed, and can completely disable change tracking:
100+
By default, the ``observes(eventsOfKind:)`` method filters events that are notified to ``databaseDidChange(with:)``. It is the most efficient and recommended change filtering technique, because it is only called once before a database query is executed, and can completely disable change tracking:
101101

102102
```swift
103103
// Calls `observes(eventsOfKind:)` once.
@@ -125,8 +125,7 @@ class PlayerObserver: TransactionObserver {
125125

126126
When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of transactions.
127127

128-
In addition to ``observes(eventsOfKind:)``, transaction observers can implement the `observesAllDatabaseChanges` getter. When set to `true`, the observer will
129-
observe all changes to the database.
128+
The filtering performed by ``observes(eventsOfKind:)`` makes a transaction observer unaware of changes performed by SQLite statements that are not a `DELETE`, `INSERT` or `UPDATE` statement compiled and executed by GRDB. You can lift this limitation with the ``TransactionObserver/databaseEventObservationStrategy``.
130129

131130
## Observation Extent
132131

@@ -235,7 +234,7 @@ The changes and transactions that are not automatically notified to transaction
235234

236235
- Read-only transactions.
237236
- Changes and transactions performed by external database connections.
238-
- Changes performed by SQLite statements that are not both compiled and executed through GRDB APIs, including statements ran from a custom SQL function that is itself invoked with GRDB APIs.
237+
- Changes performed by SQLite statements that are not a `DELETE`, `INSERT` or `UPDATE` statement compiled and executed by GRDB (this limitation can be lifted, in your custom `TransactionObserver` type, with ``TransactionObserver/databaseEventObservationStrategy``).
239238
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
240239
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
241240
- 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).
@@ -268,8 +267,10 @@ try dbQueue.write { db in
268267

269268
### Filtering Database Changes
270269

270+
- ``databaseEventObservationStrategy``
271271
- ``observes(eventsOfKind:)``
272272
- ``DatabaseEventKind``
273+
- ``DatabaseEventObservationStrategy``
273274

274275
### Handling Database Changes
275276

GRDB/Documentation.docc/Extension/ValueObservation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ This ``tracking(region:_:fetch:)`` method lets you entirely separate the **obser
228228
`ValueObservation` will not fetch and notify a fresh value whenever the database is modified in an undetectable way:
229229

230230
- Changes performed by external database connections.
231-
- Changes performed by SQLite statements that are not compiled and executed by GRDB, including statements ran from a custom SQL function that is itself invoked with GRDB APIs.
231+
- Changes performed by SQLite statements that are not a `DELETE`, `INSERT` or `UPDATE` statement compiled and executed by GRDB.
232232
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
233233
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
234234

Tests/GRDBTests/TransactionObserverObserveEverythingTest.swift renamed to Tests/GRDBTests/DatabaseEventObservationStrategyTest.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ import SQLite3
55
private class EverythingObserver: TransactionObserver {
66
var changedTables = Set<String>()
77

8+
var databaseEventObservationStrategy: DatabaseEventObservationStrategy {
9+
var strategy = DatabaseEventObservationStrategy.default
10+
strategy.requiresDatabaseEventKind = false
11+
return strategy
12+
}
13+
814
func databaseDidChange(with event: GRDB.DatabaseEvent) {
915
changedTables.insert(event.tableName)
1016
}
1117

12-
var observesAllDatabaseChanges: Bool {
13-
true
14-
}
15-
1618
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
17-
fatalError("Should not be called since observesAllDatbaaseChanges is true")
19+
fatalError("Should not be called since requiresDatabaseEventKind is false")
1820
}
1921

2022
func databaseDidCommit(_ db: GRDB.Database) {}
@@ -26,7 +28,7 @@ private struct SendablePtr: @unchecked Sendable {
2628
let ptr: OpaquePointer
2729
}
2830

29-
class TransactionObserverObserveEverythingTests: GRDBTestCase {
31+
class DatabaseEventObservationStrategyTest: GRDBTestCase {
3032
func testIndirectWrite() throws {
3133
var config = Configuration()
3234
config.prepareDatabase{ database in

0 commit comments

Comments
 (0)