Skip to content

Commit d8800bd

Browse files
Re-run limit queries without limit if they need to be re-filled
This changes the query engine to also drop the limit on limit queries that need to be refilled. It also cleans up some of the Target.getLimit() logic that leads to repetitive code.
1 parent 5600426 commit d8800bd

File tree

5 files changed

+34
-52
lines changed

5 files changed

+34
-52
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.firestore;
1616

17+
import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_LAST;
1718
import static com.google.firebase.firestore.util.Assert.fail;
1819
import static com.google.firebase.firestore.util.Assert.hardAssert;
1920
import static com.google.firebase.firestore.util.Preconditions.checkNotNull;
@@ -1216,7 +1217,7 @@ private ListenerRegistration addSnapshotListenerInternal(
12161217
}
12171218

12181219
private void validateHasExplicitOrderByForLimitToLast() {
1219-
if (query.hasLimitToLast() && query.getExplicitOrderBy().isEmpty()) {
1220+
if (query.getLimitType().equals(LIMIT_TO_LAST) && query.getExplicitOrderBy().isEmpty()) {
12201221
throw new IllegalStateException(
12211222
"limitToLast() queries require specifying at least one orderBy() clause");
12221223
}

firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -147,34 +147,16 @@ public List<Filter> getFilters() {
147147
return filters;
148148
}
149149

150-
/**
151-
* The maximum number of results to return. If there is no limit on the query, then this will
152-
* cause an assertion failure.
153-
*/
154-
public long getLimitToFirst() {
155-
hardAssert(hasLimitToFirst(), "Called getLimitToFirst when no limit was set");
156-
return limit;
157-
}
158-
159-
public boolean hasLimitToFirst() {
160-
return limitType == LimitType.LIMIT_TO_FIRST && limit != Target.NO_LIMIT;
161-
}
162-
163-
/**
164-
* The maximum number of last-matching results to return. If there is no limit on the query, then
165-
* this will cause an assertion failure.
166-
*/
167-
public long getLimitToLast() {
168-
hardAssert(hasLimitToLast(), "Called getLimitToLast when no limit was set");
150+
/** The maximum number of results to return or {@link Target#NO_LIMIT} if there is no limit. */
151+
public long getLimit() {
169152
return limit;
170153
}
171154

172-
public boolean hasLimitToLast() {
173-
return limitType == LimitType.LIMIT_TO_LAST && limit != Target.NO_LIMIT;
155+
public boolean hasLimit() {
156+
return limit != Target.NO_LIMIT;
174157
}
175158

176159
public LimitType getLimitType() {
177-
hardAssert(hasLimitToLast() || hasLimitToFirst(), "Called getLimitType when no limit was set");
178160
return limitType;
179161
}
180162

firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

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

17+
import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_FIRST;
18+
import static com.google.firebase.firestore.core.Query.LimitType.LIMIT_TO_LAST;
1719
import static com.google.firebase.firestore.util.Assert.hardAssert;
1820
import static com.google.firebase.firestore.util.Util.compareIntegers;
1921

@@ -147,11 +149,11 @@ public DocumentChanges computeDocChanges(
147149
// Note that this should never get used in a refill (when previousChanges is set), because there
148150
// will only be adds -- no deletes or updates.
149151
Document lastDocInLimit =
150-
(query.hasLimitToFirst() && oldDocumentSet.size() == query.getLimitToFirst())
152+
(query.getLimitType().equals(LIMIT_TO_FIRST) && oldDocumentSet.size() == query.getLimit())
151153
? oldDocumentSet.getLastDocument()
152154
: null;
153155
Document firstDocInLimit =
154-
(query.hasLimitToLast() && oldDocumentSet.size() == query.getLimitToLast())
156+
(query.getLimitType().equals(LIMIT_TO_LAST) && oldDocumentSet.size() == query.getLimit())
155157
? oldDocumentSet.getFirstDocument()
156158
: null;
157159

@@ -222,11 +224,10 @@ public DocumentChanges computeDocChanges(
222224
}
223225

224226
// Drop documents out to meet limitToFirst/limitToLast requirement.
225-
if (query.hasLimitToFirst() || query.hasLimitToLast()) {
226-
long limit = query.hasLimitToFirst() ? query.getLimitToFirst() : query.getLimitToLast();
227-
for (long i = newDocumentSet.size() - limit; i > 0; --i) {
227+
if (query.hasLimit()) {
228+
for (long i = newDocumentSet.size() - query.getLimit(); i > 0; --i) {
228229
Document oldDoc =
229-
query.hasLimitToFirst()
230+
query.getLimitType().equals(LIMIT_TO_FIRST)
230231
? newDocumentSet.getLastDocument()
231232
: newDocumentSet.getFirstDocument();
232233
newDocumentSet = newDocumentSet.remove(oldDoc.getKey());

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,14 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
117117
return null;
118118
}
119119

120-
if (indexType.equals(IndexType.PARTIAL)) {
120+
if (query.hasLimit() && indexType.equals(IndexType.PARTIAL)) {
121121
// We cannot apply a limit for targets that are served using a partial index.
122122
// If a partial index will be used to serve the target, the query may return a superset of
123123
// documents that match the target (e.g. if the index doesn't include all the target's
124124
// filters), or may return the correct set of documents in the wrong order (e.g. if the index
125125
// doesn't include a segment for one of the orderBys). Therefore a limit should not be applied
126126
// in such cases.
127-
query = query.limitToFirst(Target.NO_LIMIT);
128-
target = query.toTarget();
127+
return performQueryUsingIndex(query.limitToFirst(Target.NO_LIMIT));
129128
}
130129

131130
List<DocumentKey> keys = indexManager.getDocumentsMatchingTarget(target);
@@ -136,9 +135,12 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
136135
IndexOffset offset = indexManager.getMinOffset(target);
137136

138137
ImmutableSortedSet<Document> previousResults = applyQuery(query, indexedDocuments);
139-
if ((query.hasLimitToFirst() || query.hasLimitToLast())
140-
&& needsRefill(query.getLimitType(), keys.size(), previousResults, offset.getReadTime())) {
141-
return null;
138+
if (needsRefill(query, keys.size(), previousResults, offset.getReadTime())) {
139+
// A limit query whose boundaries change due to local edits can be re-run against the cache
140+
// by excluding the limit. This ensures that all documents that match the query's filters are
141+
// included in the result set. The SDK can then apply the limit once all local edits are
142+
// incorporated.
143+
return performQueryUsingIndex(query.limitToFirst(Target.NO_LIMIT));
142144
}
143145

144146
return appendRemainingResults(values(indexedDocuments), query, offset);
@@ -167,12 +169,7 @@ && needsRefill(query.getLimitType(), keys.size(), previousResults, offset.getRea
167169
localDocumentsView.getDocuments(remoteKeys);
168170
ImmutableSortedSet<Document> previousResults = applyQuery(query, documents);
169171

170-
if ((query.hasLimitToFirst() || query.hasLimitToLast())
171-
&& needsRefill(
172-
query.getLimitType(),
173-
remoteKeys.size(),
174-
previousResults,
175-
lastLimboFreeSnapshotVersion)) {
172+
if (needsRefill(query, remoteKeys.size(), previousResults, lastLimboFreeSnapshotVersion)) {
176173
return null;
177174
}
178175

@@ -211,7 +208,7 @@ private ImmutableSortedSet<Document> applyQuery(
211208
* Determines if a limit query needs to be refilled from cache, making it ineligible for
212209
* index-free execution.
213210
*
214-
* @param limitType The type of limit query for refill calculation.
211+
* @param query The query.
215212
* @param expectedDocumentCount The number of documents keys that matched the query at the last
216213
* snapshot.
217214
* @param sortedPreviousResults The documents that match the query based on the previous result,
@@ -221,12 +218,17 @@ private ImmutableSortedSet<Document> applyQuery(
221218
* synchronized.
222219
*/
223220
private boolean needsRefill(
224-
Query.LimitType limitType,
221+
Query query,
225222
int expectedDocumentCount,
226223
ImmutableSortedSet<Document> sortedPreviousResults,
227224
SnapshotVersion limboFreeSnapshotVersion) {
228-
// The query needs to be refilled if a previously matching document no longer matches.
225+
if (!query.hasLimit()) {
226+
// Queries without limits do not need to be refilled.
227+
return false;
228+
}
229+
229230
if (expectedDocumentCount != sortedPreviousResults.size()) {
231+
// The query needs to be refilled if a previously matching document no longer matches.
230232
return true;
231233
}
232234

@@ -237,7 +239,7 @@ private boolean needsRefill(
237239
// did not change and documents from cache will continue to be "rejected" by this boundary.
238240
// Therefore, we can ignore any modifications that don't affect the last document.
239241
Document documentAtLimitEdge =
240-
limitType == Query.LimitType.LIMIT_TO_FIRST
242+
query.getLimitType() == Query.LimitType.LIMIT_TO_FIRST
241243
? sortedPreviousResults.getMaxEntry()
242244
: sortedPreviousResults.getMinEntry();
243245
if (documentAtLimitEdge == null) {

firebase-firestore/src/test/java/com/google/firebase/firestore/bundle/BundleSerializerTest.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -712,30 +712,26 @@ private void assertDecodesNamedQuery(String json, Query query) throws JSONExcept
712712
+ json
713713
+ ",\n"
714714
+ " limitType: '"
715-
+ (query.hasLimitToLast() ? "LAST" : "FIRST")
715+
+ (query.getLimitType().equals(Query.LimitType.LIMIT_TO_LAST) ? "LAST" : "FIRST")
716716
+ "'\n"
717717
+ " },\n"
718718
+ " readTime: '2020-01-01T00:00:01.000000001Z'\n"
719719
+ "}";
720720
NamedQuery actualNamedQuery = serializer.decodeNamedQuery(new JSONObject(queryJson));
721721

722-
long limit =
723-
query.hasLimitToFirst()
724-
? query.getLimitToFirst()
725-
: (query.hasLimitToLast() ? query.getLimitToLast() : Target.NO_LIMIT);
726722
Target target =
727723
new Target(
728724
query.getPath(),
729725
query.getCollectionGroup(),
730726
query.getFilters(),
731727
query.getExplicitOrderBy(),
732-
limit,
728+
query.getLimit(),
733729
query.getStartAt(),
734730
query.getEndAt());
735731
BundledQuery bundledQuery =
736732
new BundledQuery(
737733
target,
738-
query.hasLimitToLast()
734+
query.getLimitType().equals(Query.LimitType.LIMIT_TO_LAST)
739735
? Query.LimitType.LIMIT_TO_LAST
740736
: Query.LimitType.LIMIT_TO_FIRST);
741737
NamedQuery expectedNamedQuery =

0 commit comments

Comments
 (0)