Skip to content

Commit 661aab8

Browse files
Support descending queries
I am really struggling to understand why this was broken and not detected by the existing tests. I also wonder whether this is actually the right fix and could use a thorough set of eyes.
1 parent 9b2c2d3 commit 661aab8

File tree

2 files changed

+122
-85
lines changed

2 files changed

+122
-85
lines changed

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

Lines changed: 113 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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;
2122
import com.google.firebase.firestore.model.DocumentKey;
2223
import com.google.firebase.firestore.model.FieldIndex;
@@ -185,41 +186,10 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
185186

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

224194
// If there is a startAt bound, compare the values against the existing boundary to see
225195
// if we can narrow the scope.
@@ -228,27 +198,118 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
228198
OrderBy orderBy = this.orderBys.get(i);
229199
if (orderBy.getField().equals(segment.getFieldPath())) {
230200
Value cursorValue = startAt.getPosition().get(i);
231-
if (max(segmentValue, cursorValue) == cursorValue) {
232-
segmentValue = cursorValue;
233-
segmentInclusive = startAt.isInclusive();
201+
if (max(segmentBound.first, cursorValue) == cursorValue) {
202+
segmentBound = new Pair<>(cursorValue, startAt.isInclusive());
234203
}
235204
break;
236205
}
237206
}
238207
}
239208

240-
if (segmentValue == null) {
209+
if (segmentBound.first == null) {
241210
// No lower bound exists
242211
return null;
243212
}
244213

245-
values.add(segmentValue);
246-
inclusive &= segmentInclusive;
214+
values.add(segmentBound.first);
215+
inclusive &= segmentBound.second;
247216
}
248217

249218
return new Bound(values, inclusive);
250219
}
251220

221+
/**
222+
* Returns the value for the lower bound of `segment`.
223+
*
224+
* @param segment The segment to get the value for.
225+
* @return a Pair with a nullable Value and a boolean indicating whether the bound is inclusive
226+
*/
227+
private Pair<Value, Boolean> getAscendingBound(FieldIndex.Segment segment) {
228+
Value segmentValue = null;
229+
boolean segmentInclusive = true;
230+
231+
// Process all filters to find a value for the current field segment
232+
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
233+
Value filterValue = null;
234+
boolean filterInclusive = true;
235+
236+
switch (fieldFilter.getOperator()) {
237+
case LESS_THAN:
238+
case LESS_THAN_OR_EQUAL:
239+
filterValue = Values.getLowerBound(fieldFilter.getValue().getValueTypeCase());
240+
break;
241+
case EQUAL:
242+
case IN:
243+
case GREATER_THAN_OR_EQUAL:
244+
filterValue = fieldFilter.getValue();
245+
break;
246+
case GREATER_THAN:
247+
filterValue = fieldFilter.getValue();
248+
filterInclusive = false;
249+
break;
250+
case NOT_EQUAL:
251+
case NOT_IN:
252+
filterValue = Values.MIN_VALUE;
253+
break;
254+
default:
255+
// Remaining filters cannot be used as lower bounds.
256+
}
257+
258+
if (max(segmentValue, filterValue) == filterValue) {
259+
segmentValue = filterValue;
260+
segmentInclusive = filterInclusive;
261+
}
262+
}
263+
264+
return new Pair<>(segmentValue, segmentInclusive);
265+
}
266+
267+
/**
268+
* Returns the value for the upper bound of `segment`.
269+
*
270+
* @param segment The segment to get the value for.
271+
* @return a Pair with a nullable Value and a boolean indicating whether the bound is inclusive
272+
*/
273+
private Pair<Value, Boolean> getDescendingBound(FieldIndex.Segment segment) {
274+
Value segmentValue = null;
275+
boolean segmentInclusive = true;
276+
277+
// Process all filters to find a value for the current field segment
278+
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
279+
Value filterValue = null;
280+
boolean filterInclusive = true;
281+
282+
switch (fieldFilter.getOperator()) {
283+
case GREATER_THAN_OR_EQUAL:
284+
case GREATER_THAN:
285+
filterValue = Values.getUpperBound(fieldFilter.getValue().getValueTypeCase());
286+
filterInclusive = false;
287+
break;
288+
case EQUAL:
289+
case IN:
290+
case LESS_THAN_OR_EQUAL:
291+
filterValue = fieldFilter.getValue();
292+
break;
293+
case LESS_THAN:
294+
filterValue = fieldFilter.getValue();
295+
filterInclusive = false;
296+
break;
297+
case NOT_EQUAL:
298+
case NOT_IN:
299+
filterValue = Values.MAX_VALUE;
300+
break;
301+
default:
302+
// Remaining filters cannot be used as upper bounds.
303+
}
304+
305+
if (min(segmentValue, filterValue) == filterValue) {
306+
segmentValue = filterValue;
307+
segmentInclusive = filterInclusive;
308+
}
309+
}
310+
return new Pair<>(segmentValue, segmentInclusive);
311+
}
312+
252313
/**
253314
* Returns an upper bound of field values that can be used as an ending point when scanning the
254315
* index defined by {@code fieldIndex}. Returns {@code null} if no upper bound exists.
@@ -259,66 +320,33 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
259320

260321
// For each segment, retrieve an upper bound if there is a suitable filter or endAt.
261322
for (FieldIndex.Segment segment : fieldIndex.getDirectionalSegments()) {
262-
@Nullable Value segmentValue = null;
263-
boolean segmentInclusive = true;
264-
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;
269-
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-
}
292-
293-
if (min(segmentValue, filterValue) == filterValue) {
294-
segmentValue = filterValue;
295-
segmentInclusive = filterInclusive;
296-
}
297-
}
323+
Pair<Value, Boolean> segmentBound =
324+
segment.getKind().equals(FieldIndex.Segment.Kind.ASCENDING)
325+
? getDescendingBound(segment)
326+
: getAscendingBound(segment);
298327

299-
// If there is an endAt bound, compare the values against the existing boundary to see
328+
// If there is an endAt segmentBound, compare the values against the existing boundary to see
300329
// if we can narrow the scope.
301330
if (endAt != null) {
302331
for (int i = 0; i < orderBys.size(); ++i) {
303332
OrderBy orderBy = this.orderBys.get(i);
304333
if (orderBy.getField().equals(segment.getFieldPath())) {
305334
Value cursorValue = endAt.getPosition().get(i);
306-
if (min(segmentValue, cursorValue) == cursorValue) {
307-
segmentValue = cursorValue;
308-
segmentInclusive = endAt.isInclusive();
335+
if (min(segmentBound.first, cursorValue) == cursorValue) {
336+
segmentBound = new Pair<>(cursorValue, endAt.isInclusive());
309337
}
310338
break;
311339
}
312340
}
313341
}
314342

315-
if (segmentValue == null) {
316-
// No upper bound exists
343+
if (segmentBound.first == null) {
344+
// No upper segmentBound exists
317345
return null;
318346
}
319347

320-
values.add(segmentValue);
321-
inclusive &= segmentInclusive;
348+
values.add(segmentBound.first);
349+
inclusive &= segmentBound.second;
322350
}
323351

324352
return new Bound(values, inclusive);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ public void testGreaterThanFilter() {
189189
verifyResults(query, "coll/val3");
190190
}
191191

192+
@Test
193+
public void testGreaterThanFilterWithDescendingOrder() {
194+
indexManager.addFieldIndex(fieldIndex("coll", "count", Kind.DESCENDING));
195+
addDoc("coll/val1", map("count", 1));
196+
addDoc("coll/val2", map("count", 2));
197+
Query query = query("coll").filter(filter("count", ">", 1)).orderBy(orderBy("count", "desc"));
198+
verifyResults(query, "coll/val2");
199+
}
200+
192201
@Test
193202
public void testRangeFilter() {
194203
setUpSingleValueFilter();

0 commit comments

Comments
 (0)