525 lines
18 KiB
Swift
525 lines
18 KiB
Swift
import Foundation
|
|
|
|
#if os(iOS)
|
|
import UIKit
|
|
#endif
|
|
|
|
public final class DatabaseQueue {
|
|
private let writer: SerializedDatabase
|
|
|
|
/// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers.
|
|
private var suspensionObservers: [NSObjectProtocol] = []
|
|
|
|
// MARK: - Configuration
|
|
|
|
public var configuration: Configuration {
|
|
writer.configuration
|
|
}
|
|
|
|
public var path: String {
|
|
writer.path
|
|
}
|
|
|
|
// MARK: - Initializers
|
|
|
|
/// Opens or creates an SQLite database.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
|
|
/// ```
|
|
///
|
|
/// The SQLite connection is closed when the database queue
|
|
/// 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 {
|
|
// DatabaseQueue can't perform parallel reads
|
|
var configuration = configuration
|
|
configuration.maximumReaderCount = 1
|
|
|
|
writer = try SerializedDatabase(
|
|
path: path,
|
|
configuration: configuration,
|
|
defaultLabel: "GRDB.DatabaseQueue")
|
|
|
|
// Set up journal mode unless readonly
|
|
if !configuration.readonly {
|
|
switch configuration.journalMode {
|
|
case .default:
|
|
break
|
|
case .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
|
|
}
|
|
|
|
/// Opens an in-memory SQLite database.
|
|
///
|
|
/// To create an independent in-memory database, don't pass any name. The
|
|
/// database memory is released when the database queue is deallocated:
|
|
///
|
|
/// ```swift
|
|
/// // An independent in-memory database
|
|
/// let dbQueue = try DatabaseQueue()
|
|
/// ```
|
|
///
|
|
/// When you need to open several connections to the same in-memory
|
|
/// database, give it a name:
|
|
///
|
|
/// ```swift
|
|
/// // A shared in-memory database
|
|
/// let dbQueue = try DatabaseQueue(named: "myDatabase")
|
|
/// ```
|
|
///
|
|
/// In this case, the database is automatically deleted and memory is
|
|
/// reclaimed when the last connection to the database of the given
|
|
/// name closes.
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/inmemorydb.html>
|
|
///
|
|
/// - parameter name: When nil, an independent in-memory database opens.
|
|
/// Otherwise, the shared in-memory database of the given name opens.
|
|
/// - parameter configuration: A configuration.
|
|
public init(named name: String? = nil, configuration: Configuration = Configuration()) throws {
|
|
let path: String
|
|
if let name {
|
|
path = "file:\(name)?mode=memory&cache=shared"
|
|
} else {
|
|
path = ":memory:"
|
|
}
|
|
|
|
writer = try SerializedDatabase(
|
|
path: path,
|
|
configuration: configuration,
|
|
defaultLabel: "GRDB.DatabaseQueue")
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// @unchecked because of suspensionObservers
|
|
extension DatabaseQueue: @unchecked Sendable { }
|
|
|
|
extension DatabaseQueue {
|
|
|
|
// MARK: - Memory management
|
|
|
|
/// Free as much memory as possible.
|
|
///
|
|
/// This method blocks the current thread until all database accesses are completed.
|
|
public func releaseMemory() {
|
|
writer.sync { $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(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)),
|
|
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
object: nil)
|
|
center.addObserver(
|
|
self,
|
|
selector: #selector(DatabaseQueue.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 asynchronously
|
|
writer.async { db in
|
|
db.releaseMemory()
|
|
application.endBackgroundTask(task)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
|
|
writer.async { db in
|
|
db.releaseMemory()
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
extension DatabaseQueue: DatabaseReader {
|
|
public func close() throws {
|
|
try writer.sync { try $0.close() }
|
|
}
|
|
|
|
// MARK: - Interrupting Database Operations
|
|
|
|
public func interrupt() {
|
|
writer.interrupt()
|
|
}
|
|
|
|
// MARK: - Database Suspension
|
|
|
|
func suspend() {
|
|
writer.suspend()
|
|
}
|
|
|
|
func resume() {
|
|
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 {
|
|
try writer.sync { db in
|
|
try db.isolated(readOnly: true) {
|
|
try value(db)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
writer.async { db in
|
|
defer {
|
|
// Ignore error because we can not notify it.
|
|
try? db.commit()
|
|
try? db.endReadOnly()
|
|
}
|
|
|
|
do {
|
|
// Enter read-only mode before starting a transaction, so that the
|
|
// transaction commit does not trigger database observation.
|
|
// See <https://github.com/groue/GRDB.swift/pull/1213>.
|
|
try db.beginReadOnly()
|
|
try db.beginTransaction(.deferred)
|
|
value(.success(db))
|
|
} catch {
|
|
value(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
public func unsafeRead<T>(_ value: (Database) throws -> T) rethrows -> T {
|
|
try writer.sync(value)
|
|
}
|
|
|
|
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
writer.async { value(.success($0)) }
|
|
}
|
|
|
|
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) rethrows -> T {
|
|
try writer.reentrantSync(value)
|
|
}
|
|
|
|
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
|
|
// DatabaseQueue can't perform parallel reads.
|
|
// Perform a blocking read instead.
|
|
return DatabaseFuture(Result {
|
|
// Check that we're on the writer queue, as documented
|
|
try writer.execute { db in
|
|
try db.isolated(readOnly: true) {
|
|
try value(db)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
public func spawnConcurrentRead(_ 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.")
|
|
|
|
defer {
|
|
// Ignore error because we can not notify it.
|
|
try? db.commit()
|
|
try? db.endReadOnly()
|
|
}
|
|
|
|
do {
|
|
// Enter read-only mode before starting a transaction, so that the
|
|
// transaction commit does not trigger database observation.
|
|
// See <https://github.com/groue/GRDB.swift/pull/1213>.
|
|
try db.beginReadOnly()
|
|
try db.beginTransaction(.deferred)
|
|
value(.success(db))
|
|
} catch {
|
|
value(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// Observe from the writer database connection.
|
|
return _addWriteOnly(
|
|
observation: observation,
|
|
scheduling: scheduler,
|
|
onChange: onChange)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DatabaseQueue: DatabaseWriter {
|
|
// MARK: - Writing in Database
|
|
|
|
/// 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 dbQueue.inTransaction { db in
|
|
/// try Player(name: "Arthur").insert(db)
|
|
/// try Player(name: "Barbara").insert(db)
|
|
/// return .commit
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - 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 inTransaction(
|
|
_ kind: Database.TransactionKind? = nil,
|
|
_ updates: (Database) throws -> Database.TransactionCompletion)
|
|
throws
|
|
{
|
|
try writer.sync { db in
|
|
try db.inTransaction(kind) {
|
|
try updates(db)
|
|
}
|
|
}
|
|
}
|
|
|
|
@_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 {
|
|
try writer.sync(updates)
|
|
}
|
|
|
|
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
|
|
writer.async { updates(.success($0)) }
|
|
}
|
|
|
|
/// Executes database operations, and returns their result after they have
|
|
/// finished executing.
|
|
///
|
|
/// This method is identical to
|
|
/// ``DatabaseWriter/writeWithoutTransaction(_:)-4qh1w``
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let newPlayerCount = try dbQueue.inDatabase { db in
|
|
/// try Player(name: "Arthur").insert(db)
|
|
/// return try Player.fetchCount(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Database operations run in the writer dispatch queue, serialized
|
|
/// with all database updates performed by this `DatabaseWriter`.
|
|
///
|
|
/// The ``Database`` argument to `updates` is valid only during the
|
|
/// execution of the closure. Do not store or return the database connection
|
|
/// for later use.
|
|
///
|
|
/// It is a programmer error to call this method from another database
|
|
/// access method. Doing so raises a "Database methods are not reentrant"
|
|
/// fatal error at runtime.
|
|
///
|
|
/// - warning: Database operations are not wrapped in a transaction. They
|
|
/// can see changes performed by concurrent writes or writes performed by
|
|
/// other processes: two identical requests performed by the `updates`
|
|
/// closure may not return the same value. Concurrent database accesses
|
|
/// can see partial updates performed by the `updates` closure.
|
|
///
|
|
/// - parameter updates: A closure which accesses the database.
|
|
/// - throws: The error thrown by `updates`.
|
|
public func inDatabase<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
|
try writer.sync(updates)
|
|
}
|
|
|
|
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
|
try writer.reentrantSync(updates)
|
|
}
|
|
|
|
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
|
|
writer.async(updates)
|
|
}
|
|
}
|
|
|
|
// MARK: - Temp Copy
|
|
|
|
extension DatabaseQueue {
|
|
/// Returns a connection to an in-memory copy of the database at `path`.
|
|
///
|
|
/// Changes performed on the returned connection do not impact the
|
|
/// original database at `path`.
|
|
///
|
|
/// The database memory is released when the returned connection
|
|
/// is deallocated.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let path = "/path/to/database.sqlite"
|
|
/// let dbQueue = try DatabaseQueue.inMemoryCopy(fromPath: path)
|
|
/// ```
|
|
public static func inMemoryCopy(
|
|
fromPath path: String,
|
|
configuration: Configuration = Configuration())
|
|
throws -> DatabaseQueue
|
|
{
|
|
var sourceConfig = configuration
|
|
sourceConfig.readonly = true
|
|
let source = try DatabaseQueue(path: path, configuration: sourceConfig)
|
|
|
|
var copyConfig = configuration
|
|
copyConfig.readonly = false
|
|
let result = try DatabaseQueue(configuration: copyConfig)
|
|
|
|
try source.backup(to: result)
|
|
|
|
if configuration.readonly {
|
|
// Result was not opened read-only so that we could perform the
|
|
// copy. And SQLITE_OPEN_READONLY has no effect on in-memory
|
|
// databases anyway.
|
|
//
|
|
// So let's simulate read-only with PRAGMA query_only.
|
|
try result.inDatabase { db in
|
|
try db.beginReadOnly()
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/// Returns a connection to a private, temporary, on-disk copy of the
|
|
/// database at `path`.
|
|
///
|
|
/// Changes performed on the returned connection do not impact the
|
|
/// original database at `path`.
|
|
///
|
|
/// The on-disk copy will be automatically deleted from disk as soon as
|
|
/// the returned connection is closed or deallocated.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// let path = "/path/to/database.sqlite"
|
|
/// let dbQueue = try DatabaseQueue.temporaryCopy(fromPath: path)
|
|
/// ```
|
|
public static func temporaryCopy(
|
|
fromPath path: String,
|
|
configuration: Configuration = Configuration())
|
|
throws -> DatabaseQueue
|
|
{
|
|
var sourceConfig = configuration
|
|
sourceConfig.readonly = true
|
|
let source = try DatabaseQueue(path: path, configuration: sourceConfig)
|
|
|
|
// <https://www.sqlite.org/c3ref/open.html>
|
|
// > If the filename is an empty string, then a private, temporary
|
|
// > on-disk database will be created. This private database will be
|
|
// > automatically deleted as soon as the database connection
|
|
// > is closed.
|
|
var copyConfig = configuration
|
|
copyConfig.readonly = false
|
|
let result = try DatabaseQueue(path: "", configuration: copyConfig)
|
|
|
|
try source.backup(to: result)
|
|
|
|
if configuration.readonly {
|
|
// Result was not opened read-only so that we could perform the
|
|
// copy. So let's simulate read-only with PRAGMA query_only.
|
|
try result.inDatabase { db in
|
|
try db.beginReadOnly()
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|