Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GRDB 7: Add missing Sendable conformances #1607

Merged
merged 30 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ad74526
BusyCallback and BusyMode are Sendable
groue Aug 25, 2024
0457a62
TransactionClock is Sendable
groue Mar 30, 2024
8e860e6
Configuration is Sendable
groue Mar 30, 2024
876a087
Coding strategies are Sendable
groue Mar 30, 2024
b752229
DatabaseFunction is Sendable
groue Feb 10, 2024
300e5fe
DatabaseFunction is Identifiable
groue Aug 25, 2024
a5806a0
[BREAKING] DatabaseFunction is no longer Hashable
groue Aug 25, 2024
405312f
DatabaseCollation is Sendable
groue Feb 10, 2024
eedd165
DatabaseCollation is Identifiable
groue Aug 25, 2024
c1f59df
[BREAKING] DatabaseCollation is no longer Hashable
groue Aug 25, 2024
55cdb4e
DatabaseMigrator is Sendable
groue Feb 11, 2024
961f6c4
FilterCursor can't be made Sendable
groue Mar 30, 2024
7aff0b9
RowAdapter is Sendable
groue Mar 30, 2024
5333dbe
LogErrorFunction is Sendable
groue Mar 30, 2024
e93e4c5
ReadWriteBox is Sendable
groue Feb 11, 2024
b587afb
Pool is Sendable
groue Feb 11, 2024
b0c5725
OnDemandFuture fulfill is Sendable
groue Mar 31, 2024
1036371
DatabasePromise is Sendable
groue Aug 25, 2024
0fdd910
⚠️ Make TableAlias Sendable even if it is not yet.
groue Mar 31, 2024
56e2ab8
SQLAssociationCondition is Sendable
groue Apr 24, 2024
30569ab
OrderedDictionary is conditionally Sendable
groue Apr 23, 2024
aff662c
SQL is Sendable
groue Mar 31, 2024
cc5798c
WALSnapshotTransaction is Sendable
groue Mar 31, 2024
47aa1f5
TODO
groue Aug 26, 2024
4222854
DatabaseRegionConvertible is Sendable
groue Aug 27, 2024
c56b529
DatabaseCancellable is Sendable
groue Aug 27, 2024
aac8720
DatabaseRegionObservation is Sendable
groue Aug 27, 2024
849bba2
Refactor ValueReducer for Swift concurrency
groue Aug 27, 2024
c08b1fa
ValueObservationScheduler is Sendable
groue Aug 31, 2024
b1424d1
[BREAKING] PersistenceContainer is Sendable
groue Aug 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SQLite3
import Dispatch
import Foundation

public struct Configuration {
public struct Configuration: Sendable {

// MARK: - Misc options

Expand Down Expand Up @@ -195,7 +195,7 @@ public struct Configuration {

// MARK: - Managing SQLite Connections

private var setups: [(Database) throws -> Void] = []
private var setups: [@Sendable (Database) throws -> Void] = []

/// Defines a function to run whenever an SQLite connection is opened.
///
Expand Down Expand Up @@ -232,7 +232,7 @@ public struct Configuration {
///
/// On newly created databases files, ``DatabasePool`` activates the WAL
/// mode after the preparation functions have run.
public mutating func prepareDatabase(_ setup: @escaping (Database) throws -> Void) {
public mutating func prepareDatabase(_ setup: @escaping @Sendable (Database) throws -> Void) {
setups.append(setup)
}

Expand Down Expand Up @@ -432,9 +432,25 @@ public struct Configuration {
/// through a `SerializedDatabase`.
var threadingMode = Database.ThreadingMode.default

var SQLiteConnectionDidOpen: (() -> Void)?
var SQLiteConnectionWillClose: ((SQLiteConnection) -> Void)?
var SQLiteConnectionDidClose: (() -> Void)?
private(set) var SQLiteConnectionDidOpen: (@Sendable () -> Void)?
private(set) var SQLiteConnectionWillClose: (@Sendable (SQLiteConnection) -> Void)?
private(set) var SQLiteConnectionDidClose: (@Sendable () -> Void)?

// Workaround https://github.com/apple/swift/issues/72727
mutating func onConnectionDidOpen(_ callback: @escaping @Sendable () -> Void) {
SQLiteConnectionDidOpen = callback
}

// Workaround https://github.com/apple/swift/issues/72727
mutating func onConnectionWillClose(_ callback: @escaping @Sendable (SQLiteConnection) -> Void) {
SQLiteConnectionWillClose = callback
}

// Workaround https://github.com/apple/swift/issues/72727
mutating func onConnectionDidClose(_ callback: @escaping @Sendable () -> Void) {
SQLiteConnectionDidClose = callback
}

var SQLiteOpenFlags: CInt {
var flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE)
if sqlite3_libversion_number() >= 3037000 {
Expand Down
4 changes: 4 additions & 0 deletions GRDB/Core/Cursor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,10 @@ public final class FilterCursor<Base: Cursor> {
}
}

// Explicit non-conformance to Sendable.
@available(*, unavailable)
extension FilterCursor: Sendable { }

extension FilterCursor: Cursor {
public func next() throws -> Base.Element? {
while let element = try base.next() {
Expand Down
46 changes: 35 additions & 11 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand All @@ -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: <https://www.sqlite.org/errlog.html>
public static var logError: LogErrorFunction? = nil {
nonisolated(unsafe) public static var logError: LogErrorFunction? = nil {
didSet {
if logError != nil {
_registerErrorLogCallback { (_, code, message) in
Expand Down Expand Up @@ -361,10 +385,10 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
private var trace: ((TraceEvent) -> Void)?

/// The registered custom SQL functions.
private var functions = Set<DatabaseFunction>()
private var functions: [DatabaseFunction.ID: DatabaseFunction] = [:]

/// The registered custom SQL collations.
private var collations = Set<DatabaseCollation>()
private var collations: [DatabaseCollation.ID: DatabaseCollation] = [:]

/// Support for `beginReadOnly()` and `endReadOnly()`.
private var readOnlyDepth = 0
Expand Down Expand Up @@ -707,13 +731,13 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
/// let dbPool = try DatabasePool(path: ..., configuration: config)
/// ```
public func add(function: DatabaseFunction) {
functions.update(with: function)
functions[function.id] = function
function.install(in: self)
}

/// Removes a custom SQL function.
public func remove(function: DatabaseFunction) {
functions.remove(function)
functions.removeValue(forKey: function.id)
function.uninstall(in: self)
}

Expand All @@ -734,7 +758,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
/// let dbPool = try DatabasePool(path: ..., configuration: config)
/// ```
public func add(collation: DatabaseCollation) {
collations.update(with: collation)
collations[collation.id] = collation
let collationPointer = Unmanaged.passUnretained(collation).toOpaque()
let code = sqlite3_create_collation_v2(
sqliteConnection,
Expand All @@ -753,7 +777,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib

/// Removes a collation.
public func remove(collation: DatabaseCollation) {
collations.remove(collation)
collations.removeValue(forKey: collation.id)
sqlite3_create_collation_v2(
sqliteConnection,
collation.name,
Expand Down Expand Up @@ -1830,8 +1854,8 @@ extension Database {

// MARK: - Database-Related Types

/// See BusyMode and <https://www.sqlite.org/c3ref/busy_handler.html>
public typealias BusyCallback = (_ numberOfTries: Int) -> Bool
/// See ``BusyMode`` and <https://www.sqlite.org/c3ref/busy_handler.html>
public typealias BusyCallback = @Sendable (_ numberOfTries: Int) -> Bool

/// When there are several connections to a database, a connection may try
/// to access the database while it is locked by another connection.
Expand Down Expand Up @@ -1859,7 +1883,7 @@ extension Database {
/// - <https://www.sqlite.org/c3ref/busy_handler.html>
/// - <https://www.sqlite.org/lang_transaction.html>
/// - <https://www.sqlite.org/wal.html>
public enum BusyMode {
public enum BusyMode: Sendable {
/// The `SQLITE_BUSY` error is immediately returned to the connection
/// that tries to access the locked database.
case immediateError
Expand Down Expand Up @@ -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.
///
Expand Down
50 changes: 29 additions & 21 deletions GRDB/Core/DatabaseCollation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,36 @@ import Foundation
/// - ``localizedCompare``
/// - ``localizedStandardCompare``
/// - ``unicodeCompare``
public final class DatabaseCollation {
public final class DatabaseCollation: Identifiable, Sendable {
/// The identifier of an SQLite collation.
///
/// SQLite identifies collations by their name (case insensitive).
public struct ID: Hashable {
var name: String

// Collation equality is based on the sqlite3_strnicmp SQLite function.
// (see https://www.sqlite.org/c3ref/create_collation.html). Computing
// a hash value that honors the Swift Hashable contract (value equality
// implies hash equality) is thus non trivial. But it's not that
// important, since this hashValue is only used when one adds
// or removes a collation from a database connection.
public func hash(into hasher: inout Hasher) {
hasher.combine(0)
}

/// Two collations are equal if they share the same name (case insensitive)
public static func == (lhs: Self, rhs: Self) -> Bool {
// See <https://www.sqlite.org/c3ref/create_collation.html>
return sqlite3_stricmp(lhs.name, rhs.name) == 0
}
}

/// The identifier of the collation.
public var id: ID { ID(name: name) }

/// The name of the collation.
public let name: String
let function: (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult
let function: @Sendable (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult

/// Creates a collation.
///
Expand All @@ -49,7 +75,7 @@ public final class DatabaseCollation {
/// - parameters:
/// - name: The collation name.
/// - function: A function that compares two strings.
public init(_ name: String, function: @escaping (String, String) -> ComparisonResult) {
public init(_ name: String, function: @escaping @Sendable (String, String) -> ComparisonResult) {
self.name = name
self.function = { (length1, buffer1, length2, buffer2) in
// Buffers are not C strings: they do not end with \0.
Expand All @@ -67,21 +93,3 @@ public final class DatabaseCollation {
}
}
}

extension DatabaseCollation: Hashable {
// Collation equality is based on the sqlite3_strnicmp SQLite function.
// (see https://www.sqlite.org/c3ref/create_collation.html). Computing
// a hash value that honors the Swift Hashable contract (value equality
// implies hash equality) is thus non trivial. But it's not that
// important, since this hashValue is only used when one adds
// or removes a collation from a database connection.
public func hash(into hasher: inout Hasher) {
hasher.combine(0)
}

/// Two collations are equal if they share the same name (case insensitive)
public static func == (lhs: DatabaseCollation, rhs: DatabaseCollation) -> Bool {
// See <https://www.sqlite.org/c3ref/create_collation.html>
return sqlite3_stricmp(lhs.name, rhs.name) == 0
}
}
4 changes: 4 additions & 0 deletions GRDB/Core/DatabaseError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ extension DatabaseError {
static func connectionIsClosed() -> Self {
DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed")
}

static func snapshotIsLost() -> Self {
DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.")
}
}

// Support for `catch DatabaseError.SQLITE_XXX`
Expand Down
49 changes: 21 additions & 28 deletions GRDB/Core/DatabaseFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@ import SQLite3
/// - ``localizedUppercase``
/// - ``lowercase``
/// - ``uppercase``
public final class DatabaseFunction: Hashable {
// SQLite identifies functions by (name + argument count)
private struct Identity: Hashable {
public final class DatabaseFunction: Identifiable, Sendable {
/// The identifier of an SQLite function.
///
/// SQLite identifies functions by their name and argument count.
public struct ID: Hashable, Sendable {
let name: String
let nArg: CInt // -1 for variadic functions
}

/// The name of the SQL function
public var name: String { identity.name }
private let identity: Identity
/// The name of the SQL function.
public var name: String { id.name }

/// The identifier of the SQL function.
public let id: ID
let isPure: Bool
private let kind: Kind
private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) }
Expand Down Expand Up @@ -82,11 +86,11 @@ public final class DatabaseFunction: Hashable {
_ name: String,
argumentCount: Int? = nil,
pure: Bool = false,
function: @escaping ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?)
function: @escaping @Sendable ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?)
{
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
self.id = ID(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
self.isPure = pure
self.kind = .function{ (argc, argv) in
self.kind = .function { (argc, argv) in
let arguments = (0..<Int(argc)).map { index in
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
}
Expand Down Expand Up @@ -148,7 +152,7 @@ public final class DatabaseFunction: Hashable {
pure: Bool = false,
aggregate: Aggregate.Type)
{
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
self.id = ID(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
self.isPure = pure
self.kind = .aggregate { Aggregate() }
}
Expand Down Expand Up @@ -207,8 +211,8 @@ public final class DatabaseFunction: Hashable {

let code = sqlite3_create_function_v2(
db.sqliteConnection,
identity.name,
identity.nArg,
id.name,
id.nArg,
eTextRep,
definitionP,
kind.xFunc,
Expand All @@ -230,8 +234,8 @@ public final class DatabaseFunction: Hashable {
func uninstall(in db: Database) {
let code = sqlite3_create_function_v2(
db.sqliteConnection,
identity.name,
identity.nArg,
id.name,
id.nArg,
eTextRep,
nil, nil, nil, nil, nil)

Expand Down Expand Up @@ -274,12 +278,12 @@ public final class DatabaseFunction: Hashable {

/// A function kind: an "SQL function" or an "aggregate".
/// See <http://sqlite.org/capi3ref.html#sqlite3_create_function>
private enum Kind {
private enum Kind: Sendable {
/// A regular function: SELECT f(1)
case function((CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?)
case function(@Sendable (CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?)

/// An aggregate: SELECT f(foo) FROM bar GROUP BY baz
case aggregate(() -> any DatabaseAggregate)
case aggregate(@Sendable () -> any DatabaseAggregate)

/// Feeds the `pApp` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
Expand Down Expand Up @@ -424,17 +428,6 @@ public final class DatabaseFunction: Hashable {
}
}

extension DatabaseFunction {
public func hash(into hasher: inout Hasher) {
hasher.combine(identity)
}

/// Two functions are equal if they share the same name and arity.
public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool {
lhs.identity == rhs.identity
}
}

/// The protocol for custom SQLite aggregates.
///
/// For example:
Expand Down
6 changes: 2 additions & 4 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,15 @@ public final class DatabasePool {
// 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)
makeElement: { [readerConfiguration] index in
return try SerializedDatabase(
path: path,
configuration: readerConfiguration,
defaultLabel: "GRDB.DatabasePool",
purpose: "reader.\(readerCount)")
purpose: "reader.\(index)")
})

// Set up journal mode unless readonly
Expand Down
Loading