Skip to content

Commit 4d49364

Browse files
authored
Fix overlay bug with multiple field patches (#3623)
1 parent 1f25801 commit 4d49364

File tree

13 files changed

+201
-32
lines changed

13 files changed

+201
-32
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ by opting into a release at
33
[go/firebase-android-release](http:go/firebase-android-release) (Googlers only).
44

55
# Unreleased
6+
- [fixed] Fixed an issue where patching multiple fields shadows each other (#3528).
7+
8+
# 24.1.1
69
- [fixed] Fixed an issue in the experimental index engine that might have
710
caused Firestore to exclude document results for limit queries with local
811
modifications.

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsResult.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17+
import static com.google.firebase.firestore.model.DocumentCollections.emptyDocumentMap;
18+
1719
import com.google.firebase.database.collection.ImmutableSortedMap;
1820
import com.google.firebase.firestore.model.Document;
1921
import com.google.firebase.firestore.model.DocumentKey;
22+
import java.util.Map;
2023

2124
/**
2225
* Represents a set of document along with their mutation batch ID.
@@ -33,6 +36,16 @@ public final class LocalDocumentsResult {
3336
this.documents = documents;
3437
}
3538

39+
public static LocalDocumentsResult fromOverlayedDocuments(
40+
int batchId, Map<DocumentKey, OverlayedDocument> overlays) {
41+
ImmutableSortedMap<DocumentKey, Document> documents = emptyDocumentMap();
42+
for (Map.Entry<DocumentKey, OverlayedDocument> entry : overlays.entrySet()) {
43+
documents = documents.insert(entry.getKey(), entry.getValue().getDocument());
44+
}
45+
46+
return new LocalDocumentsResult(batchId, documents);
47+
}
48+
3649
public int getBatchId() {
3750
return batchId;
3851
}

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,29 +119,45 @@ ImmutableSortedMap<DocumentKey, Document> getLocalViewOfDocuments(
119119
Map<DocumentKey, MutableDocument> docs, Set<DocumentKey> existenceStateChanged) {
120120
Map<DocumentKey, Overlay> overlays = new HashMap<>();
121121
populateOverlays(overlays, docs.keySet());
122-
return computeViews(docs, overlays, existenceStateChanged);
122+
ImmutableSortedMap<DocumentKey, Document> result = emptyDocumentMap();
123+
for (Map.Entry<DocumentKey, OverlayedDocument> entry :
124+
computeViews(docs, overlays, existenceStateChanged).entrySet()) {
125+
result = result.insert(entry.getKey(), entry.getValue().getDocument());
126+
}
127+
128+
return result;
123129
}
124130

125131
/**
126-
* Similar to {@link #getDocuments}, but creates the local view from the given {@code baseDocs}
127-
* without retrieving documents from the local store.
132+
* Gets the overlayed documents for the given document map, which will include the local view of
133+
* those documents and a {@code FieldMask} indicating which fields are mutated locally, null if
134+
* overlay is a Set or Delete mutation.
128135
*
129136
* @param docs The documents to apply local mutations to get the local views.
130137
*/
131-
ImmutableSortedMap<DocumentKey, Document> getLocalViewOfDocuments(
138+
Map<DocumentKey, OverlayedDocument> getOverlayedDocuments(
132139
Map<DocumentKey, MutableDocument> docs) {
133140
Map<DocumentKey, Overlay> overlays = new HashMap<>();
134141
populateOverlays(overlays, docs.keySet());
135142
return computeViews(docs, overlays, new HashSet<>());
136143
}
137144

138-
/*Computes the local view for doc */
139-
private ImmutableSortedMap<DocumentKey, Document> computeViews(
145+
/**
146+
* Computes the local view for the given documents.
147+
*
148+
* @param docs The documents to compute views for. It also has the base version of the documents.
149+
* @param overlays The overlays that need to be applied to the given base version of the
150+
* documents.
151+
* @param existenceStateChanged A set of documents whose existence states might have changed. This
152+
* is used to determine if we need to re-calculate overlays from mutation queues.
153+
* @return A map represents the local documents view.
154+
*/
155+
private Map<DocumentKey, OverlayedDocument> computeViews(
140156
Map<DocumentKey, MutableDocument> docs,
141157
Map<DocumentKey, Overlay> overlays,
142158
Set<DocumentKey> existenceStateChanged) {
143-
ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
144159
Map<DocumentKey, MutableDocument> recalculateDocuments = new HashMap<>();
160+
Map<DocumentKey, FieldMask> mutatedFields = new HashMap<>();
145161
for (MutableDocument doc : docs.values()) {
146162
Overlay overlay = overlays.get(doc.getKey());
147163
// Recalculate an overlay if the document's existence state is changed due to a remote
@@ -154,19 +170,26 @@ private ImmutableSortedMap<DocumentKey, Document> computeViews(
154170
&& (overlay == null || overlay.getMutation() instanceof PatchMutation)) {
155171
recalculateDocuments.put(doc.getKey(), doc);
156172
} else if (overlay != null) {
173+
mutatedFields.put(doc.getKey(), overlay.getMutation().getFieldMask());
157174
overlay.getMutation().applyToLocalView(doc, null, Timestamp.now());
158175
}
159176
}
160177

161-
recalculateAndSaveOverlays(recalculateDocuments);
178+
Map<DocumentKey, FieldMask> recalculatedFields =
179+
recalculateAndSaveOverlays(recalculateDocuments);
180+
mutatedFields.putAll(recalculatedFields);
162181

182+
Map<DocumentKey, OverlayedDocument> result = new HashMap<>();
163183
for (Map.Entry<DocumentKey, MutableDocument> entry : docs.entrySet()) {
164-
results = results.insert(entry.getKey(), entry.getValue());
184+
result.put(
185+
entry.getKey(),
186+
new OverlayedDocument(entry.getValue(), mutatedFields.get(entry.getKey())));
165187
}
166-
return results;
188+
return result;
167189
}
168190

169-
private void recalculateAndSaveOverlays(Map<DocumentKey, MutableDocument> docs) {
191+
private Map<DocumentKey, FieldMask> recalculateAndSaveOverlays(
192+
Map<DocumentKey, MutableDocument> docs) {
170193
List<MutationBatch> batches =
171194
mutationQueue.getAllMutationBatchesAffectingDocumentKeys(docs.keySet());
172195

@@ -211,6 +234,8 @@ private void recalculateAndSaveOverlays(Map<DocumentKey, MutableDocument> docs)
211234
}
212235
documentOverlayCache.saveOverlays(entry.getKey(), overlays);
213236
}
237+
238+
return masks;
214239
}
215240

216241
/**
@@ -311,9 +336,9 @@ LocalDocumentsResult getNextDocuments(String collectionGroup, IndexOffset offset
311336
}
312337

313338
populateOverlays(overlays, docs.keySet());
314-
ImmutableSortedMap<DocumentKey, Document> localDocs =
339+
Map<DocumentKey, OverlayedDocument> localDocs =
315340
computeViews(docs, overlays, Collections.emptySet());
316-
return new LocalDocumentsResult(largestBatchId, localDocs);
341+
return LocalDocumentsResult.fromOverlayedDocuments(largestBatchId, localDocs);
317342
}
318343

319344
/**

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ public LocalDocumentsResult writeLocally(List<Mutation> mutations) {
258258
}
259259
// Load and apply all existing mutations. This lets us compute the current base state for
260260
// all non-idempotent transforms before applying any additional user-provided writes.
261-
ImmutableSortedMap<DocumentKey, Document> documents =
262-
localDocuments.getLocalViewOfDocuments(remoteDocs);
261+
Map<DocumentKey, OverlayedDocument> overlayedDocuments =
262+
localDocuments.getOverlayedDocuments(remoteDocs);
263263

264264
// For non-idempotent mutations (such as `FieldValue.increment()`), we record the base
265265
// state in a separate patch mutation. This is later used to guarantee consistent values
@@ -268,7 +268,8 @@ public LocalDocumentsResult writeLocally(List<Mutation> mutations) {
268268
List<Mutation> baseMutations = new ArrayList<>();
269269
for (Mutation mutation : mutations) {
270270
ObjectValue baseValue =
271-
mutation.extractTransformBaseValue(documents.get(mutation.getKey()));
271+
mutation.extractTransformBaseValue(
272+
overlayedDocuments.get(mutation.getKey()).getDocument());
272273
if (baseValue != null) {
273274
// NOTE: The base state should only be applied if there's some existing
274275
// document to override, so use a Precondition of exists=true
@@ -284,9 +285,10 @@ public LocalDocumentsResult writeLocally(List<Mutation> mutations) {
284285
MutationBatch batch =
285286
mutationQueue.addMutationBatch(localWriteTime, baseMutations, mutations);
286287
Map<DocumentKey, Mutation> overlays =
287-
batch.applyToLocalDocumentSet(documents, docsWithoutRemoteVersion);
288+
batch.applyToLocalDocumentSet(overlayedDocuments, docsWithoutRemoteVersion);
288289
documentOverlayCache.saveOverlays(batch.getBatchId(), overlays);
289-
return new LocalDocumentsResult(batch.getBatchId(), documents);
290+
return LocalDocumentsResult.fromOverlayedDocuments(
291+
batch.getBatchId(), overlayedDocuments);
290292
});
291293
}
292294

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.local;
16+
17+
import com.google.firebase.firestore.model.Document;
18+
import com.google.firebase.firestore.model.mutation.FieldMask;
19+
import javax.annotation.Nullable;
20+
21+
/** Represents a local view (overlay) of a document, and the fields that are locally mutated. */
22+
public class OverlayedDocument {
23+
private Document overlayedDocument;
24+
private FieldMask mutatedFields;
25+
26+
OverlayedDocument(Document overlayedDocument, FieldMask mutatedFields) {
27+
this.overlayedDocument = overlayedDocument;
28+
this.mutatedFields = mutatedFields;
29+
}
30+
31+
public Document getDocument() {
32+
return overlayedDocument;
33+
}
34+
35+
/**
36+
* The fields that are locally mutated by patch mutations. If the overlayed document is from set
37+
* or delete mutations, this returns null.
38+
*/
39+
public @Nullable FieldMask getMutatedFields() {
40+
return mutatedFields;
41+
}
42+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/DeleteMutation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,9 @@ public void applyToRemoteDocument(MutableDocument document, MutationResult mutat
7979

8080
return previousMask;
8181
}
82+
83+
@Override
84+
public @Nullable FieldMask getFieldMask() {
85+
return null;
86+
}
8287
}

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/Mutation.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ public abstract void applyToRemoteDocument(
164164
public abstract @Nullable FieldMask applyToLocalView(
165165
MutableDocument document, @Nullable FieldMask previousMask, Timestamp localWriteTime);
166166

167+
/**
168+
* Returns a {@code FieldMask} representing the fields that will be changed by applying this
169+
* mutation. Returns {@code null} if the mutation will overwrite the entire document.
170+
*/
171+
public abstract @Nullable FieldMask getFieldMask();
172+
167173
/** Helper for derived classes to implement .equals(). */
168174
boolean hasSameKeyAndPrecondition(Mutation other) {
169175
return key.equals(other.key) && precondition.equals(other.precondition);

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/MutationBatch.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818

1919
import androidx.annotation.Nullable;
2020
import com.google.firebase.Timestamp;
21-
import com.google.firebase.database.collection.ImmutableSortedMap;
22-
import com.google.firebase.firestore.model.Document;
21+
import com.google.firebase.firestore.local.OverlayedDocument;
2322
import com.google.firebase.firestore.model.DocumentKey;
2423
import com.google.firebase.firestore.model.MutableDocument;
2524
import com.google.firebase.firestore.model.SnapshotVersion;
@@ -101,14 +100,12 @@ public void applyToRemoteDocument(MutableDocument document, MutationBatchResult
101100
}
102101

103102
/**
104-
* Computes the local view of a document given all the mutations in this batch. Returns an {@link
105-
* FieldMask} representing all the fields that are mutated.
103+
* Computes the local view of a document given all the mutations in this batch.
104+
*
105+
* @param mutatedFields The document to be mutated.
106+
* @param mutatedFields Fields that are already mutated before applying the current one.
107+
* @return An {@link FieldMask} representing all the fields that are mutated.
106108
*/
107-
public FieldMask applyToLocalView(MutableDocument document) {
108-
FieldMask mutatedFields = FieldMask.fromSet(new HashSet<>());
109-
return applyToLocalView(document, mutatedFields);
110-
}
111-
112109
public FieldMask applyToLocalView(MutableDocument document, @Nullable FieldMask mutatedFields) {
113110
// First, apply the base state. This allows us to apply non-idempotent transform against a
114111
// consistent set of values.
@@ -136,7 +133,7 @@ public FieldMask applyToLocalView(MutableDocument document, @Nullable FieldMask
136133
* applications.
137134
*/
138135
public Map<DocumentKey, Mutation> applyToLocalDocumentSet(
139-
ImmutableSortedMap<DocumentKey, Document> documentMap,
136+
Map<DocumentKey, OverlayedDocument> documentMap,
140137
Set<DocumentKey> documentsWithoutRemoteVersion) {
141138
// TODO(mrschmidt): This implementation is O(n^2). If we iterate through the mutations first
142139
// (as done in `applyToLocalView(MutableDocument d)`), we can reduce the complexity to
@@ -145,8 +142,8 @@ public Map<DocumentKey, Mutation> applyToLocalDocumentSet(
145142
for (DocumentKey key : getKeys()) {
146143
// TODO(mutabledocuments): This method should take a map of MutableDocuments and we should
147144
// remove this cast.
148-
MutableDocument document = (MutableDocument) documentMap.get(key);
149-
FieldMask mutatedFields = applyToLocalView(document);
145+
MutableDocument document = (MutableDocument) documentMap.get(key).getDocument();
146+
FieldMask mutatedFields = applyToLocalView(document, documentMap.get(key).getMutatedFields());
150147
// Set mutationFields to null if the document is only from local mutations, this creates
151148
// a Set(or Delete) mutation, instead of trying to create a patch mutation as the overlay.
152149
mutatedFields = documentsWithoutRemoteVersion.contains(key) ? null : mutatedFields;

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/PatchMutation.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ public ObjectValue getValue() {
101101
* Returns the mask to apply to {@link #getValue}, where only fields that are in both the
102102
* fieldMask and the value will be updated.
103103
*/
104-
public FieldMask getMask() {
104+
@Override
105+
public FieldMask getFieldMask() {
105106
return mask;
106107
}
107108

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/SetMutation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ public FieldMask applyToLocalView(
103103
return null;
104104
}
105105

106+
@Override
107+
public @Nullable FieldMask getFieldMask() {
108+
return null;
109+
}
110+
106111
/** Returns the object value to use when setting the document. */
107112
public ObjectValue getValue() {
108113
return value;

firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/VerifyMutation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,9 @@ public FieldMask applyToLocalView(
6666
MutableDocument document, @Nullable FieldMask previousMask, Timestamp localWriteTime) {
6767
throw Assert.fail("VerifyMutation should only be used in Transactions.");
6868
}
69+
70+
@Override
71+
public @Nullable FieldMask getFieldMask() {
72+
return null;
73+
}
6974
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public com.google.firestore.v1.Write encodeMutation(Mutation mutation) {
269269
builder.setUpdate(encodeDocument(mutation.getKey(), ((SetMutation) mutation).getValue()));
270270
} else if (mutation instanceof PatchMutation) {
271271
builder.setUpdate(encodeDocument(mutation.getKey(), ((PatchMutation) mutation).getValue()));
272-
builder.setUpdateMask(encodeDocumentMask(((PatchMutation) mutation).getMask()));
272+
builder.setUpdateMask(encodeDocumentMask((mutation.getFieldMask())));
273273
} else if (mutation instanceof DeleteMutation) {
274274
builder.setDelete(encodeKey(mutation.getKey()));
275275
} else if (mutation instanceof VerifyMutation) {

0 commit comments

Comments
 (0)