-
-
Notifications
You must be signed in to change notification settings - Fork 744
/
Copy pathTransactionObserver.swift
1653 lines (1454 loc) · 64.3 KB
/
TransactionObserver.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Import C SQLite functions
#if GRDBCIPHER
import SQLCipher
#elseif SWIFT_PACKAGE
import GRDBSQLite
#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER
import SQLite3
#endif
extension Database {
// MARK: - Database Observation
/// Adds a transaction observer on the database connection, so that it
/// gets notified of database changes and transactions.
///
/// This method has no effect on read-only database connections.
///
/// For example:
///
/// ```swift
/// let myObserver = MyObserver()
/// try dbQueue.write { db in
/// db.add(transactionObserver: myObserver)
/// }
/// ```
///
/// - parameter transactionObserver: A transaction observer.
/// - parameter extent: The duration of the observation. The default is
/// the observer lifetime (observation lasts until observer
/// is deallocated).
public func add(
transactionObserver: some TransactionObserver,
extent: TransactionObservationExtent = .observerLifetime)
{
SchedulingWatchdog.preconditionValidQueue(self)
guard let observationBroker else { return }
// Drop cached statements that delete, because the addition of an
// observer may change the need for truncate optimization prevention.
publicStatementCache.removeAll { $0.canDeleteRows }
internalStatementCache.removeAll { $0.canDeleteRows }
observationBroker.add(transactionObserver: transactionObserver, extent: extent)
}
/// Removes a transaction observer from the database connection.
///
/// For example:
///
/// ```swift
/// let myObserver = MyObserver()
/// try dbQueue.write { db in
/// db.remove(transactionObserver: myObserver)
/// }
/// ```
public func remove(transactionObserver: some TransactionObserver) {
SchedulingWatchdog.preconditionValidQueue(self)
guard let observationBroker else { return }
// Drop cached statements that delete, because the removal of an
// observer may change the need for truncate optimization prevention.
publicStatementCache.removeAll { $0.canDeleteRows }
internalStatementCache.removeAll { $0.canDeleteRows }
observationBroker.remove(transactionObserver: transactionObserver)
}
/// Registers closures to be executed after the next or current
/// transaction completes.
///
/// This method helps synchronizing the database with other resources,
/// such as files, or system services.
///
/// In the example below, a `CLLocationManager` starts monitoring a
/// `CLRegion` if and only if it has successfully been stored in
/// the database:
///
/// ```swift
/// /// Inserts a region in the database, and start monitoring upon
/// /// successful insertion.
/// func startMonitoring(_ db: Database, region: CLRegion) throws {
/// // Make sure database is inside a transaction
/// try db.inSavepoint {
///
/// // Save the region in the database
/// try insert(...)
///
/// // Start monitoring if and only if the insertion is
/// // eventually committed to disk
/// db.afterNextTransaction { _ in
/// // locationManager prefers the main queue:
/// DispatchQueue.main.async {
/// locationManager.startMonitoring(for: region)
/// }
/// }
///
/// return .commit
/// }
/// }
/// ```
///
/// The method above won't trigger the location manager if the transaction
/// is eventually rollbacked (explicitly, or because of an error).
///
/// The `onCommit` and `onRollback` closures are executed in the writer
/// dispatch queue, serialized will all database updates.
///
/// - precondition: Database connection is not read-only.
/// - parameter onCommit: A closure executed on transaction commit.
/// - parameter onRollback: A closure executed on transaction rollback.
public func afterNextTransaction(
onCommit: @escaping @Sendable (Database) -> Void,
onRollback: @escaping @Sendable (Database) -> Void = { _ in })
{
class TransactionHandler: TransactionObserver {
let onCommit: @Sendable (Database) -> Void
let onRollback: @Sendable (Database) -> Void
init(
onCommit: @escaping @Sendable (Database) -> Void,
onRollback: @escaping @Sendable (Database) -> Void
) {
self.onCommit = onCommit
self.onRollback = onRollback
}
// Ignore changes
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { false }
func databaseDidChange(with event: DatabaseEvent) { }
func databaseDidCommit(_ db: Database) {
onCommit(db)
}
func databaseDidRollback(_ db: Database) {
onRollback(db)
}
}
// We don't notify read-only transactions to transaction observers
GRDBPrecondition(!isReadOnly, "Read-only transactions are not notified")
add(
transactionObserver: TransactionHandler(onCommit: onCommit, onRollback: onRollback),
extent: .nextTransaction)
}
/// The extent of the observation performed by a ``TransactionObserver``.
public enum TransactionObservationExtent: Sendable {
/// Observation lasts until observer is deallocated.
case observerLifetime
/// Observation lasts until the next transaction.
case nextTransaction
/// Observation lasts until the database is closed.
case databaseLifetime
}
}
// MARK: - DatabaseObservationBroker
/// This class provides support for transaction observers.
///
/// Let's have a detailed look at how a transaction observer is notified:
///
/// class MyObserver: TransactionObserver {
/// func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool
/// func databaseDidChange(with event: DatabaseEvent)
/// func databaseWillCommit() throws
/// func databaseDidCommit(_ db: Database)
/// func databaseDidRollback(_ db: Database)
/// }
///
/// First observer is added, and a transaction is started. At this point,
/// there's not much to say:
///
/// let observer = MyObserver()
/// dbQueue.add(transactionObserver: observer)
/// dbQueue.inDatabase { db in
/// try db.execute(sql: "BEGIN TRANSACTION")
///
/// Then a statement is executed:
///
/// try db.execute(sql: "INSERT INTO document ...")
///
/// The observation process starts when the statement is *compiled*:
/// sqlite3_set_authorizer tells that the statement performs insertion into the
/// `document` table. Generally speaking, statements may have many effects, by
/// the mean of foreign key actions and SQL triggers. SQLite takes care of
/// exposing all those effects to sqlite3_set_authorizer.
///
/// When the statement is *about to be executed*, the broker queries the
/// observer.observes(eventsOfKind:) method. If it returns true, the observer is
/// *activated*.
///
/// During the statement *execution*, SQLite tells that a row has been inserted
/// through sqlite3_update_hook: the broker calls the observer.databaseDidChange(with:)
/// method, if and only if the observer has been activated at the previous step.
///
/// Now a savepoint is started:
///
/// try db.execute(sql: "SAVEPOINT foo")
///
/// Statement compilation has sqlite3_set_authorizer tell that this statement
/// begins a "foo" savepoint.
///
/// After the statement *has been executed*, the broker knows that the SQLite
/// [savepoint stack](https://www.sqlite.org/lang_savepoint.html) contains the
/// "foo" savepoint.
///
/// Then another statement is executed:
///
/// try db.execute(sql: "INSERT INTO document ...")
///
/// This time, when the statement is *executed* and SQLite tells that a row has
/// been inserted, the broker buffers the change event instead of immediately
/// notifying the activated observers. That is because the savepoint can be
/// rollbacked, and GRDB guarantees observers that they are only notified of
/// changes that have an opportunity to be committed.
///
/// The savepoint is released:
///
/// try db.execute(sql: "RELEASE SAVEPOINT foo")
///
/// Statement compilation has sqlite3_set_authorizer tell that this statement
/// releases the "foo" savepoint.
///
/// After the statement *has been executed*, the broker knows that the SQLite
/// [savepoint stack](https://www.sqlite.org/lang_savepoint.html) is now empty,
/// and notifies the buffered changes to activated observers.
///
/// Finally the transaction is committed:
///
/// try db.execute(sql: "COMMIT")
///
/// During the statement *execution*, SQLite tells the broker that the
/// transaction is about to be committed through sqlite3_commit_hook. The broker
/// invokes observer.databaseWillCommit(). If the observer throws an error, the
/// broker asks SQLite to rollback the transaction. Otherwise, the broker lets
/// the transaction complete.
///
/// After the statement *has been executed*, the broker calls
/// observer.databaseDidCommit().
class DatabaseObservationBroker {
private unowned let database: Database
/// The savepoint stack allows us to hold database event notifications until
/// all savepoints are released. The goal is to only tell transaction
/// observers about database changes that have a chance to be committed
/// on disk.
private let savepointStack = SavepointStack()
/// Tracks the transaction completion, as reported by the
/// `sqlite3_commit_hook` and `sqlite3_rollback_hook` callbacks.
private var transactionCompletion = TransactionCompletion.none
/// The registered transaction observers.
private var transactionObservations: [TransactionObservation] = []
/// The observers for an individual statement execution.
private var statementObservations: [StatementObservation] = [] {
didSet {
let isEmpty = statementObservations.isEmpty
if isEmpty != oldValue.isEmpty {
if isEmpty {
// Avoid processing database changes if nobody is interested
uninstallUpdateHook()
} else {
installUpdateHook()
}
}
}
}
init(_ database: Database) {
self.database = database
}
// MARK: - Transaction observers
func add(transactionObserver: some TransactionObserver, extent: Database.TransactionObservationExtent) {
transactionObservations.append(TransactionObservation(observer: transactionObserver, extent: extent))
}
func remove(transactionObserver: some TransactionObserver) {
transactionObservations.removeFirst { $0.isWrapping(transactionObserver) }
}
/// Called from ``TransactionObserver/stopObservingDatabaseChangesUntilNextTransaction()``.
func disableUntilNextTransaction(transactionObserver: some TransactionObserver) {
if let observation = transactionObservations.first(where: { $0.isWrapping(transactionObserver) }) {
observation.isEnabled = false
statementObservations.removeFirst { $0.transactionObservation === observation }
}
}
func notifyChanges(withEventsOfKind eventKinds: [DatabaseEventKind]) throws {
// Support for stopObservingDatabaseChangesUntilNextTransaction()
SchedulingWatchdog.current!.databaseObservationBroker = self
defer {
SchedulingWatchdog.current!.databaseObservationBroker = nil
}
for observation in transactionObservations where observation.isEnabled {
if eventKinds.contains(where: { observation.observes(eventsOfKind: $0) }) {
observation.databaseDidChange()
}
}
}
// MARK: - Statement execution
/// Returns true if there exists some transaction observer interested in
/// the deletions in the given table.
func observesDeletions(on table: String) -> Bool {
transactionObservations.contains { observation in
observation.observes(eventsOfKind: .delete(tableName: table))
}
}
/// Prepares observation of changes that are about to be performed by the statement.
func statementWillExecute(_ statement: Statement) {
if !database.isReadOnly && !transactionObservations.isEmpty {
// As statement executes, it may trigger database changes that will
// be notified to transaction observers. As a consequence, observers
// may disable themselves with stopObservingDatabaseChangesUntilNextTransaction()
//
// This method takes no argument, and requires access to the "current
// broker", which is a per-thread global stored in
// SchedulingWatchdog.current:
SchedulingWatchdog.current!.databaseObservationBroker = self
// Fill statementObservations with observations that are interested
// in the kind of database events performed by the statement, as
// reported by `sqlite3_set_authorizer`.
//
// Those statementObservations will be notified of individual changes
// in databaseWillChange() and databaseDidChange().
let authorizerEventKinds = statement.authorizerEventKinds
switch authorizerEventKinds.count {
case 0:
// Statement has no effect on any database table.
//
// For example: PRAGMA foreign_keys = ON
statementObservations = []
case 1:
// We'll execute a simple statement without any side effect.
// Eventual database events will thus all have the same kind. All
// detabase events can be notified to interested observations.
//
// For example, if one observes all deletions in the table T, then
// all individual deletions of DELETE FROM T are notified:
let eventKind = authorizerEventKinds[0]
statementObservations = transactionObservations.compactMap { observation in
guard observation.observes(eventsOfKind: eventKind) else {
// observation is not interested
return nil
}
// Observation will be notified of all individual events
return StatementObservation(
transactionObservation: observation,
trackingEvents: .all)
}
default:
// We'll execute a complex statement with side effects performed by
// an SQL trigger or a foreign key action. Eventual database events
// may not all have the same kind: we need to filter them before
// notifying interested observations.
//
// For example, if DELETE FROM T1 generates deletions in T1 and T2
// by the mean of a foreign key action, then when one only observes
// deletions in T1, one must not be notified of deletions in T2:
statementObservations = transactionObservations.compactMap { observation in
let observedEventKinds = authorizerEventKinds.filter(observation.observes)
if observedEventKinds.isEmpty {
// observation is not interested
return nil
}
// Observation will only be notified of individual events
// it is interested into.
return StatementObservation(
transactionObservation: observation,
trackingEvents: .matching(
observedEventKinds: observedEventKinds,
authorizerEventKinds: authorizerEventKinds))
}
}
}
transactionCompletion = .none
}
/// May throw the user-provided cancelled commit error, if a transaction
/// observer has cancelled a transaction.
func statementDidFail(_ statement: Statement) throws {
// Undo statementWillExecute
statementObservations = []
SchedulingWatchdog.current!.databaseObservationBroker = nil
// Reset transactionCompletion before databaseDidRollback eventually
// executes other statements.
let transactionCompletion = self.transactionCompletion
self.transactionCompletion = .none
switch transactionCompletion {
case .rollback:
// Don't notify observers because we're in a failed implicit
// transaction here (like an INSERT which fails with
// SQLITE_CONSTRAINT error)
databaseDidRollback(notifyTransactionObservers: false)
case .cancelledCommit(let error):
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
throw error
default:
break
}
}
/// May throw the user-provided cancelled commit error, if the statement
/// commits an empty transaction, and a transaction observer cancels this
/// empty transaction.
func statementDidExecute(_ statement: Statement) throws {
// Undo statementWillExecute
if transactionObservations.isEmpty == false {
statementObservations = []
SchedulingWatchdog.current!.databaseObservationBroker = nil
}
// Has statement any effect on transaction/savepoints?
if let transactionEffect = statement.transactionEffect {
switch transactionEffect {
case .beginTransaction:
break
case .commitTransaction: // 1. A COMMIT statement has been executed
if case .none = transactionCompletion { // 2. sqlite3_commit_hook was not called
// 1+2 mean that an empty deferred transaction has been completed:
//
// BEGIN DEFERRED TRANSACTION; COMMIT
//
// This special case has a dedicated handling:
try databaseDidCommitEmptyDeferredTransaction()
return
}
case .rollbackTransaction:
break
case .beginSavepoint(let name):
savepointStack.savepointDidBegin(name)
case .releaseSavepoint(let name): // 1. A RELEASE SAVEPOINT statement has been executed
savepointStack.savepointDidRelease(name)
if case .none = transactionCompletion, // 2. sqlite3_commit_hook was not called
!database.isInsideTransaction // 3. database is no longer inside a transaction
{
// 1+2+3 mean that an empty deferred transaction has been completed:
//
// SAVEPOINT foo; RELEASE SAVEPOINT foo
//
// This special case has a dedicated handling:
try databaseDidCommitEmptyDeferredTransaction()
return
}
if savepointStack.isEmpty {
notifyBufferedEvents()
}
case .rollbackSavepoint(let name):
savepointStack.savepointDidRollback(name)
}
}
// Reset transactionCompletion before databaseDidCommit or
// databaseDidRollback eventually execute other statements.
let transactionCompletion = self.transactionCompletion
self.transactionCompletion = .none
switch transactionCompletion {
case .commit:
databaseDidCommit()
case .rollback:
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
default:
break
}
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
// Called from sqlite3_preupdate_hook
private func databaseWillChange(with event: DatabasePreUpdateEvent) {
assert(!database.isReadOnly, "Read-only transactions are not notified")
if savepointStack.isEmpty {
// Notify now
for statementObservation in statementObservations where statementObservation.tracksEvent(event) {
statementObservation.transactionObservation.databaseWillChange(with: event)
}
} else {
// Buffer
savepointStack.eventsBuffer.append((event: event.copy(), statementObservations: statementObservations))
}
}
#endif
// Called from sqlite3_update_hook
private func databaseDidChange(with event: DatabaseEvent) {
assert(!database.isReadOnly, "Read-only transactions are not notified")
// We're about to call the databaseDidChange(with:) method of
// transaction observers. In this method, observers may disable
// themselves with stopObservingDatabaseChangesUntilNextTransaction()
//
// This method takes no argument, and requires access to the "current
// broker", which is a per-thread global stored in
// SchedulingWatchdog.current:
assert(SchedulingWatchdog.current?.databaseObservationBroker != nil)
if savepointStack.isEmpty {
// Notify now
for statementObservation in statementObservations where statementObservation.tracksEvent(event) {
statementObservation.transactionObservation.databaseDidChange(with: event)
}
} else {
// Buffer
savepointStack.eventsBuffer.append((event: event.copy(), statementObservations: statementObservations))
}
}
// MARK: - End of transaction
// Called from sqlite3_commit_hook and databaseDidCommitEmptyDeferredTransaction()
private func databaseWillCommit() throws {
notifyBufferedEvents()
if !database.isReadOnly {
for observation in transactionObservations {
try observation.databaseWillCommit()
}
}
}
// Called from statementDidExecute
private func databaseDidCommit() {
savepointStack.clear()
if !database.isReadOnly {
// Observers must be able to access the database, even if the
// task that has performed the commit is cancelled.
database.ignoringCancellation {
for observation in transactionObservations {
observation.databaseDidCommit(database)
}
}
}
databaseDidEndTransaction()
}
// Called from statementDidExecute
/// May throw a cancelled commit error, if a transaction observer cancels
/// the empty transaction.
private func databaseDidCommitEmptyDeferredTransaction() throws {
// A statement that ends a transaction has been executed. But for
// SQLite, no transaction at all has started, and sqlite3_commit_hook
// was not triggered:
//
// try db.execute(sql: "BEGIN DEFERRED TRANSACTION")
// try db.execute(sql: "COMMIT") // <- no sqlite3_commit_hook callback invocation
//
// Should we tell transaction observers of this transaction, or not?
// The code says that a transaction was open, but SQLite says the
// opposite. How do we lift this ambiguity? Should we notify of
// *transactions expressed in the code*, or *SQLite transactions* only?
//
// If we would notify of SQLite transactions only, then we'd notify of
// all transactions expressed in the code, but empty deferred
// transaction. This means that we'd make an exception. And exceptions
// are the recipe for both surprise and confusion.
//
// For example, is the code below expected to print "did commit"?
//
// db.afterNextTransaction { _ in print("did commit") }
// try db.inTransaction {
// performSomeTask(db)
// return .commit
// }
//
// Yes it is. And the only way to make it reliably print "did commit" is
// to behave consistently, regardless of the implementation of the
// `performSomeTask` function. Even if the `performSomeTask` is empty,
// even if we actually execute an empty deferred transaction.
//
// For better or for worse, let's simulate a transaction:
//
// 2023-11-26: I'm glad we did, because that's how we support calls
// to `Database.notifyChanges(in:)` from an empty transaction, as a
// way to tell transaction observers about changes performed by some
// external connection.
do {
try databaseWillCommit()
databaseDidCommit()
} catch {
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
throw error
}
}
// Called from statementDidExecute or statementDidFail
private func databaseDidRollback(notifyTransactionObservers: Bool) {
savepointStack.clear()
if notifyTransactionObservers {
assert(!database.isReadOnly, "Read-only transactions are not notified")
// Observers must be able to access the database, even if the
// task that has performed the commit is cancelled.
database.ignoringCancellation {
for observation in transactionObservations {
observation.databaseDidRollback(database)
}
}
}
databaseDidEndTransaction()
}
// Called from both databaseDidCommit() and databaseDidRollback()
private func databaseDidEndTransaction() {
assert(!database.isInsideTransaction)
// Remove transaction observations that are no longer observing, because
// a transaction observer registered with the `.observerLifetime` extent
// was deallocated, or because a transaction observer was registered
// with the `.nextTransaction` extent.
transactionObservations = transactionObservations.filter(\.isObserving)
// Undo disableUntilNextTransaction(transactionObserver:)
for observation in transactionObservations {
observation.isEnabled = true
}
}
private func notifyBufferedEvents() {
// We're about to call the databaseDidChange(with:) method of
// transaction observers. In this method, observers may disable
// themselves with stopObservingDatabaseChangesUntilNextTransaction()
//
// This method takes no argument, and requires access to the "current
// broker", which is a per-thread global stored in
// SchedulingWatchdog.current.
//
// Normally, notifyBufferedEvents() is called as part of statement
// execution, and the current broker has been set in
// statementWillExecute(). An assertion should be enough:
//
// assert(SchedulingWatchdog.current?.databaseObservationBroker != nil)
//
// But we have to deal with a particular case:
//
// let journalMode = String.fetchOne(db, sql: "PRAGMA journal_mode = wal")
//
// It triggers the commit hook when the "PRAGMA journal_mode = wal"
// statement is finalized, long after it has executed:
//
// 1. Statement.deinit()
// 2. sqlite3_finalize()
// 3. commit hook
// 4. DatabaseObservationBroker.databaseWillCommit()
// 5. DatabaseObservationBroker.notifyBufferedEvents()
//
// I don't know if this behavior is something that can be relied
// upon. One would naively expect, for example, that changing the
// journal mode would trigger the commit hook in sqlite3_step(),
// not in sqlite3_finalize().
//
// Anyway: in this scenario, statementWillExecute() has not been
// called, and the current broker is nil.
//
// Let's not try to outsmart SQLite, and build a complex state machine.
// Instead, let's just make sure that the current broker is set to self
// when this method is called.
let watchDog = SchedulingWatchdog.current!
watchDog.databaseObservationBroker = self
defer {
watchDog.databaseObservationBroker = nil
}
// Now we can safely notify:
let eventsBuffer = savepointStack.eventsBuffer
savepointStack.clear()
for (event, statementObservations) in eventsBuffer {
assert(statementObservations.isEmpty || !database.isReadOnly, "Read-only transactions are not notified")
for statementObservation in statementObservations where statementObservation.tracksEvent(event) {
event.send(to: statementObservation.transactionObservation)
}
}
}
// MARK: - SQLite hooks
func installCommitAndRollbackHooks() {
let brokerPointer = Unmanaged.passUnretained(self).toOpaque()
sqlite3_commit_hook(database.sqliteConnection, { brokerPointer in
let broker = Unmanaged<DatabaseObservationBroker>.fromOpaque(brokerPointer!).takeUnretainedValue()
do {
try broker.databaseWillCommit()
broker.transactionCompletion = .commit
// Next step: statementDidExecute(_:)
return 0
} catch {
broker.transactionCompletion = .cancelledCommit(error)
// Next step: sqlite3_rollback_hook callback
return 1
}
}, brokerPointer)
sqlite3_rollback_hook(database.sqliteConnection, { brokerPointer in
let broker = Unmanaged<DatabaseObservationBroker>.fromOpaque(brokerPointer!).takeUnretainedValue()
switch broker.transactionCompletion {
case .cancelledCommit:
// Next step: statementDidFail(_:)
break
default:
broker.transactionCompletion = .rollback
// Next step: statementDidExecute(_:)
}
}, brokerPointer)
}
private func installUpdateHook() {
let brokerPointer = Unmanaged.passUnretained(self).toOpaque()
sqlite3_update_hook(
database.sqliteConnection,
{ (brokerPointer, updateKind, databaseNameCString, tableNameCString, rowID) in
let broker = Unmanaged<DatabaseObservationBroker>.fromOpaque(brokerPointer!).takeUnretainedValue()
broker.databaseDidChange(
with: DatabaseEvent(
kind: DatabaseEvent.Kind(rawValue: updateKind)!,
rowID: rowID,
databaseNameCString: databaseNameCString,
tableNameCString: tableNameCString))
},
brokerPointer)
#if SQLITE_ENABLE_PREUPDATE_HOOK
sqlite3_preupdate_hook(
database.sqliteConnection,
// swiftlint:disable:next line_length
{ (brokerPointer, databaseConnection, updateKind, databaseNameCString, tableNameCString, initialRowID, finalRowID) in
let broker = Unmanaged<DatabaseObservationBroker>.fromOpaque(brokerPointer!).takeUnretainedValue()
broker.databaseWillChange(
with: DatabasePreUpdateEvent(
connection: databaseConnection!,
kind: DatabasePreUpdateEvent.Kind(rawValue: updateKind)!,
initialRowID: initialRowID,
finalRowID: finalRowID,
databaseNameCString: databaseNameCString,
tableNameCString: tableNameCString))
},
brokerPointer)
#endif
}
private func uninstallUpdateHook() {
sqlite3_update_hook(database.sqliteConnection, nil, nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
sqlite3_preupdate_hook(database.sqliteConnection, nil, nil)
#endif
}
/// The various SQLite transactions completions, as reported by the
/// `sqlite3_commit_hook` and `sqlite3_rollback_hook` callbacks.
fileprivate enum TransactionCompletion {
/// Transaction state is unchanged.
case none
/// Transaction turns committed.
case commit
/// Transaction turns rollbacked.
case rollback
/// Transaction turns rollbacked because a transaction observer has
/// cancelled a commit by throwing an error.
case cancelledCommit(Error)
}
}
// MARK: - TransactionObserver
public protocol TransactionObserver: AnyObject {
/// Returns whether specific kinds of database changes should be notified
/// to the observer.
///
/// When this method returns false, database events of this kind are not
/// notified to the ``databaseDidChange(with:)`` method.
///
/// For example:
///
/// ```swift
/// // An observer that is only interested in the "player" table
/// class PlayerObserver: TransactionObserver {
/// func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
/// return eventKind.tableName == "player"
/// }
/// }
/// ```
///
/// When this method returns true for deletion events, the observer
/// prevents the
/// [truncate optimization](https://www.sqlite.org/lang_delete.html#the_truncate_optimization)
/// from being applied on the observed tables.
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool
/// Called when the database was modified in some unspecified way.
///
/// This method allows a transaction observer to handle changes that are
/// not automatically detected. See <doc:GRDB/TransactionObserver#Dealing-with-Undetected-Changes>
/// and ``Database/notifyChanges(in:)`` for more information.
///
/// The exact nature of changes is unknown, but they comply to the
/// ``observes(eventsOfKind:)`` test.
func databaseDidChange()
/// Called when the database is changed by an insert, update, or
/// delete event.
///
/// The change is pending until the current transaction ends. See
/// ``databaseWillCommit()-7mksu``, ``databaseDidCommit(_:)`` and
/// ``databaseDidRollback(_:)``.
///
/// The observer has an opportunity to stop receiving further change events
/// from the current transaction by calling the
/// ``stopObservingDatabaseChangesUntilNextTransaction()`` method.
///
/// - note: The event is only valid for the duration of this method call.
/// If you need to keep it longer, store a copy: `event.copy()`.
///
/// - precondition: This method must not access the observed writer
/// database connection.
func databaseDidChange(with event: DatabaseEvent)
/// Called when a transaction is about to be committed.
///
/// The transaction observer has an opportunity to rollback pending changes
/// by throwing an error from this method.
///
/// - precondition: This method must not access the observed writer
/// database connection.
/// - throws: The eventual error that rollbacks pending changes.
func databaseWillCommit() throws
/// Called when a transaction has been committed on disk.
func databaseDidCommit(_ db: Database)
/// Called when a transaction has been rollbacked.
func databaseDidRollback(_ db: Database)
#if SQLITE_ENABLE_PREUPDATE_HOOK
/// Called when the database is changed by an insert, update, or
/// delete event.
///
/// Notifies before a database change (insert, update, or delete)
/// with change information (initial / final values for the row's
/// columns). (Called *before* ``databaseDidChange(with:)``.)
///
/// The change is pending until the end of the current transaction,
/// and you always get a second chance to get basic event information in
/// the ``databaseDidChange(with:)`` callback.
///
/// This callback is mostly useful for calculating detailed change
/// information for a row, and provides the initial / final values.
///
/// The event is only valid for the duration of this method call. If you
/// need to keep it longer, store a copy: `event.copy()`
///
/// - warning: this method must not access the database.
///
/// **Availability Info**
///
/// Requires SQLite compiled with option SQLITE_ENABLE_PREUPDATE_HOOK.
///
/// As of macOS 10.11.5, and iOS 9.3.2, the built-in SQLite library
/// does not have this enabled, so you'll need to compile your own
/// version of SQLite:
/// See <https://github.com/groue/GRDB.swift/blob/master/Documentation/CustomSQLiteBuilds.md>
func databaseWillChange(with event: DatabasePreUpdateEvent)
#endif
}
extension TransactionObserver {
/// The default implementation does nothing.
public func databaseWillCommit() throws { }
#if SQLITE_ENABLE_PREUPDATE_HOOK
/// The default implementation does nothing.
public func databaseWillChange(with event: DatabasePreUpdateEvent) { }
#endif
/// The default implementation does nothing.
public func databaseDidChange() { }
/// Prevents the observer from receiving further change notifications
/// until the next transaction.
///
/// After this method has been called, the ``databaseDidChange(with:)``
/// and ``databaseDidChange()-7olv7`` methods won't be called until the
/// next transaction.
///
/// For example:
///
/// ```swift
/// // An observer that is only interested in the "player" table
/// class PlayerObserver: TransactionObserver {
/// var playerTableWasModified = false
///
/// func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
/// return eventKind.tableName == "player"
/// }
///
/// func databaseDidChange() {
/// playerTableWasModified = true
///
/// // It is pointless to keep on tracking further changes:
/// stopObservingDatabaseChangesUntilNextTransaction()
/// }
///
/// func databaseDidChange(with event: DatabaseEvent) {
/// playerTableWasModified = true
///
/// // It is pointless to keep on tracking further changes:
/// stopObservingDatabaseChangesUntilNextTransaction()
/// }
/// }
/// ```
///
/// - precondition: This method must be called from
/// ``databaseDidChange(with:)`` or ``databaseDidChange()-7olv7``.
public func stopObservingDatabaseChangesUntilNextTransaction() {
guard let broker = SchedulingWatchdog.current?.databaseObservationBroker else {
fatalError("""
stopObservingDatabaseChangesUntilNextTransaction must be called \
from the `databaseDidChange()` or `databaseDidChange(with:)` methods
""")
}
broker.disableUntilNextTransaction(transactionObserver: self)
}
}
// MARK: - TransactionObservation
/// This class manages the observation extent of a transaction observer
final class TransactionObservation {
let extent: Database.TransactionObservationExtent
/// A disabled observation is not interested in individual database changes.
/// It is still interested in transactions commits & rollbacks.
var isEnabled = true
private weak var weakObserver: (any TransactionObserver)?
private var strongObserver: (any TransactionObserver)?
private var observer: (any TransactionObserver)? { strongObserver ?? weakObserver }
fileprivate var isObserving: Bool {
observer != nil
}
init(observer: some TransactionObserver, extent: Database.TransactionObservationExtent) {
self.extent = extent
switch extent {
case .observerLifetime:
weakObserver = observer
case .nextTransaction:
// This strong reference will be released in databaseDidCommit() and databaseDidRollback()
strongObserver = observer
case .databaseLifetime:
strongObserver = observer
}
}
func isWrapping(_ observer: some TransactionObserver) -> Bool {
self.observer === observer
}
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
observer?.observes(eventsOfKind: eventKind) ?? false
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
func databaseWillChange(with event: DatabasePreUpdateEvent) {