Replies: 1 comment
-
I'm glad to see this as a genericized form of my single-row tables tooling in #7. Regardless, I have some thoughts. Any With Overall, this isn't much of an issue for a single record joined from a few tables. However, it would be easy to misuse That being said, In your particular implementation above, I think we can make it slightly less constrained by allowing records with composite keys or just general unique indexes to be allowed. GRDB has a Another consideration would be the use of an optional as the underlying return type. To keep it consistent with This brings us to something like: import Dependencies
import GRDB
import Sharing
import SwiftUI
extension SharedKey {
/// A key that can read and write a GRDB record.
public static func record<Record>(
key: any DatabaseValueConvertible & Sendable & Hashable
) -> Self where Self == RecordKey<Record> {
RecordKey(keys: ["id": key])
}
/// A key that can read and write a GRDB record.
public static func record<Record>(
keys: [String: (any DatabaseValueConvertible & Sendable & Hashable)?]
) -> Self where Self == RecordKey<Record> {
RecordKey(keys: keys)
}
}
public struct RecordKey<Record: Sendable & Equatable>: SharedKey
where Record: FetchableRecord & MutablePersistableRecord {
let database: any DatabaseWriter
let keys: [String: (any DatabaseValueConvertible & Sendable & Hashable)?]
let animation: Animation?
public typealias ID = RecordKeyID
public var id: ID {
ID(rawValue: self.keys)
}
public init(
keys: [String: (any DatabaseValueConvertible & Sendable & Hashable)?],
animation: Animation? = nil
) {
@Dependency(\.defaultDatabase) var database
self.database = database
self.keys = keys
self.animation = animation
}
public func load(context: LoadContext<Record>, continuation: LoadContinuation<Record>) {
continuation.resume(
with: Result { try self.database.read { db in try Record.find(db, key: self.keys) } }
)
}
public func subscribe(
context: LoadContext<Record>,
subscriber: SharedSubscriber<Record>
) -> SharedSubscription {
let cancellable = ValueObservation.tracking { db in try Record.find(db, key: self.keys) }
.start(in: self.database, scheduling: .animation(self.animation)) { error in
subscriber.yield(throwing: error)
} onChange: { newValue in
subscriber.yield(newValue)
}
return SharedSubscription { cancellable.cancel() }
}
public func save(_ value: Record?, context: SaveContext, continuation: SaveContinuation) {
guard var value else {
continuation.resume()
return
}
do {
try self.database.write { db in
try value.save(db)
}
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
public struct RecordKeyID: Hashable {
fileprivate let rawValue: [String: AnyHashableSendable?]
fileprivate init(rawValue: [String: (any DatabaseValueConvertible & Sendable & Hashable)?]) {
self.rawValue = rawValue.mapValues { value in
value.map { AnyHashableSendable($0) }
}
}
} Overall, I think it would be great if this could be less constrained such that it works with records that require more complex joins to fetch, but it would have to be done in such a way to uphold the load-save coherence property. |
Beta Was this translation helpful? Give feedback.
-
Here's a few tools I'm using to read and write from GRDB. It's pretty amazing, though I'm still feeling out when and if to use it. Offering these as starting points for more exploration and discussion.
RecordKey
This
SharedKey
takes theID
of anyFetchableRecord
, persisting any changes back to the database. Very simple but also pretty limited.UpdateKey
This is more general approach to writable keys, introducing
UpdateKeyRequest
to define howupdate
is defined in addition tofetch
I came to this pattern in a case where I was using
RecordKey
above, but needed to extend the fetch from a single record to the record plus some associated join data. In this case I want the record to be savable but not the associated data.Beta Was this translation helpful? Give feedback.
All reactions