Skip to content

Commit 54f6ee7

Browse files
Support descending queries (#3499)
1 parent f103c35 commit 54f6ee7

File tree

8 files changed

+315
-155
lines changed

8 files changed

+315
-155
lines changed

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

Lines changed: 143 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import static com.google.firebase.firestore.model.Values.max;
1818
import static com.google.firebase.firestore.model.Values.min;
1919

20+
import android.util.Pair;
2021
import androidx.annotation.Nullable;
22+
import com.google.firebase.firestore.core.OrderBy.Direction;
2123
import com.google.firebase.firestore.model.DocumentKey;
2224
import com.google.firebase.firestore.model.FieldIndex;
2325
import com.google.firebase.firestore.model.FieldPath;
@@ -185,65 +187,18 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
185187

186188
// For each segment, retrieve a lower bound if there is a suitable filter or startAt.
187189
for (FieldIndex.Segment segment : fieldIndex.getDirectionalSegments()) {
188-
Value segmentValue = null;
189-
boolean segmentInclusive = true;
190+
Pair<Value, Boolean> segmentBound =
191+
segment.getKind().equals(FieldIndex.Segment.Kind.ASCENDING)
192+
? getAscendingBound(segment, startAt)
193+
: getDescendingBound(segment, startAt);
190194

191-
// Process all filters to find a value for the current field segment
192-
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
193-
Value filterValue = null;
194-
boolean filterInclusive = true;
195-
196-
switch (fieldFilter.getOperator()) {
197-
case LESS_THAN:
198-
case LESS_THAN_OR_EQUAL:
199-
filterValue = Values.getLowerBound(fieldFilter.getValue().getValueTypeCase());
200-
break;
201-
case EQUAL:
202-
case IN:
203-
case GREATER_THAN_OR_EQUAL:
204-
filterValue = fieldFilter.getValue();
205-
break;
206-
case GREATER_THAN:
207-
filterValue = fieldFilter.getValue();
208-
filterInclusive = false;
209-
break;
210-
case NOT_EQUAL:
211-
case NOT_IN:
212-
filterValue = Values.MIN_VALUE;
213-
break;
214-
default:
215-
// Remaining filters cannot be used as lower bounds.
216-
}
217-
218-
if (max(segmentValue, filterValue) == filterValue) {
219-
segmentValue = filterValue;
220-
segmentInclusive = filterInclusive;
221-
}
222-
}
223-
224-
// If there is a startAt bound, compare the values against the existing boundary to see
225-
// if we can narrow the scope.
226-
if (startAt != null) {
227-
for (int i = 0; i < orderBys.size(); ++i) {
228-
OrderBy orderBy = this.orderBys.get(i);
229-
if (orderBy.getField().equals(segment.getFieldPath())) {
230-
Value cursorValue = startAt.getPosition().get(i);
231-
if (max(segmentValue, cursorValue) == cursorValue) {
232-
segmentValue = cursorValue;
233-
segmentInclusive = startAt.isInclusive();
234-
}
235-
break;
236-
}
237-
}
238-
}
239-
240-
if (segmentValue == null) {
195+
if (segmentBound.first == null) {
241196
// No lower bound exists
242197
return null;
243198
}
244199

245-
values.add(segmentValue);
246-
inclusive &= segmentInclusive;
200+
values.add(segmentBound.first);
201+
inclusive &= segmentBound.second;
247202
}
248203

249204
return new Bound(values, inclusive);
@@ -259,75 +214,159 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
259214

260215
// For each segment, retrieve an upper bound if there is a suitable filter or endAt.
261216
for (FieldIndex.Segment segment : fieldIndex.getDirectionalSegments()) {
262-
@Nullable Value segmentValue = null;
263-
boolean segmentInclusive = true;
217+
Pair<Value, Boolean> segmentBound =
218+
segment.getKind().equals(FieldIndex.Segment.Kind.ASCENDING)
219+
? getDescendingBound(segment, endAt)
220+
: getAscendingBound(segment, endAt);
264221

265-
// Process all filters to find a value for the current field segment
266-
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
267-
Value filterValue = null;
268-
boolean filterInclusive = true;
222+
if (segmentBound.first == null) {
223+
// No upper bound exists
224+
return null;
225+
}
269226

270-
switch (fieldFilter.getOperator()) {
271-
case GREATER_THAN_OR_EQUAL:
272-
case GREATER_THAN:
273-
filterValue = Values.getUpperBound(fieldFilter.getValue().getValueTypeCase());
274-
filterInclusive = false;
275-
break;
276-
case EQUAL:
277-
case IN:
278-
case LESS_THAN_OR_EQUAL:
279-
filterValue = fieldFilter.getValue();
280-
break;
281-
case LESS_THAN:
282-
filterValue = fieldFilter.getValue();
283-
filterInclusive = false;
284-
break;
285-
case NOT_EQUAL:
286-
case NOT_IN:
287-
filterValue = Values.MAX_VALUE;
288-
break;
289-
default:
290-
// Remaining filters cannot be used as upper bounds.
291-
}
227+
values.add(segmentBound.first);
228+
inclusive &= segmentBound.second;
229+
}
292230

293-
if (min(segmentValue, filterValue) == filterValue) {
294-
segmentValue = filterValue;
295-
segmentInclusive = filterInclusive;
296-
}
231+
return new Bound(values, inclusive);
232+
}
233+
234+
/**
235+
* Returns the value for an ascending bound of `segment`.
236+
*
237+
* @param segment The segment to get the value for.
238+
* @param bound A bound to restrict the index range.
239+
* @return a Pair with a nullable Value and a boolean indicating whether the bound is inclusive
240+
*/
241+
private Pair<Value, Boolean> getAscendingBound(
242+
FieldIndex.Segment segment, @Nullable Bound bound) {
243+
Value segmentValue = null;
244+
boolean segmentInclusive = true;
245+
246+
// Process all filters to find a value for the current field segment
247+
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
248+
Value filterValue = null;
249+
boolean filterInclusive = true;
250+
251+
switch (fieldFilter.getOperator()) {
252+
case LESS_THAN:
253+
case LESS_THAN_OR_EQUAL:
254+
filterValue = Values.getLowerBound(fieldFilter.getValue().getValueTypeCase());
255+
break;
256+
case EQUAL:
257+
case IN:
258+
case GREATER_THAN_OR_EQUAL:
259+
filterValue = fieldFilter.getValue();
260+
break;
261+
case GREATER_THAN:
262+
filterValue = fieldFilter.getValue();
263+
filterInclusive = false;
264+
break;
265+
case NOT_EQUAL:
266+
case NOT_IN:
267+
filterValue = Values.MIN_VALUE;
268+
break;
269+
default:
270+
// Remaining filters cannot be used as bound.
297271
}
298272

299-
// If there is an endAt bound, compare the values against the existing boundary to see
300-
// if we can narrow the scope.
301-
if (endAt != null) {
302-
for (int i = 0; i < orderBys.size(); ++i) {
303-
OrderBy orderBy = this.orderBys.get(i);
304-
if (orderBy.getField().equals(segment.getFieldPath())) {
305-
Value cursorValue = endAt.getPosition().get(i);
306-
if (min(segmentValue, cursorValue) == cursorValue) {
307-
segmentValue = cursorValue;
308-
segmentInclusive = endAt.isInclusive();
309-
}
310-
break;
273+
if (max(segmentValue, filterValue) == filterValue) {
274+
segmentValue = filterValue;
275+
segmentInclusive = filterInclusive;
276+
}
277+
}
278+
279+
// If there is an additional bound, compare the values against the existing range to see if we
280+
// can narrow the scope.
281+
if (bound != null) {
282+
for (int i = 0; i < orderBys.size(); ++i) {
283+
OrderBy orderBy = this.orderBys.get(i);
284+
if (orderBy.getField().equals(segment.getFieldPath())) {
285+
Value cursorValue = bound.getPosition().get(i);
286+
if (max(segmentValue, cursorValue) == cursorValue) {
287+
segmentValue = cursorValue;
288+
segmentInclusive = bound.isInclusive();
311289
}
312290
}
313291
}
292+
}
314293

315-
if (segmentValue == null) {
316-
// No upper bound exists
317-
return null;
294+
return new Pair<>(segmentValue, segmentInclusive);
295+
}
296+
297+
/**
298+
* Returns the value for a descending bound of `segment`.
299+
*
300+
* @param segment The segment to get the value for.
301+
* @param bound A bound to restrict the index range.
302+
* @return a Pair with a nullable Value and a boolean indicating whether the bound is inclusive
303+
*/
304+
private Pair<Value, Boolean> getDescendingBound(
305+
FieldIndex.Segment segment, @Nullable Bound bound) {
306+
Value segmentValue = null;
307+
boolean segmentInclusive = true;
308+
309+
// Process all filters to find a value for the current field segment
310+
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
311+
Value filterValue = null;
312+
boolean filterInclusive = true;
313+
314+
switch (fieldFilter.getOperator()) {
315+
case GREATER_THAN_OR_EQUAL:
316+
case GREATER_THAN:
317+
filterValue = Values.getUpperBound(fieldFilter.getValue().getValueTypeCase());
318+
filterInclusive = false;
319+
break;
320+
case EQUAL:
321+
case IN:
322+
case LESS_THAN_OR_EQUAL:
323+
filterValue = fieldFilter.getValue();
324+
break;
325+
case LESS_THAN:
326+
filterValue = fieldFilter.getValue();
327+
filterInclusive = false;
328+
break;
329+
case NOT_EQUAL:
330+
case NOT_IN:
331+
filterValue = Values.MAX_VALUE;
332+
break;
333+
default:
334+
// Remaining filters cannot be used as bound.
318335
}
319336

320-
values.add(segmentValue);
321-
inclusive &= segmentInclusive;
337+
if (min(segmentValue, filterValue) == filterValue) {
338+
segmentValue = filterValue;
339+
segmentInclusive = filterInclusive;
340+
}
322341
}
323342

324-
return new Bound(values, inclusive);
343+
// If there is an additional bound, compare the values against the existing range to see if we
344+
// can narrow the scope.
345+
if (bound != null) {
346+
for (int i = 0; i < orderBys.size(); ++i) {
347+
OrderBy orderBy = this.orderBys.get(i);
348+
if (orderBy.getField().equals(segment.getFieldPath())) {
349+
Value cursorValue = bound.getPosition().get(i);
350+
if (min(segmentValue, cursorValue) == cursorValue) {
351+
segmentValue = cursorValue;
352+
segmentInclusive = bound.isInclusive();
353+
}
354+
}
355+
}
356+
}
357+
358+
return new Pair<>(segmentValue, segmentInclusive);
325359
}
326360

327361
public List<OrderBy> getOrderBy() {
328362
return this.orderBys;
329363
}
330364

365+
/** Returns the order of the document key component. */
366+
public Direction getKeyOrder() {
367+
return this.orderBys.get(this.orderBys.size() - 1).getDirection();
368+
}
369+
331370
/** Returns a canonical string representing this target. */
332371
public String getCanonicalId() {
333372
if (memoizedCanonicalId != null) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.google.firebase.firestore.model.ResourcePath;
2525
import java.util.Collection;
2626
import java.util.List;
27-
import java.util.Set;
2827

2928
/**
3029
* Represents a set of indexes that are used to execute queries efficiently.
@@ -95,7 +94,7 @@ public interface IndexManager {
9594
* Returns the documents that match the given target based on the provided index or {@code null}
9695
* if the query cannot be served from an index.
9796
*/
98-
Set<DocumentKey> getDocumentsMatchingTarget(Target target);
97+
List<DocumentKey> getDocumentsMatchingTarget(Target target);
9998

10099
/** Returns the next collection group to update. Returns {@code null} if no group exists. */
101100
@Nullable

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.util.HashMap;
3030
import java.util.HashSet;
3131
import java.util.List;
32-
import java.util.Set;
3332

3433
/** An in-memory implementation of IndexManager. */
3534
class MemoryIndexManager implements IndexManager {
@@ -70,7 +69,7 @@ public FieldIndex getFieldIndex(Target target) {
7069

7170
@Override
7271
@Nullable
73-
public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
72+
public List<DocumentKey> getDocumentsMatchingTarget(Target target) {
7473
// Field indices are not supported with memory persistence.
7574
return null;
7675
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
import com.google.firebase.firestore.model.SnapshotVersion;
2929
import com.google.firebase.firestore.util.Logger;
3030
import java.util.Collections;
31+
import java.util.List;
3132
import java.util.Map;
32-
import java.util.Set;
3333
import javax.annotation.Nullable;
3434

3535
/**
@@ -110,7 +110,7 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
110110
return null;
111111
}
112112

113-
Set<DocumentKey> keys = indexManager.getDocumentsMatchingTarget(target);
113+
List<DocumentKey> keys = indexManager.getDocumentsMatchingTarget(target);
114114
if (keys == null) {
115115
return null;
116116
}

0 commit comments

Comments
 (0)