Add FetchKeyCache.invalidate for swappable database wrappers.#455
Add FetchKeyCache.invalidate for swappable database wrappers.#455no33jou wants to merge 1 commit intopointfreeco:mainfrom
Conversation
b143a2a to
181f781
Compare
|
Hi @no33jou, it seems to me that you are only able to reproduce this "bug" by making a custom conformance to Further, there are other ways to accomplish this "swappable" database idea that are not literally changing the database connection. You can attach/detach databases too. I would encourage you to explore that option as an alternative, or you will need to open a discussion on GRDB to get a better understanding of why custom conformances to |
`FetchKeyID` keyed on `ObjectIdentifier(database)` goes stale when a transparent wrapper swaps its inner pool while keeping its own object identity. Add a `PersistentDatabaseIdentity` hook so wrappers can forward identity to the current inner reader; plain `DatabasePool` / `DatabaseQueue` keep the existing `ObjectIdentifier` fallback.
181f781 to
a805ee6
Compare
|
Thanks for the response. One thing the contract argument doesn't explain: Why they diverge — swift-sharing init/load asymmetry: // SharedReader.init(wrappedValue:_:) — unwraps .Default
self.init(wrappedValue: wrappedValue(), key.base)
// SharedReader.load(_:) — does not unwrap
projectedValue = SharedReader(
reference: persistentReferences.value(forKey: key, ...)
)
// PersistentReferences.value(forKey:) — matches by exact type
storage[key.id] as? Weak<Key>Combined with sqlite-data's overload choice ( Fix (PR ready, ~30 lines + tests, 236 tests pass, no API change for public protocol PersistentDatabaseIdentity {
var persistentIdentity: AnyHashable { get }
}
// FetchKeyID derives databaseID from this hook when available,
// else falls back to ObjectIdentifier(database).
// Wrappers forward to current inner reader → swap invalidates cache.Decoupled from swift-sharing's asymmetry, so future symmetrization won't regress FetchAll. Branch: https://github.com/no33jou/sqlite-data/tree/fix/fetchkey-id-from-inner-pool (ATTACH/DETACH is a non-starter for per-user DB isolation — multi-week rearchitecture of StructuredQueries + region tracking. But that's secondary; the divergence reproduces regardless of swap implementation.) |
|
@no33jou Your branch still depends on subclassing |
|
fwiw I'm using |
Linked discussion: #454
Problem
A
DatabaseWriterwrapper that keeps its own object identity stable across inner-connection swaps causes@FetchOnepush to stop.FetchKeyIDusesObjectIdentifier(database); swift-sharing's persistent-reference cache hands back the reference whoseValueObservationis bound to the closed previous connection.@FetchAllonly escapes by accident (different generic-cast path insidePersistentReferences).Repro (5 sec, runs
swift run Repro/swift run Repro -- --fix):https://github.com/no33jou/fetchone-mutable-db-repro
Change
Sources/SQLiteData/FetchKeyCache.swift—public enum FetchKeyCache { public static func invalidate(database:) }. Internally an_FetchKeyRegistry(lock-isolated) keys reattach closures byObjectIdentifier(database).Sources/SQLiteData/Internal/FetchKey.swift—subscribe(...)now registers a reattach closure on each subscribe;invalidatecancels the existing GRDB cancellable and starts a fresh one against the wrapper's current inner connection. TheSharedSubscription's cancel closure unregisters from the registry.invalidatedoes not emit a new value to existing observers. Callers refresh state via the standard.load(...)flow after invalidating; this is the same call shape they already use after a connection swap, and it keeps the reattach safely outside of any in-progress observer callback (see code comments for the deadlock scenario this avoids).Additive: no public API surface change for callers that don't swap inner connections. No behavior change unless
FetchKeyCache.invalidate(database:)is called.Tests
Tests/SQLiteDataTests/FetchKeyCacheTests.swiftadds two cases against a minimalSwappableWriter:pushDropsAfterInnerSwap_withoutInvalidate— withoutinvalidate, writes to the new connection do not reach@FetchOne. Locks the regression in.pushRestoredAfterInvalidate— withinvalidate, push resumes against the new connection.I also verified the tests catch the bug: temporarily reverting
FetchKey.swiftwhile keeping the test file andFetchKeyCache.swiftmakes test (2) fail withunread → 0 ≠ 1. Test (1) keeps passing — it's the regression sanity, not the fix verification.Compatibility
public enum FetchKeyCache.invalidate(database:)._FetchKeyRegistryis@unchecked Sendableguarded byLockIsolated. Process-wide lifetime, matching the swift-sharing cache it shadows.FetchKey.subscribereturn value or the publicFetchKeyRequestprotocol.Open questions for review
FetchKeyCache.invalidate(database:)vsSQLiteData.invalidateFetchKeys(for:)vs something nested underFetchKey. Happy to follow your preference.<doc:SwappableDatabase>article inDocumentation.docc— can do as a follow-up if useful.