Skip to content

Commit e8da9bb

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 e8da9bb

File tree

2 files changed

+125
-88
lines changed

2 files changed

+125
-88
lines changed

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

Lines changed: 116 additions & 88 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,65 +186,33 @@ 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;
189+
Pair<Value, Boolean> segmentBound =
190+
segment.getKind().equals(FieldIndex.Segment.Kind.ASCENDING)
191+
? getAscendingBound(segment)
192+
: getDescendingBound(segment);
190193

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.
194+
// If there is a startAt bound, compare the values against the existing boundary to see if we
195+
// can narrow the scope.
226196
if (startAt != null) {
227197
for (int i = 0; i < orderBys.size(); ++i) {
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);
@@ -259,71 +228,130 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
259228

260229
// For each segment, retrieve an upper bound if there is a suitable filter or endAt.
261230
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;
231+
Pair<Value, Boolean> segmentBound =
232+
segment.getKind().equals(FieldIndex.Segment.Kind.ASCENDING)
233+
? getDescendingBound(segment)
234+
: getAscendingBound(segment);
269235

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-
}
298-
299-
// If there is an endAt bound, compare the values against the existing boundary to see
300-
// if we can narrow the scope.
236+
// If there is an endAt bound, compare the values against the existing boundary to see if we
237+
// can narrow the scope.
301238
if (endAt != null) {
302239
for (int i = 0; i < orderBys.size(); ++i) {
303240
OrderBy orderBy = this.orderBys.get(i);
304241
if (orderBy.getField().equals(segment.getFieldPath())) {
305242
Value cursorValue = endAt.getPosition().get(i);
306-
if (min(segmentValue, cursorValue) == cursorValue) {
307-
segmentValue = cursorValue;
308-
segmentInclusive = endAt.isInclusive();
243+
if (min(segmentBound.first, cursorValue) == cursorValue) {
244+
segmentBound = new Pair<>(cursorValue, endAt.isInclusive());
309245
}
310246
break;
311247
}
312248
}
313249
}
314250

315-
if (segmentValue == null) {
316-
// No upper bound exists
251+
if (segmentBound.first == null) {
252+
// No upper segmentBound exists
317253
return null;
318254
}
319255

320-
values.add(segmentValue);
321-
inclusive &= segmentInclusive;
256+
values.add(segmentBound.first);
257+
inclusive &= segmentBound.second;
322258
}
323259

324260
return new Bound(values, inclusive);
325261
}
326262

263+
/**
264+
* Returns the value for an ascending bound of `segment`.
265+
*
266+
* @param segment The segment to get the value for.
267+
* @return a Pair with a nullable Value and a boolean indicating whether the bound is inclusive
268+
*/
269+
private Pair<Value, Boolean> getAscendingBound(FieldIndex.Segment segment) {
270+
Value segmentValue = null;
271+
boolean segmentInclusive = true;
272+
273+
// Process all filters to find a value for the current field segment
274+
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
275+
Value filterValue = null;
276+
boolean filterInclusive = true;
277+
278+
switch (fieldFilter.getOperator()) {
279+
case LESS_THAN:
280+
case LESS_THAN_OR_EQUAL:
281+
filterValue = Values.getLowerBound(fieldFilter.getValue().getValueTypeCase());
282+
break;
283+
case EQUAL:
284+
case IN:
285+
case GREATER_THAN_OR_EQUAL:
286+
filterValue = fieldFilter.getValue();
287+
break;
288+
case GREATER_THAN:
289+
filterValue = fieldFilter.getValue();
290+
filterInclusive = false;
291+
break;
292+
case NOT_EQUAL:
293+
case NOT_IN:
294+
filterValue = Values.MIN_VALUE;
295+
break;
296+
default:
297+
// Remaining filters cannot be used as bound.
298+
}
299+
300+
if (max(segmentValue, filterValue) == filterValue) {
301+
segmentValue = filterValue;
302+
segmentInclusive = filterInclusive;
303+
}
304+
}
305+
306+
return new Pair<>(segmentValue, segmentInclusive);
307+
}
308+
309+
/**
310+
* Returns the value for a descending bound of `segment`.
311+
*
312+
* @param segment The segment to get the value for.
313+
* @return a Pair with a nullable Value and a boolean indicating whether the bound is inclusive
314+
*/
315+
private Pair<Value, Boolean> getDescendingBound(FieldIndex.Segment segment) {
316+
Value segmentValue = null;
317+
boolean segmentInclusive = true;
318+
319+
// Process all filters to find a value for the current field segment
320+
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
321+
Value filterValue = null;
322+
boolean filterInclusive = true;
323+
324+
switch (fieldFilter.getOperator()) {
325+
case GREATER_THAN_OR_EQUAL:
326+
case GREATER_THAN:
327+
filterValue = Values.getUpperBound(fieldFilter.getValue().getValueTypeCase());
328+
filterInclusive = false;
329+
break;
330+
case EQUAL:
331+
case IN:
332+
case LESS_THAN_OR_EQUAL:
333+
filterValue = fieldFilter.getValue();
334+
break;
335+
case LESS_THAN:
336+
filterValue = fieldFilter.getValue();
337+
filterInclusive = false;
338+
break;
339+
case NOT_EQUAL:
340+
case NOT_IN:
341+
filterValue = Values.MAX_VALUE;
342+
break;
343+
default:
344+
// Remaining filters cannot be used as bound.
345+
}
346+
347+
if (min(segmentValue, filterValue) == filterValue) {
348+
segmentValue = filterValue;
349+
segmentInclusive = filterInclusive;
350+
}
351+
}
352+
return new Pair<>(segmentValue, segmentInclusive);
353+
}
354+
327355
public List<OrderBy> getOrderBy() {
328356
return this.orderBys;
329357
}

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)