Skip to content

Commit 78d5d35

Browse files
schmidt-sebastianjeremyjiang-dev
authored andcommitted
Don't use Indexes for outdated limit queries (#3549)
1 parent a946e64 commit 78d5d35

File tree

4 files changed

+86
-18
lines changed

4 files changed

+86
-18
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ Android changes are not released automatically. Ensure that changes are released
22
by opting into a release at
33
[go/firebase-android-release](http:go/firebase-android-release) (Googlers only).
44

5+
# Unreleased
6+
- [changed] Fixed an issue in the experimental index engine that might have
7+
caused Firestore to exclude document results for limit queries with local
8+
modifications.
9+
510
# 24.1.0
611
- [feature] Added experimental support for indexed query execution. Indexes can
712
be enabled by invoking `FirebaseFirestore.setIndexConfiguration()` with the

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,15 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
117117

118118
ImmutableSortedMap<DocumentKey, Document> indexedDocuments =
119119
localDocumentsView.getDocuments(keys);
120-
return appendRemainingResults(
121-
values(indexedDocuments), query, indexManager.getMinOffset(target));
120+
IndexOffset offset = indexManager.getMinOffset(target);
121+
122+
ImmutableSortedSet<Document> previousResults = applyQuery(query, indexedDocuments);
123+
if ((query.hasLimitToFirst() || query.hasLimitToLast())
124+
&& needsRefill(query.getLimitType(), keys.size(), previousResults, offset.getReadTime())) {
125+
return null;
126+
}
127+
128+
return appendRemainingResults(values(indexedDocuments), query, offset);
122129
}
123130

124131
/**
@@ -146,7 +153,10 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
146153

147154
if ((query.hasLimitToFirst() || query.hasLimitToLast())
148155
&& needsRefill(
149-
query.getLimitType(), previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
156+
query.getLimitType(),
157+
remoteKeys.size(),
158+
previousResults,
159+
lastLimboFreeSnapshotVersion)) {
150160
return null;
151161
}
152162

@@ -186,19 +196,21 @@ private ImmutableSortedSet<Document> applyQuery(
186196
* index-free execution.
187197
*
188198
* @param limitType The type of limit query for refill calculation.
189-
* @param sortedPreviousResults The documents that matched the query when it was last
190-
* synchronized, sorted by the query's comparator.
191-
* @param remoteKeys The document keys that matched the query at the last snapshot.
199+
* @param expectedDocumentCount The number of documents keys that matched the query at the last
200+
* snapshot.
201+
* @param sortedPreviousResults The documents that match the query based on the previous result,
202+
* sorted by the query's comparator. The size of the result set may be different from
203+
* `expectedDocumentCount` if documents cease to match the query.
192204
* @param limboFreeSnapshotVersion The version of the snapshot when the query was last
193205
* synchronized.
194206
*/
195207
private boolean needsRefill(
196208
Query.LimitType limitType,
209+
int expectedDocumentCount,
197210
ImmutableSortedSet<Document> sortedPreviousResults,
198-
ImmutableSortedSet<DocumentKey> remoteKeys,
199211
SnapshotVersion limboFreeSnapshotVersion) {
200212
// The query needs to be refilled if a previously matching document no longer matches.
201-
if (remoteKeys.size() != sortedPreviousResults.size()) {
213+
if (expectedDocumentCount != sortedPreviousResults.size()) {
202214
return true;
203215
}
204216

firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
7272
return queryEngine.getDocumentsMatchingQuery(query, lastLimboFreeSnapshotVersion, remoteKeys);
7373
}
7474

75-
/** Returns the query engine that is used as the backing implementation. */
76-
QueryEngine getSubject() {
77-
return queryEngine;
78-
}
79-
8075
/**
8176
* Returns the number of documents returned by the RemoteDocumentCache's `getAll()` API (since the
8277
* last call to `resetCounts()`)
@@ -147,7 +142,6 @@ public Map<DocumentKey, MutableDocument> getAll(
147142
String collectionGroup, IndexOffset offset, int limit) {
148143
Map<DocumentKey, MutableDocument> result = subject.getAll(collectionGroup, offset, limit);
149144
documentsReadByCollection[0] += result.size();
150-
151145
return result;
152146
}
153147

firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.common.truth.Truth.assertThat;
1818
import static com.google.firebase.firestore.testutil.TestUtil.addedRemoteEvent;
19+
import static com.google.firebase.firestore.testutil.TestUtil.deleteMutation;
1920
import static com.google.firebase.firestore.testutil.TestUtil.deletedDoc;
2021
import static com.google.firebase.firestore.testutil.TestUtil.doc;
2122
import static com.google.firebase.firestore.testutil.TestUtil.fieldIndex;
@@ -35,6 +36,7 @@
3536
import com.google.firebase.firestore.model.FieldIndex;
3637
import java.util.Arrays;
3738
import java.util.Collection;
39+
import java.util.Collections;
3840
import org.junit.Test;
3941
import org.junit.runner.RunWith;
4042
import org.robolectric.RobolectricTestRunner;
@@ -141,15 +143,15 @@ public void testDeletedDocumentRemovesIndex() {
141143
backfillIndexes();
142144

143145
executeQuery(query);
144-
assertRemoteDocumentsRead(/* byKey= */ 1, /* byQuery= */ 0);
146+
assertRemoteDocumentsRead(/* byKey= */ 1, /* byCollection= */ 0);
145147
assertQueryReturned("coll/a");
146148

147149
applyRemoteEvent(
148150
updateRemoteEvent(deletedDoc("coll/a", 0), singletonList(targetId), emptyList()));
149151

150152
// No backfill needed for deleted document.
151153
executeQuery(query);
152-
assertRemoteDocumentsRead(/* byKey= */ 0, /* byQuery= */ 0);
154+
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 0);
153155
assertQueryReturned();
154156
}
155157

@@ -168,7 +170,7 @@ public void testUsesIndexes() {
168170
backfillIndexes();
169171

170172
executeQuery(query);
171-
assertRemoteDocumentsRead(/* byKey= */ 1, /* byQuery= */ 0);
173+
assertRemoteDocumentsRead(/* byKey= */ 1, /* byCollection= */ 0);
172174
assertQueryReturned("coll/a");
173175
}
174176

@@ -188,7 +190,7 @@ public void testUsesPartiallyIndexedRemoteDocumentsWhenAvailable() {
188190
applyRemoteEvent(addedRemoteEvent(doc("coll/b", 20, map("matches", true)), targetId));
189191

190192
executeQuery(query);
191-
assertRemoteDocumentsRead(/* byKey= */ 1, /* byQuery= */ 1);
193+
assertRemoteDocumentsRead(/* byKey= */ 1, /* byCollection= */ 1);
192194
assertQueryReturned("coll/a", "coll/b");
193195
}
194196

@@ -210,6 +212,61 @@ public void testUsesPartiallyIndexedOverlaysWhenAvailable() {
210212
assertQueryReturned("coll/a", "coll/b");
211213
}
212214

215+
@Test
216+
public void testDoesNotUseIndexForLimitQueryWhenIndexIsOutdated() {
217+
FieldIndex index =
218+
fieldIndex("coll", 0, FieldIndex.INITIAL_STATE, "count", FieldIndex.Segment.Kind.ASCENDING);
219+
configureFieldIndexes(singletonList(index));
220+
221+
Query query = query("coll").orderBy(orderBy("count")).limitToFirst(2);
222+
int targetId = allocateQuery(query);
223+
224+
applyRemoteEvent(
225+
addedRemoteEvent(
226+
Arrays.asList(
227+
doc("coll/a", 10, map("count", 1)),
228+
doc("coll/b", 10, map("count", 2)),
229+
doc("coll/c", 10, map("count", 3))),
230+
Collections.singletonList(targetId),
231+
Collections.emptyList()));
232+
backfillIndexes();
233+
234+
writeMutation(deleteMutation("coll/b"));
235+
236+
executeQuery(query);
237+
// The query engine first reads the documents by key and then discards the results, which means
238+
// that we read both by key and by collection.
239+
assertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 3);
240+
assertOverlaysRead(/* byKey= */ 2, /* byCollection= */ 1);
241+
assertQueryReturned("coll/a", "coll/c");
242+
}
243+
244+
@Test
245+
public void testUsesIndexForLimitQueryWhenIndexIsUpdated() {
246+
FieldIndex index =
247+
fieldIndex("coll", 0, FieldIndex.INITIAL_STATE, "count", FieldIndex.Segment.Kind.ASCENDING);
248+
configureFieldIndexes(singletonList(index));
249+
250+
Query query = query("coll").orderBy(orderBy("count")).limitToFirst(2);
251+
int targetId = allocateQuery(query);
252+
253+
applyRemoteEvent(
254+
addedRemoteEvent(
255+
Arrays.asList(
256+
doc("coll/a", 10, map("count", 1)),
257+
doc("coll/b", 10, map("count", 2)),
258+
doc("coll/c", 10, map("count", 3))),
259+
Collections.singletonList(targetId),
260+
Collections.emptyList()));
261+
writeMutation(deleteMutation("coll/b"));
262+
backfillIndexes();
263+
264+
executeQuery(query);
265+
assertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 0);
266+
assertOverlaysRead(/* byKey= */ 2, /* byCollection= */ 0);
267+
assertQueryReturned("coll/a", "coll/c");
268+
}
269+
213270
@Test
214271
public void testIndexesServerTimestamps() {
215272
FieldIndex index =

0 commit comments

Comments
 (0)