-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Open
Labels
Description
Description
When a document is created with setData() and then updateData() is called on the same document before the server confirms the setData(), snapshot listeners drop a field from the optimistic state reconstruction.
Expected: all fields from the original setData() should be present in the snapshot, merged with any pending updateData() mutations.
Actual: a plain string field is completely absent from the snapshot data — not even present in the dictionary keys — even though:
- The field was confirmed present in the data passed to
setData()(logged immediately before the call) - A direct
getDocument(source: .server)confirms the server has the field - No
updateData()call ever touches or deletes that field - Other plain fields from the same
setData()call are preserved correctly
Diagnostic metadata on the broken snapshot:
document.metadata.hasPendingWrites = truedocument.metadata.isFromCache = false
This was discovered via Flutter/FlutterFire (see firebase/flutterfire#18027).
Reproducing the issue
Native Swift reproduction:
import FirebaseFirestore
let db = Firestore.firestore()
let docRef = db.collection("test").document()
// 1. Create document with setData() — do NOT await completion (offline-first pattern)
docRef.setData([
"ownerId": "user_123", // plain String
"title": "Test item", // plain String
"createdAt": FieldValue.serverTimestamp(), // transform
"status": 0 // plain Int
])
// 2. Call updateData() before the server confirms the setData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
docRef.updateData([
"retryCount": 1,
"errorMessage": FieldValue.delete()
])
}
// 3. A snapshot listener on a query covering this document
// returns the doc with 'ownerId' missing from the data dictionary.
db.collection("test")
.order(by: "createdAt", descending: true)
.addSnapshotListener { snapshot, error in
guard let documents = snapshot?.documents else { return }
for doc in documents {
let data = doc.data()
print("ownerId: \(data["ownerId"] ?? "nil")") // Expected: "user_123", Actual: nil
print("title: \(data["title"] ?? "nil")") // "Test item" ✓
print("status: \(data["status"] ?? "nil")") // 0 ✓
print("keys: \(Array(data.keys))") // "ownerId" missing from keys
// Direct server fetch confirms the field exists
doc.reference.getDocument(source: .server) { serverDoc, _ in
if let serverData = serverDoc?.data() {
print("server ownerId: \(serverData["ownerId"] ?? "nil")") // "user_123" ✓
}
}
}
}Conditions that trigger the bug:
setData()must includeFieldValue.serverTimestamp()fieldsupdateData()must be called before the server confirms thesetData()- The snapshot listener must use default source (not cache-only)
- Multiple
updateData()calls increase the likelihood of the field being dropped
Firebase SDK Version
12.8.0
Xcode Version
26.2
Installation Method
CocoaPods
Firebase Product(s)
Firestore
Targeted Platforms
iOS
Relevant Log Output
# Observed via Flutter diagnostic logging (flutterfire is a pass-through to the native SDK):
# 1. setData() data includes ownerId
[DIAG] ownerId=user_123, data={ownerId: user_123, title: Test item, createdAt: FieldValue(...), status: 0}
# 2. Snapshot listener: ownerId not in keys
[DIAG] ownerId missing, hasPendingWrites=true, isFromCache=false, keys=[title, retryCount, createdAt, status]
# 3. Direct server fetch: ownerId present
[DIAG] server doc: exists=true, ownerId=user_123, keys=[ownerId, title, retryCount, createdAt, status]If using Swift Package Manager, the project's Package.resolved
N/A — using CocoaPods (via Flutter).
If using CocoaPods, the project's Podfile.lock
Expand Podfile.lock snippet
- Firebase/Firestore (12.8.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.8.0)
- FirebaseFirestore (12.8.0):
- FirebaseFirestoreBinary (= 12.8.0)
- FirebaseFirestoreAbseilBinary (1.2024072200.0)
- FirebaseFirestoreBinary (12.8.0):
- FirebaseCore (= 12.8.0)
- FirebaseCoreExtension (= 12.8.0)
- FirebaseFirestoreInternalBinary (= 12.8.0)
- FirebaseSharedSwift (= 12.8.0)
- FirebaseFirestoreGRPCBoringSSLBinary (1.69.0)
- FirebaseFirestoreGRPCCoreBinary (1.69.0)Reactions are currently unavailable