Skip to content

Commit

Permalink
Merge pull request #1618 from groue/dev/GRDB7-Sendable-database-access
Browse files Browse the repository at this point in the history
GRDB 7: Sendable database accesses
  • Loading branch information
groue authored Sep 14, 2024
2 parents a7a9c75 + 945f398 commit 4f1587a
Show file tree
Hide file tree
Showing 35 changed files with 537 additions and 384 deletions.
46 changes: 29 additions & 17 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ extension DatabasePool: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func read<T>(
public func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
Expand Down Expand Up @@ -390,7 +390,9 @@ extension DatabasePool: DatabaseReader {
}
}

public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
Expand Down Expand Up @@ -435,7 +437,7 @@ extension DatabasePool: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func unsafeRead<T>(
public func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
guard let readerPool else {
Expand Down Expand Up @@ -469,7 +471,9 @@ extension DatabasePool: DatabaseReader {
}
}

public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
Expand Down Expand Up @@ -514,7 +518,9 @@ extension DatabasePool: DatabaseReader {
}
}

public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func spawnConcurrentRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
asyncConcurrentRead(value)
}

Expand Down Expand Up @@ -555,7 +561,9 @@ extension DatabasePool: DatabaseReader {
/// ```
///
/// - parameter value: A function that accesses the database.
public func asyncConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncConcurrentRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
Expand Down Expand Up @@ -714,7 +722,9 @@ extension DatabasePool: DatabaseReader {
///
/// - 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) {
func asyncWALSnapshotTransaction(
_ completion: @escaping @Sendable (Result<WALSnapshotTransaction, Error>) -> Void
) {
guard let readerPool else {
completion(.failure(DatabaseError.connectionIsClosed()))
return
Expand All @@ -740,9 +750,8 @@ extension DatabasePool: DatabaseReader {
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
if configuration.readonly {
// The easy case: the database does not change
return _addReadOnly(
Expand Down Expand Up @@ -771,9 +780,8 @@ extension DatabasePool: DatabaseReader {
private func _addConcurrent<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
assert(!configuration.readonly, "Use _addReadOnly(observation:) instead")
assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead")
let observer = ValueConcurrentObserver(
Expand All @@ -796,7 +804,7 @@ extension DatabasePool: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func writeWithoutTransaction<T>(
public func writeWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(updates)
Expand All @@ -813,7 +821,7 @@ extension DatabasePool: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func barrierWriteWithoutTransaction<T>(
public func barrierWriteWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
let dbAccess = CancellableDatabaseAccess()
Expand All @@ -833,7 +841,9 @@ extension DatabasePool: DatabaseWriter {
}
}

public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
public func asyncBarrierWriteWithoutTransaction(
_ updates: @escaping @Sendable (Result<Database, Error>) -> Void
) {
guard let readerPool else {
updates(.failure(DatabaseError.connectionIsClosed()))
return
Expand Down Expand Up @@ -887,7 +897,9 @@ extension DatabasePool: DatabaseWriter {
try writer.reentrantSync(updates)
}

public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
public func asyncWriteWithoutTransaction(
_ updates: @escaping @Sendable (Database) -> Void
) {
writer.async(updates)
}
}
Expand Down
33 changes: 21 additions & 12 deletions GRDB/Core/DatabaseQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ extension DatabaseQueue: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func read<T>(
public func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute { db in
Expand All @@ -244,7 +244,9 @@ extension DatabaseQueue: DatabaseReader {
}
}

public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
writer.async { db in
defer {
// Ignore error because we can not notify it.
Expand All @@ -271,21 +273,25 @@ extension DatabaseQueue: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func unsafeRead<T>(
public func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(value)
}

public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (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 spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func spawnConcurrentRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
Expand Down Expand Up @@ -315,9 +321,8 @@ extension DatabaseQueue: DatabaseReader {
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
if configuration.readonly {
// The easy case: the database does not change
return _addReadOnly(
Expand Down Expand Up @@ -382,7 +387,7 @@ extension DatabaseQueue: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func writeWithoutTransaction<T>(
public func writeWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(updates)
Expand All @@ -394,13 +399,15 @@ extension DatabaseQueue: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func barrierWriteWithoutTransaction<T>(
public func barrierWriteWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(updates)
}

public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
public func asyncBarrierWriteWithoutTransaction(
_ updates: @escaping @Sendable (Result<Database, Error>) -> Void
) {
writer.async { updates(.success($0)) }
}

Expand Down Expand Up @@ -446,7 +453,9 @@ extension DatabaseQueue: DatabaseWriter {
try writer.reentrantSync(updates)
}

public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
public func asyncWriteWithoutTransaction(
_ updates: @escaping @Sendable (Database) -> Void
) {
writer.async(updates)
}
}
Expand Down
47 changes: 27 additions & 20 deletions GRDB/Core/DatabaseReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// database access, or the error thrown by `value`, or
/// `CancellationError` if the task is cancelled.
@available(iOS 13, macOS 10.15, tvOS 13, *)
func read<T>(
func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T

Expand Down Expand Up @@ -247,7 +247,9 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// - parameter value: A closure which accesses the database. Its argument
/// is a `Result` that provides the database connection, or the failure
/// that would prevent establishing the read access to the database.
func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void)
func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
)

/// Executes database operations, and returns their result after they have
/// finished executing.
Expand Down Expand Up @@ -322,7 +324,7 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// database access, or the error thrown by `value`, or
/// `CancellationError` if the task is cancelled.
@available(iOS 13, macOS 10.15, tvOS 13, *)
func unsafeRead<T>(
func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T

Expand Down Expand Up @@ -357,7 +359,9 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// - parameter value: A closure which accesses the database. Its argument
/// is a `Result` that provides the database connection, or the failure
/// that would prevent establishing the read access to the database.
func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void)
func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
)

/// Executes database operations, and returns their result after they have
/// finished executing.
Expand Down Expand Up @@ -415,8 +419,8 @@ public protocol DatabaseReader: AnyObject, Sendable {
func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable
}

extension DatabaseReader {
Expand Down Expand Up @@ -534,9 +538,8 @@ extension DatabaseReader {
@available(iOS 13, macOS 10.15, tvOS 13, *)
public func readPublisher<Output>(
receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main,
value: @escaping (Database) throws -> Output)
-> DatabasePublishers.Read<Output>
{
value: @escaping @Sendable (Database) throws -> Output
) -> DatabasePublishers.Read<Output> {
OnDemandFuture { fulfill in
self.asyncRead { dbResult in
fulfill(dbResult.flatMap { db in Result { try value(db) } })
Expand Down Expand Up @@ -582,9 +585,8 @@ extension DatabaseReader {
func _addReadOnly<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
if scheduler.immediateInitialValue() {
do {
// Perform a reentrant read, in case the observation would be
Expand Down Expand Up @@ -659,13 +661,15 @@ extension AnyDatabaseReader: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func read<T>(
public func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await base.read(value)
}

public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
base.asyncRead(value)
}

Expand All @@ -675,13 +679,15 @@ extension AnyDatabaseReader: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func unsafeRead<T>(
public func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await base.unsafeRead(value)
}

public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
base.asyncUnsafeRead(value)
}

Expand All @@ -692,9 +698,8 @@ extension AnyDatabaseReader: DatabaseReader {
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
base._add(
observation: observation,
scheduling: scheduler,
Expand Down Expand Up @@ -753,7 +758,9 @@ extension DatabaseSnapshotReader {
}

// There is no such thing as an unsafe access to a snapshot.
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
asyncRead(value)
}
}
Loading

0 comments on commit 4f1587a

Please sign in to comment.