add swiftUI code
This commit is contained in:
@@ -0,0 +1,893 @@
|
||||
import Dispatch
|
||||
import Foundation
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public final class DatabasePool {
|
||||
private let writer: SerializedDatabase
|
||||
|
||||
/// The pool of reader connections.
|
||||
/// It is constant, until close() sets it to nil.
|
||||
private var readerPool: Pool<SerializedDatabase>?
|
||||
|
||||
@LockedBox var databaseSnapshotCount = 0
|
||||
|
||||
/// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers.
|
||||
private var suspensionObservers: [NSObjectProtocol] = []
|
||||
|
||||
// MARK: - Database Information
|
||||
|
||||
public var configuration: Configuration {
|
||||
writer.configuration
|
||||
}
|
||||
|
||||
/// The path to the database.
|
||||
public var path: String {
|
||||
writer.path
|
||||
}
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
/// Opens or creates an SQLite database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
||||
/// ```
|
||||
///
|
||||
/// The SQLite connections are closed when the database pool
|
||||
/// gets deallocated.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - path: The path to the database file.
|
||||
/// - configuration: A configuration.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public init(path: String, configuration: Configuration = Configuration()) throws {
|
||||
GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1")
|
||||
|
||||
// Writer
|
||||
writer = try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: configuration,
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "writer")
|
||||
|
||||
// Readers
|
||||
var readerConfiguration = DatabasePool.readerConfiguration(configuration)
|
||||
|
||||
// Readers can't allow dangling transactions because there's no
|
||||
// guarantee that one can get the same reader later in order to close
|
||||
// an opened transaction.
|
||||
readerConfiguration.allowsUnsafeTransactions = false
|
||||
|
||||
var readerCount = 0
|
||||
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)")
|
||||
})
|
||||
|
||||
// Set up journal mode unless readonly
|
||||
if !configuration.readonly {
|
||||
switch configuration.journalMode {
|
||||
case .default, .wal:
|
||||
try writer.sync {
|
||||
try $0.setUpWALMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSuspension()
|
||||
|
||||
// Be a nice iOS citizen, and don't consume too much memory
|
||||
// See https://github.com/groue/GRDB.swift/#memory-management
|
||||
#if os(iOS)
|
||||
if configuration.automaticMemoryManagement {
|
||||
setupMemoryManagement()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Remove block-based Notification observers.
|
||||
suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||
|
||||
// Undo job done in setupMemoryManagement()
|
||||
//
|
||||
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error
|
||||
// Explicit unregistration is required before macOS 10.11.
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
// Close reader connections before the writer connection.
|
||||
// Context: https://github.com/groue/GRDB.swift/issues/739
|
||||
readerPool = nil
|
||||
}
|
||||
|
||||
/// Returns a Configuration suitable for readonly connections on a
|
||||
/// WAL database.
|
||||
private static func readerConfiguration(_ configuration: Configuration) -> Configuration {
|
||||
var configuration = configuration
|
||||
|
||||
configuration.readonly = true
|
||||
|
||||
// Readers use deferred transactions by default.
|
||||
// Other transaction kinds are forbidden by SQLite in read-only connections.
|
||||
configuration.defaultTransactionKind = .deferred
|
||||
|
||||
// <https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode>
|
||||
// > But there are some obscure cases where a query against a WAL-mode
|
||||
// > database can return SQLITE_BUSY, so applications should be prepared
|
||||
// > for that happenstance.
|
||||
// >
|
||||
// > - If another database connection has the database mode open in
|
||||
// > exclusive locking mode [...]
|
||||
// > - When the last connection to a particular database is closing,
|
||||
// > that connection will acquire an exclusive lock for a short time
|
||||
// > while it cleans up the WAL and shared-memory files [...]
|
||||
// > - If the last connection to a database crashed, then the first new
|
||||
// > connection to open the database will start a recovery process. An
|
||||
// > exclusive lock is held during recovery. [...]
|
||||
//
|
||||
// The whole point of WAL readers is to avoid SQLITE_BUSY, so let's
|
||||
// setup a busy handler for pool readers, in order to workaround those
|
||||
// "obscure cases" that may happen when the database is shared between
|
||||
// multiple processes.
|
||||
if configuration.readonlyBusyMode == nil {
|
||||
configuration.readonlyBusyMode = .timeout(10)
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
// @unchecked because of databaseSnapshotCount, readerPool and suspensionObservers
|
||||
extension DatabasePool: @unchecked Sendable { }
|
||||
|
||||
extension DatabasePool {
|
||||
|
||||
// MARK: - Memory management
|
||||
|
||||
/// Frees as much memory as possible, by disposing non-essential memory.
|
||||
///
|
||||
/// This method is synchronous, and blocks the current thread until all
|
||||
/// database accesses are completed.
|
||||
///
|
||||
/// This method closes all read-only connections, unless the
|
||||
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
|
||||
/// is set.
|
||||
///
|
||||
/// - 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() }
|
||||
|
||||
if configuration.persistentReadOnlyConnections {
|
||||
// Keep existing readers
|
||||
readerPool?.forEach { reader in
|
||||
reader.sync { $0.releaseMemory() }
|
||||
}
|
||||
} else {
|
||||
// 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.
|
||||
///
|
||||
/// This method eventually closes all read-only connections, unless the
|
||||
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
|
||||
/// is set.
|
||||
///
|
||||
/// 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() {
|
||||
if configuration.persistentReadOnlyConnections {
|
||||
// Keep existing readers
|
||||
readerPool?.forEach { reader in
|
||||
reader.async { $0.releaseMemory() }
|
||||
}
|
||||
} else {
|
||||
// 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 { $0.releaseMemory() }
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
/// Listens to UIApplicationDidEnterBackgroundNotification and
|
||||
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
|
||||
/// as much memory as possible.
|
||||
private func setupMemoryManagement() {
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)),
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(DatabasePool.applicationDidEnterBackground(_:)),
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidEnterBackground(_ notification: NSNotification) {
|
||||
guard let application = notification.object as? UIApplication else {
|
||||
return
|
||||
}
|
||||
|
||||
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
|
||||
if task == .invalid {
|
||||
// Release memory synchronously
|
||||
releaseMemory()
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
|
||||
releaseMemoryEventually()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension DatabasePool: DatabaseReader {
|
||||
|
||||
public func close() throws {
|
||||
try readerPool?.barrier {
|
||||
// Close writer connection first. If we can't close it,
|
||||
// don't close readers.
|
||||
//
|
||||
// This allows us to exit this method as fully closed (read and
|
||||
// writes fail), or not closed at all (reads and writes succeed).
|
||||
//
|
||||
// Unfortunately, this introduces a regression for
|
||||
// https://github.com/groue/GRDB.swift/issues/739.
|
||||
// TODO: fix this regression.
|
||||
try writer.sync { try $0.close() }
|
||||
|
||||
// OK writer is closed. Now close readers and
|
||||
// eventually prevent any future read access
|
||||
defer { readerPool = nil }
|
||||
|
||||
try readerPool?.forEach { reader in
|
||||
try reader.sync { try $0.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interrupting Database Operations
|
||||
|
||||
public func interrupt() {
|
||||
writer.interrupt()
|
||||
readerPool?.forEach { $0.interrupt() }
|
||||
}
|
||||
|
||||
// MARK: - Database Suspension
|
||||
|
||||
func suspend() {
|
||||
if configuration.readonly {
|
||||
// read-only WAL connections can't acquire locks and do not need to
|
||||
// be suspended.
|
||||
return
|
||||
}
|
||||
writer.suspend()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
if configuration.readonly {
|
||||
// read-only WAL connections can't acquire locks and do not need to
|
||||
// be suspended.
|
||||
return
|
||||
}
|
||||
writer.resume()
|
||||
}
|
||||
|
||||
private func setupSuspension() {
|
||||
if configuration.observesSuspensionNotifications {
|
||||
let center = NotificationCenter.default
|
||||
suspensionObservers.append(center.addObserver(
|
||||
forName: Database.suspendNotification,
|
||||
object: nil,
|
||||
queue: nil,
|
||||
using: { [weak self] _ in self?.suspend() }
|
||||
))
|
||||
suspensionObservers.append(center.addObserver(
|
||||
forName: Database.resumeNotification,
|
||||
object: nil,
|
||||
queue: nil,
|
||||
using: { [weak self] _ in self?.resume() }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reading from Database
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func read<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.get { reader in
|
||||
try reader.sync { db in
|
||||
try db.isolated {
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
value(.failure(DatabaseError.connectionIsClosed()))
|
||||
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(.reuse)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.get { reader in
|
||||
try reader.sync { db in
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
value(.failure(DatabaseError.connectionIsClosed()))
|
||||
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(.reuse)
|
||||
}
|
||||
do {
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
if let reader = currentReader {
|
||||
return try reader.reentrantSync(value)
|
||||
} else if writer.onValidQueue {
|
||||
return try writer.execute(value)
|
||||
} else {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.get { reader in
|
||||
try reader.sync { db in
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
|
||||
// The semaphore that blocks until futureResult is defined:
|
||||
let futureSemaphore = DispatchSemaphore(value: 0)
|
||||
var futureResult: Result<T, Error>? = nil
|
||||
|
||||
asyncConcurrentRead { dbResult in
|
||||
// Fetch and release the future
|
||||
futureResult = dbResult.flatMap { db in Result { try value(db) } }
|
||||
futureSemaphore.signal()
|
||||
}
|
||||
|
||||
return DatabaseFuture {
|
||||
// Block the future until results are fetched
|
||||
_ = futureSemaphore.wait(timeout: .distantFuture)
|
||||
return try futureResult!.get()
|
||||
}
|
||||
}
|
||||
|
||||
public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
asyncConcurrentRead(value)
|
||||
}
|
||||
|
||||
/// Performs an asynchronous read access.
|
||||
///
|
||||
/// This method must be called from the writer dispatch queue, outside of
|
||||
/// any transaction. You'll get a fatal error otherwise.
|
||||
///
|
||||
/// The `value` function is guaranteed to see the database in the last
|
||||
/// committed state at the moment this method is called. Eventual
|
||||
/// concurrent database updates are not visible from the function.
|
||||
///
|
||||
/// This method returns as soon as the isolation guarantee described above
|
||||
/// has been established.
|
||||
///
|
||||
/// In the example below, the number of players is fetched concurrently with
|
||||
/// the player insertion. Yet it is guaranteed to be zero:
|
||||
///
|
||||
/// ```swift
|
||||
/// try writer.asyncWriteWithoutTransaction { db in
|
||||
/// // Delete all players
|
||||
/// try Player.deleteAll()
|
||||
///
|
||||
/// // Count players concurrently
|
||||
/// writer.asyncConcurrentRead { dbResult in
|
||||
/// do {
|
||||
/// let db = try dbResult.get()
|
||||
/// // Guaranteed to be zero
|
||||
/// let count = try Player.fetchCount(db)
|
||||
/// } catch {
|
||||
/// // Handle error
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Insert a player
|
||||
/// try Player(...).insert(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter value: A function that accesses the database.
|
||||
public func asyncConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
// Check that we're on the writer queue...
|
||||
writer.execute { db in
|
||||
// ... and that no transaction is opened.
|
||||
GRDBPrecondition(!db.isInsideTransaction, """
|
||||
must not be called from inside a transaction. \
|
||||
If this error is raised from a DatabasePool.write block, use \
|
||||
DatabasePool.writeWithoutTransaction instead (and use \
|
||||
transactions when needed).
|
||||
""")
|
||||
}
|
||||
|
||||
// The semaphore that blocks the writing dispatch queue until snapshot
|
||||
// isolation has been established:
|
||||
let isolationSemaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
do {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
reader.async { db in
|
||||
defer {
|
||||
try? db.commit() // Ignore commit error
|
||||
releaseReader(.reuse)
|
||||
}
|
||||
do {
|
||||
// https://www.sqlite.org/isolation.html
|
||||
//
|
||||
// > In WAL mode, SQLite exhibits "snapshot isolation". When
|
||||
// > a read transaction starts, that reader continues to see
|
||||
// > an unchanging "snapshot" of the database file as it
|
||||
// > existed at the moment in time when the read transaction
|
||||
// > started. Any write transactions that commit while the
|
||||
// > read transaction is active are still invisible to the
|
||||
// > read transaction, because the reader is seeing a
|
||||
// > snapshot of database file from a prior moment in time.
|
||||
//
|
||||
// That's exactly what we need. But what does "when read
|
||||
// transaction starts" mean?
|
||||
//
|
||||
// http://www.sqlite.org/lang_transaction.html
|
||||
//
|
||||
// > Deferred [transaction] means that no locks are acquired
|
||||
// > on the database until the database is first accessed.
|
||||
// > [...] Locks are not acquired until the first read or
|
||||
// > write operation. [...] Because the acquisition of locks
|
||||
// > is deferred until they are needed, it is possible that
|
||||
// > another thread or process could create a separate
|
||||
// > transaction and write to the database after the BEGIN
|
||||
// > on the current thread has executed.
|
||||
//
|
||||
// Now that's precise enough: SQLite defers snapshot
|
||||
// isolation until the first SELECT:
|
||||
//
|
||||
// Reader Writer
|
||||
// BEGIN DEFERRED TRANSACTION
|
||||
// UPDATE ... (1)
|
||||
// Here the change (1) is visible from the reader
|
||||
// SELECT ...
|
||||
// UPDATE ... (2)
|
||||
// Here the change (2) is not visible from the reader
|
||||
//
|
||||
// We thus have to perform a select that establishes the
|
||||
// snapshot isolation before we release the writer queue:
|
||||
//
|
||||
// Reader Writer
|
||||
// BEGIN DEFERRED TRANSACTION
|
||||
// SELECT anything
|
||||
// UPDATE ... (1)
|
||||
// Here the change (1) is not visible from the reader
|
||||
//
|
||||
// Since any select goes, use `PRAGMA schema_version`.
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
} catch {
|
||||
isolationSemaphore.signal()
|
||||
value(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// Now that we have an isolated snapshot of the last commit, we
|
||||
// can release the writer queue.
|
||||
isolationSemaphore.signal()
|
||||
|
||||
value(.success(db))
|
||||
}
|
||||
} catch {
|
||||
isolationSemaphore.signal()
|
||||
value(.failure(error))
|
||||
}
|
||||
|
||||
// Block the writer queue until snapshot isolation success or error
|
||||
_ = isolationSemaphore.wait(timeout: .distantFuture)
|
||||
}
|
||||
|
||||
/// Invalidates open read-only SQLite connections.
|
||||
///
|
||||
/// After this method is called, read-only database access methods will use
|
||||
/// new SQLite connections.
|
||||
///
|
||||
/// Eventual concurrent read-only accesses are not interrupted, and
|
||||
/// proceed until completion.
|
||||
///
|
||||
/// - This method closes all read-only connections, even if the
|
||||
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
|
||||
/// is set.
|
||||
public func invalidateReadOnlyConnections() {
|
||||
readerPool?.removeAll()
|
||||
}
|
||||
|
||||
/// Returns a reader that can be used from the current dispatch queue,
|
||||
/// if any.
|
||||
private var currentReader: SerializedDatabase? {
|
||||
guard let readerPool else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var readers: [SerializedDatabase] = []
|
||||
readerPool.forEach { reader in
|
||||
// We can't check for reader.onValidQueue here because
|
||||
// Pool.forEach() runs its closure argument in some arbitrary
|
||||
// dispatch queue. We thus extract the reader so that we can query
|
||||
// it below.
|
||||
readers.append(reader)
|
||||
}
|
||||
|
||||
// Now the readers array contains some readers. The pool readers may
|
||||
// already be different, because some other thread may have started
|
||||
// a new read, for example.
|
||||
//
|
||||
// This doesn't matter: the reader we are looking for is already on
|
||||
// its own dispatch queue. If it exists, is still in use, thus still
|
||||
// in the pool, and thus still relevant for our check:
|
||||
return readers.first { $0.onValidQueue }
|
||||
}
|
||||
|
||||
// MARK: - WAL Snapshot Transactions
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// Returns a long-lived WAL snapshot transaction on a reader connection.
|
||||
func walSnapshotTransaction() throws -> WALSnapshotTransaction {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
return try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
|
||||
// Discard the connection if the transaction could not be
|
||||
// properly ended. If we'd reuse it, the next read would
|
||||
// fail because we'd fail starting a read transaction.
|
||||
releaseReader(isInsideTransaction ? .discard : .reuse)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a long-lived WAL snapshot transaction on a reader connection.
|
||||
///
|
||||
/// - important: The `completion` argument is executed in a serial
|
||||
/// dispatch queue, so make sure you use the transaction asynchronously.
|
||||
func asyncWALSnapshotTransaction(_ completion: @escaping (Result<WALSnapshotTransaction, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
completion(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
completion(result.flatMap { reader, releaseReader in
|
||||
Result {
|
||||
try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
|
||||
// Discard the connection if the transaction could not be
|
||||
// properly ended. If we'd reuse it, the next read would
|
||||
// fail because we'd fail starting a read transaction.
|
||||
releaseReader(isInsideTransaction ? .discard : .reuse)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Database Observation
|
||||
|
||||
public func _add<Reducer: ValueReducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
{
|
||||
if configuration.readonly {
|
||||
// The easy case: the database does not change
|
||||
return _addReadOnly(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
|
||||
} else if observation.requiresWriteAccess {
|
||||
// Observe from the writer database connection.
|
||||
return _addWriteOnly(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
|
||||
} else {
|
||||
// DatabasePool can perform concurrent observation
|
||||
return _addConcurrent(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
}
|
||||
}
|
||||
|
||||
/// A concurrent observation fetches the initial value without waiting for
|
||||
/// the writer.
|
||||
private func _addConcurrent<Reducer: ValueReducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
{
|
||||
assert(!configuration.readonly, "Use _addReadOnly(observation:) instead")
|
||||
assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead")
|
||||
let observer = ValueConcurrentObserver(
|
||||
dbPool: self,
|
||||
scheduler: scheduler,
|
||||
trackingMode: observation.trackingMode,
|
||||
reducer: observation.makeReducer(),
|
||||
events: observation.events,
|
||||
onChange: onChange)
|
||||
return observer.start()
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabasePool: DatabaseWriter {
|
||||
// MARK: - Writing in Database
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func writeWithoutTransaction<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func barrierWriteWithoutTransaction<T>(_ updates: (Database) throws -> T) throws -> T {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.barrier {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
updates(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
readerPool.asyncBarrier {
|
||||
self.writer.sync { updates(.success($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps database operations inside a database transaction.
|
||||
///
|
||||
/// The `updates` function runs in the writer dispatch queue, serialized
|
||||
/// with all database updates.
|
||||
///
|
||||
/// If `updates` throws an error, the transaction is rollbacked and the
|
||||
/// error is rethrown. If it returns
|
||||
/// ``Database/TransactionCompletion/rollback``, the transaction is also
|
||||
/// rollbacked, but no error is thrown.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbPool.writeInTransaction { db in
|
||||
/// try Player(name: "Arthur").insert(db)
|
||||
/// try Player(name: "Barbara").insert(db)
|
||||
/// return .commit
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - precondition: This method is not reentrant.
|
||||
/// - parameters:
|
||||
/// - kind: The transaction type (default nil). If nil, the transaction
|
||||
/// type is the ``Configuration/defaultTransactionKind`` of the
|
||||
/// the ``configuration``.
|
||||
/// - updates: A function that updates the database.
|
||||
/// - throws: The error thrown by `updates`, or by the wrapping transaction.
|
||||
public func writeInTransaction(
|
||||
_ kind: Database.TransactionKind? = nil,
|
||||
_ updates: (Database) throws -> Database.TransactionCompletion)
|
||||
throws
|
||||
{
|
||||
try writer.sync { db in
|
||||
try db.inTransaction(kind) {
|
||||
try updates(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.reentrantSync(updates)
|
||||
}
|
||||
|
||||
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
|
||||
writer.async(updates)
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabasePool {
|
||||
|
||||
// MARK: - Snapshots
|
||||
|
||||
/// Creates a database snapshot that serializes accesses to an unchanging
|
||||
/// database content, as it exists at the moment the snapshot is created.
|
||||
///
|
||||
/// It is a programmer error to create a snapshot from the writer dispatch
|
||||
/// queue when a transaction is opened:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbPool.write { db in
|
||||
/// try Player.deleteAll()
|
||||
///
|
||||
/// // fatal error: makeSnapshot() must not be called from inside a transaction
|
||||
/// let snapshot = try dbPool.makeSnapshot()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To avoid this fatal error, create the snapshot *before* or *after*
|
||||
/// the transaction:
|
||||
///
|
||||
/// ```swift
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
///
|
||||
/// try dbPool.writeWithoutTransaction { db in
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
///
|
||||
/// try db.inTransaction {
|
||||
/// try Player.deleteAll()
|
||||
/// return .commit
|
||||
/// }
|
||||
///
|
||||
/// // OK
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
/// }
|
||||
///
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
/// ```
|
||||
public func makeSnapshot() throws -> DatabaseSnapshot {
|
||||
// Sanity check
|
||||
if writer.onValidQueue {
|
||||
writer.execute { db in
|
||||
GRDBPrecondition(
|
||||
!db.isInsideTransaction,
|
||||
"makeSnapshot() must not be called from inside a transaction.")
|
||||
}
|
||||
}
|
||||
|
||||
return try DatabaseSnapshot(
|
||||
path: path,
|
||||
configuration: DatabasePool.readerConfiguration(writer.configuration),
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "snapshot.\($databaseSnapshotCount.increment())")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// Creates a database snapshot that allows concurrent accesses to an
|
||||
/// unchanging database content, as it exists at the moment the snapshot
|
||||
/// is created.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
|
||||
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
|
||||
/// or if this method is called from a write transaction, or if the
|
||||
/// wal file is missing or truncated (size zero).
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
|
||||
public func makeSnapshotPool() throws -> DatabaseSnapshotPool {
|
||||
try unsafeReentrantRead { db in
|
||||
try DatabaseSnapshotPool(db)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user