From 9ceae3fb81b9e0a0dde1d013e3f0257f023f69a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Mar 2024 19:36:05 +0100 Subject: [PATCH] LogErrorFunction is Sendable --- GRDB/Core/Database.swift | 30 ++++++++++++++++++--- TODO.md | 2 +- Tests/GRDBTests/DatabasePoolTests.swift | 4 +-- Tests/GRDBTests/DatabaseQueueTests.swift | 2 +- Tests/GRDBTests/DatabaseSnapshotTests.swift | 2 +- Tests/GRDBTests/GRDBTestCase.swift | 14 +++++----- 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index dec9d1273b..fb0715e4e6 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -122,8 +122,14 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_ /// - ``logError`` /// - ``releaseMemory()`` /// - ``trace(options:_:)`` +/// +/// ### Supporting Types +/// +/// - ``BusyCallback`` +/// - ``BusyMode`` /// - ``CheckpointMode`` /// - ``DatabaseBackupProgress`` +/// - ``LogErrorFunction`` /// - ``StorageClass`` /// - ``TraceEvent`` /// - ``TracingOptions`` @@ -143,8 +149,26 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// The error logging function. /// + /// SQLite can be configured to invoke a callback function containing + /// an error code and a terse error message whenever anomalies occur. + /// + /// This global error callback must be configured early in the lifetime + /// of your application: + /// + /// ```swift + /// Database.logError = { (resultCode, message) in + /// NSLog("%@", "SQLite error \(resultCode): \(message)") + /// } + /// ``` + /// + /// - warning: Database.logError must be set before any database + /// connection is opened. This includes the connections that your + /// application opens with GRDB, but also connections opened by + /// other tools, such as third-party libraries. Setting it after a + /// connection has been opened is an SQLite misuse, and has no effect. + /// /// Related SQLite documentation: - public static var logError: LogErrorFunction? = nil { + nonisolated(unsafe) public static var logError: LogErrorFunction? = nil { didSet { if logError != nil { _registerErrorLogCallback { (_, code, message) in @@ -1830,7 +1854,7 @@ extension Database { // MARK: - Database-Related Types - /// See BusyMode and + /// See ``BusyMode`` and public typealias BusyCallback = @Sendable (_ numberOfTries: Int) -> Bool /// When there are several connections to a database, a connection may try @@ -2019,7 +2043,7 @@ extension Database { } /// An error log function that takes an error code and message. - public typealias LogErrorFunction = (_ resultCode: ResultCode, _ message: String) -> Void + public typealias LogErrorFunction = @Sendable (_ resultCode: ResultCode, _ message: String) -> Void /// An SQLite storage class. /// diff --git a/TODO.md b/TODO.md index 132ca83bc9..c9ed6dceb5 100644 --- a/TODO.md +++ b/TODO.md @@ -109,7 +109,7 @@ - [X] GRDB7: Sendable: RowAdapter (d138af26) - [ ] GRDB7: Sendable: ValueObservationScheduler (8429eb68) - [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) -- [ ] GRDB7: Sendable: LogErrorFunction (f362518d) +- [X] GRDB7: Sendable: LogErrorFunction (f362518d) - [ ] GRDB7: Sendable: ReadWriteBox (57a86a0e) - [ ] GRDB7: Sendable: Pool (f13b2d2e) - [ ] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index 2e42bb5ccd..21b76f64ea 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -397,7 +397,7 @@ class DatabasePoolTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try dbPool.write { db in @@ -427,7 +427,7 @@ class DatabasePoolTests: GRDBTestCase { // // The first comes from GRDB, and the second, depending on the SQLite // version, from `sqlite3_close_v2()`. Write the test so that it always pass: - XCTAssert(lastMessage!.contains("unfinalized statement")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement")) // Database is in a zombie state. // In the zombie state, access throws SQLITE_MISUSE diff --git a/Tests/GRDBTests/DatabaseQueueTests.swift b/Tests/GRDBTests/DatabaseQueueTests.swift index bf0bb5371f..3f748f06ea 100644 --- a/Tests/GRDBTests/DatabaseQueueTests.swift +++ b/Tests/GRDBTests/DatabaseQueueTests.swift @@ -458,7 +458,7 @@ class DatabaseQueueTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try dbQueue.inDatabase { db in diff --git a/Tests/GRDBTests/DatabaseSnapshotTests.swift b/Tests/GRDBTests/DatabaseSnapshotTests.swift index 6730a8984f..1cb422ecdd 100644 --- a/Tests/GRDBTests/DatabaseSnapshotTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotTests.swift @@ -424,7 +424,7 @@ class DatabaseSnapshotTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try snapshot.read { db in diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 7fe7ac6b3d..a4151604ed 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -12,15 +12,15 @@ import XCTest @testable import GRDB // Support for Database.logError -var lastResultCode: ResultCode? = nil -var lastMessage: String? = nil +struct SQLiteDiagnostic { + var resultCode: ResultCode + var message: String +} +private let lastSQLiteDiagnosticMutex = Mutex(nil) +var lastSQLiteDiagnostic: SQLiteDiagnostic? { lastSQLiteDiagnosticMutex.load() } let logErrorSetup: Void = { - let lock = NSLock() Database.logError = { (resultCode, message) in - lock.lock() - defer { lock.unlock() } - lastResultCode = resultCode - lastMessage = message + lastSQLiteDiagnosticMutex.store(SQLiteDiagnostic(resultCode: resultCode, message: message)) } }()