order, Object[] fieldValues, boolea
for (Object fieldValue : fieldValues) {
Object sanitizedValue;
- FieldPath fieldPath = fieldOrderIterator.next().fieldPath;
+ FieldReference fieldReference = fieldOrderIterator.next().fieldReference;
- if (fieldPath.equals(FieldPath.DOCUMENT_ID)) {
+ if (FieldPath.isDocumentId(fieldReference.getFieldPath())) {
sanitizedValue = convertReference(fieldValue);
} else {
sanitizedValue = CustomClassMapper.serialize(fieldValue);
}
- Value encodedValue =
- UserDataConverter.encodeValue(fieldPath, sanitizedValue, UserDataConverter.ARGUMENT);
+ Value encodedValue = encodeValue(fieldReference, sanitizedValue);
if (encodedValue == null) {
throw FirestoreException.invalidState(
@@ -405,7 +436,7 @@ private Object convertReference(Object fieldValue) {
DocumentReference reference;
if (fieldValue instanceof String) {
- reference = new DocumentReference(firestore, basePath.append((String) fieldValue));
+ reference = new DocumentReference(rpcContext, basePath.append((String) fieldValue));
} else if (fieldValue instanceof DocumentReference) {
reference = (DocumentReference) fieldValue;
} else {
@@ -463,9 +494,13 @@ public Query whereEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value)
if (isUnaryComparison(value)) {
Builder newOptions = options.toBuilder();
- UnaryFilter newFieldFilter = new UnaryFilter(fieldPath, value);
+ StructuredQuery.UnaryFilter.Operator op =
+ value == null
+ ? StructuredQuery.UnaryFilter.Operator.IS_NULL
+ : StructuredQuery.UnaryFilter.Operator.IS_NAN;
+ UnaryFilter newFieldFilter = new UnaryFilter(fieldPath.toProto(), op);
newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter));
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
} else {
return whereHelper(fieldPath, EQUAL, value);
}
@@ -713,6 +748,11 @@ public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull List extends Objec
private Query whereHelper(
FieldPath fieldPath, StructuredQuery.FieldFilter.Operator operator, Object value) {
+ Preconditions.checkArgument(
+ !isUnaryComparison(value),
+ "Cannot use '%s' in field comparison. Use an equality filter instead.",
+ value);
+
if (fieldPath.equals(FieldPath.DOCUMENT_ID)) {
if (operator == ARRAY_CONTAINS || operator == ARRAY_CONTAINS_ANY) {
throw new IllegalArgumentException(
@@ -738,9 +778,10 @@ private Query whereHelper(
}
Builder newOptions = options.toBuilder();
- ComparisonFilter newFieldFilter = new ComparisonFilter(fieldPath, operator, value);
+ ComparisonFilter newFieldFilter =
+ new ComparisonFilter(fieldPath.toProto(), operator, encodeValue(fieldPath, value));
newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter));
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -794,10 +835,10 @@ public Query orderBy(@Nonnull FieldPath fieldPath, @Nonnull Direction direction)
+ "startAfter(), endBefore() or endAt().");
Builder newOptions = options.toBuilder();
- FieldOrder newFieldOrder = new FieldOrder(fieldPath, direction);
+ FieldOrder newFieldOrder = new FieldOrder(fieldPath.toProto(), direction);
newOptions.setFieldOrders(append(options.getFieldOrders(), newFieldOrder));
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -809,7 +850,7 @@ public Query orderBy(@Nonnull FieldPath fieldPath, @Nonnull Direction direction)
@Nonnull
public Query limit(int limit) {
return new Query(
- firestore, options.toBuilder().setLimit(limit).setLimitType(LimitType.First).build());
+ rpcContext, options.toBuilder().setLimit(limit).setLimitType(LimitType.First).build());
}
/**
@@ -827,7 +868,7 @@ public Query limit(int limit) {
@Nonnull
public Query limitToLast(int limit) {
return new Query(
- firestore, options.toBuilder().setLimit(limit).setLimitType(LimitType.Last).build());
+ rpcContext, options.toBuilder().setLimit(limit).setLimitType(LimitType.Last).build());
}
/**
@@ -838,7 +879,7 @@ public Query limitToLast(int limit) {
*/
@Nonnull
public Query offset(int offset) {
- return new Query(firestore, options.toBuilder().setOffset(offset).build());
+ return new Query(rpcContext, options.toBuilder().setOffset(offset).build());
}
/**
@@ -857,7 +898,7 @@ public Query startAt(@Nonnull DocumentSnapshot snapshot) {
Builder newOptions = options.toBuilder();
newOptions.setFieldOrders(fieldOrders);
newOptions.setStartCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -873,7 +914,7 @@ public Query startAt(Object... fieldValues) {
Builder newOptions = options.toBuilder();
newOptions.setStartCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -918,7 +959,7 @@ public Query select(FieldPath... fieldPaths) {
}
Builder newOptions = options.toBuilder().setFieldProjections(fieldProjections.build());
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -937,7 +978,7 @@ public Query startAfter(@Nonnull DocumentSnapshot snapshot) {
Builder newOptions = options.toBuilder();
newOptions.setFieldOrders(fieldOrders);
newOptions.setStartCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -954,7 +995,7 @@ public Query startAfter(Object... fieldValues) {
Builder newOptions = options.toBuilder();
newOptions.setStartCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -973,7 +1014,7 @@ public Query endBefore(@Nonnull DocumentSnapshot snapshot) {
Builder newOptions = options.toBuilder();
newOptions.setFieldOrders(fieldOrders);
newOptions.setEndCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -990,7 +1031,7 @@ public Query endBefore(Object... fieldValues) {
Builder newOptions = options.toBuilder();
newOptions.setEndCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -1006,7 +1047,7 @@ public Query endAt(Object... fieldValues) {
Builder newOptions = options.toBuilder();
newOptions.setEndCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/**
@@ -1025,7 +1066,7 @@ public Query endAt(@Nonnull DocumentSnapshot snapshot) {
Builder newOptions = options.toBuilder();
newOptions.setFieldOrders(fieldOrders);
newOptions.setEndCursor(cursor);
- return new Query(firestore, newOptions.build());
+ return new Query(rpcContext, newOptions.build());
}
/** Build the final Firestore query. */
@@ -1067,7 +1108,7 @@ StructuredQuery.Builder buildQuery() {
// Flip the orderBy directions since we want the last results
order =
new FieldOrder(
- order.fieldPath,
+ order.fieldReference,
order.direction.equals(Direction.ASCENDING)
? Direction.DESCENDING
: Direction.ASCENDING);
@@ -1162,6 +1203,120 @@ public void onCompleted() {
null);
}
+ /**
+ * Returns the {@link RunQueryRequest} that this Query instance represents. The request contains
+ * the serialized form of all Query constraints.
+ *
+ * Runtime metadata (as required for `limitToLast()` queries) is not serialized and as such,
+ * the serialized request will return the results in the original backend order.
+ *
+ * @return the serialized RunQueryRequest
+ */
+ public RunQueryRequest toProto() {
+ RunQueryRequest.Builder request = RunQueryRequest.newBuilder();
+ request.setStructuredQuery(buildQuery()).setParent(options.getParentPath().toString());
+ return request.build();
+ }
+
+ /**
+ * Returns a Query instance that can be used to execute the provided {@link RunQueryRequest}.
+ *
+ *
Only RunQueryRequests that pertain to the same project as the Firestore instance can be
+ * deserialized.
+ *
+ *
Runtime metadata (as required for `limitToLast()` queries) is not restored and as such, the
+ * results for limitToLast() queries will be returned in the original backend order.
+ *
+ * @param firestore a Firestore instance to apply the query to
+ * @param proto the serialized RunQueryRequest
+ * @return a Query instance that can be used to execute the RunQueryRequest
+ */
+ public static Query fromProto(Firestore firestore, RunQueryRequest proto) {
+ Preconditions.checkState(
+ FirestoreRpcContext.class.isAssignableFrom(firestore.getClass()),
+ "The firestore instance passed to this method must also implement FirestoreRpcContext.");
+ return fromProto((FirestoreRpcContext>) firestore, proto);
+ }
+
+ private static Query fromProto(FirestoreRpcContext> rpcContext, RunQueryRequest proto) {
+ QueryOptions.Builder queryOptions = QueryOptions.builder();
+ StructuredQuery structuredQuery = proto.getStructuredQuery();
+
+ ResourcePath parentPath = ResourcePath.create(proto.getParent());
+ if (!rpcContext.getDatabaseName().equals(parentPath.getDatabaseName().toString())) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Cannot deserialize query from different Firestore project (\"%s\" vs \"%s\")",
+ rpcContext.getDatabaseName(), parentPath.getDatabaseName()));
+ }
+ queryOptions.setParentPath(parentPath);
+
+ Preconditions.checkArgument(
+ structuredQuery.getFromCount() == 1,
+ "Can only deserialize query with exactly one collection selector.");
+ queryOptions.setCollectionId(structuredQuery.getFrom(0).getCollectionId());
+ queryOptions.setAllDescendants(structuredQuery.getFrom(0).getAllDescendants());
+
+ if (structuredQuery.hasWhere()) {
+ Filter where = structuredQuery.getWhere();
+ if (where.hasCompositeFilter()) {
+ CompositeFilter compositeFilter = where.getCompositeFilter();
+ ImmutableList.Builder fieldFilters = ImmutableList.builder();
+ for (Filter filter : compositeFilter.getFiltersList()) {
+ fieldFilters.add(FieldFilter.fromProto(filter));
+ }
+ queryOptions.setFieldFilters(fieldFilters.build());
+ } else {
+ queryOptions.setFieldFilters(ImmutableList.of(FieldFilter.fromProto(where)));
+ }
+ }
+
+ ImmutableList.Builder fieldOrders = ImmutableList.builder();
+ for (Order order : structuredQuery.getOrderByList()) {
+ fieldOrders.add(
+ new FieldOrder(order.getField(), Direction.valueOf(order.getDirection().name())));
+ }
+ queryOptions.setFieldOrders(fieldOrders.build());
+
+ if (structuredQuery.hasLimit()) {
+ queryOptions.setLimit(structuredQuery.getLimit().getValue());
+ }
+
+ if (structuredQuery.getOffset() != 0) {
+ queryOptions.setOffset(structuredQuery.getOffset());
+ }
+
+ if (structuredQuery.hasSelect()) {
+ queryOptions.setFieldProjections(
+ ImmutableList.copyOf(structuredQuery.getSelect().getFieldsList()));
+ }
+
+ if (structuredQuery.hasStartAt()) {
+ queryOptions.setStartCursor(structuredQuery.getStartAt());
+ }
+
+ if (structuredQuery.hasEndAt()) {
+ queryOptions.setEndCursor(structuredQuery.getEndAt());
+ }
+
+ return new Query(rpcContext, queryOptions.build());
+ }
+
+ private Value encodeValue(FieldReference fieldReference, Object value) {
+ return encodeValue(FieldPath.fromDotSeparatedString(fieldReference.getFieldPath()), value);
+ }
+
+ private Value encodeValue(FieldPath fieldPath, Object value) {
+ Object sanitizedObject = CustomClassMapper.serialize(value);
+ Value encodedValue =
+ UserDataConverter.encodeValue(fieldPath, sanitizedObject, UserDataConverter.ARGUMENT);
+ if (encodedValue == null) {
+ throw FirestoreException.invalidState(
+ "Cannot use Firestore sentinels in FieldFilter or cursors");
+ }
+ return encodedValue;
+ }
+
/** Stream observer that captures DocumentSnapshots as well as the Query read time. */
private abstract static class QuerySnapshotObserver
implements ApiStreamObserver {
@@ -1216,7 +1371,7 @@ public void onNext(RunQueryResponse response) {
Document document = response.getDocument();
QueryDocumentSnapshot documentSnapshot =
QueryDocumentSnapshot.fromDocument(
- firestore, Timestamp.fromProto(response.getReadTime()), document);
+ rpcContext, Timestamp.fromProto(response.getReadTime()), document);
documentObserver.onNext(documentSnapshot);
}
@@ -1243,7 +1398,7 @@ public void onCompleted() {
}
};
- firestore.streamRequest(request.build(), observer, firestore.getClient().runQueryCallable());
+ rpcContext.streamRequest(request.build(), observer, rpcContext.getClient().runQueryCallable());
}
/**
@@ -1264,7 +1419,7 @@ public ApiFuture get() {
*/
@Nonnull
public ListenerRegistration addSnapshotListener(@Nonnull EventListener listener) {
- return addSnapshotListener(firestore.getClient().getExecutor(), listener);
+ return addSnapshotListener(rpcContext.getClient().getExecutor(), listener);
}
/**
@@ -1327,23 +1482,25 @@ public int compare(QueryDocumentSnapshot doc1, QueryDocumentSnapshot doc2) {
: fieldOrders.get(fieldOrders.size() - 1).direction;
List orderBys = new ArrayList<>(fieldOrders);
- orderBys.add(new FieldOrder(FieldPath.DOCUMENT_ID, lastDirection));
+ orderBys.add(new FieldOrder(FieldPath.DOCUMENT_ID.toProto(), lastDirection));
for (FieldOrder orderBy : orderBys) {
int comp;
- if (orderBy.fieldPath.equals(FieldPath.documentId())) {
+ String path = orderBy.fieldReference.getFieldPath();
+ if (FieldPath.isDocumentId(path)) {
comp =
doc1.getReference()
.getResourcePath()
.compareTo(doc2.getReference().getResourcePath());
} else {
+ FieldPath fieldPath = FieldPath.fromDotSeparatedString(path);
Preconditions.checkState(
- doc1.contains(orderBy.fieldPath) && doc2.contains(orderBy.fieldPath),
+ doc1.contains(fieldPath) && doc2.contains(fieldPath),
"Can only compare fields that exist in the DocumentSnapshot."
+ " Please include the fields you are ordering on in your select() call.");
- Value v1 = doc1.extractField(orderBy.fieldPath);
- Value v2 = doc2.extractField(orderBy.fieldPath);
+ Value v1 = doc1.extractField(fieldPath);
+ Value v2 = doc2.extractField(fieldPath);
comp = com.google.cloud.firestore.Order.INSTANCE.compare(v1, v2);
}
@@ -1385,11 +1542,11 @@ public boolean equals(Object obj) {
return false;
}
Query query = (Query) obj;
- return Objects.equals(firestore, query.firestore) && Objects.equals(options, query.options);
+ return Objects.equals(rpcContext, query.rpcContext) && Objects.equals(options, query.options);
}
@Override
public int hashCode() {
- return Objects.hash(firestore, options);
+ return Objects.hash(rpcContext, options);
}
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QueryDocumentSnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QueryDocumentSnapshot.java
index 899e5f160..c0575586d 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QueryDocumentSnapshot.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QueryDocumentSnapshot.java
@@ -16,6 +16,7 @@
package com.google.cloud.firestore;
+import com.google.api.core.InternalExtensionOnly;
import com.google.cloud.Timestamp;
import com.google.common.base.Preconditions;
import com.google.firestore.v1.Document;
@@ -36,22 +37,23 @@
* test mocks. Subclassing is not supported in production code and new SDK releases may break code
* that does so.
*/
+@InternalExtensionOnly
public class QueryDocumentSnapshot extends DocumentSnapshot {
- protected QueryDocumentSnapshot(
- FirestoreImpl firestore,
+ QueryDocumentSnapshot(
+ FirestoreRpcContext> rpcContext,
DocumentReference docRef,
Map fields,
Timestamp readTime,
Timestamp updateTime,
Timestamp createTime) { // Elevated access level for mocking.
- super(firestore, docRef, fields, readTime, updateTime, createTime);
+ super(rpcContext, docRef, fields, readTime, updateTime, createTime);
}
static QueryDocumentSnapshot fromDocument(
- FirestoreImpl firestore, Timestamp readTime, Document document) {
+ FirestoreRpcContext> rpcContext, Timestamp readTime, Document document) {
return new QueryDocumentSnapshot(
- firestore,
- new DocumentReference(firestore, ResourcePath.create(document.getName())),
+ rpcContext,
+ new DocumentReference(rpcContext, ResourcePath.create(document.getName())),
document.getFieldsMap(),
readTime,
Timestamp.fromProto(document.getUpdateTime()),
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ResourcePath.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ResourcePath.java
index bc96e89bd..7f305475f 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ResourcePath.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ResourcePath.java
@@ -55,7 +55,7 @@ static ResourcePath create(DatabaseRootName databaseName) {
static ResourcePath create(String resourceName) {
String[] parts = resourceName.split("/");
- if (parts.length >= 6 && parts[0].equals("projects") && parts[2].equals("databases")) {
+ if (parts.length >= 5 && parts[0].equals("projects") && parts[2].equals("databases")) {
String[] path = Arrays.copyOfRange(parts, 5, parts.length);
return create(
DatabaseRootName.of(parts[1], parts[3]),
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java
index a3315ca6d..c71297a65 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java
@@ -33,19 +33,27 @@
import static com.google.cloud.firestore.LocalFirestoreHelper.unaryFilter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doAnswer;
import com.google.api.gax.rpc.ApiStreamObserver;
import com.google.api.gax.rpc.ServerStreamingCallable;
import com.google.cloud.Timestamp;
+import com.google.cloud.firestore.Query.ComparisonFilter;
+import com.google.cloud.firestore.Query.FieldFilter;
import com.google.cloud.firestore.spi.v1.FirestoreRpc;
+import com.google.common.io.BaseEncoding;
import com.google.firestore.v1.ArrayValue;
import com.google.firestore.v1.RunQueryRequest;
import com.google.firestore.v1.StructuredQuery;
import com.google.firestore.v1.StructuredQuery.Direction;
import com.google.firestore.v1.StructuredQuery.FieldFilter.Operator;
import com.google.firestore.v1.Value;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
@@ -835,4 +843,118 @@ public void equalsTest() {
assertEquals(query.limit(42).offset(1337), query.offset(1337).limit(42));
assertEquals(query.limit(42).offset(1337).hashCode(), query.offset(1337).limit(42).hashCode());
}
+
+ @Test
+ public void serializationTest() {
+ assertSerialization(query);
+ query = query.whereEqualTo("a", null);
+ assertSerialization(query);
+ query = query.whereEqualTo("b", Double.NaN);
+ assertSerialization(query);
+ query = query.whereGreaterThan("c", 1);
+ assertSerialization(query);
+ query = query.whereGreaterThanOrEqualTo(FieldPath.of("d", ".e."), 2);
+ assertSerialization(query);
+ query = query.whereLessThan("f", 3);
+ assertSerialization(query);
+ query = query.whereLessThanOrEqualTo(FieldPath.of("g", ".h."), 4);
+ assertSerialization(query);
+ query = query.whereIn("i", Collections.singletonList(5));
+ assertSerialization(query);
+ query = query.whereArrayContains("j", Collections.singletonList(6));
+ assertSerialization(query);
+ query = query.whereArrayContainsAny("k", Collections.singletonList(7));
+ assertSerialization(query);
+ query = query.orderBy("l");
+ assertSerialization(query);
+ query = query.orderBy(FieldPath.of("m", ".n."), Query.Direction.DESCENDING);
+ assertSerialization(query);
+ query = query.startAt("o");
+ assertSerialization(query);
+ query = query.startAfter("p");
+ assertSerialization(query);
+ query = query.endBefore("q");
+ assertSerialization(query);
+ query = query.endAt("r");
+ assertSerialization(query);
+ query = query.limit(8);
+ assertSerialization(query);
+ query = query.offset(9);
+ assertSerialization(query);
+ }
+
+ private void assertSerialization(Query query) {
+ RunQueryRequest runQueryRequest = query.toProto();
+ Query deserializedQuery = Query.fromProto(firestoreMock, runQueryRequest);
+ assertEquals(runQueryRequest, deserializedQuery.toProto());
+ assertEquals(deserializedQuery, query);
+ }
+
+ @Test
+ public void serializationVerifiesDatabaseName() {
+ RunQueryRequest runQueryRequest = query.toProto();
+ runQueryRequest =
+ runQueryRequest.toBuilder().setParent("projects/foo/databases/(default)/documents").build();
+
+ try {
+ Query.fromProto(firestoreMock, runQueryRequest);
+ fail("Expected serializtion error");
+ } catch (IllegalArgumentException e) {
+ assertEquals(
+ "Cannot deserialize query from different Firestore project "
+ + "(\"projects/test-project/databases/(default)\" vs "
+ + "\"projects/foo/databases/(default)\")",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void ensureFromProtoWorksWithAProxy() throws InvalidProtocolBufferException {
+ Object o =
+ Proxy.newProxyInstance(
+ QueryTest.class.getClassLoader(),
+ new Class[] {Firestore.class, FirestoreRpcContext.class},
+ new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ // use the reflection lookup of the method name so intellij will refactor it along
+ // with the method name if it ever happens.
+ Method getDatabaseNameMethod =
+ FirestoreRpcContext.class.getDeclaredMethod("getDatabaseName");
+ if (method.equals(getDatabaseNameMethod)) {
+ return "projects/test-project/databases/(default)";
+ } else {
+ return null;
+ }
+ }
+ });
+
+ assertTrue(o instanceof Firestore);
+ assertTrue(o instanceof FirestoreRpcContext);
+
+ // Code used to generate the below base64 encoded RunQueryRequest
+ // RunQueryRequest proto = firestoreMock.collection("testing-collection")
+ // .whereEqualTo("enabled", true).toProto();
+ // String base64String = BaseEncoding.base64().encode(proto.toByteArray());
+ String base64Proto =
+ "CjNwcm9qZWN0cy90ZXN0LXByb2plY3QvZGF0YWJhc2VzLyhkZWZhdWx0KS9kb2N1bWVudHMSKxIUEhJ0ZXN0aW5nLWNvbGxlY3Rpb24aExIRCgkSB2VuYWJsZWQQBRoCCAE=";
+
+ byte[] bytes = BaseEncoding.base64().decode(base64Proto);
+ RunQueryRequest runQueryRequest = RunQueryRequest.parseFrom(bytes);
+
+ Query query = Query.fromProto((Firestore) o, runQueryRequest);
+ ResourcePath path = query.options.getParentPath();
+ assertEquals("projects/test-project/databases/(default)/documents", path.getName());
+ assertEquals("testing-collection", query.options.getCollectionId());
+ FieldFilter next = query.options.getFieldFilters().iterator().next();
+ assertEquals("enabled", next.fieldReference.getFieldPath());
+
+ if (next instanceof ComparisonFilter) {
+ ComparisonFilter comparisonFilter = (ComparisonFilter) next;
+ assertFalse(comparisonFilter.isInequalityFilter());
+ assertEquals(Value.newBuilder().setBooleanValue(true).build(), comparisonFilter.value);
+ } else {
+ fail("expect filter to be a comparison filter");
+ }
+ }
}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java
index e3b54526c..6ac8d3d1d 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java
@@ -58,6 +58,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.firestore.v1.RunQueryRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -1260,4 +1261,12 @@ public void onCompleted() {
assertEquals(ref3.getId(), documentSnapshots.get(2).getId());
assertEquals(3, documentSnapshots.size());
}
+
+ @Test
+ public void testInstanceReturnedByGetServiceCanBeUsedToDeserializeAQuery() throws Exception {
+ Firestore fs = FirestoreOptions.getDefaultInstance().getService();
+ RunQueryRequest proto = fs.collection("coll").whereEqualTo("bob", "alice").toProto();
+ fs.close();
+ Query.fromProto(fs, proto);
+ }
}