Skip to content

Commit 830d5cc

Browse files
Allowing DocumentReferences for Query cursors
2 parents 1471ac8 + 5315364 commit 830d5cc

File tree

6 files changed

+127
-28
lines changed

6 files changed

+127
-28
lines changed

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ B append(BasePath<B> path) {
7373
return createPathWithSegments(components.build());
7474
}
7575

76+
/**
77+
* Checks to see if this path is a prefix of (or equals) another path.
78+
*
79+
* @param path the path to check against
80+
* @return true if current path is a prefix of the other path.
81+
*/
82+
boolean isPrefixOf(B path) {
83+
ImmutableList<String> prefixSegments = getSegments();
84+
ImmutableList<String> childSegments = path.getSegments();
85+
if (prefixSegments.size() > path.getSegments().size()) {
86+
return false;
87+
}
88+
for (int i = 0; i < prefixSegments.size(); i++) {
89+
if (!prefixSegments.get(i).equals(childSegments.get(i))) {
90+
return false;
91+
}
92+
}
93+
return true;
94+
}
95+
7696
abstract String[] splitChildPath(String path);
7797

7898
abstract B createPathWithSegments(ImmutableList<String> segments);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ public void remove() {
381381
};
382382
}
383383

384+
ResourcePath getResourcePath() {
385+
return path;
386+
}
387+
384388
@Override
385389
public String toString() {
386390
return String.format("DocumentReference{path=%s}", path);

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

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,6 @@ public class Query {
5555
final FirestoreImpl firestore;
5656
final QueryOptions options;
5757

58-
@Override
59-
public boolean equals(Object o) {
60-
if (this == o) {
61-
return true;
62-
}
63-
if (o == null || getClass() != o.getClass()) {
64-
return false;
65-
}
66-
Query query = (Query) o;
67-
return Objects.equals(path, query.path)
68-
&& Objects.equals(firestore, query.firestore)
69-
&& Objects.equals(options, query.options);
70-
}
71-
72-
@Override
73-
public int hashCode() {
74-
return Objects.hash(path, firestore, options);
75-
}
76-
7758
/** The direction of a sort. */
7859
public enum Direction {
7960
ASCENDING(StructuredQuery.Direction.ASCENDING),
@@ -254,9 +235,31 @@ private Cursor createCursor(Object[] fieldValues, boolean before) {
254235
.getField()
255236
.getFieldPath()
256237
.equals(FieldPath.DOCUMENT_ID.getEncodedPath())) {
257-
Preconditions.checkArgument(
258-
fieldValues[i] instanceof String, "Argument at index %d must be a String", i);
259-
sanitizedValue = new DocumentReference(firestore, path.append((String) fieldValues[i]));
238+
DocumentReference cursorDocument;
239+
if (fieldValues[i] instanceof String) {
240+
cursorDocument = new DocumentReference(firestore, path.append((String) fieldValues[i]));
241+
} else if (fieldValues[i] instanceof DocumentReference) {
242+
cursorDocument = (DocumentReference) fieldValues[i];
243+
} else {
244+
throw new IllegalArgumentException(
245+
"The corresponding value for FieldPath.documentId() must be a String or a "
246+
+ "DocumentReference.");
247+
}
248+
249+
if (!this.path.isPrefixOf(cursorDocument.getResourcePath())) {
250+
throw new IllegalArgumentException(
251+
String.format(
252+
"'%s' is not part of the query result set and cannot be used as a query boundary.",
253+
cursorDocument.getPath()));
254+
}
255+
if (!cursorDocument.getParent().getResourcePath().equals(this.path)) {
256+
throw new IllegalArgumentException(
257+
String.format(
258+
"Only a direct child can be used as a query boundary. Found: '%s'",
259+
cursorDocument.getPath()));
260+
}
261+
262+
sanitizedValue = cursorDocument;
260263
} else {
261264
sanitizedValue = CustomClassMapper.serialize(fieldValues[i]);
262265
}
@@ -770,4 +773,27 @@ public void onCompleted() {
770773

771774
return result;
772775
}
776+
777+
ResourcePath getResourcePath() {
778+
return path;
779+
}
780+
781+
@Override
782+
public boolean equals(Object o) {
783+
if (this == o) {
784+
return true;
785+
}
786+
if (o == null || getClass() != o.getClass()) {
787+
return false;
788+
}
789+
Query query = (Query) o;
790+
return Objects.equals(path, query.path)
791+
&& Objects.equals(firestore, query.firestore)
792+
&& Objects.equals(options, query.options);
793+
}
794+
795+
@Override
796+
public int hashCode() {
797+
return Objects.hash(path, firestore, options);
798+
}
773799
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
import javax.annotation.Nullable;
3737

3838
/**
39-
* Abstract class that collects and bundles all write operations for {@link Transaction} and
40-
* {@link WriteBatch}.
39+
* Abstract class that collects and bundles all write operations for {@link Transaction} and {@link
40+
* WriteBatch}.
4141
*/
4242
abstract class UpdateBuilder<T extends UpdateBuilder> {
4343

google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,14 +410,22 @@ public static StructuredQuery select(FieldPath fieldPath) {
410410
}
411411

412412
public static StructuredQuery startAt(boolean before) {
413+
return startAt(string(), before);
414+
}
415+
416+
public static StructuredQuery startAt(Value value, boolean before) {
413417
StructuredQuery.Builder structuredQuery = StructuredQuery.newBuilder();
414-
structuredQuery.setStartAt(Cursor.newBuilder().setBefore(before).addValues(string()));
418+
structuredQuery.setStartAt(Cursor.newBuilder().setBefore(before).addValues(value));
415419
return structuredQuery.build();
416420
}
417421

418422
public static StructuredQuery endAt(boolean before) {
423+
return endAt(string(), before);
424+
}
425+
426+
public static StructuredQuery endAt(Value value, boolean before) {
419427
StructuredQuery.Builder structuredQuery = StructuredQuery.newBuilder();
420-
structuredQuery.setEndAt(Cursor.newBuilder().setBefore(before).addValues(string()));
428+
structuredQuery.setEndAt(Cursor.newBuilder().setBefore(before).addValues(value));
421429
return structuredQuery.build();
422430
}
423431

google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.google.cloud.firestore.spi.v1beta1.FirestoreRpc;
3737
import com.google.firestore.v1beta1.RunQueryRequest;
3838
import com.google.firestore.v1beta1.StructuredQuery;
39+
import com.google.firestore.v1beta1.Value;
3940
import java.util.Arrays;
4041
import java.util.Iterator;
4142
import java.util.concurrent.Semaphore;
@@ -255,14 +256,54 @@ public void withStartAt() throws Exception {
255256
streamObserverCapture.capture(),
256257
Matchers.<ServerStreamingCallable>any());
257258

258-
query.orderBy("foo").startAt("bar").get().get();
259+
query.orderBy("foo").orderBy(FieldPath.documentId()).startAt("bar", "foo").get().get();
260+
261+
Value documentBoundary =
262+
Value.newBuilder().setReferenceValue(query.getResourcePath().toString() + "/foo").build();
259263

260264
RunQueryRequest queryRequest =
261-
query(order("foo", StructuredQuery.Direction.ASCENDING), startAt(true));
265+
query(
266+
order("foo", StructuredQuery.Direction.ASCENDING),
267+
order("__name__", StructuredQuery.Direction.ASCENDING),
268+
startAt(true),
269+
startAt(documentBoundary, true));
262270

263271
assertEquals(queryRequest, runQuery.getValue());
264272
}
265273

274+
@Test
275+
public void withInvalidStartAt() throws Exception {
276+
try {
277+
query.orderBy(FieldPath.documentId()).startAt(42).get();
278+
fail();
279+
} catch (IllegalArgumentException e) {
280+
assertEquals(
281+
"The corresponding value for FieldPath.documentId() must be a String or a "
282+
+ "DocumentReference.",
283+
e.getMessage());
284+
}
285+
286+
try {
287+
query.orderBy(FieldPath.documentId()).startAt("coll/doc/coll").get();
288+
fail();
289+
} catch (IllegalArgumentException e) {
290+
assertEquals(
291+
"Only a direct child can be used as a query boundary. Found: "
292+
+ "'projects/test-project/databases/(default)/documents/coll/coll/doc/coll'",
293+
e.getMessage());
294+
}
295+
296+
try {
297+
query.orderBy(FieldPath.documentId()).startAt(firestoreMock.document("foo/bar")).get();
298+
fail();
299+
} catch (IllegalArgumentException e) {
300+
assertEquals(
301+
"'projects/test-project/databases/(default)/documents/foo/bar' is not part of "
302+
+ "the query result set and cannot be used as a query boundary.",
303+
e.getMessage());
304+
}
305+
}
306+
266307
@Test
267308
public void withStartAfter() throws Exception {
268309
doAnswer(queryResponse())

0 commit comments

Comments
 (0)