-
-
Notifications
You must be signed in to change notification settings - Fork 744
/
Copy pathConfiguration.swift
494 lines (451 loc) · 18.1 KB
/
Configuration.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
// Import C SQLite functions
#if GRDBCIPHER
import SQLCipher
#elseif SWIFT_PACKAGE
import GRDBSQLite
#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER
import SQLite3
#endif
#if !canImport(Darwin)
@preconcurrency
#endif
import Dispatch
import Foundation
public struct Configuration: Sendable {
// MARK: - Misc options
/// A boolean value indicating whether foreign key support is enabled.
///
/// The default is true.
///
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>.
public var foreignKeysEnabled = true
/// A boolean value indicating whether an SQLite connection is read-only.
///
/// The default is false.
///
/// ```swift
/// var config = Configuration()
/// config.readonly = true
///
/// let dbQueue = try DatabaseQueue( // or DatabasePool
/// path: "/path/to/database.sqlite",
/// configuration: config)
/// ```
public var readonly = false
/// A label that describes a database connection.
///
/// You can query this label at runtime:
///
/// ```swift
/// var config = Configuration()
/// config.label = "MyDatabase"
/// let dbQueue = try DatabaseQueue(configuration: config)
///
/// try dbQueue.read { db in
/// print(db.configuration.label) // Prints "MyDatabase"
/// }
/// ```
///
/// The configuration label is also used to name ``Database`` connections
/// (their ``Database/description`` property), and the various dispatch
/// queues created by GRDB, visible in debugging sessions and crash logs.
///
/// Those connection names and dispatch queue labels are intended for
/// debugging only. Their format may change between GRDB releases.
/// Applications should not depend on connection names and dispatch
/// queue labels.
///
/// If the configuration label is nil, the current GRDB implementation uses
/// the following names:
///
/// - `GRDB.DatabaseQueue`: the (unique) connection of a DatabaseQueue
/// - `GRDB.DatabasePool.writer`: the (unique) writer connection of
/// a DatabasePool
/// - `GRDB.DatabasePool.reader.N`, where N is 1, 2, ...: one of the reader
/// connection(s) of a DatabasePool. N may get bigger than the maximum
/// number of concurrent readers, as SQLite connections get closed and new
/// ones are opened.
/// - `GRDB.DatabasePool.snapshot.N`: the connection of a DatabaseSnapshot.
/// N grows with the number of snapshots.
///
/// If the configuration label is not nil, for example "MyDatabase", the
/// current GRDB implementation uses the following names:
///
/// - `MyDatabase`: the (unique) connection of a DatabaseQueue
/// - `MyDatabase.writer`: the (unique) writer connection of a DatabasePool
/// - `MyDatabase.reader.N`, where N is 1, 2, ...: one of the reader
/// connection(s) of a DatabasePool. N may get bigger than the maximum
/// number of concurrent readers, as SQLite connections get closed and new
/// ones are opened.
/// - `MyDatabase.snapshot.N`: the connection of a DatabaseSnapshot. N grows
/// with the number of snapshots.
///
/// The default configuration label is nil.
public var label: String? = nil
/// A boolean value indicating whether SQLite 3.29+ interprets
/// double-quoted strings as string literals when they does not match any
/// valid identifier.
///
/// The default and recommended value is false:
///
/// ```swift
/// // Error: no such column: missingColumn
/// let name = try String.fetchOne(db, sql: """
/// SELECT "missingColumn" FROM "player"
/// """)
/// ```
///
/// When true, or before SQLite version 3.29.0, double-quoted strings that
/// do not match any valid identifier are interpreted as string literals,
/// as in the example below. This is an SQLite
/// [misfeature](https://sqlite.org/quirks.html#double_quoted_string_literals_are_accepted):
///
/// ```swift
/// // MISFEATURE: This query succeeds with result "missingColumn"
/// let name = try String.fetchOne(db, sql: """
/// SELECT "missingColumn" FROM "player"
/// """)
/// ```
public var acceptsDoubleQuotedStringLiterals = false
/// A boolean value indicating whether the database connection listens to
/// the ``Database/suspendNotification`` and ``Database/resumeNotification``
/// notifications.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// Set this flag to true when you apply the technique described in
/// <doc:DatabaseSharing#How-to-limit-the-0xDEAD10CC-exception>. See
/// ``Database/suspendNotification`` for more informations about
/// suspended databases.
public var observesSuspensionNotifications = false
/// A boolean value indicating whether statement arguments are visible in
/// the description of database errors and trace events.
///
/// The default and recommended value is false: statement arguments are not
/// visible in database errors and trace events, preventing sensitive
/// information from leaking in unexpected places. For example:
///
/// ```swift
/// db.trace { event in
/// // By default, sensitive information is NOT printed
/// // when a statement is traced:
/// print(event)
/// }
///
/// do {
/// // The sensitive information to protect
/// let email = "..."
/// let player = try Player.filter(Column("email") == email).fetchOne(db)
/// } catch {
/// // By default, sensitive information is NOT printed
/// // when an error occurs:
/// print(error)
/// }
/// ```
///
/// For debugging purpose, you can set this flag to true, and get more
/// precise database reports. It is your responsibility to prevent sensitive
/// information from leaking in unexpected locations, so you should not set
/// this flag in release builds (think about GDPR and other
/// privacy-related rules):
///
/// ```swift
/// var config = Configuration()
/// #if DEBUG
/// // Enable verbose debugging in DEBUG builds only
/// config.publicStatementArguments = true
/// #endif
///
/// db.trace { event in
/// // Sensitive information is printed in DEBUG builds:
/// print(event)
/// }
///
/// do {
/// // The sensitive information to protect
/// let email = "..."
/// let player = try Player.filter(Column("email") == email).fetchOne(db)
/// } catch {
/// // Sensitive information is printed in DEBUG builds:
/// print(error)
/// }
/// ```
public var publicStatementArguments = false
/// The clock that feeds ``Database/transactionDate``.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// The default clock is ``DefaultTransactionClock`` (which returns the
/// start date of the current transaction).
///
/// For example:
///
/// ```swift
/// var config = Configuration()
/// config.transactionClock = .custom { db in /* return some Date */ }
/// ```
public var transactionClock: any TransactionClock = .default
// MARK: - Managing SQLite Connections
private var setups: [@Sendable (Database) throws -> Void] = []
/// Defines a function to run whenever an SQLite connection is opened.
///
/// The preparation function is run before the connection is made available
/// for database access methods.
///
/// This method can be called several times. The preparation functions are
/// run in the same order.
///
/// For example:
///
/// ```swift
/// var config = Configuration()
/// config.prepareDatabase { db in
/// // Prints all SQL statements
/// db.trace { print("SQL >", $0) }
/// }
/// ```
///
/// When you use a ``DatabasePool``, preparation functions are called for
/// the writer connection and all reader connections. You can distinguish
/// them by querying `db.configuration.readonly`:
///
/// ```swift
/// var config = Configuration()
/// config.prepareDatabase { db in
/// if db.configuration.readonly {
/// // reader connection
/// } else {
/// // writer connection
/// }
/// }
/// ```
///
/// On newly created databases files, ``DatabasePool`` activates the WAL
/// mode after the preparation functions have run.
public mutating func prepareDatabase(_ setup: @escaping @Sendable (Database) throws -> Void) {
setups.append(setup)
}
// MARK: - Transactions
/// A boolean value indicating whether it is valid to leave a transaction
/// opened at the end of a database access method.
///
/// The default value is false: not completing a transaction is a
/// programmer error:
///
/// ```swift
/// let dbQueue = try DatabaseQueue()
///
/// // fatal error: A transaction has been left opened at the end of a database access
/// try dbQueue.inDatabase { db in
/// try db.beginTransaction()
/// }
/// ```
///
/// When true, one can leave opened transaction at the end of database
/// access method:
///
/// ```swift
/// var config = Configuration()
/// config.allowsUnsafeTransactions = true
/// let dbQueue = try DatabaseQueue(configuration: config)
///
/// try dbQueue.inDatabase { db in
/// try db.beginTransaction()
/// }
///
/// try dbQueue.inDatabase { db in
/// try db.commit()
/// }
/// ```
///
/// This configuration flag has no effect on ``DatabasePool`` reader
/// connections: those never allow leaving a transaction opened at the end
/// of a read access.
public var allowsUnsafeTransactions = false
// MARK: - Journal Mode
/// Defines how the journal mode is configured when the database
/// connection is opened.
///
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
public enum JournalModeConfiguration: Sendable {
/// The default setup has ``DatabaseQueue`` perform no specific
/// configuration of the journal mode, and ``DatabasePool``
/// configure the database for the WAL mode (just like the
/// ``wal`` case).
case `default`
/// The journal mode is set to WAL (plus extra configurations that
/// make life easier with WAL databases).
case wal
}
/// Defines how the journal mode is configured when the database
/// connection is opened.
///
/// This configuration is ignored when ``readonly`` is true.
///
/// The default value has ``DatabaseQueue`` perform no specific
/// configuration of the journal mode, and ``DatabasePool`` configure
/// the database for the WAL mode.
///
/// Applications that need to open a WAL database with a
/// ``DatabaseQueue`` should set the `journalMode` to `wal`:
///
/// ```swift
/// // Open a WAL database with DatabaseQueue
/// var config = Configuration()
/// config.journalMode = .wal
/// let dbQueue = try DatabaseQueue(path: "...", configuration: config)
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
public var journalMode = JournalModeConfiguration.default
// MARK: - Concurrency
/// Defines the how `SQLITE_BUSY` errors are handled.
///
/// The default is ``Database/BusyMode/immediateError``.
///
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html#busy>
public var busyMode: Database.BusyMode = .immediateError
/// The behavior in case of SQLITE_BUSY error, for read-only connections.
/// If nil, GRDB picks a default one.
var readonlyBusyMode: Database.BusyMode? = nil
/// The maximum number of concurrent reader connections.
///
/// This configuration has effect on ``DatabasePool`` and
/// ``DatabaseSnapshotPool`` only. The default value is 5.
///
/// You can query this value at runtime in order to get the actual capacity
/// for concurrent reads of any ``DatabaseReader``. In this context,
/// ``DatabaseQueue`` and ``DatabaseSnapshot`` have a capacity of 1,
/// because they can't perform two concurrent reads. For example:
///
/// ```swift
/// var config = Configuration()
/// config.maximumReaderCount = 5
///
/// let path = "/path/to/database.sqlite"
/// let dbQueue = try DatabaseQueue(path: path, configuration: config)
/// let dbPool = try DatabasePool(path: path, configuration: config)
/// let dbSnapshot = try dbPool.makeSnapshot()
///
/// print(dbQueue.configuration.maximumReaderCount) // 1
/// print(dbPool.configuration.maximumReaderCount) // 5
/// print(dbSnapshot.configuration.maximumReaderCount) // 1
/// ```
public var maximumReaderCount: Int = 5
/// The quality of service of database accesses.
///
/// The quality of service is ignored if you supply a ``targetQueue``.
///
/// The default is `userInitiated`.
public var qos: DispatchQoS = .userInitiated
/// The effective quality of service of read-only database accesses.
public var readQoS: DispatchQoS {
targetQueue?.qos ?? self.qos
}
/// The effective quality of service of write database accesses.
public var writeQoS: DispatchQoS {
writeTargetQueue?.qos ?? targetQueue?.qos ?? self.qos
}
/// The target dispatch queue for database accesses.
///
/// Database connections which are not read-only will prefer
/// ``writeTargetQueue`` instead, if it is not nil.
///
/// When you use ``DatabasePool``, make sure this queue is concurrent. This
/// is because in a serial dispatch queue, no concurrent database access can
/// happen, and you may experience deadlocks.
///
/// If the queue is nil, all database accesses happen in unspecified
/// dispatch queues whose quality of service is determined by the
/// ``qos`` property.
///
/// The default is nil.
public var targetQueue: DispatchQueue? = nil
/// The target dispatch queue for write database accesses.
///
/// If this queue is nil, writer connections are controlled
/// by ``targetQueue``.
///
/// The default is nil.
public var writeTargetQueue: DispatchQueue? = nil
#if os(iOS)
/// A boolean value indicating whether the database connection releases
/// memory when entering the background or upon receiving a memory warning
/// in iOS.
///
/// The default is true.
public var automaticMemoryManagement = true
#endif
/// A boolean value indicating whether read-only connections should be
/// kept open.
///
/// This configuration flag applies to ``DatabasePool`` only. The
/// default value is false.
///
/// When the flag is false, a `DatabasePool` closes read-only
/// connections when requested to dispose non-essential memory with
/// ``DatabasePool/releaseMemory()``. When true, those connections are
/// kept open.
///
/// Consider setting this flag to true when profiling your application
/// reveals that a lot of time is spent opening new SQLite connections.
public var persistentReadOnlyConnections = false
// MARK: - Factory Configuration
/// Creates a factory configuration.
public init() { }
// MARK: - Not Public
/// The SQLite [threading mode](https://www.sqlite.org/threadsafe.html).
///
/// - Note: Only the multi-thread mode (`SQLITE_OPEN_NOMUTEX`) is currently
/// supported, since all <doc:DatabaseConnections> access SQLite connections
/// through a `SerializedDatabase`.
var threadingMode = Database.ThreadingMode.default
private(set) var SQLiteConnectionDidOpen: (@Sendable () -> Void)?
private(set) var SQLiteConnectionWillClose: (@Sendable (SQLiteConnection) -> Void)?
private(set) var SQLiteConnectionDidClose: (@Sendable () -> Void)?
// Workaround https://github.com/apple/swift/issues/72727
mutating func onConnectionDidOpen(_ callback: @escaping @Sendable () -> Void) {
SQLiteConnectionDidOpen = callback
}
// Workaround https://github.com/apple/swift/issues/72727
mutating func onConnectionWillClose(_ callback: @escaping @Sendable (SQLiteConnection) -> Void) {
SQLiteConnectionWillClose = callback
}
// Workaround https://github.com/apple/swift/issues/72727
mutating func onConnectionDidClose(_ callback: @escaping @Sendable () -> Void) {
SQLiteConnectionDidClose = callback
}
var SQLiteOpenFlags: CInt {
var flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE)
if sqlite3_libversion_number() >= 3037000 {
flags |= 0x02000000 // SQLITE_OPEN_EXRESCODE
}
return threadingMode.SQLiteOpenFlags | flags
}
func setUp(_ db: Database) throws {
for f in setups {
try f(db)
}
}
func identifier(defaultLabel: String, purpose: String? = nil) -> String {
(self.label ?? defaultLabel) + (purpose.map { "." + $0 } ?? "")
}
/// Creates a DispatchQueue which has the quality of service and target
/// queue of write accesses.
func makeWriterDispatchQueue(label: String) -> DispatchQueue {
if let targetQueue = writeTargetQueue ?? targetQueue {
return DispatchQueue(label: label, target: targetQueue)
} else {
return DispatchQueue(label: label, qos: qos)
}
}
/// Creates a DispatchQueue which has the quality of service and target
/// queue of read accesses.
func makeReaderDispatchQueue(label: String) -> DispatchQueue {
if let targetQueue {
return DispatchQueue(label: label, target: targetQueue)
} else {
return DispatchQueue(label: label, qos: qos)
}
}
}