Skip to content

Commit 3996548

Browse files
feat!: add support for the Query Partition API (#202)
1 parent 15d68cd commit 3996548

File tree

11 files changed

+334
-91
lines changed

11 files changed

+334
-91
lines changed

google-cloud-firestore/clirr-ignored-differences.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,19 @@
142142
<to>*</to>
143143
</difference>
144144

145+
<!--
146+
Query Partition API Feature
147+
-->
148+
<difference>
149+
<differenceType>7012</differenceType>
150+
<className>com/google/cloud/firestore/spi/v1/FirestoreRpc</className>
151+
<method>com.google.api.gax.rpc.UnaryCallable partitionQueryPagedCallable()</method>
152+
</difference>
153+
<difference>
154+
<differenceType>7006</differenceType>
155+
<className>com/google/cloud/firestore/Firestore</className>
156+
<method>com.google.cloud.firestore.Query collectionGroup(java.lang.String)</method>
157+
<to>com.google.cloud.firestore.CollectionGroup</to>
158+
</difference>
159+
145160
</differences>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore;
18+
19+
import com.google.api.gax.rpc.ApiException;
20+
import com.google.api.gax.rpc.ApiExceptions;
21+
import com.google.api.gax.rpc.ApiStreamObserver;
22+
import com.google.cloud.firestore.v1.FirestoreClient;
23+
import com.google.firestore.v1.Cursor;
24+
import com.google.firestore.v1.PartitionQueryRequest;
25+
import javax.annotation.Nullable;
26+
27+
/**
28+
* A Collection Group query matches all documents that are contained in a collection or
29+
* subcollection with a specific collection ID.
30+
*/
31+
public class CollectionGroup extends Query {
32+
CollectionGroup(FirestoreRpcContext<?> rpcContext, String collectionId) {
33+
super(
34+
rpcContext,
35+
QueryOptions.builder()
36+
.setParentPath(rpcContext.getResourcePath())
37+
.setCollectionId(collectionId)
38+
.setAllDescendants(true)
39+
.build());
40+
}
41+
42+
/**
43+
* Partitions a query by returning partition cursors that can be used to run the query in
44+
* parallel. The returned partition cursors are split points that can be used as starting/end
45+
* points for the query results.
46+
*
47+
* @param desiredPartitionCount The desired maximum number of partition points. The number must be
48+
* strictly positive. The actual number of partitions returned may be fewer.
49+
* @param observer a stream observer that receives the result of the Partition request.
50+
*/
51+
public void getPartitions(
52+
long desiredPartitionCount, ApiStreamObserver<QueryPartition> observer) {
53+
// Partition queries require explicit ordering by __name__.
54+
Query queryWithDefaultOrder = orderBy(FieldPath.DOCUMENT_ID);
55+
56+
PartitionQueryRequest.Builder request = PartitionQueryRequest.newBuilder();
57+
request.setStructuredQuery(queryWithDefaultOrder.buildQuery());
58+
request.setParent(options.getParentPath().toString());
59+
60+
// Since we are always returning an extra partition (with en empty endBefore cursor), we
61+
// reduce the desired partition count by one.
62+
request.setPartitionCount(desiredPartitionCount - 1);
63+
64+
final FirestoreClient.PartitionQueryPagedResponse response;
65+
try {
66+
response =
67+
ApiExceptions.callAndTranslateApiException(
68+
rpcContext.sendRequest(
69+
request.build(), rpcContext.getClient().partitionQueryPagedCallable()));
70+
} catch (ApiException exception) {
71+
throw FirestoreException.apiException(exception);
72+
}
73+
74+
@Nullable Object[] lastCursor = null;
75+
for (Cursor cursor : response.iterateAll()) {
76+
Object[] decodedCursorValue = new Object[cursor.getValuesCount()];
77+
for (int i = 0; i < cursor.getValuesCount(); ++i) {
78+
decodedCursorValue[i] = UserDataConverter.decodeValue(rpcContext, cursor.getValues(i));
79+
}
80+
observer.onNext(new QueryPartition(queryWithDefaultOrder, lastCursor, decodedCursorValue));
81+
lastCursor = decodedCursorValue;
82+
}
83+
observer.onNext(new QueryPartition(queryWithDefaultOrder, lastCursor, null));
84+
observer.onCompleted();
85+
}
86+
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,9 @@
2323
import com.google.firestore.v1.Document;
2424
import com.google.firestore.v1.Value;
2525
import com.google.firestore.v1.Write;
26-
import java.util.ArrayList;
2726
import java.util.Date;
2827
import java.util.HashMap;
2928
import java.util.Iterator;
30-
import java.util.List;
3129
import java.util.Map;
3230
import java.util.Objects;
3331
import javax.annotation.Nonnull;
@@ -115,48 +113,6 @@ static DocumentSnapshot fromMissing(
115113
return new DocumentSnapshot(rpcContext, documentReference, null, readTime, null, null);
116114
}
117115

118-
private Object decodeValue(Value v) {
119-
Value.ValueTypeCase typeCase = v.getValueTypeCase();
120-
switch (typeCase) {
121-
case NULL_VALUE:
122-
return null;
123-
case BOOLEAN_VALUE:
124-
return v.getBooleanValue();
125-
case INTEGER_VALUE:
126-
return v.getIntegerValue();
127-
case DOUBLE_VALUE:
128-
return v.getDoubleValue();
129-
case TIMESTAMP_VALUE:
130-
return Timestamp.fromProto(v.getTimestampValue());
131-
case STRING_VALUE:
132-
return v.getStringValue();
133-
case BYTES_VALUE:
134-
return Blob.fromByteString(v.getBytesValue());
135-
case REFERENCE_VALUE:
136-
String pathName = v.getReferenceValue();
137-
return new DocumentReference(rpcContext, ResourcePath.create(pathName));
138-
case GEO_POINT_VALUE:
139-
return new GeoPoint(
140-
v.getGeoPointValue().getLatitude(), v.getGeoPointValue().getLongitude());
141-
case ARRAY_VALUE:
142-
List<Object> list = new ArrayList<>();
143-
List<Value> lv = v.getArrayValue().getValuesList();
144-
for (Value iv : lv) {
145-
list.add(decodeValue(iv));
146-
}
147-
return list;
148-
case MAP_VALUE:
149-
Map<String, Object> outputMap = new HashMap<>();
150-
Map<String, Value> inputMap = v.getMapValue().getFieldsMap();
151-
for (Map.Entry<String, Value> entry : inputMap.entrySet()) {
152-
outputMap.put(entry.getKey(), decodeValue(entry.getValue()));
153-
}
154-
return outputMap;
155-
default:
156-
throw FirestoreException.invalidState(String.format("Unknown Value Type: %s", typeCase));
157-
}
158-
}
159-
160116
/**
161117
* Returns the time at which this snapshot was read.
162118
*
@@ -214,7 +170,7 @@ public Map<String, Object> getData() {
214170

215171
Map<String, Object> decodedFields = new HashMap<>();
216172
for (Map.Entry<String, Value> entry : fields.entrySet()) {
217-
Object decodedValue = decodeValue(entry.getValue());
173+
Object decodedValue = UserDataConverter.decodeValue(rpcContext, entry.getValue());
218174
decodedFields.put(entry.getKey(), decodedValue);
219175
}
220176
return decodedFields;
@@ -293,7 +249,7 @@ public Object get(@Nonnull FieldPath fieldPath) {
293249
return null;
294250
}
295251

296-
return decodeValue(value);
252+
return UserDataConverter.decodeValue(rpcContext, value);
297253
}
298254

299255
/**

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ public interface Firestore extends Service<FirestoreOptions>, AutoCloseable {
5656
Iterable<CollectionReference> listCollections();
5757

5858
/**
59-
* Creates and returns a new @link{Query} that includes all documents in the database that are
60-
* contained in a collection or subcollection with the given @code{collectionId}.
59+
* Creates and returns a new {@link CollectionGroup} that includes all documents in the database
60+
* that are contained in a collection or subcollection with the given @code{collectionId}.
6161
*
6262
* @param collectionId Identifies the collections to query over. Every collection or subcollection
6363
* with this ID as the last segment of its path will be included. Cannot contain a slash.
6464
* @return The created Query.
6565
*/
66-
Query collectionGroup(@Nonnull String collectionId);
66+
CollectionGroup collectionGroup(@Nonnull String collectionId);
6767

6868
/**
6969
* Executes the given updateFunction and then attempts to commit the changes applied within the

google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,12 @@ public void onCompleted() {
263263

264264
@Nonnull
265265
@Override
266-
public Query collectionGroup(@Nonnull final String collectionId) {
266+
public CollectionGroup collectionGroup(@Nonnull final String collectionId) {
267267
Preconditions.checkArgument(
268268
!collectionId.contains("/"),
269269
String.format(
270270
"Invalid collectionId '%s'. Collection IDs must not contain '/'.", collectionId));
271-
return new Query(this, collectionId);
271+
return new CollectionGroup(this, collectionId);
272272
}
273273

274274
@Nonnull

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -296,21 +296,7 @@ abstract static class Builder {
296296
.build());
297297
}
298298

299-
/**
300-
* Creates a Collection Group query that matches all documents directly nested under a
301-
* specifically named collection
302-
*/
303-
Query(FirestoreRpcContext<?> rpcContext, String collectionId) {
304-
this(
305-
rpcContext,
306-
QueryOptions.builder()
307-
.setParentPath(rpcContext.getResourcePath())
308-
.setCollectionId(collectionId)
309-
.setAllDescendants(true)
310-
.build());
311-
}
312-
313-
private Query(FirestoreRpcContext<?> rpcContext, QueryOptions queryOptions) {
299+
protected Query(FirestoreRpcContext<?> rpcContext, QueryOptions queryOptions) {
314300
this.rpcContext = rpcContext;
315301
this.options = queryOptions;
316302
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore;
18+
19+
import java.util.Arrays;
20+
import java.util.Objects;
21+
import javax.annotation.Nullable;
22+
23+
/**
24+
* A split point that can be used in a query as a starting and/or end point for the query results.
25+
* The cursors returned by {@link #getStartAt()} and {@link #getEndBefore()} can only be used in a
26+
* query that matches the constraint of query that produced this partition.
27+
*/
28+
public class QueryPartition {
29+
private final Query query;
30+
@Nullable private final Object[] startAt;
31+
@Nullable private final Object[] endBefore;
32+
33+
public QueryPartition(Query query, @Nullable Object[] startAt, @Nullable Object[] endBefore) {
34+
this.query = query;
35+
this.startAt = startAt;
36+
this.endBefore = endBefore;
37+
}
38+
39+
/**
40+
* The cursor that defines the first result for this partition. {@code null} if this is the first
41+
* partition.
42+
*
43+
* @return a cursor value that can be used with {@link Query#startAt(Object...)} or {@code null}
44+
* if this is the first partition.
45+
*/
46+
@Nullable
47+
public Object[] getStartAt() {
48+
return startAt;
49+
}
50+
51+
/**
52+
* The cursor that defines the first result after this partition. {@code null} if this is the last
53+
* partition.
54+
*
55+
* @return a cursor value that can be used with {@link Query#endBefore(Object...)} or {@code null}
56+
* if this is the last partition.
57+
*/
58+
@Nullable
59+
public Object[] getEndBefore() {
60+
return endBefore;
61+
}
62+
63+
/**
64+
* Returns a query that only returns the documents for this partition.
65+
*
66+
* @return a query partitioned by a {@link Query#startAt(Object...)} and {@link
67+
* Query#endBefore(Object...)} cursor.
68+
*/
69+
public Query createQuery() {
70+
Query baseQuery = query;
71+
if (startAt != null) {
72+
baseQuery = baseQuery.startAt(startAt);
73+
}
74+
if (endBefore != null) {
75+
baseQuery = baseQuery.endBefore(endBefore);
76+
}
77+
return baseQuery;
78+
}
79+
80+
@Override
81+
public boolean equals(Object o) {
82+
if (this == o) return true;
83+
if (!(o instanceof QueryPartition)) return false;
84+
QueryPartition partition = (QueryPartition) o;
85+
return query.equals(partition.query)
86+
&& Arrays.equals(startAt, partition.startAt)
87+
&& Arrays.equals(endBefore, partition.endBefore);
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
int result = Objects.hash(query);
93+
result = 31 * result + Arrays.hashCode(startAt);
94+
result = 31 * result + Arrays.hashCode(endBefore);
95+
return result;
96+
}
97+
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/UserDataConverter.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import com.google.firestore.v1.MapValue;
2323
import com.google.firestore.v1.Value;
2424
import com.google.protobuf.NullValue;
25+
import java.util.ArrayList;
2526
import java.util.Date;
27+
import java.util.HashMap;
2628
import java.util.List;
2729
import java.util.Map;
2830
import java.util.concurrent.TimeUnit;
@@ -181,4 +183,46 @@ static Value encodeValue(
181183

182184
throw FirestoreException.invalidState("Cannot convert %s to Firestore Value", sanitizedObject);
183185
}
186+
187+
static Object decodeValue(FirestoreRpcContext<?> rpcContext, Value v) {
188+
Value.ValueTypeCase typeCase = v.getValueTypeCase();
189+
switch (typeCase) {
190+
case NULL_VALUE:
191+
return null;
192+
case BOOLEAN_VALUE:
193+
return v.getBooleanValue();
194+
case INTEGER_VALUE:
195+
return v.getIntegerValue();
196+
case DOUBLE_VALUE:
197+
return v.getDoubleValue();
198+
case TIMESTAMP_VALUE:
199+
return Timestamp.fromProto(v.getTimestampValue());
200+
case STRING_VALUE:
201+
return v.getStringValue();
202+
case BYTES_VALUE:
203+
return Blob.fromByteString(v.getBytesValue());
204+
case REFERENCE_VALUE:
205+
String pathName = v.getReferenceValue();
206+
return new DocumentReference(rpcContext, ResourcePath.create(pathName));
207+
case GEO_POINT_VALUE:
208+
return new GeoPoint(
209+
v.getGeoPointValue().getLatitude(), v.getGeoPointValue().getLongitude());
210+
case ARRAY_VALUE:
211+
List<Object> list = new ArrayList<>();
212+
List<Value> lv = v.getArrayValue().getValuesList();
213+
for (Value iv : lv) {
214+
list.add(decodeValue(rpcContext, iv));
215+
}
216+
return list;
217+
case MAP_VALUE:
218+
Map<String, Object> outputMap = new HashMap<>();
219+
Map<String, Value> inputMap = v.getMapValue().getFieldsMap();
220+
for (Map.Entry<String, Value> entry : inputMap.entrySet()) {
221+
outputMap.put(entry.getKey(), decodeValue(rpcContext, entry.getValue()));
222+
}
223+
return outputMap;
224+
default:
225+
throw FirestoreException.invalidState(String.format("Unknown Value Type: %s", typeCase));
226+
}
227+
}
184228
}

0 commit comments

Comments
 (0)