Update pods
This commit is contained in:
parent
38799b1c6e
commit
2f925b7eb8
@ -245,8 +245,13 @@ public struct Configuration {
|
||||
///
|
||||
/// The quality of service is ignored if you supply a target queue.
|
||||
///
|
||||
/// Default: .default
|
||||
public var qos: DispatchQoS = .default
|
||||
/// Default: .userInitiated
|
||||
public var qos: DispatchQoS = .userInitiated
|
||||
|
||||
/// The quality of service of read accesses
|
||||
var readQoS: DispatchQoS {
|
||||
targetQueue?.qos ?? self.qos
|
||||
}
|
||||
|
||||
/// A target queue for database accesses.
|
||||
///
|
||||
@ -270,7 +275,15 @@ public struct Configuration {
|
||||
///
|
||||
/// Default: nil
|
||||
public var writeTargetQueue: DispatchQueue? = nil
|
||||
|
||||
|
||||
#if os(iOS)
|
||||
/// Sets whether GRDB will release memory when entering the background or
|
||||
/// upon receiving a memory warning in iOS.
|
||||
///
|
||||
/// Default: true
|
||||
public var automaticMemoryManagement = true
|
||||
#endif
|
||||
|
||||
// MARK: - Factory Configuration
|
||||
|
||||
/// Creates a factory configuration
|
||||
@ -316,17 +329,4 @@ public struct Configuration {
|
||||
return DispatchQueue(label: label, qos: qos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a DispatchQueue which has the quality of service of
|
||||
/// read accesses.
|
||||
///
|
||||
/// The returned queue has no target queue, and won't create deadlocks when
|
||||
/// used synchronously from a database access.
|
||||
func makeDispatchQueue(label: String) -> DispatchQueue {
|
||||
if let targetQueue = targetQueue {
|
||||
return DispatchQueue(label: label, qos: targetQueue.qos)
|
||||
} else {
|
||||
return DispatchQueue(label: label, qos: qos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,8 +21,9 @@ extension RangeReplaceableCollection {
|
||||
/// - parameter cursor: The cursor whose elements feed the collection.
|
||||
public init<C: Cursor>(_ cursor: C) throws where C.Element == Element {
|
||||
self.init()
|
||||
// Use `forEach` in order to deal with <https://github.com/groue/GRDB.swift/issues/1124>.
|
||||
// See `Statement.forEachStep(_:)` for more information.
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try cursor.forEach { append($0) }
|
||||
}
|
||||
|
||||
@ -38,6 +39,9 @@ extension RangeReplaceableCollection {
|
||||
public init<C: Cursor>(_ cursor: C, minimumCapacity: Int) throws where C.Element == Element {
|
||||
self.init()
|
||||
reserveCapacity(minimumCapacity)
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try cursor.forEach { append($0) }
|
||||
}
|
||||
}
|
||||
@ -58,6 +62,9 @@ extension Dictionary {
|
||||
throws where Value == [C.Element]
|
||||
{
|
||||
self.init()
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try cursor.forEach { value in
|
||||
try self[keyForValue(value), default: []].append(value)
|
||||
}
|
||||
@ -84,6 +91,9 @@ extension Dictionary {
|
||||
throws where Value == [C.Element]
|
||||
{
|
||||
self.init(minimumCapacity: minimumCapacity)
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try cursor.forEach { value in
|
||||
try self[keyForValue(value), default: []].append(value)
|
||||
}
|
||||
@ -106,6 +116,9 @@ extension Dictionary {
|
||||
throws where C.Element == (Key, Value)
|
||||
{
|
||||
self.init()
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try keysAndValues.forEach { key, value in
|
||||
if updateValue(value, forKey: key) != nil {
|
||||
fatalError("Duplicate values for key: '\(String(describing: key))'")
|
||||
@ -135,6 +148,9 @@ extension Dictionary {
|
||||
throws where C.Element == (Key, Value)
|
||||
{
|
||||
self.init(minimumCapacity: minimumCapacity)
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try keysAndValues.forEach { key, value in
|
||||
if updateValue(value, forKey: key) != nil {
|
||||
fatalError("Duplicate values for key: '\(String(describing: key))'")
|
||||
@ -153,6 +169,9 @@ extension Set {
|
||||
/// - parameter cursor: A cursor of values to gather into a set.
|
||||
public init<C: Cursor>(_ cursor: C) throws where C.Element == Element {
|
||||
self.init()
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try cursor.forEach { insert($0) }
|
||||
}
|
||||
|
||||
@ -168,6 +187,9 @@ extension Set {
|
||||
/// storage buffer.
|
||||
public init<C: Cursor>(_ cursor: C, minimumCapacity: Int) throws where C.Element == Element {
|
||||
self.init(minimumCapacity: minimumCapacity)
|
||||
// Prefer `forEach` over `next()` looping, as a slight performance
|
||||
// improvement due to the single `sqlite3_stmt_busy` check for
|
||||
// database cursors.
|
||||
try cursor.forEach { insert($0) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,7 +233,9 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
setupDefaultFunctions()
|
||||
setupDefaultCollations()
|
||||
setupAuthorizer()
|
||||
observationBroker.installCommitAndRollbackHooks()
|
||||
if !configuration.readonly {
|
||||
observationBroker.installCommitAndRollbackHooks()
|
||||
}
|
||||
try activateExtendedCodes()
|
||||
|
||||
#if SQLITE_HAS_CODEC
|
||||
@ -546,38 +548,11 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
finally: endReadOnly)
|
||||
}
|
||||
|
||||
// MARK: - Snapshots
|
||||
|
||||
#if SQLITE_ENABLE_SNAPSHOT
|
||||
/// Returns a snapshot that must be freed with `sqlite3_snapshot_free`.
|
||||
///
|
||||
/// See <https://www.sqlite.org/c3ref/snapshot.html>
|
||||
func takeVersionSnapshot() throws -> UnsafeMutablePointer<sqlite3_snapshot> {
|
||||
var snapshot: UnsafeMutablePointer<sqlite3_snapshot>?
|
||||
let code = withUnsafeMutablePointer(to: &snapshot) {
|
||||
sqlite3_snapshot_get(sqliteConnection, "main", $0)
|
||||
}
|
||||
guard code == SQLITE_OK else {
|
||||
throw DatabaseError(resultCode: code, message: lastErrorMessage)
|
||||
}
|
||||
if let snapshot = snapshot {
|
||||
return snapshot
|
||||
} else {
|
||||
throw DatabaseError(resultCode: .SQLITE_INTERNAL) // WTF SQLite?
|
||||
}
|
||||
/// Returns whether database connection is read-only.
|
||||
var isReadOnly: Bool {
|
||||
_readOnlyDepth > 0 || configuration.readonly
|
||||
}
|
||||
|
||||
func wasChanged(since initialSnapshot: UnsafeMutablePointer<sqlite3_snapshot>) throws -> Bool {
|
||||
let secondSnapshot = try takeVersionSnapshot()
|
||||
defer {
|
||||
sqlite3_snapshot_free(secondSnapshot)
|
||||
}
|
||||
let cmp = sqlite3_snapshot_cmp(initialSnapshot, secondSnapshot)
|
||||
assert(cmp <= 0, "Unexpected snapshot ordering")
|
||||
return cmp < 0
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Recording of the selected region
|
||||
|
||||
func recordingSelection<T>(_ region: inout DatabaseRegion, _ block: () throws -> T) rethrows -> T {
|
||||
@ -939,10 +914,16 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
/// This method is not reentrant: you can't nest transactions.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - kind: The transaction type (default nil). If nil, the transaction
|
||||
/// type is configuration.defaultTransactionKind, which itself
|
||||
/// defaults to .deferred. See <https://www.sqlite.org/lang_transaction.html>
|
||||
/// for more information.
|
||||
/// - kind: The transaction type (default nil).
|
||||
///
|
||||
/// If nil, and the database connection is read-only, the transaction
|
||||
/// kind is `.deferred`.
|
||||
///
|
||||
/// If nil, and the database connection is not read-only, the
|
||||
/// transaction kind is `configuration.defaultTransactionKind`.
|
||||
///
|
||||
/// See <https://www.sqlite.org/lang_transaction.html> for
|
||||
/// more information.
|
||||
/// - block: A block that executes SQL statements and return either
|
||||
/// .commit or .rollback.
|
||||
/// - throws: The error thrown by the block.
|
||||
@ -1005,13 +986,20 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
/// - parameter readOnly: If true, writes are forbidden.
|
||||
func isolated<T>(readOnly: Bool = false, _ block: () throws -> T) throws -> T {
|
||||
var result: T?
|
||||
try inSavepoint {
|
||||
if readOnly {
|
||||
result = try self.readOnly(block)
|
||||
} else {
|
||||
result = try block()
|
||||
if readOnly {
|
||||
// Enter read-only mode before starting a transaction, so that the
|
||||
// transaction commit does not trigger database observation.
|
||||
try self.readOnly {
|
||||
try inSavepoint {
|
||||
result = try block()
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try inSavepoint {
|
||||
result = try block()
|
||||
return .commit
|
||||
}
|
||||
return .commit
|
||||
}
|
||||
return result!
|
||||
}
|
||||
@ -1055,7 +1043,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
//
|
||||
// For those two reasons, we open a transaction instead of a
|
||||
// top-level savepoint.
|
||||
try inTransaction(configuration.defaultTransactionKind, block)
|
||||
try inTransaction { try block() }
|
||||
return
|
||||
}
|
||||
|
||||
@ -1123,13 +1111,20 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
|
||||
/// Begins a database transaction.
|
||||
///
|
||||
/// - parameter kind: The transaction type (default nil). If nil, the
|
||||
/// transaction type is configuration.defaultTransactionKind, which itself
|
||||
/// defaults to .deferred. See <https://www.sqlite.org/lang_transaction.html>
|
||||
/// for more information.
|
||||
/// - throws: The error thrown by the block.
|
||||
/// - parameter kind: The transaction type (default nil).
|
||||
///
|
||||
/// If nil, and the database connection is read-only, the transaction kind
|
||||
/// is `.deferred`.
|
||||
///
|
||||
/// If nil, and the database connection is not read-only, the transaction
|
||||
/// kind is `configuration.defaultTransactionKind`.
|
||||
///
|
||||
/// See <https://www.sqlite.org/lang_transaction.html> for
|
||||
/// more information.
|
||||
/// - throws: A DatabaseError whenever an SQLite error occurs.
|
||||
public func beginTransaction(_ kind: TransactionKind? = nil) throws {
|
||||
let kind = kind ?? configuration.defaultTransactionKind
|
||||
// SQLite throws an error for non-deferred transactions when read-only.
|
||||
let kind = kind ?? (isReadOnly ? .deferred : configuration.defaultTransactionKind)
|
||||
try execute(sql: "BEGIN \(kind.rawValue) TRANSACTION")
|
||||
assert(sqlite3_get_autocommit(sqliteConnection) == 0)
|
||||
}
|
||||
@ -1189,7 +1184,8 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
|
||||
|
||||
// MARK: - Memory Management
|
||||
|
||||
func releaseMemory() {
|
||||
public func releaseMemory() {
|
||||
SchedulingWatchdog.preconditionValidQueue(self)
|
||||
sqlite3_db_release_memory(sqliteConnection)
|
||||
schemaCache.clear()
|
||||
internalStatementCache.clear()
|
||||
|
||||
@ -57,14 +57,17 @@ public final class DatabasePool: DatabaseWriter {
|
||||
readerConfiguration.allowsUnsafeTransactions = false
|
||||
|
||||
var readerCount = 0
|
||||
readerPool = Pool(maximumCount: configuration.maximumReaderCount, makeElement: {
|
||||
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
|
||||
return try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: readerConfiguration,
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "reader.\(readerCount)")
|
||||
})
|
||||
readerPool = Pool(
|
||||
maximumCount: configuration.maximumReaderCount,
|
||||
qos: configuration.readQoS,
|
||||
makeElement: {
|
||||
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
|
||||
return try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: readerConfiguration,
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "reader.\(readerCount)")
|
||||
})
|
||||
|
||||
// Activate WAL Mode unless readonly
|
||||
if !configuration.readonly {
|
||||
@ -100,7 +103,9 @@ public final class DatabasePool: DatabaseWriter {
|
||||
// Be a nice iOS citizen, and don't consume too much memory
|
||||
// See https://github.com/groue/GRDB.swift/#memory-management
|
||||
#if os(iOS)
|
||||
setupMemoryManagement()
|
||||
if configuration.automaticMemoryManagement {
|
||||
setupMemoryManagement()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -151,13 +156,6 @@ public final class DatabasePool: DatabaseWriter {
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
/// Blocks the current thread until all database connections have
|
||||
/// executed the *body* block.
|
||||
fileprivate func forEachConnection(_ body: (Database) -> Void) {
|
||||
writer.sync(body)
|
||||
readerPool?.forEach { $0.sync(body) }
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.6) && canImport(_Concurrency)
|
||||
@ -169,19 +167,49 @@ extension DatabasePool {
|
||||
|
||||
// MARK: - Memory management
|
||||
|
||||
/// Free as much memory as possible.
|
||||
/// Frees as much memory as possible, by disposing non-essential memory from
|
||||
/// the writer connection, and closing all reader connections.
|
||||
///
|
||||
/// This method blocks the current thread until all database accesses
|
||||
/// are completed.
|
||||
/// This method is synchronous, and blocks the current thread until all
|
||||
/// database accesses are completed.
|
||||
///
|
||||
/// - warning: This method can prevent concurrent reads from executing,
|
||||
/// until it returns. Prefer ``releaseMemoryEventually()`` if you intend
|
||||
/// to keep on using the database while releasing memory.
|
||||
public func releaseMemory() {
|
||||
// Release writer memory
|
||||
writer.sync { $0.releaseMemory() }
|
||||
// Release readers memory by closing all connections
|
||||
|
||||
// Release readers memory by closing all connections.
|
||||
//
|
||||
// We must use a barrier in order to guarantee that memory has been
|
||||
// freed (reader connections closed) when the method exits, as
|
||||
// documented.
|
||||
//
|
||||
// Without the barrier, connections would only close _eventually_ (after
|
||||
// their eventual concurrent jobs have completed).
|
||||
readerPool?.barrier {
|
||||
readerPool?.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
/// Eventually frees as much memory as possible, by disposing non-essential
|
||||
/// memory from the writer connection, and closing all reader connections.
|
||||
///
|
||||
/// Unlike ``releaseMemory()``, this method does not prevent concurrent
|
||||
/// database accesses when it is executing. But it does not notify when
|
||||
/// non-essential memory has been freed.
|
||||
public func releaseMemoryEventually() {
|
||||
// Release readers memory by eventually closing all reader connections
|
||||
// (they will close after their current jobs have completed).
|
||||
readerPool?.removeAll()
|
||||
|
||||
// Release writer memory eventually.
|
||||
writer.async { db in
|
||||
db.releaseMemory()
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
/// Listens to UIApplicationDidEnterBackgroundNotification and
|
||||
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
|
||||
@ -213,12 +241,20 @@ extension DatabasePool {
|
||||
|
||||
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
|
||||
if task == .invalid {
|
||||
// Perform releaseMemory() synchronously.
|
||||
// Release memory synchronously
|
||||
releaseMemory()
|
||||
} else {
|
||||
// Perform releaseMemory() asynchronously.
|
||||
DispatchQueue.global().async {
|
||||
self.releaseMemory()
|
||||
// Release memory eventually.
|
||||
//
|
||||
// We don't know when reader connections will be closed (because
|
||||
// they may be currently in use), so we don't quite know when
|
||||
// reader memory will be freed (which would be the ideal timing for
|
||||
// ending our background task).
|
||||
//
|
||||
// So let's just end the background task after the writer connection
|
||||
// has freed its memory. That's better than nothing.
|
||||
releaseMemoryEventually()
|
||||
writer.async { _ in
|
||||
application.endBackgroundTask(task)
|
||||
}
|
||||
}
|
||||
@ -226,9 +262,7 @@ extension DatabasePool {
|
||||
|
||||
@objc
|
||||
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
|
||||
DispatchQueue.global().async {
|
||||
self.releaseMemory()
|
||||
}
|
||||
releaseMemoryEventually()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -330,40 +364,33 @@ extension DatabasePool: DatabaseReader {
|
||||
}
|
||||
|
||||
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
// First async jump in order to grab a reader connection.
|
||||
// Honor configuration dispatching (qos/targetQueue).
|
||||
let label = configuration.identifier(
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "asyncRead")
|
||||
configuration
|
||||
.makeReaderDispatchQueue(label: label)
|
||||
.async {
|
||||
do {
|
||||
guard let readerPool = self.readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
guard let readerPool = self.readerPool else {
|
||||
value(.failure(DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed")))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
do {
|
||||
let (reader, releaseReader) = try result.get()
|
||||
// Second async jump because that's how `Pool.async` has to be used.
|
||||
reader.async { db in
|
||||
defer {
|
||||
try? db.commit() // Ignore commit error
|
||||
releaseReader()
|
||||
}
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
|
||||
// Second async jump because sync could deadlock if
|
||||
// configuration has a serial targetQueue.
|
||||
reader.async { db in
|
||||
defer {
|
||||
try? db.commit() // Ignore commit error
|
||||
releaseReader()
|
||||
}
|
||||
do {
|
||||
// The block isolation comes from the DEFERRED transaction.
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
do {
|
||||
// The block isolation comes from the DEFERRED transaction.
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
@ -381,38 +408,31 @@ extension DatabasePool: DatabaseReader {
|
||||
}
|
||||
|
||||
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
// First async jump in order to grab a reader connection.
|
||||
// Honor configuration dispatching (qos/targetQueue).
|
||||
let label = configuration.identifier(
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "asyncUnsafeRead")
|
||||
configuration
|
||||
.makeReaderDispatchQueue(label: label)
|
||||
.async {
|
||||
do {
|
||||
guard let readerPool = self.readerPool else {
|
||||
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed")
|
||||
guard let readerPool = self.readerPool else {
|
||||
value(.failure(DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed")))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
do {
|
||||
let (reader, releaseReader) = try result.get()
|
||||
// Second async jump because that's how `Pool.async` has to be used.
|
||||
reader.async { db in
|
||||
defer {
|
||||
releaseReader()
|
||||
}
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
|
||||
// Second async jump because sync could deadlock if
|
||||
// configuration has a serial targetQueue.
|
||||
reader.async { db in
|
||||
defer {
|
||||
releaseReader()
|
||||
}
|
||||
do {
|
||||
// The block isolation comes from the DEFERRED transaction.
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
do {
|
||||
// The block isolation comes from the DEFERRED transaction.
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
|
||||
@ -43,7 +43,9 @@ public final class DatabaseQueue: DatabaseWriter {
|
||||
// Be a nice iOS citizen, and don't consume too much memory
|
||||
// See https://github.com/groue/GRDB.swift/#memory-management
|
||||
#if os(iOS)
|
||||
setupMemoryManagement()
|
||||
if configuration.automaticMemoryManagement {
|
||||
setupMemoryManagement()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -117,12 +119,12 @@ extension DatabaseQueue {
|
||||
|
||||
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
|
||||
if task == .invalid {
|
||||
// Perform releaseMemory() synchronously.
|
||||
// Release memory synchronously
|
||||
releaseMemory()
|
||||
} else {
|
||||
// Perform releaseMemory() asynchronously.
|
||||
DispatchQueue.global().async {
|
||||
self.releaseMemory()
|
||||
// Release memory asynchronously
|
||||
writer.async { db in
|
||||
db.releaseMemory()
|
||||
application.endBackgroundTask(task)
|
||||
}
|
||||
}
|
||||
@ -130,8 +132,8 @@ extension DatabaseQueue {
|
||||
|
||||
@objc
|
||||
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
|
||||
DispatchQueue.global().async {
|
||||
self.releaseMemory()
|
||||
writer.async { db in
|
||||
db.releaseMemory()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -14,14 +14,6 @@ public final class DatabaseSnapshot: DatabaseReader {
|
||||
serializedDatabase.configuration
|
||||
}
|
||||
|
||||
#if SQLITE_ENABLE_SNAPSHOT
|
||||
typealias Version = UnsafeMutablePointer<sqlite3_snapshot>
|
||||
// Support for ValueObservation in DatabasePool
|
||||
let version: Version?
|
||||
#else
|
||||
typealias Version = Void
|
||||
#endif
|
||||
|
||||
init(path: String, configuration: Configuration = Configuration(), defaultLabel: String, purpose: String) throws {
|
||||
var configuration = DatabasePool.readerConfiguration(configuration)
|
||||
configuration.allowsUnsafeTransactions = true // Snaphost keeps a long-lived transaction
|
||||
@ -31,8 +23,8 @@ public final class DatabaseSnapshot: DatabaseReader {
|
||||
configuration: configuration,
|
||||
defaultLabel: defaultLabel,
|
||||
purpose: purpose)
|
||||
|
||||
let version: Version? = try serializedDatabase.sync { db in
|
||||
|
||||
try serializedDatabase.sync { db in
|
||||
// Assert WAL mode
|
||||
let journalMode = try String.fetchOne(db, sql: "PRAGMA journal_mode")
|
||||
guard journalMode == "wal" else {
|
||||
@ -44,29 +36,12 @@ public final class DatabaseSnapshot: DatabaseReader {
|
||||
|
||||
// Acquire snapshot isolation
|
||||
try db.internalCachedStatement(sql: "SELECT rootpage FROM sqlite_master LIMIT 1").makeCursor().next()
|
||||
|
||||
#if SQLITE_ENABLE_SNAPSHOT
|
||||
// We must expect an error: https://www.sqlite.org/c3ref/snapshot_get.html
|
||||
// > At least one transaction must be written to it first.
|
||||
return try? db.takeVersionSnapshot()
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
#if SQLITE_ENABLE_SNAPSHOT
|
||||
self.version = version
|
||||
#endif
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Leave snapshot isolation
|
||||
serializedDatabase.reentrantSync { db in
|
||||
#if SQLITE_ENABLE_SNAPSHOT
|
||||
if let version = version {
|
||||
sqlite3_snapshot_free(version)
|
||||
}
|
||||
#endif
|
||||
try? db.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +287,7 @@ extension Row {
|
||||
/// The result is nil if the row does not contain the column.
|
||||
public subscript(_ columnName: String) -> DatabaseValueConvertible? {
|
||||
// IMPLEMENTATION NOTE
|
||||
// This method has a single know use case: checking if the value is nil,
|
||||
// This method has a single known use case: checking if the value is nil,
|
||||
// as in:
|
||||
//
|
||||
// if row["foo"] != nil { ... }
|
||||
@ -1285,7 +1285,7 @@ extension Row {
|
||||
throws -> [Row]
|
||||
{
|
||||
// The cursor reuses a single mutable row. Return immutable copies.
|
||||
return try Array(fetchCursor(statement, arguments: arguments, adapter: adapter).map { $0.copy() })
|
||||
try Array(fetchCursor(statement, arguments: arguments, adapter: adapter).map { $0.copy() })
|
||||
}
|
||||
|
||||
/// Returns a set of rows fetched from a prepared statement.
|
||||
@ -1306,7 +1306,7 @@ extension Row {
|
||||
throws -> Set<Row>
|
||||
{
|
||||
// The cursor reuses a single mutable row. Return immutable copies.
|
||||
return try Set(fetchCursor(statement, arguments: arguments, adapter: adapter).map { $0.copy() })
|
||||
try Set(fetchCursor(statement, arguments: arguments, adapter: adapter).map { $0.copy() })
|
||||
}
|
||||
|
||||
/// Returns a single row fetched from a prepared statement.
|
||||
@ -2089,27 +2089,27 @@ protocol RowImpl {
|
||||
extension RowImpl {
|
||||
func copiedRow(_ row: Row) -> Row {
|
||||
// unless customized, assume unsafe and unadapted row
|
||||
return Row(impl: ArrayRowImpl(columns: Array(row)))
|
||||
Row(impl: ArrayRowImpl(columns: Array(row)))
|
||||
}
|
||||
|
||||
func unscopedRow(_ row: Row) -> Row {
|
||||
// unless customized, assume unadapted row (see AdaptedRowImpl for customization)
|
||||
return row
|
||||
row
|
||||
}
|
||||
|
||||
func unadaptedRow(_ row: Row) -> Row {
|
||||
// unless customized, assume unadapted row (see AdaptedRowImpl for customization)
|
||||
return row
|
||||
row
|
||||
}
|
||||
|
||||
func scopes(prefetchedRows: Row.PrefetchedRowsView) -> Row.ScopesView {
|
||||
// unless customized, assume unuscoped row (see AdaptedRowImpl for customization)
|
||||
return Row.ScopesView()
|
||||
Row.ScopesView()
|
||||
}
|
||||
|
||||
func hasNull(atUncheckedIndex index: Int) -> Bool {
|
||||
// unless customized, use slow check (see StatementRowImpl and AdaptedRowImpl for customization)
|
||||
return databaseValue(atUncheckedIndex: index).isNull
|
||||
databaseValue(atUncheckedIndex: index).isNull
|
||||
}
|
||||
|
||||
func fastDecode<Value: DatabaseValueConvertible & StatementColumnConvertible>(
|
||||
@ -2118,7 +2118,7 @@ extension RowImpl {
|
||||
throws -> Value
|
||||
{
|
||||
// unless customized, use slow decoding (see StatementRowImpl and AdaptedRowImpl for customization)
|
||||
return try Value.decode(
|
||||
try Value.decode(
|
||||
fromDatabaseValue: databaseValue(atUncheckedIndex: index),
|
||||
context: RowDecodingContext(row: Row(impl: self), key: .columnIndex(index)))
|
||||
}
|
||||
@ -2129,21 +2129,21 @@ extension RowImpl {
|
||||
throws -> Value?
|
||||
{
|
||||
// unless customized, use slow decoding (see StatementRowImpl and AdaptedRowImpl for customization)
|
||||
return try Value.decodeIfPresent(
|
||||
try Value.decodeIfPresent(
|
||||
fromDatabaseValue: databaseValue(atUncheckedIndex: index),
|
||||
context: RowDecodingContext(row: Row(impl: self), key: .columnIndex(index)))
|
||||
}
|
||||
|
||||
func fastDecodeDataNoCopy(atUncheckedIndex index: Int) throws -> Data {
|
||||
// unless customized, copy data (see StatementRowImpl and AdaptedRowImpl for customization)
|
||||
return try Data.decode(
|
||||
try Data.decode(
|
||||
fromDatabaseValue: databaseValue(atUncheckedIndex: index),
|
||||
context: RowDecodingContext(row: Row(impl: self), key: .columnIndex(index)))
|
||||
}
|
||||
|
||||
func fastDecodeDataNoCopyIfPresent(atUncheckedIndex index: Int) throws -> Data? {
|
||||
// unless customized, copy data (see StatementRowImpl and AdaptedRowImpl for customization)
|
||||
return try Data.decodeIfPresent(
|
||||
try Data.decodeIfPresent(
|
||||
fromDatabaseValue: databaseValue(atUncheckedIndex: index),
|
||||
context: RowDecodingContext(row: Row(impl: self), key: .columnIndex(index)))
|
||||
}
|
||||
@ -2254,7 +2254,7 @@ private struct StatementRowImpl: RowImpl {
|
||||
|
||||
func hasNull(atUncheckedIndex index: Int) -> Bool {
|
||||
// Avoid extracting values, because this modifies the SQLite statement.
|
||||
return sqlite3_column_type(sqliteStatement, Int32(index)) == SQLITE_NULL
|
||||
sqlite3_column_type(sqliteStatement, Int32(index)) == SQLITE_NULL
|
||||
}
|
||||
|
||||
func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue {
|
||||
|
||||
@ -316,24 +316,15 @@ public final class Statement {
|
||||
|
||||
/// Calls the given closure after each successful call to `sqlite3_step()`.
|
||||
///
|
||||
/// Unlike multiple calls to `step(_:)`, this method is able to deal with
|
||||
/// statements that need a specific authorizer.
|
||||
///
|
||||
/// That's how we deal with TransactionObservers that observe deletion:
|
||||
/// the authorizer prevents the truncate optimization
|
||||
/// <https://www.sqlite.org/lang_delete.html#the_truncate_optimization>.
|
||||
///
|
||||
/// That's also how we deal with <https://github.com/groue/GRDB.swift/issues/1124>,
|
||||
/// in four steps:
|
||||
///
|
||||
/// 1. `T.fetchAll(...)` calls `Array(T.fetchCursor(...))`
|
||||
/// 2. `Array(T.fetchCursor(...))` calls `Cursor.forEach(...)`
|
||||
/// 3. `DatabaseCursor.forEach(...)` calls `Statement.forEachStep(...)`
|
||||
/// 4. `Statement.forEachStep(...)` deals with the eventual authorizer.
|
||||
/// This method is slighly faster than calling `step(_:)` repeatedly, due
|
||||
/// to the single `sqlite3_stmt_busy` check.
|
||||
@usableFromInline
|
||||
func forEachStep(_ body: (SQLiteStatement) throws -> Void) throws {
|
||||
SchedulingWatchdog.preconditionValidQueue(database)
|
||||
try database.statementWillExecute(self)
|
||||
|
||||
if sqlite3_stmt_busy(sqliteStatement) == 0 {
|
||||
try database.statementWillExecute(self)
|
||||
}
|
||||
|
||||
while true {
|
||||
switch sqlite3_step(sqliteStatement) {
|
||||
@ -349,12 +340,8 @@ public final class Statement {
|
||||
}
|
||||
|
||||
/// Calls the given closure after one successful call to `sqlite3_step()`.
|
||||
///
|
||||
/// This method is unable to deal with statements that need a specific
|
||||
/// authorizer. See `forEachStep(_:)`.
|
||||
@usableFromInline
|
||||
func step<Element>(_ body: (SQLiteStatement) throws -> Element) throws -> Element? {
|
||||
// This check takes 0 time when profiled. It is, practically speaking, free.
|
||||
func step<T>(_ body: (SQLiteStatement) throws -> T) throws -> T? {
|
||||
if sqlite3_stmt_busy(sqliteStatement) == 0 {
|
||||
try database.statementWillExecute(self)
|
||||
}
|
||||
@ -439,9 +426,8 @@ extension DatabaseCursor {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Specific implementation of `forEach` in order to deal with
|
||||
// <https://github.com/groue/GRDB.swift/issues/1124>.
|
||||
// See `Statement.forEachStep(_:)` for more information.
|
||||
/// Specific implementation of `forEach`, for a slight performance
|
||||
/// improvement due to the single `sqlite3_stmt_busy` check.
|
||||
@inlinable
|
||||
public func forEach(_ body: (Element) throws -> Void) throws {
|
||||
if _isDone { return }
|
||||
|
||||
@ -220,8 +220,7 @@ class DatabaseObservationBroker {
|
||||
|
||||
/// Prepares observation of changes that are about to be performed by the statement.
|
||||
func statementWillExecute(_ statement: Statement) {
|
||||
// If any observer observes row deletions, we'll have to disable
|
||||
if transactionObservations.isEmpty == false {
|
||||
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()
|
||||
@ -310,7 +309,7 @@ class DatabaseObservationBroker {
|
||||
// SQLITE_CONSTRAINT error)
|
||||
databaseDidRollback(notifyTransactionObservers: false)
|
||||
case .cancelledCommit(let error):
|
||||
databaseDidRollback(notifyTransactionObservers: true)
|
||||
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
|
||||
throw error
|
||||
default:
|
||||
break
|
||||
@ -382,7 +381,7 @@ class DatabaseObservationBroker {
|
||||
case .commit:
|
||||
databaseDidCommit()
|
||||
case .rollback:
|
||||
databaseDidRollback(notifyTransactionObservers: true)
|
||||
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -391,6 +390,8 @@ class DatabaseObservationBroker {
|
||||
#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 (observation, predicate) in statementObservations where predicate.evaluate(event) {
|
||||
@ -405,6 +406,8 @@ class DatabaseObservationBroker {
|
||||
|
||||
// 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()
|
||||
@ -430,8 +433,10 @@ class DatabaseObservationBroker {
|
||||
// Called from sqlite3_commit_hook and databaseDidCommitEmptyDeferredTransaction()
|
||||
private func databaseWillCommit() throws {
|
||||
notifyBufferedEvents()
|
||||
for observation in transactionObservations {
|
||||
try observation.databaseWillCommit()
|
||||
if !database.isReadOnly {
|
||||
for observation in transactionObservations {
|
||||
try observation.databaseWillCommit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -439,9 +444,12 @@ class DatabaseObservationBroker {
|
||||
private func databaseDidCommit() {
|
||||
savepointStack.clear()
|
||||
|
||||
for observation in transactionObservations {
|
||||
observation.databaseDidCommit(database)
|
||||
if !database.isReadOnly {
|
||||
for observation in transactionObservations {
|
||||
observation.databaseDidCommit(database)
|
||||
}
|
||||
}
|
||||
|
||||
databaseDidEndTransaction()
|
||||
}
|
||||
|
||||
@ -485,7 +493,7 @@ class DatabaseObservationBroker {
|
||||
try databaseWillCommit()
|
||||
databaseDidCommit()
|
||||
} catch {
|
||||
databaseDidRollback(notifyTransactionObservers: true)
|
||||
databaseDidRollback(notifyTransactionObservers: !database.isReadOnly)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -495,6 +503,7 @@ class DatabaseObservationBroker {
|
||||
savepointStack.clear()
|
||||
|
||||
if notifyTransactionObservers {
|
||||
assert(!database.isReadOnly, "Read-only transactions are not notified")
|
||||
for observation in transactionObservations {
|
||||
observation.databaseDidRollback(database)
|
||||
}
|
||||
@ -566,6 +575,7 @@ class DatabaseObservationBroker {
|
||||
savepointStack.clear()
|
||||
|
||||
for (event, statementObservations) in eventsBuffer {
|
||||
assert(statementObservations.isEmpty || !database.isReadOnly, "Read-only transactions are not notified")
|
||||
for (observation, predicate) in statementObservations where predicate.evaluate(event) {
|
||||
event.send(to: observation)
|
||||
}
|
||||
|
||||
85
GRDB.swift/GRDB/Core/WALSnapshot.swift
Normal file
85
GRDB.swift/GRDB/Core/WALSnapshot.swift
Normal file
@ -0,0 +1,85 @@
|
||||
/// An instance of WALSnapshot records the state of a WAL mode database for some
|
||||
/// specific point in history.
|
||||
///
|
||||
/// We use `WALSnapshot` to help `ValueObservation` check for changes
|
||||
/// that would happen between the initial fetch, and the start of the
|
||||
/// actual observation. This class has no other purpose, and is not intended to
|
||||
/// become public.
|
||||
///
|
||||
/// It does not work with SQLCipher, because SQLCipher does not support
|
||||
/// `SQLITE_ENABLE_SNAPSHOT` correctly: we have linker errors.
|
||||
/// See <https://github.com/ericsink/SQLitePCL.raw/issues/452>.
|
||||
///
|
||||
/// With custom SQLite builds, it only works if `SQLITE_ENABLE_SNAPSHOT`
|
||||
/// is defined.
|
||||
///
|
||||
/// With system SQLite, it can only work when the SDK exposes the C apis and
|
||||
/// their availability, which means XCode 14 (identified with Swift 5.7).
|
||||
///
|
||||
/// Yes, this is an awfully complex logic.
|
||||
///
|
||||
/// See <https://www.sqlite.org/c3ref/snapshot.html>.
|
||||
final class WALSnapshot {
|
||||
#if GRDBCIPHER || (GRDBCUSTOMSQLITE && !SQLITE_ENABLE_SNAPSHOT) || compiler(<5.7)
|
||||
init?(_ db: Database) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func compare(_ other: WALSnapshot) -> CInt {
|
||||
preconditionFailure("snapshots are not available")
|
||||
}
|
||||
#else
|
||||
private let snapshot: UnsafeMutablePointer<sqlite3_snapshot>?
|
||||
|
||||
/// Returns nil if `SQLITE_ENABLE_SNAPSHOT` is not enabled, or if an
|
||||
/// error occurs.
|
||||
init?(_ db: Database) {
|
||||
var snapshot: UnsafeMutablePointer<sqlite3_snapshot>?
|
||||
let code: CInt = withUnsafeMutablePointer(to: &snapshot) {
|
||||
#if GRDBCUSTOMSQLITE
|
||||
return sqlite3_snapshot_get(db.sqliteConnection, "main", $0)
|
||||
#else
|
||||
// iOS 10.0 is always true because our minimum requirement is iOS 11.
|
||||
if #available(macOS 10.12, watchOS 3.0, tvOS 10.0, *) {
|
||||
return sqlite3_snapshot_get(db.sqliteConnection, "main", $0)
|
||||
} else {
|
||||
return SQLITE_ERROR
|
||||
}
|
||||
#endif
|
||||
}
|
||||
guard code == SQLITE_OK, let s = snapshot else {
|
||||
return nil
|
||||
}
|
||||
self.snapshot = s
|
||||
}
|
||||
|
||||
deinit {
|
||||
#if GRDBCUSTOMSQLITE
|
||||
sqlite3_snapshot_free(snapshot)
|
||||
#else
|
||||
// iOS 10.0 is always true because our minimum requirement is iOS 11.
|
||||
if #available(macOS 10.12, watchOS 3.0, tvOS 10.0, *) {
|
||||
sqlite3_snapshot_free(snapshot)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Compares two WAL snapshots.
|
||||
///
|
||||
/// `a.compare(b) < 0` iff a is older than b.
|
||||
///
|
||||
/// See <https://www.sqlite.org/c3ref/snapshot_cmp.html>.
|
||||
func compare(_ other: WALSnapshot) -> CInt {
|
||||
#if GRDBCUSTOMSQLITE
|
||||
return sqlite3_snapshot_cmp(snapshot, other.snapshot)
|
||||
#else
|
||||
// iOS 10.0 is always true because our minimum requirement is iOS 11.
|
||||
if #available(macOS 10.12, watchOS 3.0, tvOS 10.0, *) {
|
||||
return sqlite3_snapshot_cmp(snapshot, other.snapshot)
|
||||
} else {
|
||||
preconditionFailure("snapshots are not available")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif // GRDBCIPHER || (GRDBCUSTOMSQLITE && !SQLITE_ENABLE_SNAPSHOT) || compiler(<5.7)
|
||||
}
|
||||
@ -314,19 +314,14 @@ extension ValueObservation {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public func combine<
|
||||
R1: _ValueReducer,
|
||||
Combined>(
|
||||
public func combine<R1, Combined>(
|
||||
_ other: ValueObservation<R1>,
|
||||
_ transform: @escaping (Reducer.Value, R1.Value) -> Combined)
|
||||
-> ValueObservation<ValueReducers.Unavailable<Combined>>
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
Combined>(
|
||||
public func combine<R1, R2, Combined>(
|
||||
_ observation1: ValueObservation<R1>,
|
||||
_ observation2: ValueObservation<R2>,
|
||||
_ transform: @escaping (Reducer.Value, R1.Value, R2.Value) -> Combined)
|
||||
@ -334,11 +329,7 @@ extension ValueObservation {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
Combined>(
|
||||
public func combine<R1, R2, R3, Combined>(
|
||||
_ observation1: ValueObservation<R1>,
|
||||
_ observation2: ValueObservation<R2>,
|
||||
_ observation3: ValueObservation<R3>,
|
||||
@ -347,12 +338,7 @@ extension ValueObservation {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
R4: _ValueReducer,
|
||||
Combined>(
|
||||
public func combine<R1, R2, R3, R4, Combined>(
|
||||
_ observation1: ValueObservation<R1>,
|
||||
_ observation2: ValueObservation<R2>,
|
||||
_ observation3: ValueObservation<R3>,
|
||||
@ -374,19 +360,14 @@ extension ValueObservation {
|
||||
|
||||
extension ValueObservation where Reducer == ValueReducers.Auto {
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer>(
|
||||
public static func combine<R1, R2>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>)
|
||||
-> ValueObservation<ValueReducers.Unavailable<(R1.Value, R2.Value)>>
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer>(
|
||||
public static func combine<R1, R2, R3>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>,
|
||||
_ o3: ValueObservation<R3>)
|
||||
@ -394,11 +375,7 @@ extension ValueObservation where Reducer == ValueReducers.Auto {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
R4: _ValueReducer>(
|
||||
public static func combine<R1, R2, R3, R4>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>,
|
||||
_ o3: ValueObservation<R3>,
|
||||
@ -407,12 +384,7 @@ extension ValueObservation where Reducer == ValueReducers.Auto {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
R4: _ValueReducer,
|
||||
R5: _ValueReducer>(
|
||||
public static func combine<R1, R2, R3, R4, R5>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>,
|
||||
_ o3: ValueObservation<R3>,
|
||||
@ -422,13 +394,7 @@ extension ValueObservation where Reducer == ValueReducers.Auto {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
R4: _ValueReducer,
|
||||
R5: _ValueReducer,
|
||||
R6: _ValueReducer>(
|
||||
public static func combine<R1, R2, R3, R4, R5, R6>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>,
|
||||
_ o3: ValueObservation<R3>,
|
||||
@ -439,14 +405,7 @@ extension ValueObservation where Reducer == ValueReducers.Auto {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
R4: _ValueReducer,
|
||||
R5: _ValueReducer,
|
||||
R6: _ValueReducer,
|
||||
R7: _ValueReducer>(
|
||||
public static func combine<R1, R2, R3, R4, R5, R6, R7>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>,
|
||||
_ o3: ValueObservation<R3>,
|
||||
@ -458,15 +417,7 @@ extension ValueObservation where Reducer == ValueReducers.Auto {
|
||||
{ preconditionFailure() }
|
||||
|
||||
@available(*, unavailable, message: "combine is no longer available. See the \"Migrating From GRDB 4 to GRDB 5\" guide.")
|
||||
public static func combine<
|
||||
R1: _ValueReducer,
|
||||
R2: _ValueReducer,
|
||||
R3: _ValueReducer,
|
||||
R4: _ValueReducer,
|
||||
R5: _ValueReducer,
|
||||
R6: _ValueReducer,
|
||||
R7: _ValueReducer,
|
||||
R8: _ValueReducer>(
|
||||
public static func combine<R1, R2, R3, R4, R5, R6, R7, R8>(
|
||||
_ o1: ValueObservation<R1>,
|
||||
_ o2: ValueObservation<R2>,
|
||||
_ o3: ValueObservation<R3>,
|
||||
|
||||
@ -325,7 +325,7 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest {
|
||||
|
||||
/// Creates a request filtered by unique key.
|
||||
///
|
||||
/// // SELECT * FROM player WHERE ... email = 'arthur@example.com' OR ...
|
||||
/// // SELECT * FROM player WHERE ... email = 'arthur@example.com' AND ...
|
||||
/// let request = try Player...filter(keys: [["email": "arthur@example.com"], ...])
|
||||
///
|
||||
/// When executed, this request raises a fatal error if there is no unique
|
||||
|
||||
@ -196,8 +196,6 @@ extension FetchableRecord {
|
||||
adapter: RowAdapter? = nil)
|
||||
throws -> [Self]
|
||||
{
|
||||
// Use Array initializer in order to deal with <https://github.com/groue/GRDB.swift/issues/1124>.
|
||||
// See `Statement.forEachStep(_:)` for more information.
|
||||
try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
|
||||
|
||||
@ -51,13 +51,26 @@ final class Pool<T> {
|
||||
private let itemsSemaphore: DispatchSemaphore // limits the number of elements
|
||||
private let itemsGroup: DispatchGroup // knows when no element is used
|
||||
private let barrierQueue: DispatchQueue
|
||||
private let semaphoreWaitingQueue: DispatchQueue // Inspired by https://khanlou.com/2016/04/the-GCD-handbook/
|
||||
|
||||
init(maximumCount: Int, makeElement: @escaping () throws -> T) {
|
||||
/// Creates a Pool.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - maximumCount: The maximum number of elements.
|
||||
/// - qos: The quality of service of asynchronous accesses.
|
||||
/// - makeElement: A function that creates an element. It is called
|
||||
/// on demand.
|
||||
init(
|
||||
maximumCount: Int,
|
||||
qos: DispatchQoS = .unspecified,
|
||||
makeElement: @escaping () throws -> T)
|
||||
{
|
||||
GRDBPrecondition(maximumCount > 0, "Pool size must be at least 1")
|
||||
self.makeElement = makeElement
|
||||
self.itemsSemaphore = DispatchSemaphore(value: maximumCount)
|
||||
self.itemsGroup = DispatchGroup()
|
||||
self.barrierQueue = DispatchQueue(label: "GRDB.Pool.barrier", attributes: [.concurrent])
|
||||
self.barrierQueue = DispatchQueue(label: "GRDB.Pool.barrier", qos: qos, attributes: [.concurrent])
|
||||
self.semaphoreWaitingQueue = DispatchQueue(label: "GRDB.Pool.wait", qos: qos)
|
||||
}
|
||||
|
||||
/// Returns a tuple (element, release)
|
||||
@ -87,6 +100,25 @@ final class Pool<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Eventually produces a tuple (element, release), where element is
|
||||
/// intended to be used asynchronously.
|
||||
///
|
||||
/// Client must call release(), only once, after the element has been used.
|
||||
///
|
||||
/// - important: The `execute` argument is executed in a serial dispatch
|
||||
/// queue, so make sure you use the element asynchronously.
|
||||
func asyncGet(_ execute: @escaping (Result<(element: T, release: () -> Void), Error>) -> Void) {
|
||||
// Inspired by https://khanlou.com/2016/04/the-GCD-handbook/
|
||||
// > We wait on the semaphore in the serial queue, which means that
|
||||
// > we’ll have at most one blocked thread when we reach maximum
|
||||
// > executing blocks on the concurrent queue. Any other tasks the user
|
||||
// > enqueues will sit inertly on the serial queue waiting to be
|
||||
// > executed, and won’t cause new threads to be started.
|
||||
semaphoreWaitingQueue.async {
|
||||
execute(Result { try self.get() })
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a synchronous block with an element. The element turns
|
||||
/// available after the block has executed.
|
||||
func get<U>(block: (T) throws -> U) throws -> U {
|
||||
|
||||
@ -138,9 +138,11 @@ final class ValueConcurrentObserver<Reducer: ValueReducer> {
|
||||
fetch: reducer._fetch)
|
||||
self.notificationCallbacks = NotificationCallbacks(events: events, onChange: onChange)
|
||||
self.reducer = reducer
|
||||
self.reduceQueue = dbPool.configuration.makeDispatchQueue(label: dbPool.configuration.identifier(
|
||||
defaultLabel: "GRDB",
|
||||
purpose: "ValueObservation"))
|
||||
self.reduceQueue = DispatchQueue(
|
||||
label: dbPool.configuration.identifier(
|
||||
defaultLabel: "GRDB",
|
||||
purpose: "ValueObservation"),
|
||||
qos: dbPool.configuration.readQoS)
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,6 +186,9 @@ final class ValueConcurrentObserver<Reducer: ValueReducer> {
|
||||
// 1. Start the observation without waiting for a write access (the expected
|
||||
// benefit of `DatabasePool`).
|
||||
// 2. Make sure we do not miss a change (a documented guarantee)
|
||||
//
|
||||
// Support for `SQLITE_ENABLE_SNAPSHOT` is implemented by our
|
||||
// `WALSnapshot` class.
|
||||
extension ValueConcurrentObserver {
|
||||
// Starts the observation
|
||||
func start() -> DatabaseCancellable {
|
||||
@ -232,7 +237,6 @@ extension ValueConcurrentObserver {
|
||||
}
|
||||
}
|
||||
|
||||
#if SQLITE_ENABLE_SNAPSHOT
|
||||
// MARK: - Starting the Observation (with SQLITE_ENABLE_SNAPSHOT)
|
||||
|
||||
extension ValueConcurrentObserver {
|
||||
@ -246,14 +250,23 @@ extension ValueConcurrentObserver {
|
||||
// transaction to complete.
|
||||
//
|
||||
// Fetch value & tracked region in a synchronous way.
|
||||
//
|
||||
// TODO: we currently perform the initial read from a snapshot, because
|
||||
// it is a handy way to keep a read transaction open until we grab a
|
||||
// write access, and compare the database versions. The problem is that
|
||||
// we do not control the number of created shapshots: we should instead
|
||||
// use a reader from the pool.
|
||||
let initialSnapshot = try databaseAccess.dbPool.makeSnapshot()
|
||||
let (fetchedValue, initialRegion): (Reducer.Fetched, DatabaseRegion) = try initialSnapshot.read { db in
|
||||
let (fetchedValue, initialRegion, initialWALSnapshot) = try initialSnapshot.read {
|
||||
db -> (Reducer.Fetched, DatabaseRegion, WALSnapshot?) in
|
||||
// swiftlint:disable:previous closure_parameter_position
|
||||
|
||||
switch trackingMode {
|
||||
case let .constantRegion(regions):
|
||||
let fetchedValue = try databaseAccess.fetch(db)
|
||||
let region = try DatabaseRegion.union(regions)(db)
|
||||
let initialRegion = try region.observableRegion(db)
|
||||
return (fetchedValue, initialRegion)
|
||||
return (fetchedValue, initialRegion, WALSnapshot(db))
|
||||
|
||||
case .constantRegionRecordedFromSelection,
|
||||
.nonConstantRegionRecordedFromSelection:
|
||||
@ -262,7 +275,7 @@ extension ValueConcurrentObserver {
|
||||
try databaseAccess.fetch(db)
|
||||
}
|
||||
let initialRegion = try region.observableRegion(db)
|
||||
return (fetchedValue, initialRegion)
|
||||
return (fetchedValue, initialRegion, WALSnapshot(db))
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,6 +291,7 @@ extension ValueConcurrentObserver {
|
||||
asyncStartObservation(
|
||||
from: databaseAccess,
|
||||
initialSnapshot: initialSnapshot,
|
||||
initialWALSnapshot: initialWALSnapshot,
|
||||
initialRegion: initialRegion)
|
||||
|
||||
return initialValue
|
||||
@ -291,6 +305,12 @@ extension ValueConcurrentObserver {
|
||||
// for observing the database is to be able to fetch the initial value
|
||||
// without having to wait for an eventual long-running write
|
||||
// transaction to complete.
|
||||
//
|
||||
// TODO: we currently perform the initial read from a snapshot, because
|
||||
// it is a handy way to keep a read transaction open until we grab a
|
||||
// write access, and compare the database versions. The problem is that
|
||||
// we do not control the number of created shapshots: we should instead
|
||||
// use a reader from the pool.
|
||||
do {
|
||||
let initialSnapshot = try databaseAccess.dbPool.makeSnapshot()
|
||||
initialSnapshot.asyncRead { dbResult in
|
||||
@ -342,6 +362,7 @@ extension ValueConcurrentObserver {
|
||||
self.asyncStartObservation(
|
||||
from: databaseAccess,
|
||||
initialSnapshot: initialSnapshot,
|
||||
initialWALSnapshot: WALSnapshot(db),
|
||||
initialRegion: initialRegion)
|
||||
} catch {
|
||||
self.notifyError(error)
|
||||
@ -355,6 +376,7 @@ extension ValueConcurrentObserver {
|
||||
private func asyncStartObservation(
|
||||
from databaseAccess: DatabaseAccess,
|
||||
initialSnapshot: DatabaseSnapshot,
|
||||
initialWALSnapshot: WALSnapshot?,
|
||||
initialRegion: DatabaseRegion)
|
||||
{
|
||||
databaseAccess.dbPool.asyncWriteWithoutTransaction { writerDB in
|
||||
@ -369,13 +391,17 @@ extension ValueConcurrentObserver {
|
||||
try writerDB.isolated(readOnly: true) {
|
||||
// Keep DatabaseSnaphot alive until we have compared
|
||||
// database versions. It prevents database checkpointing,
|
||||
// and keeps versions (`sqlite3_snapshot`) valid
|
||||
// and keeps WAL snapshots (`sqlite3_snapshot`) valid
|
||||
// and comparable.
|
||||
let isModified: Bool = try withExtendedLifetime(initialSnapshot) {
|
||||
guard let initialVersion = initialSnapshot.version else {
|
||||
let isModified: Bool = withExtendedLifetime(initialSnapshot) {
|
||||
guard let initialWALSnapshot = initialWALSnapshot,
|
||||
let currentWALSnapshot = WALSnapshot(writerDB)
|
||||
else {
|
||||
return true
|
||||
}
|
||||
return try writerDB.wasChanged(since: initialVersion)
|
||||
let ordering = initialWALSnapshot.compare(currentWALSnapshot)
|
||||
assert(ordering <= 0, "Unexpected snapshot ordering")
|
||||
return ordering < 0
|
||||
}
|
||||
|
||||
if isModified {
|
||||
@ -436,156 +462,6 @@ extension ValueConcurrentObserver {
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
// MARK: - Starting the Observation (without SQLITE_ENABLE_SNAPSHOT)
|
||||
|
||||
extension ValueConcurrentObserver {
|
||||
/// Synchronously starts the observation, and returns the initial value.
|
||||
///
|
||||
/// Unlike `asyncStart()`, this method does not notify the initial value or error.
|
||||
private func syncStart(from databaseAccess: DatabaseAccess) throws -> Reducer.Value {
|
||||
// Start from a read access. The whole point of using a DatabasePool
|
||||
// for observing the database is to be able to fetch the initial value
|
||||
// without having to wait for an eventual long-running write
|
||||
// transaction to complete.
|
||||
//
|
||||
// Fetch in a synchronous reentrant way, in case this method is called
|
||||
// from a database access.
|
||||
let fetchedValue = try databaseAccess.dbPool.unsafeReentrantRead { db in
|
||||
try db.isolated(readOnly: true) {
|
||||
try databaseAccess.fetch(db)
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce
|
||||
let initialValue: Reducer.Value = reduceQueue.sync {
|
||||
guard let initialValue = reducer._value(fetchedValue) else {
|
||||
fatalError("Broken contract: reducer has no initial value")
|
||||
}
|
||||
return initialValue
|
||||
}
|
||||
|
||||
// Start observation
|
||||
asyncStartObservation(from: databaseAccess)
|
||||
|
||||
return initialValue
|
||||
}
|
||||
|
||||
/// Asynchronously starts the observation
|
||||
///
|
||||
/// Unlike `syncStart()`, this method does notify the initial value or error.
|
||||
private func asyncStart(from databaseAccess: DatabaseAccess) {
|
||||
// Start from a read access. The whole point of using a DatabasePool
|
||||
// for observing the database is to be able to fetch the initial value
|
||||
// without having to wait for an eventual long-running write
|
||||
// transaction to complete.
|
||||
databaseAccess.dbPool.asyncRead { dbResult in
|
||||
let isNotifying = self.lock.synchronized { self.notificationCallbacks != nil }
|
||||
guard isNotifying else { return /* Cancelled */ }
|
||||
|
||||
do {
|
||||
// Fetch
|
||||
let fetchedValue = try databaseAccess.fetch(dbResult.get())
|
||||
|
||||
// Reduce
|
||||
//
|
||||
// Reducing is performed asynchronously, so that we do not lock
|
||||
// a database dispatch queue longer than necessary.
|
||||
self.reduceQueue.async {
|
||||
let isNotifying = self.lock.synchronized { self.notificationCallbacks != nil }
|
||||
guard isNotifying else { return /* Cancelled */ }
|
||||
|
||||
guard let initialValue = self.reducer._value(fetchedValue) else {
|
||||
fatalError("Broken contract: reducer has no initial value")
|
||||
}
|
||||
|
||||
// Notify
|
||||
self.scheduler.schedule {
|
||||
// TODO: [SR-214] remove -Opt suffix when we only support Xcode 12.5.1+
|
||||
let onChangeOpt = self.lock.synchronized { self.notificationCallbacks?.onChange }
|
||||
guard let onChange = onChangeOpt else { return /* Cancelled */ }
|
||||
onChange(initialValue)
|
||||
}
|
||||
|
||||
// Start observation
|
||||
self.asyncStartObservation(from: databaseAccess)
|
||||
}
|
||||
} catch {
|
||||
self.notifyError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func asyncStartObservation(from databaseAccess: DatabaseAccess) {
|
||||
databaseAccess.dbPool.asyncWriteWithoutTransaction { writerDB in
|
||||
// TODO: [SR-214] remove -Opt suffix when we only support Xcode 12.5.1+
|
||||
let eventsOpt = self.lock.synchronized { self.notificationCallbacks?.events }
|
||||
guard let events = eventsOpt else { return /* Cancelled */ }
|
||||
|
||||
// We don't know if database has changed or not.
|
||||
// So we assume it did, so that we are sure that we do
|
||||
// not miss a database change.
|
||||
events.databaseDidChange?()
|
||||
|
||||
do {
|
||||
// Fetch
|
||||
let fetchedValue: Reducer.Fetched
|
||||
|
||||
switch self.trackingMode {
|
||||
case let .constantRegion(regions):
|
||||
fetchedValue = try writerDB.isolated(readOnly: true) {
|
||||
try databaseAccess.fetch(writerDB)
|
||||
}
|
||||
let region = try DatabaseRegion.union(regions)(writerDB)
|
||||
let observedRegion = try region.observableRegion(writerDB)
|
||||
events.willTrackRegion?(observedRegion)
|
||||
self.startObservation(writerDB, observedRegion: observedRegion)
|
||||
|
||||
case .constantRegionRecordedFromSelection,
|
||||
.nonConstantRegionRecordedFromSelection:
|
||||
var region = DatabaseRegion()
|
||||
fetchedValue = try writerDB.recordingSelection(®ion) {
|
||||
try writerDB.isolated(readOnly: true) {
|
||||
try databaseAccess.fetch(writerDB)
|
||||
}
|
||||
}
|
||||
let observedRegion = try region.observableRegion(writerDB)
|
||||
events.willTrackRegion?(observedRegion)
|
||||
self.startObservation(writerDB, observedRegion: observedRegion)
|
||||
}
|
||||
|
||||
// Reduce
|
||||
//
|
||||
// Reducing is performed asynchronously, so that we do not lock
|
||||
// the writer dispatch queue longer than necessary.
|
||||
//
|
||||
// Important: reduceQueue.async guarantees the same ordering
|
||||
// between transactions and notifications!
|
||||
self.reduceQueue.async {
|
||||
let isNotifying = self.lock.synchronized { self.notificationCallbacks != nil }
|
||||
guard isNotifying else { return /* Cancelled */ }
|
||||
|
||||
let value = self.reducer._value(fetchedValue)
|
||||
|
||||
// Notify
|
||||
if let value = value {
|
||||
self.scheduler.schedule {
|
||||
// TODO: [SR-214] remove -Opt suffix when we only support Xcode 12.5.1+
|
||||
let onChangeOpt = self.lock.synchronized { self.notificationCallbacks?.onChange }
|
||||
guard let onChange = onChangeOpt else { return /* Cancelled */ }
|
||||
onChange(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.stopDatabaseObservation(writerDB)
|
||||
self.notifyError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Observing Database Transactions
|
||||
|
||||
extension ValueConcurrentObserver: TransactionObserver {
|
||||
|
||||
@ -139,9 +139,11 @@ final class ValueWriteOnlyObserver<Writer: DatabaseWriter, Reducer: ValueReducer
|
||||
fetch: reducer._fetch)
|
||||
self.notificationCallbacks = NotificationCallbacks(events: events, onChange: onChange)
|
||||
self.reducer = reducer
|
||||
self.reduceQueue = writer.configuration.makeDispatchQueue(label: writer.configuration.identifier(
|
||||
defaultLabel: "GRDB",
|
||||
purpose: "ValueObservation"))
|
||||
self.reduceQueue = DispatchQueue(
|
||||
label: writer.configuration.identifier(
|
||||
defaultLabel: "GRDB",
|
||||
purpose: "ValueObservation"),
|
||||
qos: writer.configuration.readQoS)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,13 +11,13 @@
|
||||
|
||||
---
|
||||
|
||||
**Latest release**: April 16, 2022 • [version 5.23.0](https://github.com/groue/GRDB.swift/tree/v5.23.0) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 4 to GRDB 5](Documentation/GRDB5MigrationGuide.md)
|
||||
**Latest release**: July 9, 2022 • [version 5.26.0](https://github.com/groue/GRDB.swift/tree/v5.26.0) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 4 to GRDB 5](Documentation/GRDB5MigrationGuide.md)
|
||||
|
||||
**Requirements**: iOS 11.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ • SQLite 3.8.5+ • Swift 5.3+ / Xcode 12+
|
||||
|
||||
| Swift version | GRDB version |
|
||||
| -------------- | ----------------------------------------------------------- |
|
||||
| **Swift 5.3+** | **v5.23.0** |
|
||||
| **Swift 5.3+** | **v5.26.0** |
|
||||
| Swift 5.2 | [v5.12.0](https://github.com/groue/GRDB.swift/tree/v5.12.0) |
|
||||
| Swift 5.1 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) |
|
||||
| Swift 5 | [v4.14.0](https://github.com/groue/GRDB.swift/tree/v4.14.0) |
|
||||
@ -63,7 +63,7 @@ See [Why Adopt GRDB?](Documentation/WhyAdoptGRDB.md) if you are looking for your
|
||||
|
||||
## Features
|
||||
|
||||
GRDB ships with:
|
||||
Programming tools for both database beginners and SQLite experts:
|
||||
|
||||
- [Access to raw SQL and SQLite](#sqlite-api)
|
||||
- [Records](#records): Fetching and persistence methods for your custom structs and class hierarchies.
|
||||
@ -72,18 +72,21 @@ GRDB ships with:
|
||||
- [WAL Mode Support](#database-pools): Extra performance for multi-threaded applications.
|
||||
- [Migrations]: Transform your database as your application evolves.
|
||||
- [Database Observation]: Observe database changes and transactions.
|
||||
- [Swift Concurrency]: `try await` your database (Xcode 13.3.1+).
|
||||
- [Combine Support]: Access and observe the database with Combine publishers.
|
||||
- [RxSwift Support](http://github.com/RxSwiftCommunity/RxGRDB): Access and observe the database with RxSwift observables.
|
||||
- [Full-Text Search]
|
||||
- [Encryption](#encryption)
|
||||
- [Support for Custom SQLite Builds](Documentation/CustomSQLiteBuilds.md)
|
||||
|
||||
In-depth integration with our programming environment:
|
||||
|
||||
- [Swift Concurrency]: `try await` your database (Xcode 13.3.1+).
|
||||
- [SwiftUI](http://github.com/groue/GRDBQuery): Access and observe the database from your SwiftUI views.
|
||||
- [Combine](Documentation/Combine.md): Access and observe the database with Combine publishers.
|
||||
- [RxSwift](http://github.com/RxSwiftCommunity/RxGRDB): Access and observe the database with RxSwift observables.
|
||||
|
||||
## Usage
|
||||
|
||||
<details open>
|
||||
<summary>Start using the database in four easy steps</summary>
|
||||
<summary>Start using the database in four steps</summary>
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
@ -318,7 +321,7 @@ Documentation
|
||||
|
||||
#### Reference
|
||||
|
||||
- [GRDB Reference](http://groue.github.io/GRDB.swift/docs/5.23/index.html) (generated by [Jazzy](https://github.com/realm/jazzy))
|
||||
- [GRDB Reference](http://groue.github.io/GRDB.swift/docs/5.26/index.html) (generated by [Jazzy](https://github.com/realm/jazzy))
|
||||
|
||||
#### Getting Started
|
||||
|
||||
@ -488,6 +491,8 @@ let newPlaceCount = try dbQueue.write { db -> Int in
|
||||
}
|
||||
```
|
||||
|
||||
See the [Concurrency] guide for asynchronous database accesses.
|
||||
|
||||
**A database queue serializes accesses to the database**, which means that there is never more than one thread that uses the database.
|
||||
|
||||
- When you don't need to modify the database, prefer the `read` method. It prevents any modification to the database.
|
||||
@ -545,6 +550,8 @@ let newPlaceCount = try dbPool.write { db -> Int in
|
||||
}
|
||||
```
|
||||
|
||||
See the [Concurrency] guide for asynchronous database accesses.
|
||||
|
||||
**Database pools allow several threads to access the database at the same time:**
|
||||
|
||||
- When you don't need to modify the database, prefer the `read` method, because several threads can perform reads in parallel.
|
||||
@ -610,7 +617,7 @@ do {
|
||||
>
|
||||
> :warning: **Warning**: The SQLite version that ships with old operating systems (prior to OSX 10.12, tvOS 10.0, and watchOS 3.0) outputs statement arguments in the [trace events](#how-do-i-print-a-request-as-sql), regardless of the `publicStatementArguments` flag.
|
||||
|
||||
See [Configuration](http://groue.github.io/GRDB.swift/docs/5.23/Structs/Configuration.html) for more details and configuration options.
|
||||
See [Configuration](http://groue.github.io/GRDB.swift/docs/5.26/Structs/Configuration.html) for more details and configuration options.
|
||||
|
||||
|
||||
SQLite API
|
||||
@ -667,7 +674,7 @@ try dbQueue.write { db in
|
||||
}
|
||||
```
|
||||
|
||||
The `?` and colon-prefixed keys like `:score` in the SQL query are the **statements arguments**. You pass arguments with arrays or dictionaries, as in the example above. See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.23/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments.
|
||||
The `?` and colon-prefixed keys like `:score` in the SQL query are the **statements arguments**. You pass arguments with arrays or dictionaries, as in the example above. See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.26/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments.
|
||||
|
||||
You can also embed query arguments right into your SQL queries, with the `literal` argument label, as in the example below. See [SQL Interpolation] for more details.
|
||||
|
||||
@ -925,7 +932,7 @@ try dbQueue.read { db in
|
||||
let dictionary = try Dictionary(uniqueKeysWithValues: cursor)
|
||||
```
|
||||
|
||||
- **Cursors adopt the [Cursor](http://groue.github.io/GRDB.swift/docs/5.23/Protocols/Cursor.html) protocol, which looks a lot like standard [lazy sequences](https://developer.apple.com/reference/swift/lazysequenceprotocol) of Swift.** As such, cursors come with many convenience methods: `compactMap`, `contains`, `dropFirst`, `dropLast`, `drop(while:)`, `enumerated`, `filter`, `first`, `flatMap`, `forEach`, `joined`, `joined(separator:)`, `max`, `max(by:)`, `min`, `min(by:)`, `map`, `prefix`, `prefix(while:)`, `reduce`, `reduce(into:)`, `suffix`:
|
||||
- **Cursors adopt the [Cursor](http://groue.github.io/GRDB.swift/docs/5.26/Protocols/Cursor.html) protocol, which looks a lot like standard [lazy sequences](https://developer.apple.com/reference/swift/lazysequenceprotocol) of Swift.** As such, cursors come with many convenience methods: `compactMap`, `contains`, `dropFirst`, `dropLast`, `drop(while:)`, `enumerated`, `filter`, `first`, `flatMap`, `forEach`, `joined`, `joined(separator:)`, `max`, `max(by:)`, `min`, `min(by:)`, `map`, `prefix`, `prefix(while:)`, `reduce`, `reduce(into:)`, `suffix`:
|
||||
|
||||
```swift
|
||||
// Prints all Github links
|
||||
@ -1004,7 +1011,7 @@ let rows = try Row.fetchAll(db,
|
||||
arguments: ["name": "Arthur"])
|
||||
```
|
||||
|
||||
See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.23/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments.
|
||||
See [Values](#values) for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.26/Structs/StatementArguments.html) for a detailed documentation of SQLite arguments.
|
||||
|
||||
Unlike row arrays that contain copies of the database rows, row cursors are close to the SQLite metal, and require a little care:
|
||||
|
||||
@ -1289,7 +1296,7 @@ GRDB ships with built-in support for the following value types:
|
||||
|
||||
- Generally speaking, all types that adopt the [DatabaseValueConvertible](#custom-value-types) protocol.
|
||||
|
||||
Values can be used as [statement arguments](http://groue.github.io/GRDB.swift/docs/5.23/Structs/StatementArguments.html):
|
||||
Values can be used as [statement arguments](http://groue.github.io/GRDB.swift/docs/5.26/Structs/StatementArguments.html):
|
||||
|
||||
```swift
|
||||
let url: URL = ...
|
||||
@ -1760,7 +1767,7 @@ try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
|
||||
}
|
||||
```
|
||||
|
||||
Transactions can't be left opened unless you set the [allowsUnsafeTransactions](http://groue.github.io/GRDB.swift/docs/5.23/Structs/Configuration.html) configuration flag:
|
||||
Transactions can't be left opened unless you set the [allowsUnsafeTransactions](http://groue.github.io/GRDB.swift/docs/5.26/Structs/Configuration.html) configuration flag:
|
||||
|
||||
```swift
|
||||
// fatal error: A transaction has been left opened at the end of a database access
|
||||
@ -1872,7 +1879,7 @@ try dbQueue.write { db in
|
||||
}
|
||||
```
|
||||
|
||||
The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.23/Structs/StatementArguments.html), which happens to adopt the ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral protocols).
|
||||
The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.26/Structs/StatementArguments.html), which happens to adopt the ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral protocols).
|
||||
|
||||
```swift
|
||||
insertStatement.arguments = ["name": "Arthur", "score": 1000]
|
||||
@ -1955,7 +1962,7 @@ See also `Database.execute(sql:)` in the [Executing Updates](#executing-updates)
|
||||
|
||||
> :point_up: **Note**: it is a programmer error to reuse a prepared statement that has failed: GRDB may crash if you do so.
|
||||
|
||||
For more information about prepared statements, see the [Statement reference](http://groue.github.io/GRDB.swift/docs/5.23/Classes/Statement.html).
|
||||
For more information about prepared statements, see the [Statement reference](http://groue.github.io/GRDB.swift/docs/5.26/Classes/Statement.html).
|
||||
|
||||
|
||||
### Prepared Statements Cache
|
||||
@ -2698,7 +2705,7 @@ try Place.fetchSet(db, sql: "SELECT ...", arguments:...) // Set<Place>
|
||||
try Place.fetchOne(db, sql: "SELECT ...", arguments:...) // Place?
|
||||
```
|
||||
|
||||
See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll`, `fetchSet` and `fetchOne` methods. See [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.23/Structs/StatementArguments.html) for more information about the query arguments.
|
||||
See [fetching methods](#fetching-methods) for information about the `fetchCursor`, `fetchAll`, `fetchSet` and `fetchOne` methods. See [StatementArguments](http://groue.github.io/GRDB.swift/docs/5.26/Structs/StatementArguments.html) for more information about the query arguments.
|
||||
|
||||
> :point_up: **Note**: for performance reasons, the same row argument to `init(row:)` is reused during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy: `self.row = row.copy()`.
|
||||
|
||||
@ -3138,7 +3145,7 @@ protocol EncodableRecord {
|
||||
}
|
||||
```
|
||||
|
||||
See [DatabaseColumnDecodingStrategy](https://groue.github.io/GRDB.swift/docs/5.23/Enums/DatabaseColumnDecodingStrategy.html) and [DatabaseColumnEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.23/Enums/DatabaseColumnEncodingStrategy.html) to learn about all available strategies.
|
||||
See [DatabaseColumnDecodingStrategy](https://groue.github.io/GRDB.swift/docs/5.26/Enums/DatabaseColumnDecodingStrategy.html) and [DatabaseColumnEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.26/Enums/DatabaseColumnEncodingStrategy.html) to learn about all available strategies.
|
||||
|
||||
|
||||
### Date and UUID Coding Strategies
|
||||
@ -3160,7 +3167,7 @@ protocol EncodableRecord {
|
||||
}
|
||||
```
|
||||
|
||||
See [DatabaseDateDecodingStrategy](https://groue.github.io/GRDB.swift/docs/5.23/Enums/DatabaseDateDecodingStrategy.html), [DatabaseDateEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.23/Enums/DatabaseDateEncodingStrategy.html), and [DatabaseUUIDEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.23/Enums/DatabaseUUIDEncodingStrategy.html) to learn about all available strategies.
|
||||
See [DatabaseDateDecodingStrategy](https://groue.github.io/GRDB.swift/docs/5.26/Enums/DatabaseDateDecodingStrategy.html), [DatabaseDateEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.26/Enums/DatabaseDateEncodingStrategy.html), and [DatabaseUUIDEncodingStrategy](https://groue.github.io/GRDB.swift/docs/5.26/Enums/DatabaseUUIDEncodingStrategy.html) to learn about all available strategies.
|
||||
|
||||
There is no customization of uuid decoding, because UUID can already decode all its encoded variants (16-bytes blobs and uuid strings, both uppercase and lowercase).
|
||||
|
||||
@ -4685,7 +4692,7 @@ Player // SELECT * FROM player
|
||||
```
|
||||
|
||||
|
||||
Raw SQL snippets are also accepted, with eventual [arguments](http://groue.github.io/GRDB.swift/docs/5.23/Structs/StatementArguments.html):
|
||||
Raw SQL snippets are also accepted, with eventual [arguments](http://groue.github.io/GRDB.swift/docs/5.26/Structs/StatementArguments.html):
|
||||
|
||||
```swift
|
||||
// SELECT DATE(creationDate), COUNT(*) FROM player WHERE name = 'Arthur' GROUP BY date(creationDate)
|
||||
@ -5591,7 +5598,7 @@ try Player.customRequest().fetchAll(db) // [Player]
|
||||
|
||||
- The `adapted(_:)` method eases the consumption of complex rows with [row adapters](#row-adapters). See [Joined Queries Support](#joined-queries-support) for some sample code that uses this method.
|
||||
|
||||
- [AnyFetchRequest](http://groue.github.io/GRDB.swift/docs/5.23/Structs/AnyFetchRequest.html): a type-erased request.
|
||||
- [AnyFetchRequest](http://groue.github.io/GRDB.swift/docs/5.26/Structs/AnyFetchRequest.html): a type-erased request.
|
||||
|
||||
|
||||
## Joined Queries Support
|
||||
@ -6683,6 +6690,8 @@ By default, database holds weak references to its transaction observers: they ar
|
||||
**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).
|
||||
|
||||
> :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).
|
||||
>
|
||||
> :point_up: **Note**: Transactions performed during read-only database accesses are not notified.
|
||||
|
||||
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:
|
||||
|
||||
@ -6794,7 +6803,7 @@ class PlayerScoreObserver: TransactionObserver {
|
||||
}
|
||||
```
|
||||
|
||||
When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of commits and rollbacks:
|
||||
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):
|
||||
|
||||
```swift
|
||||
class PureTransactionObserver: TransactionObserver {
|
||||
@ -6891,7 +6900,7 @@ DatabaseRegion helps [ValueObservation] and [DatabaseRegionObservation] track ch
|
||||
|
||||
For example, if you observe the region of `Player.select(max(Column("score")))`, then you'll get be notified of all changes performed on the `score` column of the `player` table (updates, insertions and deletions), even if they do not modify the value of the maximum score. However, you will not get any notification for changes performed on other database tables, or updates to other columns of the player table.
|
||||
|
||||
For more details, see the [reference](https://groue.github.io/GRDB.swift/docs/5.23/Structs/DatabaseRegion.html).
|
||||
For more details, see the [reference](https://groue.github.io/GRDB.swift/docs/5.26/Structs/DatabaseRegion.html).
|
||||
|
||||
|
||||
#### The DatabaseRegionConvertible Protocol
|
||||
@ -7709,12 +7718,36 @@ dbPool.releaseMemory()
|
||||
|
||||
This method blocks the current thread until all current database accesses are completed, and the memory collected.
|
||||
|
||||
> :warning: **Warning**: If `DatabasePool.releaseMemory()` is called while a long read is performed concurrently, then no other read access will be possible until this long read has completed, and the memory has been released. If this does not suit your application needs, look for the asynchronous options below:
|
||||
|
||||
You can release memory in an asynchronous way as well:
|
||||
|
||||
```swift
|
||||
// On a DatabaseQueue
|
||||
dbQueue.asyncWriteWithoutTransaction { db in
|
||||
db.releaseMemory()
|
||||
}
|
||||
|
||||
// On a DatabasePool
|
||||
dbPool.releaseMemoryEventually()
|
||||
```
|
||||
|
||||
`DatabasePool.releaseMemoryEventually()` does not block the current thread, and does not prevent concurrent database accesses. In exchange for this convenience, you don't know when memory has been freed.
|
||||
|
||||
|
||||
### Memory Management on iOS
|
||||
|
||||
**The iOS operating system likes applications that do not consume much memory.**
|
||||
|
||||
[Database queues](#database-queues) and [pools](#database-pools) automatically call the `releaseMemory` method when the application receives a memory warning, and when the application enters background.
|
||||
[Database queues](#database-queues) and [pools](#database-pools) automatically free non-essential memory when the application receives a memory warning, and when the application enters background.
|
||||
|
||||
You can opt out of this automatic memory management:
|
||||
|
||||
```swift
|
||||
var config = Configuration()
|
||||
config.automaticMemoryManagement = false
|
||||
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config) // or DatabasePool
|
||||
```
|
||||
|
||||
|
||||
## Data Protection
|
||||
@ -8231,7 +8264,7 @@ When this is the case, there are two possible explanations:
|
||||
try db.execute(sql: "UPDATE player SET name = ?", arguments: [name])
|
||||
```
|
||||
|
||||
For more information, see [Double-quoted String Literals Are Accepted](https://sqlite.org/quirks.html#dblquote), and [Configuration.acceptsDoubleQuotedStringLiterals](http://groue.github.io/GRDB.swift/docs/5.23/Structs/Configuration.html#/s:4GRDB13ConfigurationV33acceptsDoubleQuotedStringLiteralsSbvp).
|
||||
For more information, see [Double-quoted String Literals Are Accepted](https://sqlite.org/quirks.html#dblquote), and [Configuration.acceptsDoubleQuotedStringLiterals](http://groue.github.io/GRDB.swift/docs/5.26/Structs/Configuration.html#/s:4GRDB13ConfigurationV33acceptsDoubleQuotedStringLiteralsSbvp).
|
||||
|
||||
|
||||
|
||||
@ -8372,7 +8405,7 @@ This chapter has [moved](Documentation/Concurrency.md#database-snapshots).
|
||||
|
||||
#### DatabaseWriter and DatabaseReader Protocols
|
||||
|
||||
This chapter was removed. See the references of [DatabaseReader](http://groue.github.io/GRDB.swift/docs/5.23/Protocols/DatabaseReader.html) and [DatabaseWriter](http://groue.github.io/GRDB.swift/docs/5.23/Protocols/DatabaseWriter.html).
|
||||
This chapter was removed. See the references of [DatabaseReader](http://groue.github.io/GRDB.swift/docs/5.26/Protocols/DatabaseReader.html) and [DatabaseWriter](http://groue.github.io/GRDB.swift/docs/5.26/Protocols/DatabaseWriter.html).
|
||||
|
||||
#### Asynchronous APIs
|
||||
|
||||
@ -8425,7 +8458,7 @@ This chapter was renamed to [Embedding SQL in Query Interface Requests].
|
||||
[Sharing a Database]: Documentation/SharingADatabase.md
|
||||
[FAQ]: #faq
|
||||
[Database Observation]: #database-changes-observation
|
||||
[SQLRequest]: http://groue.github.io/GRDB.swift/docs/5.23/Structs/SQLRequest.html
|
||||
[SQLRequest]: http://groue.github.io/GRDB.swift/docs/5.26/Structs/SQLRequest.html
|
||||
[SQL literal]: Documentation/SQLInterpolation.md#sql-literal
|
||||
[Identifiable]: https://developer.apple.com/documentation/swift/identifiable
|
||||
[Query Interface Organization]: Documentation/QueryInterfaceOrganization.md
|
||||
|
||||
@ -12,7 +12,7 @@ PODS:
|
||||
- CocoaLumberjack
|
||||
- LibSignalClient (>= 0.15.0)
|
||||
- SignalCoreKit
|
||||
- GRDB.swift/SQLCipher (5.23.0):
|
||||
- GRDB.swift/SQLCipher (5.26.0):
|
||||
- SQLCipher (>= 3.4.0)
|
||||
- LibMobileCoin/CoreHTTP (1.2.0-pre10):
|
||||
- SwiftProtobuf (~> 1.5)
|
||||
@ -262,7 +262,7 @@ SPEC CHECKSUMS:
|
||||
BonMot: fb2b6a2209cb3149aca37b7131d49c051c04ae86
|
||||
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
|
||||
Curve25519Kit: 1b98dc2d6e7e3d5b6756b05b96758f6161e6d58d
|
||||
GRDB.swift: e4a950fe99d113ea5d24571d49eaae0062303c14
|
||||
GRDB.swift: 1395cb3556df6b16ed69dfc74c3886abc75d2825
|
||||
LibMobileCoin: 40d2d7d685321a6128871b6c6b4bb7cba25e848f
|
||||
libPhoneNumber-iOS: 2d26d0a38933eee2702962a4dbdec2fc20e5ef9f
|
||||
LibSignalClient: 0979e736d85a419b75ecedc36ffcea821a5cea14
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.23.0</string>
|
||||
<string>5.26.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user