You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A DatabaseWriter wrapper that preserves its own object identity while swapping its underlying connection (typical "account switch" pattern in apps that keep @Dependency(\.defaultDatabase) stable across logins) silently breaks @FetchOne. Writes against the new connection never reach the property wrapper. @FetchAll is not affected — by accident, not by design.
FetchKeyID (Sources/SQLiteData/Internal/FetchKey.swift L172-188) hashes ObjectIdentifier(database). swift-sharing'"'"'s PersistentReferences dedupes by key.id, so a wrapper that keeps its own identity but swaps inner pools hits the cached _PersistentReference. That reference'"'"'s GRDB ValueObservation is still subscribed to the previous inner connection (now closed); writes to the new connection never reach the @FetchOne.
@FetchAll happens to escape because FetchKey<[T]>.Default (_SharedKeyDefault wrapping) takes a different generic-cast path inside PersistentReferences.value(forKey:) and misses the cache by accident.
Proposed fix
Add FetchKeyCache.invalidate(database:). Wrappers call it after swapping the inner connection. Subscriptions registered against that wrapper'"'"'s ObjectIdentifier reattach to the wrapper'"'"'s current inner connection; existing GRDB cancellables are released. The next .load() reflects the new connection'"'"'s state and pushes resume.
Additive, no public API breakage. Code that doesn'"'"'t swap an inner connection is unaffected.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
A
DatabaseWriterwrapper that preserves its own object identity while swapping its underlying connection (typical "account switch" pattern in apps that keep@Dependency(\.defaultDatabase)stable across logins) silently breaks@FetchOne. Writes against the new connection never reach the property wrapper.@FetchAllis not affected — by accident, not by design.Reproduction
Minimal Swift package, runs in 5 sec:
https://github.com/no33jou/fetchone-mutable-db-repro
The two
[Identity] mutable: ObjectIdentifier(0x…)lines printed before and after the swap are identical — that is the bug'"'"'s fingerprint.Versions: sqlite-data 1.6.1 · swift-sharing 2.7.4 · GRDB 7.x · macOS 14+.
Root cause
FetchKeyID(Sources/SQLiteData/Internal/FetchKey.swift L172-188) hashesObjectIdentifier(database). swift-sharing'"'"'sPersistentReferencesdedupes bykey.id, so a wrapper that keeps its own identity but swaps inner pools hits the cached_PersistentReference. That reference'"'"'s GRDBValueObservationis still subscribed to the previous inner connection (now closed); writes to the new connection never reach the@FetchOne.@FetchAllhappens to escape becauseFetchKey<[T]>.Default(_SharedKeyDefaultwrapping) takes a different generic-cast path insidePersistentReferences.value(forKey:)and misses the cache by accident.Proposed fix
Add
FetchKeyCache.invalidate(database:). Wrappers call it after swapping the inner connection. Subscriptions registered against that wrapper'"'"'sObjectIdentifierreattach to the wrapper'"'"'s current inner connection; existing GRDB cancellables are released. The next.load()reflects the new connection'"'"'s state and pushes resume.Additive, no public API breakage. Code that doesn'"'"'t swap an inner connection is unaffected.
PR: #455
Alternatives considered
invalidateAll()/invalidate(forKey:)): wider blast radius, pollutes the cache abstraction. Rejected for round 1.$prop = FetchOne(wrappedValue: ...)): works (see repro--fix) but every user has to re-derive it. Not discoverable.Happy to discuss API shape and scope before merge.
All reactions