1717import androidx .annotation .Nullable ;
1818import com .google .firebase .firestore .core .OrderBy .Direction ;
1919import com .google .firebase .firestore .model .DocumentKey ;
20+ import com .google .firebase .firestore .model .FieldIndex ;
2021import 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 ;
2125import 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