Skip to content

Commit 0cacfca

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 79fcba5 + 9ed8c84 commit 0cacfca

263 files changed

Lines changed: 1106 additions & 4770 deletions

File tree

Some content is hidden

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

.github/workflows/CI.yml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,23 @@ jobs:
107107
- uses: actions/checkout@v5
108108
- name: ${{ matrix.name }}
109109
run: make test_framework_SQLCipher4Encrypted
110-
CustomSQLite:
111-
name: CustomSQLite
112-
runs-on: ${{ matrix.runsOn }}
113-
env:
114-
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
115-
timeout-minutes: 60
116-
strategy:
117-
fail-fast: false
118-
matrix:
119-
include:
120-
- xcode: "Xcode_16.4.app"
121-
runsOn: macOS-15
122-
name: "Xcode 16.4"
123-
steps:
124-
- uses: actions/checkout@v5
125-
- name: ${{ matrix.name }}
126-
run: make test_framework_GRDBCustomSQLiteOSX
110+
# CustomSQLite:
111+
# name: CustomSQLite
112+
# runs-on: ${{ matrix.runsOn }}
113+
# env:
114+
# DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
115+
# timeout-minutes: 60
116+
# strategy:
117+
# fail-fast: false
118+
# matrix:
119+
# include:
120+
# - xcode: "Xcode_16.4.app"
121+
# runsOn: macOS-15
122+
# name: "Xcode 16.4"
123+
# steps:
124+
# - uses: actions/checkout@v5
125+
# - name: ${{ matrix.name }}
126+
# run: make test_framework_GRDBCustomSQLiteOSX
127127
XCFramework:
128128
name: XCFramework
129129
runs-on: ${{ matrix.runsOn }}

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
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.11.x` Releases - [7.11.0](#7110)
1011
- `7.10.x` Releases - [7.10.0](#7100)
1112
- `7.9.x` Releases - [7.9.0](#790)
1213
- `7.8.x` Releases - [7.8.0](#780)
@@ -142,9 +143,16 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
142143

143144
---
144145

146+
## 7.11.0
147+
148+
Released June 1, 2026
149+
150+
- **New**: Add option to disable database change filtering in `TransactionObserver` by [@simolus3](https://github.com/simolus3) in [#1864](https://github.com/groue/GRDB.swift/pull/1864)
151+
- **Fixed**: Fix upsert for WITHOUT ROWID tables by [@groue](https://github.com/groue) in [#1858](https://github.com/groue/GRDB.swift/pull/1858)
152+
145153
## 7.10.0
146154

147-
Released February 15, 2025
155+
Released February 15, 2026
148156

149157
- **Documentation fixes** by [@bellebethcooper](https://github.com/bellebethcooper), [@Cykelero](https://github.com/Cykelero), and [@leejungyeob](https://github.com/leejungyeob) in [#1842](https://github.com/groue/GRDB.swift/pull/1842), [#1846](https://github.com/groue/GRDB.swift/pull/1846), [#1848](https://github.com/groue/GRDB.swift/pull/1848)
150158
- **New**: Linux adjustments by [@thinkpractice](https://github.com/thinkpractice) in [#1825](https://github.com/groue/GRDB.swift/pull/1825)

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

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

GRDB.xcodeproj/project.pbxproj

Lines changed: 26 additions & 2081 deletions
Large diffs are not rendered by default.

GRDB/Core/TransactionObserver.swift

Lines changed: 152 additions & 61 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
@@ -334,65 +338,14 @@ class DatabaseObservationBroker {
334338
SchedulingWatchdog.current!.databaseObservationBroker = self
335339

336340
// Fill statementObservations with observations that are interested
337-
// in the kind of database events performed by the statement, as
338-
// reported by `sqlite3_set_authorizer`.
339-
//
341+
// in the kind of database events performed by the statement.
340342
// Those statementObservations will be notified of individual changes
341343
// in databaseWillChange() and databaseDidChange().
342-
let authorizerEventKinds = statement.authorizerEventKinds
343-
344-
switch authorizerEventKinds.count {
345-
case 0:
346-
// Statement has no effect on any database table.
347-
//
348-
// For example: PRAGMA foreign_keys = ON
349-
statementObservations = []
350-
case 1:
351-
// We'll execute a simple statement without any side effect.
352-
// Eventual database events will thus all have the same kind. All
353-
// detabase events can be notified to interested observations.
354-
//
355-
// For example, if one observes all deletions in the table T, then
356-
// all individual deletions of DELETE FROM T are notified:
357-
let eventKind = authorizerEventKinds[0]
358-
statementObservations = transactionObservations.compactMap { observation in
359-
guard observation.observes(eventsOfKind: eventKind) else {
360-
// observation is not interested
361-
return nil
362-
}
363-
364-
// Observation will be notified of all individual events
365-
return StatementObservation(
366-
transactionObservation: observation,
367-
trackingEvents: .all)
368-
}
369-
default:
370-
// We'll execute a complex statement with side effects performed by
371-
// an SQL trigger or a foreign key action. Eventual database events
372-
// may not all have the same kind: we need to filter them before
373-
// notifying interested observations.
374-
//
375-
// For example, if DELETE FROM T1 generates deletions in T1 and T2
376-
// by the mean of a foreign key action, then when one only observes
377-
// deletions in T1, one must not be notified of deletions in T2:
378-
statementObservations = transactionObservations.compactMap { observation in
379-
let observedEventKinds = authorizerEventKinds.filter(observation.observes)
380-
if observedEventKinds.isEmpty {
381-
// observation is not interested
382-
return nil
383-
}
384-
385-
// Observation will only be notified of individual events
386-
// it is interested into.
387-
return StatementObservation(
388-
transactionObservation: observation,
389-
trackingEvents: .matching(
390-
observedEventKinds: observedEventKinds,
391-
authorizerEventKinds: authorizerEventKinds))
392-
}
344+
statementObservations = transactionObservations.compactMap { observation in
345+
observation.statementObservation(for: statement)
393346
}
394347
}
395-
348+
396349
transactionCompletion = .none
397350
}
398351

@@ -798,16 +751,78 @@ class DatabaseObservationBroker {
798751
}
799752
}
800753

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+
801783
// MARK: - TransactionObserver
802784

803785
public protocol TransactionObserver: AnyObject {
804786

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+
805823
/// Returns whether specific kinds of database changes should be notified
806824
/// to the observer.
807825
///
808-
/// When this method returns false, database events of this kind are not
809-
/// notified to the ``databaseDidChange(with:)`` method.
810-
///
811826
/// For example:
812827
///
813828
/// ```swift
@@ -823,6 +838,11 @@ public protocol TransactionObserver: AnyObject {
823838
/// prevents the
824839
/// [truncate optimization](https://www.sqlite.org/lang_delete.html#the_truncate_optimization)
825840
/// from being applied on the observed tables.
841+
///
842+
/// - Note: This method is not called when the result of
843+
/// ``databaseEventObservationStrategy`` has the
844+
/// ``DatabaseEventObservationStrategy/requiresDatabaseEventKind``
845+
/// flag set to false.
826846
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool
827847

828848
/// Called when the database was modified in some unspecified way.
@@ -838,8 +858,12 @@ public protocol TransactionObserver: AnyObject {
838858
/// Called when the database is changed by an insert, update, or
839859
/// delete event.
840860
///
841-
/// The change is pending until the current transaction ends. See
842-
/// ``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
843867
/// ``databaseDidRollback(_:)``.
844868
///
845869
/// The observer has an opportunity to stop receiving further change events
@@ -902,6 +926,11 @@ public protocol TransactionObserver: AnyObject {
902926
}
903927

904928
extension TransactionObserver {
929+
/// The default implementation does not observe statements.
930+
public var databaseEventObservationStrategy: DatabaseEventObservationStrategy {
931+
.default
932+
}
933+
905934
/// The default implementation does nothing.
906935
public func databaseWillCommit() throws { }
907936

@@ -996,7 +1025,69 @@ final class TransactionObservation {
9961025
}
9971026

9981027
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
999-
observer?.observes(eventsOfKind: eventKind) ?? false
1028+
guard let observer else {
1029+
return false
1030+
}
1031+
return !observer.databaseEventObservationStrategy.requiresDatabaseEventKind || observer.observes(eventsOfKind: eventKind)
1032+
}
1033+
1034+
func statementObservation(for statement: Statement) -> StatementObservation? {
1035+
guard let observer else {
1036+
return nil
1037+
}
1038+
1039+
if !observer.databaseEventObservationStrategy.requiresDatabaseEventKind {
1040+
return StatementObservation(
1041+
transactionObservation: self,
1042+
trackingEvents: .all)
1043+
}
1044+
1045+
let authorizerEventKinds = statement.authorizerEventKinds
1046+
1047+
switch authorizerEventKinds.count {
1048+
case 0:
1049+
// Statement has no effect on any database table.
1050+
//
1051+
// For example: PRAGMA foreign_keys = ON
1052+
return nil
1053+
case 1:
1054+
// We'll execute a simple statement without any side effect.
1055+
// Eventual database events will thus all have the same kind. All
1056+
// detabase events can be notified to interested observations.
1057+
//
1058+
// For example, if one observes all deletions in the table T, then
1059+
// all individual deletions of DELETE FROM T are notified:
1060+
guard observer.observes(eventsOfKind: authorizerEventKinds[0]) else {
1061+
// observation is not interested
1062+
return nil
1063+
}
1064+
1065+
// Observation will be notified of all individual events
1066+
return StatementObservation(
1067+
transactionObservation: self,
1068+
trackingEvents: .all)
1069+
default:
1070+
// We'll execute a complex statement with side effects performed by
1071+
// an SQL trigger or a foreign key action. Eventual database events
1072+
// may not all have the same kind: we need to filter them before
1073+
// notifying interested observations.
1074+
//
1075+
// For example, if DELETE FROM T1 generates deletions in T1 and T2
1076+
// by the mean of a foreign key action, then when one only observes
1077+
// deletions in T1, one must not be notified of deletions in T2:
1078+
let observedEventKinds = authorizerEventKinds.filter(observer.observes)
1079+
if observedEventKinds.isEmpty {
1080+
// observation is not interested
1081+
return nil
1082+
}
1083+
// Observation will only be notified of individual events
1084+
// it is interested into.
1085+
return StatementObservation(
1086+
transactionObservation: self,
1087+
trackingEvents: .matching(
1088+
observedEventKinds: observedEventKinds,
1089+
authorizerEventKinds: authorizerEventKinds))
1090+
}
10001091
}
10011092

10021093
#if SQLITE_ENABLE_PREUPDATE_HOOK

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.
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: 6 additions & 2 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,6 +125,8 @@ 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+
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``.
129+
128130
## Observation Extent
129131

130132
**You can specify how long an observer is notified of database changes and transactions.**
@@ -232,7 +234,7 @@ The changes and transactions that are not automatically notified to transaction
232234

233235
- Read-only transactions.
234236
- Changes and transactions performed by external database connections.
235-
- Changes performed by SQLite statements that are not both compiled and executed through 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``).
236238
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
237239
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
238240
- 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).
@@ -265,8 +267,10 @@ try dbQueue.write { db in
265267

266268
### Filtering Database Changes
267269

270+
- ``databaseEventObservationStrategy``
268271
- ``observes(eventsOfKind:)``
269272
- ``DatabaseEventKind``
273+
- ``DatabaseEventObservationStrategy``
270274

271275
### Handling Database Changes
272276

0 commit comments

Comments
 (0)