Skip to content

Commit 8d3f125

Browse files
Add Query Planner (#2900)
1 parent 27790de commit 8d3f125

File tree

5 files changed

+659
-28
lines changed

5 files changed

+659
-28
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -313,15 +313,8 @@ Task<Void> configureIndices(String json) {
313313
FieldPath fieldPath = FieldPath.fromServerFormat(field.getString("fieldPath"));
314314
if ("CONTAINS".equals(field.optString("arrayConfig"))) {
315315
fieldIndex = fieldIndex.withAddedField(fieldPath, FieldIndex.Segment.Kind.CONTAINS);
316-
} else if ("ASCENDING".equals(field.optString("order"))) {
317-
fieldIndex = fieldIndex.withAddedField(fieldPath, FieldIndex.Segment.Kind.ASCENDING);
318-
} else if ("DESCENDING".equals(field.optString("order"))) {
319-
fieldIndex = fieldIndex.withAddedField(fieldPath, FieldIndex.Segment.Kind.DESCENDING);
320316
} else {
321-
throw new IllegalArgumentException(
322-
String.format(
323-
"Index definition not valid (for field \"%s\" of collection \"%s\")",
324-
fieldPath, fieldIndex.getCollectionId()));
317+
fieldIndex = fieldIndex.withAddedField(fieldPath, FieldIndex.Segment.Kind.ORDERED);
325318
}
326319
parsedIndices.add(fieldIndex);
327320
}

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -299,18 +299,10 @@ public Index encodeFieldIndex(FieldIndex fieldIndex) {
299299
for (FieldIndex.Segment segment : fieldIndex) {
300300
Index.IndexField.Builder indexField = Index.IndexField.newBuilder();
301301
indexField.setFieldPath(segment.getFieldPath().canonicalString());
302-
switch (segment.getKind()) {
303-
case ASCENDING:
304-
indexField.setOrder(Index.IndexField.Order.ASCENDING);
305-
break;
306-
case DESCENDING:
307-
indexField.setOrder(Index.IndexField.Order.DESCENDING);
308-
break;
309-
case CONTAINS:
310-
indexField.setArrayConfig(Index.IndexField.ArrayConfig.CONTAINS);
311-
break;
312-
default:
313-
throw fail("Unknown index kind %s", segment.getKind());
302+
if (segment.getKind() == FieldIndex.Segment.Kind.CONTAINS) {
303+
indexField.setArrayConfig(Index.IndexField.ArrayConfig.CONTAINS);
304+
} else {
305+
indexField.setOrder(Index.IndexField.Order.ASCENDING);
314306
}
315307
index.addFields(indexField);
316308
}

firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldIndex.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@
2323
* An index definition for field indices in Firestore.
2424
*
2525
* <p>Every index is associated with a collection. The definition contains a list of fields and the
26-
* indexes sorting order (which can be {@link Segment.Kind#ASCENDING}, {@link
27-
* Segment.Kind#DESCENDING} or {@link Segment.Kind#CONTAINS} for ArrayContains/ArrayContainsAn
28-
* queries.
26+
* indexes kind (which can be {@link Segment.Kind#ORDERED} or {@link Segment.Kind#CONTAINS} for
27+
* ArrayContains/ArrayContainsAny queries.
2928
*
3029
* <p>Unlike the backend, the SDK does not differentiate between collection or collection
3130
* group-scoped indices. Every index can be used for both single collection and collection group
@@ -35,12 +34,10 @@ public final class FieldIndex implements Iterable<FieldIndex.Segment> {
3534

3635
/** An index component consisting of field path and index type. */
3736
public static final class Segment {
38-
/** The type of the index, e.g. for which sorting order it can be used. */
37+
/** The type of the index, e.g. for which type of query it can be used. */
3938
public enum Kind {
40-
/** Ascending index. Can be used for <, <=, ==, >=, > and IN with ascending ordering. */
41-
ASCENDING,
42-
/** Descending index. Can be used for <, <=, ==, >=, > and IN with descending ordering. */
43-
DESCENDING,
39+
/** Ascending index. Can be used for <, <=, ==, >=, >, !=, IN and NOT IN queries. */
40+
ORDERED,
4441
/** Contains index. Can be used for ArrayContains and ArrayContainsAny */
4542
CONTAINS
4643
}
@@ -79,6 +76,11 @@ public int hashCode() {
7976
result = 31 * result + kind.hashCode();
8077
return result;
8178
}
79+
80+
@Override
81+
public String toString() {
82+
return String.format("Segment{fieldPath=%s, kind=%s}", fieldPath, kind);
83+
}
8284
}
8385

8486
private final String collectionId;
@@ -99,6 +101,23 @@ public String getCollectionId() {
99101
return collectionId;
100102
}
101103

104+
public Segment getSegment(int index) {
105+
return segments.get(index);
106+
}
107+
108+
public int segmentCount() {
109+
return segments.size();
110+
}
111+
112+
/**
113+
* Returns a new field index that only contains the first `size` segments.
114+
*
115+
* @throws IndexOutOfBoundsException if size > segmentCount
116+
*/
117+
public FieldIndex prefix(int size) {
118+
return new FieldIndex(collectionId, segments.subList(0, size));
119+
}
120+
102121
@NonNull
103122
@Override
104123
public Iterator<Segment> iterator() {
@@ -129,4 +148,9 @@ public int hashCode() {
129148
result = 31 * result + segments.hashCode();
130149
return result;
131150
}
151+
152+
@Override
153+
public String toString() {
154+
return String.format("FieldIndex{collectionId='%s', segments=%s}", collectionId, segments);
155+
}
132156
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.model;
16+
17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
19+
import com.google.firebase.firestore.core.FieldFilter;
20+
import com.google.firebase.firestore.core.Filter;
21+
import com.google.firebase.firestore.core.OrderBy;
22+
import com.google.firebase.firestore.core.Target;
23+
import java.util.ArrayList;
24+
import java.util.HashMap;
25+
import java.util.HashSet;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Set;
29+
30+
/**
31+
* A light query planner for Firestore.
32+
*
33+
* <p>This class matches a {@link FieldIndex} against a Firestore Query {@link Target}. It
34+
* determines whether a given index can be used to serve the specified target.
35+
*
36+
* <p>Unlike the backend, the SDK only maintains two different index kinds and does not distinguish
37+
* between ascending and descending indices. Instead of ordering query results by their index order,
38+
* the SDK re-orders all query results locally, which reduces the number of indices it needs to
39+
* maintain.
40+
*
41+
* <p>The following table showcases some possible index configurations:
42+
*
43+
* <table>
44+
* <thead>
45+
* <tr>
46+
* <td>Query</td>
47+
* <td>Index</td>
48+
* </tr>
49+
* </thead>
50+
* <tbody>
51+
* <tr>
52+
* <td>where("a", "==", "a").where("b", "==", "b")</td>
53+
* <td>a ORDERED, b ORDERED</td>
54+
* </tr>
55+
* <tr>
56+
* <td>where("a", "==", "a").where("b", "==", "b")</td>
57+
* <td>a ORDERED</td>
58+
* </tr>
59+
* <tr>
60+
* <td>where("a", "==", "a").where("b", "==", "b")</td>
61+
* <td>b ORDERED</td>
62+
* </tr>
63+
* <tr>
64+
* <td>where("a", ">=", "a").orderBy("a").orderBy("b")</td>
65+
* <td>a ORDERED, b ORDERED</td>
66+
* </tr>
67+
* <tr>
68+
* <td>where("a", ">=", "a").orderBy("a").orderBy("b")</td>
69+
* <td>a ORDERED</td>
70+
* </tr>
71+
* <tr>
72+
* <td>where("a", "array-contains", "a").orderBy("b")</td>
73+
* <td>a CONTAINS, b ORDERED</td>
74+
* </tr>
75+
* <tr>
76+
* <td>where("a", "array-contains", "a").orderBy("b")</td>
77+
* <td>a CONTAINS</td>
78+
* </tr>
79+
* </tbody>
80+
* </table>
81+
*/
82+
public class TargetIndexMatcher {
83+
// The collection ID of the query target.
84+
private final String collectionId;
85+
86+
// The list of filters per field. A target can have duplicate filters for a field.
87+
private final Map<FieldPath, List<FieldFilter>> fieldFilterFields = new HashMap<>();
88+
89+
// The list of orderBy fields in the query target.
90+
private final Set<FieldPath> orderByFields = new HashSet<>();
91+
92+
public TargetIndexMatcher(Target target) {
93+
collectionId =
94+
target.getCollectionGroup() != null
95+
? target.getCollectionGroup()
96+
: target.getPath().getLastSegment();
97+
98+
for (Filter filter : target.getFilters()) {
99+
hardAssert(filter instanceof FieldFilter, "Only FieldFilters are supported");
100+
List<FieldFilter> currentFilters = fieldFilterFields.get(filter.getField());
101+
if (currentFilters == null) {
102+
currentFilters = new ArrayList<>();
103+
fieldFilterFields.put(filter.getField(), currentFilters);
104+
}
105+
currentFilters.add((FieldFilter) filter);
106+
}
107+
108+
for (OrderBy orderBy : target.getOrderBy()) {
109+
orderByFields.add(orderBy.getField());
110+
}
111+
}
112+
113+
/**
114+
* Returns whether the index can be used to serve the TargetIndexMatcher's target.
115+
*
116+
* @throws AssertionError if the index is for a different collection
117+
*/
118+
public boolean servedByIndex(FieldIndex index) {
119+
hardAssert(index.getCollectionId().equals(collectionId), "Collection IDs do not match");
120+
for (int i = 0; i < index.segmentCount(); ++i) {
121+
if (!canUseSegment(index.getSegment(i))) {
122+
return false;
123+
}
124+
}
125+
return true;
126+
}
127+
128+
private boolean canUseSegment(FieldIndex.Segment segment) {
129+
List<FieldFilter> filters = fieldFilterFields.get(segment.getFieldPath());
130+
if (filters != null) {
131+
for (FieldFilter filter : filters) {
132+
switch (filter.getOperator()) {
133+
case ARRAY_CONTAINS:
134+
case ARRAY_CONTAINS_ANY:
135+
if (segment.getKind().equals(FieldIndex.Segment.Kind.CONTAINS)) {
136+
return true;
137+
}
138+
break;
139+
default:
140+
if (segment.getKind().equals(FieldIndex.Segment.Kind.ORDERED)) {
141+
return true;
142+
}
143+
}
144+
}
145+
}
146+
147+
return orderByFields.contains(segment.getFieldPath())
148+
&& segment.getKind().equals(FieldIndex.Segment.Kind.ORDERED);
149+
}
150+
}

0 commit comments

Comments
 (0)