diff --git a/google-cloud-firestore/clirr-ignored-differences.xml b/google-cloud-firestore/clirr-ignored-differences.xml index ea94bf5b0..84adffe46 100644 --- a/google-cloud-firestore/clirr-ignored-differences.xml +++ b/google-cloud-firestore/clirr-ignored-differences.xml @@ -23,4 +23,17 @@ com/google/cloud/firestore/Firestore com.google.api.core.ApiFuture runAsyncTransaction(*) + + + 7005 + com/google/cloud/firestore/* + *(com.google.cloud.firestore.FirestoreImpl, *) + *(com.google.cloud.firestore.FirestoreRpcContext, *) + + + 7009 + com/google/cloud/firestore/* + *(com.google.cloud.firestore.FirestoreImpl, *) + + diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java index acb0d9691..c2367bed1 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java @@ -19,8 +19,11 @@ import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.core.InternalExtensionOnly; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptions; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.cloud.firestore.v1.FirestoreClient.ListDocumentsPagedResponse; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; @@ -40,16 +43,17 @@ * test mocks. Subclassing is not supported in production code and new SDK releases may break code * that does so. */ +@InternalExtensionOnly public class CollectionReference extends Query { /** * Creates a CollectionReference from a complete collection path. * - * @param firestore The Firestore client. + * @param rpcContext The Firestore client. * @param collectionPath The Path of this collection. */ - protected CollectionReference(FirestoreImpl firestore, ResourcePath collectionPath) { - super(firestore, collectionPath); + CollectionReference(FirestoreRpcContext rpcContext, ResourcePath collectionPath) { + super(rpcContext, collectionPath); } /** @@ -71,7 +75,7 @@ public String getId() { @Nullable public DocumentReference getParent() { ResourcePath parent = options.getParentPath(); - return parent.isDocument() ? new DocumentReference(firestore, parent) : null; + return parent.isDocument() ? new DocumentReference(rpcContext, parent) : null; } /** @@ -109,7 +113,7 @@ public DocumentReference document(@Nonnull String childPath) { Preconditions.checkArgument( documentPath.isDocument(), String.format("Path should point to a Document Reference: %s", getPath())); - return new DocumentReference(firestore, documentPath); + return new DocumentReference(rpcContext, documentPath); } /** @@ -133,10 +137,12 @@ public Iterable listDocuments() { final ListDocumentsPagedResponse response; try { - response = - ApiExceptions.callAndTranslateApiException( - firestore.sendRequest( - request.build(), firestore.getClient().listDocumentsPagedCallable())); + FirestoreRpc client = rpcContext.getClient(); + UnaryCallable callable = + client.listDocumentsPagedCallable(); + ListDocumentsRequest build = request.build(); + ApiFuture future = rpcContext.sendRequest(build, callable); + response = ApiExceptions.callAndTranslateApiException(future); } catch (ApiException exception) { throw FirestoreException.apiException(exception); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java index 1025ffe29..e689298e0 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java @@ -19,6 +19,7 @@ import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.core.InternalExtensionOnly; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptions; import com.google.cloud.firestore.v1.FirestoreClient.ListCollectionIdsPagedResponse; @@ -42,15 +43,16 @@ * test mocks. Subclassing is not supported in production code and new SDK releases may break code * that does so. */ +@InternalExtensionOnly public class DocumentReference { private final ResourcePath path; - private final FirestoreImpl firestore; + private final FirestoreRpcContext rpcContext; - protected DocumentReference( - FirestoreImpl firestore, ResourcePath path) { // Elevated access level for mocking. + DocumentReference( + FirestoreRpcContext rpcContext, ResourcePath path) { // Elevated access level for mocking. this.path = path; - this.firestore = firestore; + this.rpcContext = rpcContext; } /* @@ -60,7 +62,7 @@ protected DocumentReference( */ @Nonnull public Firestore getFirestore() { - return firestore; + return rpcContext.getFirestore(); } /** @@ -102,7 +104,7 @@ String getName() { */ @Nonnull public CollectionReference getParent() { - return new CollectionReference(firestore, path.getParent()); + return new CollectionReference(rpcContext, path.getParent()); } /** @@ -114,7 +116,7 @@ public CollectionReference getParent() { */ @Nonnull public CollectionReference collection(@Nonnull String collectionPath) { - return new CollectionReference(firestore, path.append(collectionPath)); + return new CollectionReference(rpcContext, path.append(collectionPath)); } /** @@ -144,7 +146,7 @@ public T apply(List results) { */ @Nonnull public ApiFuture create(@Nonnull Map fields) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.create(this, fields).commit()); } @@ -157,7 +159,7 @@ public ApiFuture create(@Nonnull Map fields) { */ @Nonnull public ApiFuture create(@Nonnull Object pojo) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.create(this, pojo).commit()); } @@ -170,7 +172,7 @@ public ApiFuture create(@Nonnull Object pojo) { */ @Nonnull public ApiFuture set(@Nonnull Map fields) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.set(this, fields).commit()); } @@ -186,7 +188,7 @@ public ApiFuture set(@Nonnull Map fields) { @Nonnull public ApiFuture set( @Nonnull Map fields, @Nonnull SetOptions options) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.set(this, fields, options).commit()); } @@ -199,7 +201,7 @@ public ApiFuture set( */ @Nonnull public ApiFuture set(@Nonnull Object pojo) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.set(this, pojo).commit()); } @@ -214,7 +216,7 @@ public ApiFuture set(@Nonnull Object pojo) { */ @Nonnull public ApiFuture set(@Nonnull Object pojo, @Nonnull SetOptions options) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.set(this, pojo, options).commit()); } @@ -227,7 +229,7 @@ public ApiFuture set(@Nonnull Object pojo, @Nonnull SetOptions opti */ @Nonnull public ApiFuture update(@Nonnull Map fields) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.update(this, fields).commit()); } @@ -241,7 +243,7 @@ public ApiFuture update(@Nonnull Map fields) { */ @Nonnull public ApiFuture update(@Nonnull Map fields, Precondition options) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.update(this, fields, options).commit()); } @@ -257,7 +259,7 @@ public ApiFuture update(@Nonnull Map fields, Precon @Nonnull public ApiFuture update( @Nonnull String field, @Nullable Object value, Object... moreFieldsAndValues) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.update(this, field, value, moreFieldsAndValues).commit()); } @@ -273,7 +275,7 @@ public ApiFuture update( @Nonnull public ApiFuture update( @Nonnull FieldPath fieldPath, @Nullable Object value, Object... moreFieldsAndValues) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.update(this, fieldPath, value, moreFieldsAndValues).commit()); } @@ -293,7 +295,7 @@ public ApiFuture update( @Nonnull String field, @Nullable Object value, Object... moreFieldsAndValues) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst( writeBatch.update(this, options, field, value, moreFieldsAndValues).commit()); } @@ -314,7 +316,7 @@ public ApiFuture update( @Nonnull FieldPath fieldPath, @Nullable Object value, Object... moreFieldsAndValues) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst( writeBatch.update(this, options, fieldPath, value, moreFieldsAndValues).commit()); } @@ -327,7 +329,7 @@ public ApiFuture update( */ @Nonnull public ApiFuture delete(@Nonnull Precondition options) { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.delete(this, options).commit()); } @@ -338,7 +340,7 @@ public ApiFuture delete(@Nonnull Precondition options) { */ @Nonnull public ApiFuture delete() { - WriteBatch writeBatch = firestore.batch(); + WriteBatch writeBatch = rpcContext.getFirestore().batch(); return extractFirst(writeBatch.delete(this).commit()); } @@ -351,7 +353,7 @@ public ApiFuture delete() { */ @Nonnull public ApiFuture get() { - return extractFirst(firestore.getAll(this)); + return extractFirst(rpcContext.getFirestore().getAll(this)); } /** @@ -364,7 +366,8 @@ public ApiFuture get() { */ @Nonnull public ApiFuture get(FieldMask fieldMask) { - return extractFirst(firestore.getAll(new DocumentReference[] {this}, fieldMask)); + return extractFirst( + rpcContext.getFirestore().getAll(new DocumentReference[] {this}, fieldMask)); } /** @@ -382,8 +385,8 @@ public Iterable listCollections() { try { response = ApiExceptions.callAndTranslateApiException( - firestore.sendRequest( - request.build(), firestore.getClient().listCollectionIdsPagedCallable())); + rpcContext.sendRequest( + request.build(), rpcContext.getClient().listCollectionIdsPagedCallable())); } catch (ApiException exception) { throw FirestoreException.apiException(exception); } @@ -456,7 +459,7 @@ public void onEvent( } listener.onEvent( DocumentSnapshot.fromMissing( - firestore, DocumentReference.this, value.getReadTime()), + rpcContext, DocumentReference.this, value.getReadTime()), null); } }); @@ -471,7 +474,7 @@ public void onEvent( @Nonnull public ListenerRegistration addSnapshotListener( @Nonnull EventListener listener) { - return addSnapshotListener(firestore.getClient().getExecutor(), listener); + return addSnapshotListener(rpcContext.getClient().getExecutor(), listener); } ResourcePath getResourcePath() { @@ -498,11 +501,11 @@ public boolean equals(Object obj) { return false; } DocumentReference that = (DocumentReference) obj; - return Objects.equals(path, that.path) && Objects.equals(firestore, that.firestore); + return Objects.equals(path, that.path) && Objects.equals(rpcContext, that.rpcContext); } @Override public int hashCode() { - return Objects.hash(path, firestore); + return Objects.hash(path, rpcContext); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java index 9133797a0..900807682 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore; +import com.google.api.core.InternalExtensionOnly; import com.google.cloud.Timestamp; import com.google.cloud.firestore.UserDataConverter.EncodingOptions; import com.google.common.base.Preconditions; @@ -44,23 +45,24 @@ * test mocks. Subclassing is not supported in production code and new SDK releases may break code * that does so. */ +@InternalExtensionOnly public class DocumentSnapshot { - private final FirestoreImpl firestore; + private final FirestoreRpcContext rpcContext; private final DocumentReference docRef; @Nullable private final Map fields; @Nullable private final Timestamp readTime; @Nullable private final Timestamp updateTime; @Nullable private final Timestamp createTime; - protected DocumentSnapshot( - FirestoreImpl firestore, + DocumentSnapshot( + FirestoreRpcContext rpcContext, DocumentReference docRef, @Nullable Map fields, @Nullable Timestamp readTime, @Nullable Timestamp updateTime, @Nullable Timestamp createTime) { // Elevated access level for mocking. - this.firestore = firestore; + this.rpcContext = rpcContext; this.docRef = docRef; this.fields = fields; this.readTime = readTime; @@ -79,7 +81,7 @@ public String getId() { } static DocumentSnapshot fromObject( - FirestoreImpl firestore, + FirestoreRpcContext rpcContext, DocumentReference docRef, Map values, EncodingOptions options) { @@ -94,14 +96,14 @@ static DocumentSnapshot fromObject( fields.put(entry.getKey(), encodedValue); } } - return new DocumentSnapshot(firestore, docRef, fields, null, null, null); + return new DocumentSnapshot(rpcContext, docRef, fields, null, null, null); } static DocumentSnapshot fromDocument( - FirestoreImpl firestore, Timestamp readTime, Document document) { + FirestoreRpcContext rpcContext, Timestamp readTime, Document document) { return new DocumentSnapshot( - firestore, - new DocumentReference(firestore, ResourcePath.create(document.getName())), + rpcContext, + new DocumentReference(rpcContext, ResourcePath.create(document.getName())), document.getFieldsMap(), readTime, Timestamp.fromProto(document.getUpdateTime()), @@ -109,8 +111,8 @@ static DocumentSnapshot fromDocument( } static DocumentSnapshot fromMissing( - FirestoreImpl firestore, DocumentReference documentReference, Timestamp readTime) { - return new DocumentSnapshot(firestore, documentReference, null, readTime, null, null); + FirestoreRpcContext rpcContext, DocumentReference documentReference, Timestamp readTime) { + return new DocumentSnapshot(rpcContext, documentReference, null, readTime, null, null); } private Object decodeValue(Value v) { @@ -132,7 +134,7 @@ private Object decodeValue(Value v) { return Blob.fromByteString(v.getBytesValue()); case REFERENCE_VALUE: String pathName = v.getReferenceValue(); - return new DocumentReference(firestore, ResourcePath.create(pathName)); + return new DocumentReference(rpcContext, ResourcePath.create(pathName)); case GEO_POINT_VALUE: return new GeoPoint( v.getGeoPointValue().getLatitude(), v.getGeoPointValue().getLongitude()); @@ -312,7 +314,7 @@ public T get(@Nonnull FieldPath fieldPath, Class valueType) { private Object convertToDateIfNecessary(Object decodedValue) { if (decodedValue instanceof Timestamp) { - if (!this.firestore.areTimestampsInSnapshotsEnabled()) { + if (!this.rpcContext.areTimestampsInSnapshotsEnabled()) { decodedValue = ((Timestamp) decodedValue).toDate(); } } @@ -487,13 +489,13 @@ public boolean equals(Object obj) { return false; } DocumentSnapshot that = (DocumentSnapshot) obj; - return Objects.equals(firestore, that.firestore) + return Objects.equals(rpcContext, that.rpcContext) && Objects.equals(docRef, that.docRef) && Objects.equals(fields, that.fields); } @Override public int hashCode() { - return Objects.hash(firestore, docRef, fields); + return Objects.hash(rpcContext, docRef, fields); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java index b8b3f47a9..b850f2522 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java @@ -19,6 +19,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.StructuredQuery; import java.util.regex.Pattern; import javax.annotation.Nonnull; @@ -30,11 +31,13 @@ @AutoValue public abstract class FieldPath extends BasePath implements Comparable { + private static final String DOCUMENT_ID_SENTINEL = "__name__"; + /** * A special sentinel FieldPath to refer to the ID of a document. It can be used in queries to * sort or filter by the document ID. */ - static final FieldPath DOCUMENT_ID = FieldPath.of("__name__"); + static final FieldPath DOCUMENT_ID = FieldPath.of(DOCUMENT_ID_SENTINEL); /** Regular expression to verify that dot-separated field paths do not contain ~*[]/. */ private static final Pattern PROHIBITED_CHARACTERS = Pattern.compile(".*[~*/\\[\\]].*"); @@ -67,6 +70,11 @@ public static FieldPath documentId() { return DOCUMENT_ID; } + /** Verifies if the provided path matches the path that is used for the Document ID sentinel. */ + static boolean isDocumentId(String path) { + return DOCUMENT_ID_SENTINEL.equals(path); + } + /** Returns a field path from a dot separated string. Does not support escaping. */ static FieldPath fromDotSeparatedString(String field) { if (PROHIBITED_CHARACTERS.matcher(field).matches()) { @@ -152,4 +160,8 @@ FieldPath createPathWithSegments(ImmutableList segments) { public String toString() { return getEncodedPath(); } + + StructuredQuery.FieldReference toProto() { + return StructuredQuery.FieldReference.newBuilder().setFieldPath(getEncodedPath()).build(); + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java index 7c00b1dbd..4aee5adae 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java @@ -48,7 +48,7 @@ * Main implementation of the Firestore client. This is the entry point for all Firestore * operations. */ -class FirestoreImpl implements Firestore { +class FirestoreImpl implements Firestore, FirestoreRpcContext { private static final Random RANDOM = new SecureRandom(); private static final int AUTO_ID_LENGTH = 20; @@ -317,34 +317,40 @@ public ApiFuture runAsyncTransaction( } /** Returns whether the user has opted into receiving dates as com.google.cloud.Timestamp. */ - boolean areTimestampsInSnapshotsEnabled() { + @Override + public boolean areTimestampsInSnapshotsEnabled() { return this.firestoreOptions.areTimestampsInSnapshotsEnabled(); } /** Returns the name of the Firestore project associated with this client. */ - String getDatabaseName() { + @Override + public String getDatabaseName() { return databasePath.getDatabaseName().toString(); } /** Returns a path to the Firestore project associated with this client. */ - ResourcePath getResourcePath() { + @Override + public ResourcePath getResourcePath() { return databasePath; } /** Returns the underlying RPC client. */ - FirestoreRpc getClient() { + @Override + public FirestoreRpc getClient() { return firestoreClient; } /** Request funnel for all read/write requests. */ - ApiFuture sendRequest( + @Override + public ApiFuture sendRequest( RequestT requestT, UnaryCallable callable) { Preconditions.checkState(!closed, "Firestore client has already been closed"); return callable.futureCall(requestT); } /** Request funnel for all unidirectional streaming requests. */ - void streamRequest( + @Override + public void streamRequest( RequestT requestT, ApiStreamObserver responseObserverT, ServerStreamingCallable callable) { @@ -353,13 +359,19 @@ void streamRequest( } /** Request funnel for all bidirectional streaming requests. */ - ApiStreamObserver streamRequest( + @Override + public ApiStreamObserver streamRequest( ApiStreamObserver responseObserverT, BidiStreamingCallable callable) { Preconditions.checkState(!closed, "Firestore client has already been closed"); return callable.bidiStreamingCall(responseObserverT); } + @Override + public FirestoreImpl getFirestore() { + return this; + } + @Override public FirestoreOptions getOptions() { return firestoreOptions; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreRpcContext.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreRpcContext.java new file mode 100644 index 000000000..807f327de --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreRpcContext.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.api.core.ApiFuture; +import com.google.api.core.InternalApi; +import com.google.api.core.InternalExtensionOnly; +import com.google.api.gax.rpc.ApiStreamObserver; +import com.google.api.gax.rpc.BidiStreamingCallable; +import com.google.api.gax.rpc.ServerStreamingCallable; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.firestore.spi.v1.FirestoreRpc; + +@InternalApi +@InternalExtensionOnly +interface FirestoreRpcContext { + + FS getFirestore(); + + boolean areTimestampsInSnapshotsEnabled(); + + String getDatabaseName(); + + ResourcePath getResourcePath(); + + FirestoreRpc getClient(); + + ApiFuture sendRequest( + RequestT requestT, UnaryCallable callable); + + void streamRequest( + RequestT requestT, + ApiStreamObserver responseObserverT, + ServerStreamingCallable callable); + + ApiStreamObserver streamRequest( + ApiStreamObserver responseObserverT, + BidiStreamingCallable callable); +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 8f1f0f82d..9d17ad00d 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -27,6 +27,7 @@ import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL; import com.google.api.core.ApiFuture; +import com.google.api.core.InternalExtensionOnly; import com.google.api.core.SettableApiFuture; import com.google.api.gax.rpc.ApiStreamObserver; import com.google.auto.value.AutoValue; @@ -63,9 +64,10 @@ * A Query which you can read or listen to. You can also construct refined Query objects by adding * filters and ordering. */ +@InternalExtensionOnly public class Query { - final FirestoreImpl firestore; + final FirestoreRpcContext rpcContext; final QueryOptions options; /** The direction of a sort. */ @@ -85,23 +87,25 @@ StructuredQuery.Direction getDirection() { } abstract static class FieldFilter { - final FieldPath fieldPath; - final Object value; + protected final FieldReference fieldReference; - FieldFilter(FieldPath fieldPath, Object value) { - this.value = value; - this.fieldPath = fieldPath; + FieldFilter(FieldReference fieldReference) { + this.fieldReference = fieldReference; } - Value encodeValue() { - Object sanitizedObject = CustomClassMapper.serialize(value); - Value encodedValue = - UserDataConverter.encodeValue(fieldPath, sanitizedObject, UserDataConverter.ARGUMENT); + static FieldFilter fromProto(StructuredQuery.Filter filter) { + Preconditions.checkArgument( + !filter.hasCompositeFilter(), "Cannot deserialize nested composite filters"); - if (encodedValue == null) { - throw FirestoreException.invalidState("Cannot use Firestore Sentinels in FieldFilter"); + if (filter.hasFieldFilter()) { + return new ComparisonFilter( + filter.getFieldFilter().getField(), + filter.getFieldFilter().getOp(), + filter.getFieldFilter().getValue()); + } else { + Preconditions.checkState(filter.hasUnaryFilter(), "Expected unary of field filter"); + return new UnaryFilter(filter.getUnaryFilter().getField(), filter.getUnaryFilter().getOp()); } - return encodedValue; } abstract boolean isInequalityFilter(); @@ -110,10 +114,12 @@ Value encodeValue() { } private static class UnaryFilter extends FieldFilter { - UnaryFilter(FieldPath fieldPath, Object value) { - super(fieldPath, value); - Preconditions.checkArgument( - isUnaryComparison(value), "Cannot use '%s' in unary comparison", value); + + private final StructuredQuery.UnaryFilter.Operator operator; + + UnaryFilter(FieldReference fieldReference, StructuredQuery.UnaryFilter.Operator operator) { + super(fieldReference); + this.operator = operator; } @Override @@ -123,27 +129,32 @@ boolean isInequalityFilter() { Filter toProto() { Filter.Builder result = Filter.newBuilder(); - - result - .getUnaryFilterBuilder() - .setField(FieldReference.newBuilder().setFieldPath(fieldPath.getEncodedPath())) - .setOp( - value == null - ? StructuredQuery.UnaryFilter.Operator.IS_NULL - : StructuredQuery.UnaryFilter.Operator.IS_NAN); - + result.getUnaryFilterBuilder().setField(fieldReference).setOp(operator); return result.build(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof UnaryFilter)) { + return false; + } + UnaryFilter other = (UnaryFilter) o; + return Objects.equals(fieldReference, other.fieldReference) + && Objects.equals(operator, other.operator); + } } - private static class ComparisonFilter extends FieldFilter { + static class ComparisonFilter extends FieldFilter { final StructuredQuery.FieldFilter.Operator operator; + final Value value; ComparisonFilter( - FieldPath fieldPath, StructuredQuery.FieldFilter.Operator operator, Object value) { - super(fieldPath, value); - Preconditions.checkArgument( - !isUnaryComparison(value), "Cannot use '%s' in field comparison", value); + FieldReference fieldReference, StructuredQuery.FieldFilter.Operator operator, Value value) { + super(fieldReference); + this.value = value; this.operator = operator; } @@ -157,33 +168,51 @@ boolean isInequalityFilter() { Filter toProto() { Filter.Builder result = Filter.newBuilder(); - - Value encodedValue = encodeValue(); - - result - .getFieldFilterBuilder() - .setField(FieldReference.newBuilder().setFieldPath(fieldPath.getEncodedPath())) - .setValue(encodedValue) - .setOp(operator); + result.getFieldFilterBuilder().setField(fieldReference).setValue(value).setOp(operator); return result.build(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ComparisonFilter)) { + return false; + } + ComparisonFilter other = (ComparisonFilter) o; + return Objects.equals(fieldReference, other.fieldReference) + && Objects.equals(operator, other.operator) + && Objects.equals(value, other.value); + } } static final class FieldOrder { - final FieldPath fieldPath; - final Direction direction; + private final FieldReference fieldReference; + private final Direction direction; - FieldOrder(FieldPath fieldPath, Direction direction) { - this.fieldPath = fieldPath; + FieldOrder(FieldReference fieldReference, Direction direction) { + this.fieldReference = fieldReference; this.direction = direction; } Order toProto() { Order.Builder result = Order.newBuilder(); - result.setField(FieldReference.newBuilder().setFieldPath(fieldPath.getEncodedPath())); + result.setField(fieldReference); result.setDirection(direction.getDirection()); return result.build(); } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldOrder)) { + return false; + } + FieldOrder filter = (FieldOrder) o; + return Objects.equals(toProto(), filter.toProto()); + } } /** Denotes whether a provided limit is applied to the beginning or the end of the result set. */ @@ -258,9 +287,9 @@ abstract static class Builder { } /** Creates a query for documents in a single collection */ - Query(FirestoreImpl firestore, ResourcePath path) { + Query(FirestoreRpcContext rpcContext, ResourcePath path) { this( - firestore, + rpcContext, QueryOptions.builder() .setParentPath(path.getParent()) .setCollectionId(path.getId()) @@ -271,18 +300,18 @@ abstract static class Builder { * Creates a Collection Group query that matches all documents directly nested under a * specifically named collection */ - Query(FirestoreImpl firestore, String collectionId) { + Query(FirestoreRpcContext rpcContext, String collectionId) { this( - firestore, + rpcContext, QueryOptions.builder() - .setParentPath(firestore.getResourcePath()) + .setParentPath(rpcContext.getResourcePath()) .setCollectionId(collectionId) .setAllDescendants(true) .build()); } - private Query(FirestoreImpl firestore, QueryOptions queryOptions) { - this.firestore = firestore; + private Query(FirestoreRpcContext rpcContext, QueryOptions queryOptions) { + this.rpcContext = rpcContext; this.options = queryOptions; } @@ -293,7 +322,7 @@ private Query(FirestoreImpl firestore, QueryOptions queryOptions) { */ @Nonnull public Firestore getFirestore() { - return firestore; + return rpcContext.getFirestore(); } /** Checks whether the provided object is NULL or NaN. */ @@ -310,13 +339,13 @@ private ImmutableList createImplicitOrderBy() { // If no explicit ordering is specified, use the first inequality to define an implicit order. for (FieldFilter fieldFilter : options.getFieldFilters()) { if (fieldFilter.isInequalityFilter()) { - implicitOrders.add(new FieldOrder(fieldFilter.fieldPath, Direction.ASCENDING)); + implicitOrders.add(new FieldOrder(fieldFilter.fieldReference, Direction.ASCENDING)); break; } } } else { for (FieldOrder fieldOrder : options.getFieldOrders()) { - if (fieldOrder.fieldPath.equals(FieldPath.DOCUMENT_ID)) { + if (FieldPath.isDocumentId(fieldOrder.fieldReference.getFieldPath())) { hasDocumentId = true; } } @@ -329,7 +358,7 @@ private ImmutableList createImplicitOrderBy() { ? Direction.ASCENDING : implicitOrders.get(implicitOrders.size() - 1).direction; - implicitOrders.add(new FieldOrder(FieldPath.documentId(), lastDirection)); + implicitOrders.add(new FieldOrder(FieldPath.documentId().toProto(), lastDirection)); } return ImmutableList.builder().addAll(implicitOrders).build(); @@ -340,13 +369,16 @@ private Cursor createCursor( List fieldValues = new ArrayList<>(); for (FieldOrder fieldOrder : order) { - if (fieldOrder.fieldPath.equals(FieldPath.DOCUMENT_ID)) { + String path = fieldOrder.fieldReference.getFieldPath(); + if (FieldPath.isDocumentId(path)) { fieldValues.add(documentSnapshot.getReference()); } else { + FieldPath fieldPath = FieldPath.fromDotSeparatedString(path); Preconditions.checkArgument( - documentSnapshot.contains(fieldOrder.fieldPath), - "Field '%s' is missing in the provided DocumentSnapshot. Please provide a document that contains values for all specified orderBy() and where() constraints."); - fieldValues.add(documentSnapshot.get(fieldOrder.fieldPath)); + documentSnapshot.contains(fieldPath), + "Field '%s' is missing in the provided DocumentSnapshot. Please provide a document " + + "that contains values for all specified orderBy() and where() constraints."); + fieldValues.add(documentSnapshot.get(fieldPath)); } } @@ -369,16 +401,15 @@ private Cursor createCursor(List 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 ListRuntime 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); + } }