diff --git a/CHANGELOG.md b/CHANGELOG.md index a715849844..969ca4280b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [3.7.0](https://github.com/googleapis/java-firestore/compare/v3.6.0...v3.7.0) (2022-10-22) + + +### Features + +* Add retry logic to COUNT queries ([#1062](https://github.com/googleapis/java-firestore/issues/1062)) ([5789e4b](https://github.com/googleapis/java-firestore/commit/5789e4b57f38e8cab82bc720b28578e86d82a2cc)) + + +### Bug Fixes + +* Fix limitToLast queries with cursors ([#1072](https://github.com/googleapis/java-firestore/issues/1072)) ([e9e1548](https://github.com/googleapis/java-firestore/commit/e9e15489112d49a7f8716300ec9af504fcfb5786)) + + +### Dependencies + +* Update beam.version to v2.42.0 ([#1066](https://github.com/googleapis/java-firestore/issues/1066)) ([03de01a](https://github.com/googleapis/java-firestore/commit/03de01ab58ab3d312bef63c54b202b8c911cef54)) +* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.0.5 ([#1071](https://github.com/googleapis/java-firestore/issues/1071)) ([4c3f122](https://github.com/googleapis/java-firestore/commit/4c3f12286424ce7601a106b4338bc89b67ad51fb)) +* Update dependency org.graalvm.buildtools:junit-platform-native to v0.9.16 ([#1064](https://github.com/googleapis/java-firestore/issues/1064)) ([a7ddc9e](https://github.com/googleapis/java-firestore/commit/a7ddc9e7fe708bd07595973c56fb2e392917915d)) +* Update dependency org.graalvm.buildtools:native-maven-plugin to v0.9.15 ([#1065](https://github.com/googleapis/java-firestore/issues/1065)) ([d9e7803](https://github.com/googleapis/java-firestore/commit/d9e7803d257b0edfae8648b925e040c7a6fcdb60)) +* Update dependency org.graalvm.buildtools:native-maven-plugin to v0.9.16 ([#1069](https://github.com/googleapis/java-firestore/issues/1069)) ([97ef50a](https://github.com/googleapis/java-firestore/commit/97ef50a518c5b207b28e0a81022cc96ed4e5e5ae)) + ## [3.6.0](https://github.com/googleapis/java-firestore/compare/v3.5.0...v3.6.0) (2022-10-04) diff --git a/README.md b/README.md index 2100049813..dd32567249 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 26.1.2 + 26.1.3 pom import @@ -41,7 +41,7 @@ If you are using Maven without BOM, add this to your dependencies: com.google.cloud google-cloud-firestore - 3.5.0 + 3.6.0 ``` @@ -49,20 +49,20 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.1.2') +implementation platform('com.google.cloud:libraries-bom:26.1.3') implementation 'com.google.cloud:google-cloud-firestore' ``` If you are using Gradle without BOM, add this to your dependencies ```Groovy -implementation 'com.google.cloud:google-cloud-firestore:3.5.0' +implementation 'com.google.cloud:google-cloud-firestore:3.6.0' ``` If you are using SBT, add this to your dependencies ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-firestore" % "3.5.0" +libraryDependencies += "com.google.cloud" % "google-cloud-firestore" % "3.6.0" ``` ## Authentication diff --git a/google-cloud-firestore-admin/pom.xml b/google-cloud-firestore-admin/pom.xml index 7c1a439b4d..88fe08627f 100644 --- a/google-cloud-firestore-admin/pom.xml +++ b/google-cloud-firestore-admin/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 google-cloud-firestore-admin - 3.6.0 + 3.7.0 jar Google Cloud Firestore Admin Client https://github.com/googleapis/java-firestore @@ -14,7 +14,7 @@ com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 diff --git a/google-cloud-firestore-bom/pom.xml b/google-cloud-firestore-bom/pom.xml index fb84ba783b..6bc84deec8 100644 --- a/google-cloud-firestore-bom/pom.xml +++ b/google-cloud-firestore-bom/pom.xml @@ -3,12 +3,12 @@ 4.0.0 com.google.cloud google-cloud-firestore-bom - 3.6.0 + 3.7.0 pom com.google.cloud google-cloud-shared-config - 1.5.3 + 1.5.4 Google Cloud Firestore BOM @@ -52,37 +52,37 @@ com.google.cloud google-cloud-firestore - 3.6.0 + 3.7.0 com.google.cloud google-cloud-firestore-admin - 3.6.0 + 3.7.0 com.google.api.grpc grpc-google-cloud-firestore-admin-v1 - 3.6.0 + 3.7.0 com.google.api.grpc grpc-google-cloud-firestore-v1 - 3.6.0 + 3.7.0 com.google.api.grpc proto-google-cloud-firestore-admin-v1 - 3.6.0 + 3.7.0 com.google.api.grpc proto-google-cloud-firestore-v1 - 3.6.0 + 3.7.0 com.google.cloud proto-google-cloud-firestore-bundle-v1 - 3.6.0 + 3.7.0 diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index d265167a46..7b03425e97 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-firestore - 3.6.0 + 3.7.0 jar Google Cloud Firestore https://github.com/googleapis/java-firestore @@ -12,7 +12,7 @@ com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 google-cloud-firestore diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java index be2bd33aa6..e99dce925b 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java @@ -21,14 +21,17 @@ import com.google.api.core.SettableApiFuture; import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.StreamController; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.v1.FirestoreSettings; import com.google.firestore.v1.RunAggregationQueryRequest; import com.google.firestore.v1.RunAggregationQueryResponse; import com.google.firestore.v1.RunQueryRequest; import com.google.firestore.v1.StructuredAggregationQuery; import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -68,25 +71,68 @@ public ApiFuture get() { @Nonnull ApiFuture get(@Nullable final ByteString transactionId) { - RunAggregationQueryRequest request = toProto(transactionId); - AggregateQueryResponseObserver responseObserver = new AggregateQueryResponseObserver(); + AggregateQueryResponseDeliverer responseDeliverer = + new AggregateQueryResponseDeliverer( + transactionId, /* startTimeNanos= */ query.rpcContext.getClock().nanoTime()); + runQuery(responseDeliverer); + return responseDeliverer.getFuture(); + } + + private void runQuery(AggregateQueryResponseDeliverer responseDeliverer) { + RunAggregationQueryRequest request = toProto(responseDeliverer.getTransactionId()); + AggregateQueryResponseObserver responseObserver = + new AggregateQueryResponseObserver(responseDeliverer); ServerStreamingCallable callable = query.rpcContext.getClient().runAggregationQueryCallable(); - query.rpcContext.streamRequest(request, responseObserver, callable); + } + + private final class AggregateQueryResponseDeliverer { + + @Nullable private final ByteString transactionId; + private final long startTimeNanos; + private final SettableApiFuture future = SettableApiFuture.create(); + private final AtomicBoolean isFutureCompleted = new AtomicBoolean(false); + + AggregateQueryResponseDeliverer(@Nullable ByteString transactionId, long startTimeNanos) { + this.transactionId = transactionId; + this.startTimeNanos = startTimeNanos; + } + + ApiFuture getFuture() { + return future; + } + + @Nullable + ByteString getTransactionId() { + return transactionId; + } + + long getStartTimeNanos() { + return startTimeNanos; + } + + void deliverResult(long count, Timestamp readTime) { + if (isFutureCompleted.compareAndSet(false, true)) { + future.set(new AggregateQuerySnapshot(AggregateQuery.this, readTime, count)); + } + } - return responseObserver.getFuture(); + void deliverError(Throwable throwable) { + if (isFutureCompleted.compareAndSet(false, true)) { + future.setException(throwable); + } + } } private final class AggregateQueryResponseObserver implements ResponseObserver { - private final SettableApiFuture future = SettableApiFuture.create(); - private final AtomicBoolean isFutureNotified = new AtomicBoolean(false); + private final AggregateQueryResponseDeliverer responseDeliverer; private StreamController streamController; - SettableApiFuture getFuture() { - return future; + AggregateQueryResponseObserver(AggregateQueryResponseDeliverer responseDeliverer) { + this.responseDeliverer = responseDeliverer; } @Override @@ -96,14 +142,10 @@ public void onStart(StreamController streamController) { @Override public void onResponse(RunAggregationQueryResponse response) { - // Ignore subsequent response messages. The RunAggregationQuery RPC returns a stream of - // responses (rather than just a single response); however, only the first response of the - // stream is actually used. Any more responses are technically errors, but since the Future - // will have already been notified, we just drop any unexpected responses. - if (!isFutureNotified.compareAndSet(false, true)) { - return; - } + // Close the stream to avoid it dangling, since we're not expecting any more responses. + streamController.cancel(); + // Extract the count and read time from the RunAggregationQueryResponse. Timestamp readTime = Timestamp.fromProto(response.getReadTime()); Value value = response.getResult().getAggregateFieldsMap().get(ALIAS_COUNT); if (value == null) { @@ -118,19 +160,30 @@ public void onResponse(RunAggregationQueryResponse response) { } long count = value.getIntegerValue(); - future.set(new AggregateQuerySnapshot(AggregateQuery.this, readTime, count)); - - // Close the stream to avoid it dangling, since we're not expecting any more responses. - streamController.cancel(); + // Deliver the result; even though the `RunAggregationQuery` RPC is a "streaming" RPC, meaning + // that `onResponse()` can be called multiple times, it _should_ only be called once for count + // queries. But even if it is called more than once, `responseDeliverer` will drop superfluous + // results. + responseDeliverer.deliverResult(count, readTime); } @Override public void onError(Throwable throwable) { - if (!isFutureNotified.compareAndSet(false, true)) { - return; + if (shouldRetry(throwable)) { + runQuery(responseDeliverer); + } else { + responseDeliverer.deliverError(throwable); } + } - future.setException(throwable); + private boolean shouldRetry(Throwable throwable) { + Set retryableCodes = + FirestoreSettings.newBuilder().runAggregationQuerySettings().getRetryableCodes(); + return query.shouldRetryQuery( + throwable, + responseDeliverer.getTransactionId(), + responseDeliverer.getStartTimeNanos(), + retryableCodes); } @Override 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 88afff02d4..410e6b9f4b 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 @@ -1289,10 +1289,12 @@ public Query endAt(@Nonnull DocumentSnapshot snapshot) { StructuredQuery.Builder buildQuery() { StructuredQuery.Builder structuredQuery = buildWithoutClientTranslation(); if (options.getLimitType().equals(LimitType.Last)) { - // Apply client translation for limitToLast. + structuredQuery.clearOrderBy(); + structuredQuery.clearStartAt(); + structuredQuery.clearEndAt(); + // Apply client translation for limitToLast. if (!options.getFieldOrders().isEmpty()) { - structuredQuery.clearOrderBy(); for (FieldOrder order : options.getFieldOrders()) { // Flip the orderBy directions since we want the last results order = @@ -1306,7 +1308,6 @@ StructuredQuery.Builder buildQuery() { } if (options.getStartCursor() != null) { - structuredQuery.clearEndAt(); // Swap the cursors to match the flipped query ordering. Cursor cursor = options @@ -1318,7 +1319,6 @@ StructuredQuery.Builder buildQuery() { } if (options.getEndCursor() != null) { - structuredQuery.clearStartAt(); // Swap the cursors to match the flipped query ordering. Cursor cursor = options @@ -1672,27 +1672,15 @@ public void onComplete() { } boolean shouldRetry(DocumentSnapshot lastDocument, Throwable t) { - if (transactionId != null) { - // Transactional queries are retried via the transaction runner. - return false; - } - if (lastDocument == null) { // Only retry if we have received a single result. Retries for RPCs with initial // failure are handled by Google Gax, which also implements backoff. return false; } - if (!isRetryableError(t)) { - return false; - } - - if (rpcContext.getTotalRequestTimeout().isZero()) { - return true; - } - - Duration duration = Duration.ofNanos(rpcContext.getClock().nanoTime() - startTimeNanos); - return duration.compareTo(rpcContext.getTotalRequestTimeout()) < 0; + Set retryableCodes = + FirestoreSettings.newBuilder().runQuerySettings().getRetryableCodes(); + return shouldRetryQuery(t, transactionId, startTimeNanos, retryableCodes); } }; @@ -1831,14 +1819,12 @@ private ImmutableList append(ImmutableList existingList, T newElement) } /** Verifies whether the given exception is retryable based on the RunQuery configuration. */ - private boolean isRetryableError(Throwable throwable) { + private boolean isRetryableError(Throwable throwable, Set retryableCodes) { if (!(throwable instanceof FirestoreException)) { return false; } - Set codes = - FirestoreSettings.newBuilder().runQuerySettings().getRetryableCodes(); Status status = ((FirestoreException) throwable).getStatus(); - for (StatusCode.Code code : codes) { + for (StatusCode.Code code : retryableCodes) { if (code.equals(StatusCode.Code.valueOf(status.getCode().name()))) { return true; } @@ -1846,6 +1832,29 @@ private boolean isRetryableError(Throwable throwable) { return false; } + /** Returns whether a query that failed in the given scenario should be retried. */ + boolean shouldRetryQuery( + Throwable throwable, + @Nullable ByteString transactionId, + long startTimeNanos, + Set retryableCodes) { + if (transactionId != null) { + // Transactional queries are retried via the transaction runner. + return false; + } + + if (!isRetryableError(throwable, retryableCodes)) { + return false; + } + + if (rpcContext.getTotalRequestTimeout().isZero()) { + return true; + } + + Duration duration = Duration.ofNanos(rpcContext.getClock().nanoTime() - startTimeNanos); + return duration.compareTo(rpcContext.getTotalRequestTimeout()) < 0; + } + /** * Returns a query that counts the documents in the result set of this query. * diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java new file mode 100644 index 0000000000..d7c345f8fe --- /dev/null +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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 static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import com.google.cloud.firestore.spi.v1.FirestoreRpc; +import org.junit.Test; + +public class FirestoreOptionsTest { + + @Test + public void instantiateWithProjectId() { + FirestoreOptions firestoreOptions = + FirestoreOptions.newBuilder().setProjectId("test-project").build(); + FirestoreImpl firestore = new FirestoreImpl(firestoreOptions, mock(FirestoreRpc.class)); + + DocumentReference document = firestore.document("coll/doc1"); + assertThat(document.getName()) + .isEqualTo("projects/test-project/databases/(default)/documents/coll/doc1"); + } + + @Test + public void instantiateWithProjectIdAndDatabaseId() { + FirestoreOptions firestoreOptions = + FirestoreOptions.newBuilder() + .setProjectId("test-project") + .setDatabaseId("test-database") + .build(); + FirestoreImpl firestore = new FirestoreImpl(firestoreOptions, mock(FirestoreRpc.class)); + + DocumentReference document = firestore.document("coll/doc1"); + assertThat(document.getName()) + .isEqualTo("projects/test-project/databases/test-database/documents/coll/doc1"); + } +} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryCountTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryCountTest.java index 5a50680970..ea9dad975f 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryCountTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryCountTest.java @@ -30,7 +30,9 @@ import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import com.google.api.core.ApiClock; import com.google.api.core.ApiFuture; import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; @@ -39,14 +41,15 @@ import com.google.firestore.v1.RunAggregationQueryRequest; import com.google.firestore.v1.RunAggregationQueryResponse; import com.google.firestore.v1.StructuredQuery; +import io.grpc.Status; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Matchers; -import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.runners.MockitoJUnitRunner; import org.threeten.bp.Duration; @@ -58,7 +61,7 @@ public class QueryCountTest { private final FirestoreImpl firestoreMock = new FirestoreImpl( FirestoreOptions.newBuilder().setProjectId("test-project").build(), - Mockito.mock(FirestoreRpc.class)); + mock(FirestoreRpc.class)); @Captor private ArgumentCaptor runAggregationQuery; @@ -211,6 +214,117 @@ public void aggregateQuerySnapshotGetQueryShouldReturnCorrectValue() throws Exce AggregateQuery aggregateQuery = query.count(); AggregateQuerySnapshot snapshot = aggregateQuery.get().get(); + assertThat(snapshot.getQuery()).isSameInstanceAs(aggregateQuery); } + + @Test + public void shouldNotRetryIfExceptionIsNotFirestoreException() { + doAnswer(aggregationQueryResponse(new NotFirestoreException())) + .doAnswer(aggregationQueryResponse()) + .when(firestoreMock) + .streamRequest( + runAggregationQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + ApiFuture future = query.count().get(); + + assertThrows(ExecutionException.class, future::get); + } + + @Test + public void shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatus() throws Exception { + doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL))) + .doAnswer(aggregationQueryResponse(42)) + .when(firestoreMock) + .streamRequest( + runAggregationQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + ApiFuture future = query.count().get(); + AggregateQuerySnapshot snapshot = future.get(); + + assertThat(snapshot.getCount()).isEqualTo(42); + } + + @Test + public void shouldNotRetryIfExceptionIsFirestoreExceptionWithNonRetryableStatus() { + doReturn(Duration.ZERO).when(firestoreMock).getTotalRequestTimeout(); + doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INVALID_ARGUMENT))) + .doAnswer(aggregationQueryResponse()) + .when(firestoreMock) + .streamRequest( + runAggregationQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + ApiFuture future = query.count().get(); + + assertThrows(ExecutionException.class, future::get); + } + + @Test + public void + shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatusWithInfiniteTimeoutWindow() + throws Exception { + doReturn(Duration.ZERO).when(firestoreMock).getTotalRequestTimeout(); + doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL))) + .doAnswer(aggregationQueryResponse(42)) + .when(firestoreMock) + .streamRequest( + runAggregationQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + ApiFuture future = query.count().get(); + AggregateQuerySnapshot snapshot = future.get(); + + assertThat(snapshot.getCount()).isEqualTo(42); + } + + @Test + public void shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatusWithinTimeoutWindow() + throws Exception { + doReturn(Duration.ofDays(999)).when(firestoreMock).getTotalRequestTimeout(); + doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL))) + .doAnswer(aggregationQueryResponse(42)) + .when(firestoreMock) + .streamRequest( + runAggregationQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + ApiFuture future = query.count().get(); + AggregateQuerySnapshot snapshot = future.get(); + + assertThat(snapshot.getCount()).isEqualTo(42); + } + + @Test + public void + shouldNotRetryIfExceptionIsFirestoreExceptionWithRetryableStatusBeyondTimeoutWindow() { + ApiClock clockMock = mock(ApiClock.class); + doReturn(clockMock).when(firestoreMock).getClock(); + doReturn(TimeUnit.SECONDS.toNanos(10)) + .doReturn(TimeUnit.SECONDS.toNanos(20)) + .doReturn(TimeUnit.SECONDS.toNanos(30)) + .when(clockMock) + .nanoTime(); + doReturn(Duration.ofSeconds(5)).when(firestoreMock).getTotalRequestTimeout(); + doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL))) + .doAnswer(aggregationQueryResponse(42)) + .when(firestoreMock) + .streamRequest( + runAggregationQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + ApiFuture future = query.count().get(); + + assertThrows(ExecutionException.class, future::get); + } + + private static final class NotFirestoreException extends Exception {} } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryCountTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryCountTest.java index 2b22c55624..e4529b9572 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryCountTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryCountTest.java @@ -139,6 +139,15 @@ public void countShouldRespectStartAtAndEndAt() throws Exception { assertThat(snapshot.getCount()).isEqualTo(6); } + @Test + public void countQueriesShouldFailIfCollectionNameIsInvalid() { + CollectionReference collection = createEmptyCollection().document().collection("__invalid__"); + ApiFuture future = collection.count().get(); + ExecutionException executionException = assertThrows(ExecutionException.class, future::get); + assertThat(executionException).hasCauseThat().hasMessageThat().contains("__invalid__"); + assertThat(executionException).hasCauseThat().hasMessageThat().contains("INVALID_ARGUMENT"); + } + @Test public void countShouldReturnNumberOfDocumentsForCollectionGroups() throws Exception { CollectionGroup collectionGroup = createCollectionGroupWithDocuments(13); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java index 1511714b1d..07b20c2205 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java @@ -17,6 +17,7 @@ package com.google.cloud.firestore.it; import static com.google.cloud.firestore.LocalFirestoreHelper.map; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static java.util.Arrays.asList; @@ -26,6 +27,7 @@ import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.DocumentChange; import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.DocumentSnapshot; import com.google.cloud.firestore.EventListener; import com.google.cloud.firestore.Firestore; import com.google.cloud.firestore.FirestoreException; @@ -39,6 +41,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Joiner.MapJoiner; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; import com.google.common.truth.Truth; import java.util.ArrayList; @@ -381,6 +384,112 @@ public void limitToLast() throws Exception { listenerAssertions.addedIdsIsAnyOf(emptyList(), asList("doc2", "doc3")); } + /** + * Verifies that QuerySnapshot for limitToLast() queries work with startAt when the full limit is + * used in the result set. + */ + @Test + public void limitToLastWithStartAtFullLimit() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").startAt(5).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc7", "doc8", "doc9"); + } + + /** + * Verifies that QuerySnapshot for limitToLast() queries work with startAt when the partial limit + * is used in the result set. + */ + @Test + public void limitToLastWithStartAtPartialLimit() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").startAt(8).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc8", "doc9"); + } + + /** + * Verifies that QuerySnapshot for limitToLast() queries work with startAfter when the full limit + * is used in the result set. + */ + @Test + public void limitToLastWithStartAfterFullLimit() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").startAfter(5).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc7", "doc8", "doc9"); + } + + /** + * Verifies that QuerySnapshot for limitToLast() queries work with startAfter when the partial + * limit is used in the result set. + */ + @Test + public void limitToLastWithStartAfterPartialLimit() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").startAfter(7).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc8", "doc9"); + } + + /** Verifies that QuerySnapshot for limitToLast() queries work with endAt. */ + @Test + public void limitToLastWithEndAt() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").endAt(5).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc3", "doc4", "doc5"); + } + + /** Verifies that QuerySnapshot for limitToLast() queries work with endBefore. */ + @Test + public void limitToLastWithEndBefore() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").endBefore(5).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc2", "doc3", "doc4"); + } + + /** + * Verifies that QuerySnapshot for limitToLast() queries work with both startAt and endAt when the + * full limit is used in the result set. + */ + @Test + public void limitToLastWithStartAtAndEndAtFullLimit() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").startAt(3).endAt(6).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc4", "doc5", "doc6"); + } + + /** + * Verifies that QuerySnapshot for limitToLast() queries work with both startAt and endAt when the + * partial limit is used in the result set. + */ + @Test + public void limitToLastWithStartAtAndEndAtPartialLimit() throws Exception { + for (int i = 0; i < 10; i++) { + setDocument("doc" + i, Collections.singletonMap("counter", i)); + } + Query query = randomColl.orderBy("counter").startAt(5).endAt(6).limitToLast(3); + assertQueryResultContainsDocsInOrder(query, "doc5", "doc6"); + } + + private static void assertQueryResultContainsDocsInOrder(Query query, String... docIds) + throws ExecutionException, InterruptedException { + QuerySnapshot snapshot = query.get().get(); + ImmutableList actualDocIds = + snapshot.getDocuments().stream().map(DocumentSnapshot::getId).collect(toImmutableList()); + assertThat(actualDocIds).containsExactlyElementsIn(docIds).inOrder(); + } + @Test public void shutdownNowTerminatesActiveListener() throws Exception { Query query = randomColl.whereEqualTo("foo", "bar"); diff --git a/grpc-google-cloud-firestore-admin-v1/pom.xml b/grpc-google-cloud-firestore-admin-v1/pom.xml index f65e88d047..a0325e5634 100644 --- a/grpc-google-cloud-firestore-admin-v1/pom.xml +++ b/grpc-google-cloud-firestore-admin-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-firestore-admin-v1 - 3.6.0 + 3.7.0 grpc-google-cloud-firestore-admin-v1 GRPC library for grpc-google-cloud-firestore-admin-v1 com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 diff --git a/grpc-google-cloud-firestore-v1/pom.xml b/grpc-google-cloud-firestore-v1/pom.xml index db65401b2d..3906c1ba1f 100644 --- a/grpc-google-cloud-firestore-v1/pom.xml +++ b/grpc-google-cloud-firestore-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-firestore-v1 - 3.6.0 + 3.7.0 grpc-google-cloud-firestore-v1 GRPC library for grpc-google-cloud-firestore-v1 com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 diff --git a/pom.xml b/pom.xml index 67d9bbf205..6acb1e0967 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-firestore-parent pom - 3.6.0 + 3.7.0 Google Cloud Firestore Parent https://github.com/googleapis/java-firestore @@ -14,7 +14,7 @@ com.google.cloud google-cloud-shared-config - 1.5.3 + 1.5.4 @@ -150,38 +150,38 @@ com.google.api.grpc proto-google-cloud-firestore-admin-v1 - 3.6.0 + 3.7.0 com.google.cloud google-cloud-firestore - 3.6.0 + 3.7.0 com.google.cloud proto-google-cloud-firestore-bundle-v1 - 3.6.0 + 3.7.0 com.google.api.grpc proto-google-cloud-firestore-v1 - 3.6.0 + 3.7.0 com.google.api.grpc grpc-google-cloud-firestore-admin-v1 - 3.6.0 + 3.7.0 com.google.api.grpc grpc-google-cloud-firestore-v1 - 3.6.0 + 3.7.0 com.google.cloud google-cloud-shared-dependencies - 3.0.4 + 3.0.5 pom import diff --git a/proto-google-cloud-firestore-admin-v1/pom.xml b/proto-google-cloud-firestore-admin-v1/pom.xml index 6d0fbc5399..3e6a2d7ad2 100644 --- a/proto-google-cloud-firestore-admin-v1/pom.xml +++ b/proto-google-cloud-firestore-admin-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-firestore-admin-v1 - 3.6.0 + 3.7.0 proto-google-cloud-firestore-admin-v1 PROTO library for proto-google-cloud-firestore-admin-v1 com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 diff --git a/proto-google-cloud-firestore-bundle-v1/pom.xml b/proto-google-cloud-firestore-bundle-v1/pom.xml index 7e1313d600..61ad25dda4 100644 --- a/proto-google-cloud-firestore-bundle-v1/pom.xml +++ b/proto-google-cloud-firestore-bundle-v1/pom.xml @@ -5,14 +5,14 @@ 4.0.0 proto-google-cloud-firestore-bundle-v1 - 3.6.0 + 3.7.0 proto-google-cloud-firestore-bundle-v1 PROTO library for proto-google-cloud-firestore-bundle-v1 com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 diff --git a/proto-google-cloud-firestore-v1/pom.xml b/proto-google-cloud-firestore-v1/pom.xml index fc86af13b1..f281b14eed 100644 --- a/proto-google-cloud-firestore-v1/pom.xml +++ b/proto-google-cloud-firestore-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-firestore-v1 - 3.6.0 + 3.7.0 proto-google-cloud-firestore-v1 PROTO library for proto-google-cloud-firestore-v1 com.google.cloud google-cloud-firestore-parent - 3.6.0 + 3.7.0 diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 95e664ad2a..a526f6edb8 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -20,7 +20,7 @@ 1.8 1.8 - 2.41.0 + 2.42.0 UTF-8 @@ -30,7 +30,7 @@ com.google.cloud google-cloud-firestore - 3.5.0 + 3.6.0 diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml index d294cde395..2bf110bda6 100644 --- a/samples/native-image-sample/pom.xml +++ b/samples/native-image-sample/pom.xml @@ -23,7 +23,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> 1.8 1.8 - 2.41.0 + 2.42.0 UTF-8 @@ -32,7 +32,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> com.google.cloud libraries-bom - 26.1.2 + 26.1.3 pom import @@ -102,7 +102,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> org.graalvm.buildtools junit-platform-native - 0.9.14 + 0.9.16 test @@ -123,7 +123,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> org.graalvm.buildtools native-maven-plugin - 0.9.14 + 0.9.16 true com.example.firestore.NativeImageFirestoreSample diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 69f5f36de2..9af1425c96 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 2.41.0 + 2.42.0 @@ -29,7 +29,7 @@ com.google.cloud google-cloud-firestore - 3.5.0 + 3.6.0 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index c9ac6aa16a..7f9987a141 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -21,7 +21,7 @@ 1.8 1.8 UTF-8 - 2.41.0 + 2.42.0 @@ -34,7 +34,7 @@ com.google.cloud libraries-bom - 26.1.2 + 26.1.3 pom import diff --git a/samples/snippets/src/test/java/com/example/firestore/beam/ExampleFirestoreWriteReadTest.java b/samples/snippets/src/test/java/com/example/firestore/beam/ExampleFirestoreWriteReadTest.java index bd94fcf817..04d5a5662d 100644 --- a/samples/snippets/src/test/java/com/example/firestore/beam/ExampleFirestoreWriteReadTest.java +++ b/samples/snippets/src/test/java/com/example/firestore/beam/ExampleFirestoreWriteReadTest.java @@ -55,7 +55,9 @@ public void setUpStream() { @AfterClass public static void tearDown() { - deleteCollection(firestore.collection(collectionId), 1); + if (firestore != null && collectionId != null) { + deleteCollection(firestore.collection(collectionId), 1); + } } @Test diff --git a/versions.txt b/versions.txt index 0a728f0562..5bd29b9a9c 100644 --- a/versions.txt +++ b/versions.txt @@ -1,11 +1,11 @@ # Format: # module:released-version:current-version -google-cloud-firestore:3.6.0:3.6.0 -google-cloud-firestore-admin:3.6.0:3.6.0 -google-cloud-firestore-bom:3.6.0:3.6.0 -grpc-google-cloud-firestore-admin-v1:3.6.0:3.6.0 -grpc-google-cloud-firestore-v1:3.6.0:3.6.0 -proto-google-cloud-firestore-admin-v1:3.6.0:3.6.0 -proto-google-cloud-firestore-v1:3.6.0:3.6.0 -proto-google-cloud-firestore-bundle-v1:3.6.0:3.6.0 +google-cloud-firestore:3.7.0:3.7.0 +google-cloud-firestore-admin:3.7.0:3.7.0 +google-cloud-firestore-bom:3.7.0:3.7.0 +grpc-google-cloud-firestore-admin-v1:3.7.0:3.7.0 +grpc-google-cloud-firestore-v1:3.7.0:3.7.0 +proto-google-cloud-firestore-admin-v1:3.7.0:3.7.0 +proto-google-cloud-firestore-v1:3.7.0:3.7.0 +proto-google-cloud-firestore-bundle-v1:3.7.0:3.7.0