17
17
import androidx .annotation .Nullable ;
18
18
import com .google .firebase .firestore .core .OrderBy .Direction ;
19
19
import com .google .firebase .firestore .model .DocumentKey ;
20
+ import com .google .firebase .firestore .model .FieldIndex ;
20
21
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 ;
21
25
import java .util .List ;
22
26
23
27
/**
@@ -31,7 +35,7 @@ public final class Target {
31
35
32
36
private @ Nullable String memoizedCannonicalId ;
33
37
34
- private final List <OrderBy > orderBy ;
38
+ private final List <OrderBy > orderBys ;
35
39
private final List <Filter > filters ;
36
40
37
41
private final ResourcePath path ;
@@ -55,13 +59,13 @@ public Target(
55
59
ResourcePath path ,
56
60
@ Nullable String collectionGroup ,
57
61
List <Filter > filters ,
58
- List <OrderBy > orderBy ,
62
+ List <OrderBy > orderBys ,
59
63
long limit ,
60
64
@ Nullable Bound startAt ,
61
65
@ Nullable Bound endAt ) {
62
66
this .path = path ;
63
67
this .collectionGroup = collectionGroup ;
64
- this .orderBy = orderBy ;
68
+ this .orderBys = orderBys ;
65
69
this .filters = filters ;
66
70
this .limit = limit ;
67
71
this .startAt = startAt ;
@@ -107,8 +111,143 @@ public boolean hasLimit() {
107
111
return endAt ;
108
112
}
109
113
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
+
110
249
public List <OrderBy > getOrderBy () {
111
- return this .orderBy ;
250
+ return this .orderBys ;
112
251
}
113
252
114
253
/** Returns a canonical string representing this target. */
@@ -177,7 +316,7 @@ public boolean equals(Object o) {
177
316
if (limit != target .limit ) {
178
317
return false ;
179
318
}
180
- if (!orderBy .equals (target .orderBy )) {
319
+ if (!orderBys .equals (target .orderBys )) {
181
320
return false ;
182
321
}
183
322
if (!filters .equals (target .filters )) {
@@ -194,7 +333,7 @@ public boolean equals(Object o) {
194
333
195
334
@ Override
196
335
public int hashCode () {
197
- int result = orderBy .hashCode ();
336
+ int result = orderBys .hashCode ();
198
337
result = 31 * result + (collectionGroup != null ? collectionGroup .hashCode () : 0 );
199
338
result = 31 * result + filters .hashCode ();
200
339
result = 31 * result + path .hashCode ();
@@ -223,13 +362,13 @@ public String toString() {
223
362
}
224
363
}
225
364
226
- if (!orderBy .isEmpty ()) {
365
+ if (!orderBys .isEmpty ()) {
227
366
builder .append (" order by " );
228
- for (int i = 0 ; i < orderBy .size (); i ++) {
367
+ for (int i = 0 ; i < orderBys .size (); i ++) {
229
368
if (i > 0 ) {
230
369
builder .append (", " );
231
370
}
232
- builder .append (orderBy .get (i ));
371
+ builder .append (orderBys .get (i ));
233
372
}
234
373
}
235
374
0 commit comments