Minimal reproduction of a @FetchOne ValueObservation push bug in
pointfreeco/sqlite-data when a
transparent DatabaseWriter wrapper swaps its inner DatabasePool.
Tracking upstream:
- Discussion: pointfreeco/sqlite-data#454
- PR: pointfreeco/sqlite-data#455
swift run Repro # reproduces the bug: unreadCount stuck at 0 after swap
swift run Repro -- --fix # applies $wrapper = FetchOne(wrappedValue: 0) workaround → push restored- sqlite-data 1.6.1
- swift-sharing 2.7.4
- swift-structured-queries 0.31.0
- GRDB 7.x
- macOS 14+
| Step | Action | Expected |
|---|---|---|
| 1 | First .load() on pool A |
unreadCount: 0 allItems: 0 |
| 2 | Write 1 row to pool A | unreadCount: 1 allItems: 1 ✅ |
| 3 | Swap inner pool A → B (mutable.database = poolB) |
wrapper ObjectIdentifier unchanged |
| 4 | .load() on pool B |
unreadCount: 0 allItems: 0 raw: 0 |
| 5 | Write 1 row to pool B | bug: unreadCount: 0 allItems: 1 ❌fix: unreadCount: 1 allItems: 1 ✅ |
Step 3 logs [Identity] mutable: ObjectIdentifier(0x…) before and after the
swap. The two values are identical — that's the bug's fingerprint.
FetchKeyID (Sources/SQLiteData/Internal/FetchKey.swift:172-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 pool A (now closed); writes to pool B never reach
@FetchOne. @FetchAll happens to escape because FetchKey<[T]>.Default
(_SharedKeyDefault wrapping) takes a different generic-cast path inside
PersistentReferences and misses the cache by accident — not by design.
MIT.