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