Skip to content

Commit 552e84e

Browse files
committed
Add modify() for atomic Model read-modify-writes
1 parent 4b5fb25 commit 552e84e

File tree

1 file changed

+46
-0
lines changed

1 file changed

+46
-0
lines changed

Sources/Blackbird/BlackbirdModel.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ public enum BlackbirdTableError: Swift.Error {
278278
case impossibleMigration(type: any BlackbirdModel.Type, description: String)
279279
}
280280

281+
public enum BlackbirdModelError: Swift.Error {
282+
/// The requested `modify` action was given a primary key that doesn't exist
283+
case primaryKeyNotFound(value: [Any])
284+
}
281285

282286
internal extension BlackbirdModel {
283287
static var table: Blackbird.Table { SchemaGenerator.shared.table(for: Self.self) }
@@ -1003,9 +1007,49 @@ extension BlackbirdModel {
10031007
action(column, label, value)
10041008
}
10051009
}
1010+
1011+
/// Update an instance atomically.
1012+
///
1013+
/// - Parameters:
1014+
/// - database: The ``Blackbird/Database`` instance to update.
1015+
/// - primaryKey: The single-column primary-key value to update.
1016+
/// - changes: A block, passed a ``Blackbird/Database/Core`` and the matching instance, in which changes can be made to `instance` and other synchronous database operations may be called on `core`. If an error is thrown from within the block, the transaction will be canceled and no changes will be written.
1017+
/// - Returns: The value returned by the `changes` block, if any.
1018+
/// - Throws: Any error thrown from the `changes` block, or ``BlackbirdModelError/primaryKeyNotFound(value:)`` if no row exists in the database with the given `primaryKey` value.
1019+
///
1020+
/// This method is preferable to ``write(to:)-(Blackbird.Database)`` to avoid race conditions.
1021+
///
1022+
public static func modify<R: Sendable>(in database: Blackbird.Database, primaryKey: Sendable, changes: (@Sendable (_ core: isolated Blackbird.Database.Core, _ instance: inout Self) throws -> R)) async throws -> R {
1023+
return try await database.transaction { core in
1024+
return try modify(in: core, primaryKey: primaryKey, changes: changes)
1025+
}
1026+
}
1027+
1028+
/// Multicolumn primary-key version of ``modify(in:primaryKey:changes:)-(Blackbird.Database,_,_)``.
1029+
public static func modify<R: Sendable>(in database: Blackbird.Database, multicolumnPrimaryKey: [Sendable], changes: (@Sendable (_ core: isolated Blackbird.Database.Core, _ instance: inout Self) throws -> R)) async throws -> R {
1030+
return try await database.transaction { core in
1031+
return try modify(in: core, multicolumnPrimaryKey: multicolumnPrimaryKey, changes: changes)
1032+
}
1033+
}
1034+
1035+
/// Synchronous version of ``modify(in:primaryKey:changes:)-(Blackbird.Database,_,_)``.
1036+
public static func modify<R: Sendable>(in core: isolated Blackbird.Database.Core, primaryKey: Sendable, changes: (@Sendable (_ core: isolated Blackbird.Database.Core, _ instance: inout Self) throws -> R)) throws -> R {
1037+
return try modify(in: core, multicolumnPrimaryKey: [primaryKey], changes: changes)
1038+
}
10061039

1040+
/// Synchronous, multicolumn primary-key version of ``modify(in:primaryKey:changes:)-(Blackbird.Database,_,_)``.
1041+
public static func modify<R: Sendable>(in core: isolated Blackbird.Database.Core, multicolumnPrimaryKey: [Sendable], changes: (@Sendable (_ core: isolated Blackbird.Database.Core, _ instance: inout Self) throws -> R)) throws -> R {
1042+
guard var instance = try read(from: core, multicolumnPrimaryKey: multicolumnPrimaryKey) else { throw BlackbirdModelError.primaryKeyNotFound(value: multicolumnPrimaryKey) }
1043+
let result = try changes(core, &instance)
1044+
let database = try core.database()
1045+
if !instance.changedColumns(in: database).isEmpty { try instance.write(to: core) }
1046+
return result
1047+
}
1048+
10071049
/// Write this instance to a database.
10081050
/// - Parameter database: The ``Blackbird/Database`` instance to write to.
1051+
///
1052+
/// For updating existing instances in the database, ``modify(in:primaryKey:changes:)-(Blackbird.Database,_,_)`` is preferable to avoid race conditions.
10091053
public func write(to database: Blackbird.Database) async throws {
10101054
try await write(to: database.core)
10111055
}
@@ -1015,6 +1059,8 @@ extension BlackbirdModel {
10151059
/// - core: The isolated ``Blackbird/Database/Core`` provided to the transaction.
10161060
///
10171061
/// For use only when the database actor is isolated within calls to ``Blackbird/Database/transaction(_:)`` or ``Blackbird/Database/cancellableTransaction(_:)``.
1062+
///
1063+
/// For updating existing instances in the database, ``modify(in:primaryKey:changes:)-(Blackbird.Database.Core,_,_)`` is preferable to avoid race conditions.
10181064
public func write(to core: isolated Blackbird.Database.Core) throws {
10191065
let table = Self.table
10201066
let database = try core.database()

0 commit comments

Comments
 (0)