Skip to content

Commit e41b233

Browse files
Add Index Bounds to Queries (#2926)
1 parent b0e18f9 commit e41b233

File tree

2 files changed

+500
-9
lines changed
  • firebase-firestore/src

2 files changed

+500
-9
lines changed

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

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
import androidx.annotation.Nullable;
1818
import com.google.firebase.firestore.core.OrderBy.Direction;
1919
import com.google.firebase.firestore.model.DocumentKey;
20+
import com.google.firebase.firestore.model.FieldIndex;
2021
import com.google.firebase.firestore.model.ResourcePath;
22+
import com.google.firebase.firestore.model.Values;
23+
import com.google.firestore.v1.Value;
24+
import java.util.ArrayList;
2125
import java.util.List;
2226

2327
/**
@@ -31,7 +35,7 @@ public final class Target {
3135

3236
private @Nullable String memoizedCannonicalId;
3337

34-
private final List<OrderBy> orderBy;
38+
private final List<OrderBy> orderBys;
3539
private final List<Filter> filters;
3640

3741
private final ResourcePath path;
@@ -55,13 +59,13 @@ public Target(
5559
ResourcePath path,
5660
@Nullable String collectionGroup,
5761
List<Filter> filters,
58-
List<OrderBy> orderBy,
62+
List<OrderBy> orderBys,
5963
long limit,
6064
@Nullable Bound startAt,
6165
@Nullable Bound endAt) {
6266
this.path = path;
6367
this.collectionGroup = collectionGroup;
64-
this.orderBy = orderBy;
68+
this.orderBys = orderBys;
6569
this.filters = filters;
6670
this.limit = limit;
6771
this.startAt = startAt;
@@ -107,8 +111,143 @@ public boolean hasLimit() {
107111
return endAt;
108112
}
109113

114+
/**
115+
* Returns a lower bound of field values that can be used as a starting point to scan the index
116+
* defined by {@code fieldIndex}.
117+
*
118+
* <p>Unlike {@link #getUpperBound}, lower bounds always exist as the SDK can use {@code null} as
119+
* a starting point for missing boundary values.
120+
*/
121+
public Bound getLowerBound(FieldIndex fieldIndex) {
122+
List<Value> values = new ArrayList<>();
123+
boolean before = true;
124+
125+
// Go through all filters to find a value for the current field segment
126+
for (FieldIndex.Segment segment : fieldIndex) {
127+
Value lowestValue = Values.NULL_VALUE;
128+
for (Filter filter : filters) {
129+
if (filter.getField().equals(segment.getFieldPath())) {
130+
FieldFilter fieldFilter = (FieldFilter) filter;
131+
switch (fieldFilter.getOperator()) {
132+
case LESS_THAN:
133+
case NOT_IN:
134+
case NOT_EQUAL:
135+
case LESS_THAN_OR_EQUAL:
136+
// These filters cannot be used as a lower bound. Skip.
137+
break;
138+
case EQUAL:
139+
case IN:
140+
case ARRAY_CONTAINS_ANY:
141+
case ARRAY_CONTAINS:
142+
case GREATER_THAN_OR_EQUAL:
143+
lowestValue = fieldFilter.getValue();
144+
break;
145+
case GREATER_THAN:
146+
lowestValue = fieldFilter.getValue();
147+
before = false;
148+
break;
149+
}
150+
}
151+
}
152+
153+
// If there is a startAt bound, compare the values against the existing boundary to see
154+
// if we can narrow the scope.
155+
if (startAt != null) {
156+
for (int i = 0; i < orderBys.size(); ++i) {
157+
OrderBy orderBy = this.orderBys.get(i);
158+
if (orderBy.getField().equals(segment.getFieldPath())) {
159+
Value cursorValue = startAt.getPosition().get(i);
160+
if (Values.compare(lowestValue, cursorValue) <= 0) {
161+
lowestValue = cursorValue;
162+
// `before` is shared by all cursor values. If any cursor value is used, we set before
163+
// to the cursor's value.
164+
before = startAt.isBefore();
165+
}
166+
break;
167+
}
168+
}
169+
}
170+
values.add(lowestValue);
171+
}
172+
173+
return new Bound(values, before);
174+
}
175+
176+
/**
177+
* Returns an upper bound of field values that can be used as an ending point when scanning the
178+
* index defined by {@code fieldIndex}.
179+
*
180+
* <p>Unlike {@link #getLowerBound}, upper bounds do not always exist since the Firestore does not
181+
* define a maximum field value. The index scan should not use an upper bound if {@code null} is
182+
* returned.
183+
*/
184+
public @Nullable Bound getUpperBound(FieldIndex fieldIndex) {
185+
List<Value> values = new ArrayList<>();
186+
boolean before = false;
187+
188+
for (FieldIndex.Segment segment : fieldIndex) {
189+
@Nullable Value largestValue = null;
190+
191+
// Go through all filters to find a value for the current field segment
192+
for (Filter filter : filters) {
193+
if (filter.getField().equals(segment.getFieldPath())) {
194+
FieldFilter fieldFilter = (FieldFilter) filter;
195+
switch (fieldFilter.getOperator()) {
196+
case GREATER_THAN:
197+
case NOT_IN:
198+
case NOT_EQUAL:
199+
case GREATER_THAN_OR_EQUAL:
200+
// These filters cannot be used as an upper bound. Skip.
201+
break;
202+
case EQUAL:
203+
case IN:
204+
case ARRAY_CONTAINS_ANY:
205+
case ARRAY_CONTAINS:
206+
case LESS_THAN_OR_EQUAL:
207+
largestValue = fieldFilter.getValue();
208+
before = true;
209+
break;
210+
case LESS_THAN:
211+
largestValue = fieldFilter.getValue();
212+
before = false;
213+
break;
214+
}
215+
}
216+
}
217+
218+
// If there is an endAt bound, compare the values against the existing boundary to see
219+
// if we can narrow the scope.
220+
if (endAt != null) {
221+
for (int i = 0; i < orderBys.size(); ++i) {
222+
OrderBy orderBy = this.orderBys.get(i);
223+
if (orderBy.getField().equals(segment.getFieldPath())) {
224+
Value cursorValue = endAt.getPosition().get(i);
225+
if (largestValue == null || Values.compare(largestValue, cursorValue) > 0) {
226+
largestValue = cursorValue;
227+
before = endAt.isBefore();
228+
}
229+
break;
230+
}
231+
}
232+
}
233+
234+
if (largestValue == null) {
235+
// No upper bound exists
236+
return null;
237+
}
238+
239+
values.add(largestValue);
240+
}
241+
242+
if (values.isEmpty()) {
243+
return null;
244+
}
245+
246+
return new Bound(values, before);
247+
}
248+
110249
public List<OrderBy> getOrderBy() {
111-
return this.orderBy;
250+
return this.orderBys;
112251
}
113252

114253
/** Returns a canonical string representing this target. */
@@ -177,7 +316,7 @@ public boolean equals(Object o) {
177316
if (limit != target.limit) {
178317
return false;
179318
}
180-
if (!orderBy.equals(target.orderBy)) {
319+
if (!orderBys.equals(target.orderBys)) {
181320
return false;
182321
}
183322
if (!filters.equals(target.filters)) {
@@ -194,7 +333,7 @@ public boolean equals(Object o) {
194333

195334
@Override
196335
public int hashCode() {
197-
int result = orderBy.hashCode();
336+
int result = orderBys.hashCode();
198337
result = 31 * result + (collectionGroup != null ? collectionGroup.hashCode() : 0);
199338
result = 31 * result + filters.hashCode();
200339
result = 31 * result + path.hashCode();
@@ -223,13 +362,13 @@ public String toString() {
223362
}
224363
}
225364

226-
if (!orderBy.isEmpty()) {
365+
if (!orderBys.isEmpty()) {
227366
builder.append(" order by ");
228-
for (int i = 0; i < orderBy.size(); i++) {
367+
for (int i = 0; i < orderBys.size(); i++) {
229368
if (i > 0) {
230369
builder.append(", ");
231370
}
232-
builder.append(orderBy.get(i));
371+
builder.append(orderBys.get(i));
233372
}
234373
}
235374

0 commit comments

Comments
 (0)