data) {
this.query = query;
this.readTime = readTime;
- this.count = count;
+ this.data = data;
}
/** Returns the query that was executed to produce this result. */
@@ -49,7 +55,99 @@ public Timestamp getReadTime() {
/** Returns the number of documents in the result set of the underlying query. */
public long getCount() {
- return count;
+ AggregateField countField = AggregateField.count();
+ Object value = get(countField);
+ if (value == null) {
+ throw new IllegalArgumentException(
+ "RunAggregationQueryResponse alias " + countField.getAlias() + " is null");
+ } else if (!(value instanceof Long)) {
+ throw new IllegalArgumentException(
+ "RunAggregationQueryResponse alias "
+ + countField.getAlias()
+ + " has incorrect type: "
+ + value.getClass().getName());
+ }
+ return (Long) value;
+ }
+
+ /** Returns the number of documents in the result set of the underlying query. */
+ @SuppressWarnings({"unused"})
+ public long get(@Nonnull AggregateField.CountAggregateField unused) {
+ return getCount();
+ }
+
+ /**
+ * Returns the result of the given aggregation from the server without coercion of data types.
+ * Throws java.lang.RuntimeException if the `aggregateField` was not requested when calling
+ * `query.aggregate(...)`.
+ *
+ * @param aggregateField The aggregation for which the value is requested.
+ * @return The result of the given aggregation.
+ */
+ @Nullable
+ public Object get(@Nonnull AggregateField aggregateField) {
+ if (!data.containsKey(aggregateField.getAlias())) {
+ throw new IllegalArgumentException(
+ "'"
+ + aggregateField.getOperator()
+ + "("
+ + aggregateField.getFieldPath()
+ + ")"
+ + "' was not requested in the aggregation query.");
+ }
+ Value value = data.get(aggregateField.getAlias());
+ if (value.hasNullValue()) {
+ return null;
+ } else if (value.hasDoubleValue()) {
+ return value.getDoubleValue();
+ } else if (value.hasIntegerValue()) {
+ return value.getIntegerValue();
+ } else {
+ throw new IllegalStateException("Found aggregation result that is not an integer nor double");
+ }
+ }
+
+ /**
+ * Returns the result of the given average aggregation. Since the result of an average aggregation
+ * performed by the server is always a double, this convenience overload can be used in lieu of
+ * the above `get` method. Throws java.lang.RuntimeException if the `aggregateField` was not
+ * requested when calling `query.aggregate(...)`.
+ *
+ * @param averageAggregateField The average aggregation for which the value is requested.
+ * @return The result of the given average aggregation.
+ */
+ @Nullable
+ public Double get(@Nonnull AggregateField.AverageAggregateField averageAggregateField) {
+ return (Double) get((AggregateField) averageAggregateField);
+ }
+
+ /**
+ * Returns the result of the given aggregation as a double. Coerces all numeric values and throws
+ * a RuntimeException if the result of the aggregate is non-numeric. In the case of coercion of
+ * long to double, uses java.lang.Long.doubleValue to perform the conversion, and may result in a
+ * loss of precision.
+ *
+ * @param aggregateField The aggregation for which the value is requested.
+ * @return The result of the given average aggregation as a double.
+ */
+ @Nullable
+ public Double getDouble(@Nonnull AggregateField aggregateField) {
+ Number result = (Number) get(aggregateField);
+ return result == null ? null : result.doubleValue();
+ }
+
+ /**
+ * Returns the result of the given aggregation as a long. Coerces all numeric values and throws a
+ * RuntimeException if the result of the aggregate is non-numeric. In case of coercion of double
+ * to long, uses java.lang.Double.longValue to perform the conversion.
+ *
+ * @param aggregateField The aggregation for which the value is requested.
+ * @return The result of the given average aggregation as a long.
+ */
+ @Nullable
+ public Long getLong(@Nonnull AggregateField aggregateField) {
+ Number result = (Number) get(aggregateField);
+ return result == null ? null : result.longValue();
}
/**
@@ -79,7 +177,7 @@ public boolean equals(Object object) {
AggregateQuerySnapshot other = (AggregateQuerySnapshot) object;
// Don't check `readTime`, because `DocumentSnapshot.equals()` doesn't either.
- return query.equals(other.query) && count == other.count;
+ return query.equals(other.query) && data.equals(other.data);
}
/**
@@ -89,6 +187,6 @@ public boolean equals(Object object) {
*/
@Override
public int hashCode() {
- return Objects.hash(query, count);
+ return Objects.hash(query, data);
}
}
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 011ad5a50b..9b9ede8593 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
@@ -61,6 +61,7 @@
import io.opencensus.trace.AttributeValue;
import io.opencensus.trace.Tracing;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
@@ -1928,7 +1929,28 @@ boolean shouldRetryQuery(
*/
@Nonnull
public AggregateQuery count() {
- return new AggregateQuery(this);
+ return new AggregateQuery(this, Collections.singletonList(AggregateField.count()));
+ }
+
+ /**
+ * Calculates the specified aggregations over the documents in the result set of the given query,
+ * without actually downloading the documents.
+ *
+ * Using this function to perform aggregations is efficient because only the final aggregation
+ * values, not the documents' data, is downloaded. This function can even perform aggregations of
+ * the documents if the result set would be prohibitively large to download entirely (e.g.
+ * thousands of documents).
+ *
+ * @return an {@link AggregateQuery} that performs aggregations on the documents in the result set
+ * of this query.
+ */
+ @Nonnull
+ public AggregateQuery aggregate(
+ @Nonnull AggregateField aggregateField1, @Nonnull AggregateField... aggregateFields) {
+ List aggregateFieldList = new ArrayList<>();
+ aggregateFieldList.add(aggregateField1);
+ aggregateFieldList.addAll(Arrays.asList(aggregateFields));
+ return new AggregateQuery(this, aggregateFieldList);
}
/**
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateFieldTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateFieldTest.java
new file mode 100644
index 0000000000..3b86b1f967
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateFieldTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AggregateFieldTest {
+
+ @Test
+ public void staticCountCreatesCountAggregateField() {
+ Object count = AggregateField.count();
+ assertThat(count instanceof AggregateField.CountAggregateField).isTrue();
+ }
+
+ @Test
+ public void staticSumCreatesSumAggregateField() {
+ Object sum = AggregateField.sum("foo");
+ assertThat(sum instanceof AggregateField.SumAggregateField).isTrue();
+ }
+
+ @Test
+ public void staticAverageCreatesAverageAggregateField() {
+ Object average = AggregateField.average("foo");
+ assertThat(average instanceof AggregateField.AverageAggregateField).isTrue();
+ }
+
+ @Test
+ public void setsProperFieldPathForSum() {
+ AggregateField.SumAggregateField sum = AggregateField.sum("foo");
+ assertThat(sum.getFieldPath().equals("foo")).isTrue();
+ }
+
+ @Test
+ public void setsProperFieldPathForAverage() {
+ AggregateField.AverageAggregateField average = AggregateField.average("foo");
+ assertThat(average.getFieldPath().equals("foo")).isTrue();
+ }
+
+ @Test
+ public void setsProperOperatorForCount() {
+ AggregateField.CountAggregateField count = AggregateField.count();
+ assertThat(count.getOperator().equals("count")).isTrue();
+ }
+
+ @Test
+ public void setsProperOperatorForSum() {
+ AggregateField.SumAggregateField sum = AggregateField.sum("foo");
+ assertThat(sum.getOperator().equals("sum")).isTrue();
+ }
+
+ @Test
+ public void setsProperOperatorForAverage() {
+ AggregateField.AverageAggregateField average = AggregateField.average("foo");
+ assertThat(average.getOperator().equals("average")).isTrue();
+ }
+
+ @Test
+ public void setsProperFieldPathWithEscapeChars() {
+ AggregateField.SumAggregateField sum = AggregateField.sum("has`invalid");
+ assertThat(sum.getFieldPath().equals("`has\\`invalid`")).isTrue();
+ }
+
+ @Test
+ public void sumSetsProperAliasWithEscapeChars() {
+ AggregateField.SumAggregateField sum = AggregateField.sum("has`invalid");
+ assertThat(sum.getAlias().equals("sum_`has\\`invalid`")).isTrue();
+ }
+
+ @Test
+ public void averageSetsProperAliasWithEscapeChars() {
+ AggregateField.AverageAggregateField average = AggregateField.average("has`invalid");
+ assertThat(average.getAlias().equals("average_`has\\`invalid`")).isTrue();
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQuerySnapshotTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQuerySnapshotTest.java
index a7f5864285..5fd1e822b9 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQuerySnapshotTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQuerySnapshotTest.java
@@ -19,6 +19,10 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.cloud.Timestamp;
+import com.google.firestore.v1.Value;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -36,116 +40,125 @@ public class AggregateQuerySnapshotTest {
private Timestamp sampleTimestamp;
private Timestamp sampleTimestamp2;
+ private Map data42;
+ private Map data24;
+
@Before
public void initializeSampleObjects() {
- sampleAggregateQuery = new AggregateQuery(mockQuery);
- sampleAggregateQuery2 = new AggregateQuery(mockQuery2);
+ sampleAggregateQuery =
+ new AggregateQuery(mockQuery, Collections.singletonList(AggregateField.count()));
+ sampleAggregateQuery2 =
+ new AggregateQuery(mockQuery2, Collections.singletonList(AggregateField.count()));
sampleTimestamp = Timestamp.ofTimeSecondsAndNanos(42, 42);
sampleTimestamp2 = Timestamp.ofTimeSecondsAndNanos(24, 24);
+ data42 = new HashMap<>();
+ data42.put("count", Value.newBuilder().setIntegerValue(42).build());
+ data24 = new HashMap<>();
+ data24.put("count", Value.newBuilder().setIntegerValue(24).build());
}
@Test
public void getQueryShouldReturnTheAggregateQuerySpecifiedToTheConstructor() {
AggregateQuerySnapshot snapshot =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot.getQuery()).isSameInstanceAs(sampleAggregateQuery);
}
@Test
public void getReadTimeShouldReturnTheTimestampSpecifiedToTheConstructor() {
AggregateQuerySnapshot snapshot =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot.getReadTime()).isSameInstanceAs(sampleTimestamp);
}
@Test
public void getCountShouldReturnTheCountSpecifiedToTheConstructor() {
AggregateQuerySnapshot snapshot =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot.getCount()).isEqualTo(42);
}
@Test
public void hashCodeShouldReturnSameHashCodeWhenConstructedWithSameObjects() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot1.hashCode()).isEqualTo(snapshot2.hashCode());
}
@Test
public void hashCodeShouldReturnDifferentHashCodeWhenConstructedDifferentAggregateQuery() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery2, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery2, sampleTimestamp, data42);
assertThat(snapshot1.hashCode()).isNotEqualTo(snapshot2.hashCode());
}
@Test
public void hashCodeShouldReturnSameHashCodeWhenConstructedDifferentTimestamp() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp2, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp2, data42);
assertThat(snapshot1.hashCode()).isEqualTo(snapshot2.hashCode());
}
@Test
public void hashCodeShouldReturnDifferentHashCodeWhenConstructedDifferentCount() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 24);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data24);
assertThat(snapshot1.hashCode()).isNotEqualTo(snapshot2.hashCode());
}
@Test
public void equalsShouldReturnFalseWhenGivenNull() {
AggregateQuerySnapshot snapshot =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot.equals(null)).isFalse();
}
@Test
public void equalsShouldReturnFalseWhenGivenADifferentObject() {
AggregateQuerySnapshot snapshot =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot.equals("Not An AggregateQuerySnapshot")).isFalse();
}
@Test
public void equalsShouldReturnFalseWhenGivenAnAggregateQuerySnapshotWithADifferentQuery() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery2, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery2, sampleTimestamp, data42);
assertThat(snapshot1.equals(snapshot2)).isFalse();
}
@Test
public void equalsShouldReturnTrueWhenGivenAnAggregateQuerySnapshotWithADifferentReadTime() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp2, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp2, data42);
assertThat(snapshot1.equals(snapshot2)).isTrue();
}
@Test
public void equalsShouldReturnFalseWhenGivenAnAggregateQuerySnapshotWithADifferentCount() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 24);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data24);
assertThat(snapshot1.equals(snapshot2)).isFalse();
}
@Test
public void equalsShouldReturnTrueWhenGivenTheSameAggregateQuerySnapshotInstance() {
AggregateQuerySnapshot snapshot =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot.equals(snapshot)).isTrue();
}
@@ -153,9 +166,9 @@ public void equalsShouldReturnTrueWhenGivenTheSameAggregateQuerySnapshotInstance
public void
equalsShouldReturnTrueWhenGivenAnAggregateQuerySnapshotConstructedWithTheSameArguments() {
AggregateQuerySnapshot snapshot1 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
AggregateQuerySnapshot snapshot2 =
- new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
+ new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, data42);
assertThat(snapshot1.equals(snapshot2)).isTrue();
}
}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java
index d7e5134740..9423ad1bb7 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java
@@ -16,10 +16,17 @@
package com.google.cloud.firestore;
+import static com.google.cloud.firestore.AggregateField.average;
+import static com.google.cloud.firestore.AggregateField.count;
+import static com.google.cloud.firestore.AggregateField.sum;
import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
import static org.mockito.Mockito.mock;
import com.google.cloud.firestore.spi.v1.FirestoreRpc;
+import java.util.List;
+import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -33,45 +40,73 @@ public class AggregateQueryTest {
@Test
public void getQueryShouldReturnTheQuerySpecifiedToTheConstructor() {
- AggregateQuery aggregateQuery = new AggregateQuery(mockQuery);
+ AggregateQuery aggregateQuery = new AggregateQuery(mockQuery, singletonList(count()));
assertThat(aggregateQuery.getQuery()).isSameInstanceAs(mockQuery);
}
@Test
- public void hashCodeShouldReturnHashCodeOfUnderlyingQuery() {
- AggregateQuery aggregateQuery = new AggregateQuery(mockQuery);
- assertThat(aggregateQuery.hashCode()).isEqualTo(mockQuery.hashCode());
+ public void hashCodeShouldReturnHashCodeOfUnderlyingQueryAndAggregateFieldList() {
+ List list = asList(count(), sum("foo"), average("bar"));
+ AggregateQuery aggregateQuery = new AggregateQuery(mockQuery, list);
+ assertThat(aggregateQuery.hashCode()).isEqualTo(Objects.hash(mockQuery, list));
}
@Test
public void equalsShouldReturnFalseWhenGivenNull() {
- AggregateQuery aggregateQuery = new AggregateQuery(mockQuery);
+ AggregateQuery aggregateQuery =
+ new AggregateQuery(mockQuery, asList(count(), sum("foo"), average("bar")));
assertThat(aggregateQuery.equals(null)).isFalse();
}
@Test
public void equalsShouldReturnFalseWhenGivenADifferentObject() {
- AggregateQuery aggregateQuery = new AggregateQuery(mockQuery);
+ AggregateQuery aggregateQuery =
+ new AggregateQuery(mockQuery, asList(count(), sum("foo"), average("bar")));
assertThat(aggregateQuery.equals("Not An AggregateQuery")).isFalse();
}
@Test
public void equalsShouldReturnFalseWhenGivenAnAggregateQueryWithADifferentQuery() {
- AggregateQuery aggregateQuery1 = new AggregateQuery(mockQuery);
- AggregateQuery aggregateQuery2 = new AggregateQuery(mockQuery2);
+ AggregateQuery aggregateQuery1 = new AggregateQuery(mockQuery, singletonList(count()));
+ AggregateQuery aggregateQuery2 = new AggregateQuery(mockQuery2, singletonList(count()));
+ assertThat(aggregateQuery1.equals(aggregateQuery2)).isFalse();
+ }
+
+ @Test
+ public void equalsShouldReturnFalseWhenGivenAnAggregateQueryWithDifferentAggregations() {
+ AggregateQuery aggregateQuery1 = new AggregateQuery(mockQuery, singletonList(count()));
+ AggregateQuery aggregateQuery2 = new AggregateQuery(mockQuery, singletonList(sum("foo")));
+ assertThat(aggregateQuery1.equals(aggregateQuery2)).isFalse();
+ }
+
+ @Test
+ public void equalsShouldReturnFalseWhenGivenAnAggregateQueryWithDifferentAggregationOrder() {
+ AggregateQuery aggregateQuery1 =
+ new AggregateQuery(mockQuery, asList(sum("foo"), average("bar")));
+ AggregateQuery aggregateQuery2 =
+ new AggregateQuery(mockQuery, asList(average("bar"), sum("foo")));
assertThat(aggregateQuery1.equals(aggregateQuery2)).isFalse();
}
@Test
public void equalsShouldReturnTrueWhenGivenTheSameAggregateQueryInstance() {
- AggregateQuery aggregateQuery = new AggregateQuery(mockQuery);
+ AggregateQuery aggregateQuery = new AggregateQuery(mockQuery, singletonList(count()));
assertThat(aggregateQuery.equals(aggregateQuery)).isTrue();
}
@Test
public void equalsShouldReturnTrueWhenGivenAnAggregateQueryWithTheSameQuery() {
- AggregateQuery aggregateQuery1 = new AggregateQuery(mockQuery);
- AggregateQuery aggregateQuery2 = new AggregateQuery(mockQuery);
+ AggregateQuery aggregateQuery1 = new AggregateQuery(mockQuery, singletonList(count()));
+ AggregateQuery aggregateQuery2 = new AggregateQuery(mockQuery, singletonList(count()));
+ assertThat(aggregateQuery1.equals(aggregateQuery2)).isTrue();
+ }
+
+ @Test
+ public void equalsShouldReturnTrueWhenGivenMultipleAggregationsWithTheSameQuery() {
+ AggregateQuery aggregateQuery1 =
+ new AggregateQuery(mockQuery, asList(count(), sum("foo"), average("bar")));
+ AggregateQuery aggregateQuery2 =
+ new AggregateQuery(mockQuery, asList(count(), sum("foo"), average("bar")));
assertThat(aggregateQuery1.equals(aggregateQuery2)).isTrue();
}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java
index 896a51e3af..b86cc0bc95 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java
@@ -341,24 +341,24 @@ public static Answer queryResponseWithDone(
}
}
- public static Answer aggregationQueryResponse() {
- return aggregationQueryResponse(42);
+ public static Answer countQueryResponse() {
+ return countQueryResponse(42);
}
- public static Answer aggregationQueryResponse(int count) {
- return aggregationQueryResponse(count, null);
+ public static Answer countQueryResponse(int count) {
+ return countQueryResponse(count, null);
}
- public static Answer aggregationQueryResponse(
+ public static Answer countQueryResponse(
int count, @Nullable Timestamp readTime) {
return streamingResponse(
new RunAggregationQueryResponse[] {
- createRunAggregationQueryResponse(count, readTime),
+ createCountQueryResponse(count, readTime),
},
/*throwable=*/ null);
}
- public static Answer aggregationQueryResponse(Throwable throwable) {
+ public static Answer countQueryResponse(Throwable throwable) {
return streamingResponse(new RunAggregationQueryResponse[] {}, throwable);
}
@@ -366,8 +366,7 @@ public static Answer aggregationQueryResponses(
int count1, int count2) {
return streamingResponse(
new RunAggregationQueryResponse[] {
- createRunAggregationQueryResponse(count1, null),
- createRunAggregationQueryResponse(count2, null),
+ createCountQueryResponse(count1, null), createCountQueryResponse(count2, null),
},
/*throwable=*/ null);
}
@@ -376,17 +375,17 @@ public static Answer aggregationQueryResponses(
int count1, Throwable throwable) {
return streamingResponse(
new RunAggregationQueryResponse[] {
- createRunAggregationQueryResponse(count1, null),
+ createCountQueryResponse(count1, null),
},
throwable);
}
- private static RunAggregationQueryResponse createRunAggregationQueryResponse(
+ private static RunAggregationQueryResponse createCountQueryResponse(
int count, @Nullable Timestamp timestamp) {
RunAggregationQueryResponse.Builder builder = RunAggregationQueryResponse.newBuilder();
builder.setResult(
AggregationResult.newBuilder()
- .putAggregateFields("count", Value.newBuilder().setIntegerValue(count).build())
+ .putAggregateFields("aggregate_0", Value.newBuilder().setIntegerValue(count).build())
.build());
if (timestamp != null) {
builder.setReadTime(timestamp.toProto());
@@ -750,11 +749,11 @@ public static RunQueryRequest query(
return request.build();
}
- public static RunAggregationQueryRequest aggregationQuery() {
- return aggregationQuery((String) null);
+ public static RunAggregationQueryRequest countQuery() {
+ return countQuery((String) null);
}
- public static RunAggregationQueryRequest aggregationQuery(@Nullable String transactionId) {
+ public static RunAggregationQueryRequest countQuery(@Nullable String transactionId) {
RunQueryRequest runQueryRequest = query(TRANSACTION_ID, false);
RunAggregationQueryRequest.Builder request =
@@ -765,7 +764,7 @@ public static RunAggregationQueryRequest aggregationQuery(@Nullable String trans
.setStructuredQuery(runQueryRequest.getStructuredQuery())
.addAggregations(
Aggregation.newBuilder()
- .setAlias("count")
+ .setAlias("aggregate_0")
.setCount(Aggregation.Count.getDefaultInstance())));
if (transactionId != null) {
@@ -775,7 +774,7 @@ public static RunAggregationQueryRequest aggregationQuery(@Nullable String trans
return request.build();
}
- public static RunAggregationQueryRequest aggregationQuery(RunQueryRequest runQueryRequest) {
+ public static RunAggregationQueryRequest countQuery(RunQueryRequest runQueryRequest) {
return RunAggregationQueryRequest.newBuilder()
.setParent(runQueryRequest.getParent())
.setStructuredAggregationQuery(
@@ -783,7 +782,7 @@ public static RunAggregationQueryRequest aggregationQuery(RunQueryRequest runQue
.setStructuredQuery(runQueryRequest.getStructuredQuery())
.addAggregations(
Aggregation.newBuilder()
- .setAlias("count")
+ .setAlias("aggregate_0")
.setCount(Aggregation.Count.getDefaultInstance())))
.build();
}
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 3cf05648e3..78222ecfa4 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
@@ -17,9 +17,8 @@
package com.google.cloud.firestore;
import static com.google.cloud.firestore.LocalFirestoreHelper.COLLECTION_ID;
-import static com.google.cloud.firestore.LocalFirestoreHelper.aggregationQuery;
-import static com.google.cloud.firestore.LocalFirestoreHelper.aggregationQueryResponse;
import static com.google.cloud.firestore.LocalFirestoreHelper.aggregationQueryResponses;
+import static com.google.cloud.firestore.LocalFirestoreHelper.countQueryResponse;
import static com.google.cloud.firestore.LocalFirestoreHelper.endAt;
import static com.google.cloud.firestore.LocalFirestoreHelper.limit;
import static com.google.cloud.firestore.LocalFirestoreHelper.order;
@@ -77,7 +76,7 @@ public void before() {
@Test
public void countShouldBeZeroForEmptyCollection() throws Exception {
- doAnswer(aggregationQueryResponse(0))
+ doAnswer(countQueryResponse(0))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -88,7 +87,7 @@ public void countShouldBeZeroForEmptyCollection() throws Exception {
@Test
public void countShouldBe99ForCollectionWith99Documents() throws Exception {
- doAnswer(aggregationQueryResponse(99))
+ doAnswer(countQueryResponse(99))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -99,19 +98,19 @@ public void countShouldBe99ForCollectionWith99Documents() throws Exception {
@Test
public void countShouldMakeCorrectRequestForACollection() throws Exception {
- doAnswer(aggregationQueryResponse(0))
+ doAnswer(countQueryResponse(0))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
CollectionReference collection = firestoreMock.collection(COLLECTION_ID);
collection.count().get();
- assertThat(runAggregationQuery.getValue()).isEqualTo(aggregationQuery());
+ assertThat(runAggregationQuery.getValue()).isEqualTo(LocalFirestoreHelper.countQuery());
}
@Test
public void countShouldMakeCorrectRequestForAComplexQuery() throws Exception {
- doAnswer(aggregationQueryResponse(0))
+ doAnswer(countQueryResponse(0))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -119,7 +118,7 @@ public void countShouldMakeCorrectRequestForAComplexQuery() throws Exception {
assertThat(runAggregationQuery.getValue())
.isEqualTo(
- aggregationQuery(
+ LocalFirestoreHelper.countQuery(
query(
limit(42),
order("foo", StructuredQuery.Direction.DESCENDING),
@@ -129,7 +128,7 @@ public void countShouldMakeCorrectRequestForAComplexQuery() throws Exception {
@Test
public void shouldReturnReadTimeFromResponse() throws Exception {
- doAnswer(aggregationQueryResponse(99, Timestamp.ofTimeSecondsAndNanos(123, 456)))
+ doAnswer(countQueryResponse(99, Timestamp.ofTimeSecondsAndNanos(123, 456)))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -163,7 +162,7 @@ public void shouldIgnoreExtraErrors() throws Exception {
@Test
public void shouldPropagateErrors() throws Exception {
Exception exception = new Exception();
- doAnswer(aggregationQueryResponse(exception))
+ doAnswer(countQueryResponse(exception))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -180,7 +179,7 @@ public void aggregateQueryGetQueryShouldReturnCorrectValue() throws Exception {
@Test
public void aggregateQuerySnapshotGetQueryShouldReturnCorrectValue() throws Exception {
- doAnswer(aggregationQueryResponse())
+ doAnswer(countQueryResponse())
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -192,8 +191,8 @@ public void aggregateQuerySnapshotGetQueryShouldReturnCorrectValue() throws Exce
@Test
public void shouldNotRetryIfExceptionIsNotFirestoreException() {
- doAnswer(aggregationQueryResponse(new NotFirestoreException()))
- .doAnswer(aggregationQueryResponse())
+ doAnswer(countQueryResponse(new NotFirestoreException()))
+ .doAnswer(countQueryResponse())
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -204,8 +203,8 @@ public void shouldNotRetryIfExceptionIsNotFirestoreException() {
@Test
public void shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatus() throws Exception {
- doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
- .doAnswer(aggregationQueryResponse(42))
+ doAnswer(countQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
+ .doAnswer(countQueryResponse(42))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -217,8 +216,8 @@ public void shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatus() thro
@Test
public void shouldNotRetryIfExceptionIsFirestoreExceptionWithNonRetryableStatus() {
- doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INVALID_ARGUMENT)))
- .doAnswer(aggregationQueryResponse())
+ doAnswer(countQueryResponse(new FirestoreException("reason", Status.INVALID_ARGUMENT)))
+ .doAnswer(countQueryResponse())
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -232,8 +231,8 @@ public void shouldNotRetryIfExceptionIsFirestoreExceptionWithNonRetryableStatus(
shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatusWithInfiniteTimeoutWindow()
throws Exception {
doReturn(Duration.ZERO).when(firestoreMock).getTotalRequestTimeout();
- doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
- .doAnswer(aggregationQueryResponse(42))
+ doAnswer(countQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
+ .doAnswer(countQueryResponse(42))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -247,8 +246,8 @@ public void shouldNotRetryIfExceptionIsFirestoreExceptionWithNonRetryableStatus(
public void shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatusWithinTimeoutWindow()
throws Exception {
doReturn(Duration.ofDays(999)).when(firestoreMock).getTotalRequestTimeout();
- doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
- .doAnswer(aggregationQueryResponse(42))
+ doAnswer(countQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
+ .doAnswer(countQueryResponse(42))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
@@ -269,8 +268,8 @@ public void shouldRetryIfExceptionIsFirestoreExceptionWithRetryableStatusWithinT
.when(clockMock)
.nanoTime();
doReturn(Duration.ofSeconds(5)).when(firestoreMock).getTotalRequestTimeout();
- doAnswer(aggregationQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
- .doAnswer(aggregationQueryResponse(42))
+ doAnswer(countQueryResponse(new FirestoreException("reason", Status.INTERNAL)))
+ .doAnswer(countQueryResponse(42))
.when(firestoreMock)
.streamRequest(runAggregationQuery.capture(), streamObserverCapture.capture(), any());
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java
index 4c559486bc..eeef59e0c8 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java
@@ -19,12 +19,12 @@
import static com.google.cloud.firestore.LocalFirestoreHelper.IMMEDIATE_RETRY_SETTINGS;
import static com.google.cloud.firestore.LocalFirestoreHelper.SINGLE_FIELD_PROTO;
import static com.google.cloud.firestore.LocalFirestoreHelper.TRANSACTION_ID;
-import static com.google.cloud.firestore.LocalFirestoreHelper.aggregationQuery;
-import static com.google.cloud.firestore.LocalFirestoreHelper.aggregationQueryResponse;
import static com.google.cloud.firestore.LocalFirestoreHelper.begin;
import static com.google.cloud.firestore.LocalFirestoreHelper.beginResponse;
import static com.google.cloud.firestore.LocalFirestoreHelper.commit;
import static com.google.cloud.firestore.LocalFirestoreHelper.commitResponse;
+import static com.google.cloud.firestore.LocalFirestoreHelper.countQuery;
+import static com.google.cloud.firestore.LocalFirestoreHelper.countQueryResponse;
import static com.google.cloud.firestore.LocalFirestoreHelper.create;
import static com.google.cloud.firestore.LocalFirestoreHelper.delete;
import static com.google.cloud.firestore.LocalFirestoreHelper.get;
@@ -697,7 +697,7 @@ public void getAggregateQuery() throws Exception {
.sendRequest(
requestCapture.capture(), ArgumentMatchers.>any());
- doAnswer(aggregationQueryResponse(42))
+ doAnswer(countQueryResponse(42))
.when(firestoreMock)
.streamRequest(
requestCapture.capture(), streamObserverCapture.capture(), ArgumentMatchers.any());
@@ -711,7 +711,7 @@ public void getAggregateQuery() throws Exception {
assertEquals(3, requests.size());
assertEquals(begin(), requests.get(0));
- assertEquals(aggregationQuery(TRANSACTION_ID), requests.get(1));
+ assertEquals(countQuery(TRANSACTION_ID), requests.get(1));
assertEquals(commit(TRANSACTION_ID), requests.get(2));
}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java
new file mode 100644
index 0000000000..2651514464
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java
@@ -0,0 +1,1084 @@
+/*
+ * Copyright 2023 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.it;
+
+import static com.google.cloud.firestore.AggregateField.average;
+import static com.google.cloud.firestore.AggregateField.sum;
+import static com.google.cloud.firestore.LocalFirestoreHelper.autoId;
+import static com.google.cloud.firestore.LocalFirestoreHelper.map;
+import static com.google.cloud.firestore.it.TestHelper.await;
+import static com.google.cloud.firestore.it.TestHelper.isRunningAgainstFirestoreEmulator;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.cloud.firestore.*;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ITQueryAggregationsTest extends ITBaseTest {
+ private CollectionReference testCollection() {
+ String collectionPath = "java-" + autoId();
+ return firestore.collection(collectionPath);
+ }
+
+ private CollectionReference testCollection(String name) {
+ return firestore.collection("java-" + name + "-" + autoId());
+ }
+
+ private CollectionReference testCollectionWithDocs(Map> docs)
+ throws InterruptedException {
+ CollectionReference collection = testCollection();
+ CollectionReference writer = firestore.collection(collection.getId());
+ writeAllDocs(writer, docs);
+ return collection;
+ }
+
+ public static void writeAllDocs(
+ CollectionReference collection, Map> docs)
+ throws InterruptedException {
+ for (Map.Entry> doc : docs.entrySet()) {
+ await(collection.document(doc.getKey()).set(doc.getValue()));
+ }
+ }
+
+ private static Map> testDocs1 =
+ map(
+ "a",
+ map("author", "authorA", "title", "titleA", "pages", 100, "foo", 1, "bar", 2, "baz", 3),
+ "b",
+ map("author", "authorB", "title", "titleB", "pages", 50, "foo", 1, "bar", 2, "baz", 3));
+
+ @Test
+ public void canRunCountUsingAggregationMethod() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot = collection.aggregate(AggregateField.count()).get().get();
+ assertThat(snapshot.getCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void allowsAliasesForLongestFieldNames() throws Exception {
+ // The longest field name allowed is 1499 characters long.
+ // Ensure that sum(longestField) and average(longestField) work.
+ StringBuilder builder = new StringBuilder(1500);
+ for (int i = 0; i < 1499; i++) {
+ builder.append("k");
+ }
+ String longestField = builder.toString();
+ Map> testDocs =
+ map("a", map(longestField, 2), "b", map(longestField, 4));
+
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection.aggregate(AggregateField.sum(longestField)).get().get();
+ assertThat(snapshot.get(AggregateField.sum(longestField))).isEqualTo(6);
+ AggregateQuerySnapshot snapshot2 =
+ collection.aggregate(AggregateField.average(longestField)).get().get();
+ assertThat(snapshot2.get(AggregateField.average(longestField))).isEqualTo(3.0);
+ }
+
+ @Test
+ public void canGetDuplicateAggregations() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot =
+ collection
+ .aggregate(AggregateField.count(), AggregateField.count(), sum("pages"), sum("pages"))
+ .get()
+ .get();
+ assertThat(snapshot.getCount()).isEqualTo(2);
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(150);
+ }
+
+ @Test
+ public void aggregateErrorMessageIfIndexIsMissing() throws Exception {
+ assumeFalse(
+ "Skip this test when running against the emulator because it does not require composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuery aggregateQuery =
+ collection
+ .whereEqualTo("key1", 42)
+ .whereLessThan("key2", 42)
+ .aggregate(AggregateField.count());
+ ExecutionException executionException =
+ assertThrows(ExecutionException.class, () -> aggregateQuery.get().get());
+ assertThat(executionException)
+ .hasCauseThat()
+ .hasMessageThat()
+ .containsMatch("FAILED_PRECONDITION:.*index.*");
+ }
+
+ @Test
+ public void canRunSumQuery() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("pages")).get().get();
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(150);
+ }
+
+ @Test
+ public void canRunAverageQuery() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("pages")).get().get();
+ assertThat(snapshot.get(average("pages"))).isEqualTo(75.0);
+ }
+
+ @Test
+ public void canGetMultipleAggregationsInTheSameQuery() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot =
+ collection.aggregate(sum("pages"), average("pages"), AggregateField.count()).get().get();
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(150);
+ assertThat(snapshot.get(average("pages"))).isEqualTo(75.0);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(2);
+ }
+
+ @Test
+ public void getCorrectTypeForSumLong() throws Exception {
+ Map> testDocs = map("a", map("foo", 100), "b", map("foo", 100));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("foo")).get().get();
+ Object sum = snapshot.get(sum("foo"));
+ assertThat(sum instanceof Long).isTrue();
+ }
+
+ @Test
+ public void getCorrectTypeForSumDouble() throws Exception {
+ Map> testDocs = map("a", map("foo", 100.5), "b", map("foo", 100));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("foo")).get().get();
+ Object sum = snapshot.get(sum("foo"));
+ assertThat(sum instanceof Double).isTrue();
+ }
+
+ @Test
+ public void getCorrectTypeForSumNaN() throws Exception {
+ Map> testDocs =
+ map("a", map("foo", 100.5), "b", map("foo", Double.NaN));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("foo")).get().get();
+ Object sum = snapshot.get(sum("foo"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum.equals(Double.NaN));
+ }
+
+ @Test
+ public void getCorrectTypeForAverageDouble() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("pages")).get().get();
+ Object average = snapshot.get((AggregateField) average("pages"));
+ assertThat(average instanceof Double).isTrue();
+ }
+
+ @Test
+ public void getCorrectTypeForAverageNaN() throws Exception {
+ Map> testDocs =
+ map("a", map("foo", 100.5), "b", map("foo", Double.NaN));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("foo")).get().get();
+ Object sum = snapshot.get(average("foo"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum.equals(Double.NaN));
+ }
+
+ @Test
+ public void getCorrectTypeForAverageNull() throws Exception {
+ CollectionReference collection = testCollection();
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("bar")).get().get();
+ Object sum = snapshot.get(average("bar"));
+ assertThat(sum == null).isTrue();
+ }
+
+ @Test
+ public void canPerformMaxAggregations() throws Exception {
+ assumeTrue(
+ "Skip this test when running against prod because it requires composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateField f1 = sum("pages");
+ AggregateField f2 = average("pages");
+ AggregateField f3 = AggregateField.count();
+ AggregateField f4 = sum("foo");
+ AggregateField f5 = sum("bar");
+ AggregateQuerySnapshot snapshot = collection.aggregate(f1, f2, f3, f4, f5).get().get();
+ assertThat(snapshot.get(f1)).isEqualTo(150);
+ assertThat(snapshot.get(f2)).isEqualTo(75.0);
+ assertThat(snapshot.get(f3)).isEqualTo(2);
+ assertThat(snapshot.get(f4)).isEqualTo(2);
+ assertThat(snapshot.get(f5)).isEqualTo(4);
+ }
+
+ @Test
+ public void cannotPerformMoreThanMaxAggregations() throws Exception {
+ // TODO: Update this test once aggregate de-duplication is implemented and more aggregation
+ // types are available.
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateField f1 = sum("pages");
+ AggregateField f2 = average("pages");
+ AggregateField f3 = AggregateField.count();
+ AggregateField f4 = sum("foo");
+ AggregateField f5 = sum("bar");
+ AggregateField f6 = sum("baz");
+ Exception exception = null;
+ try {
+ collection.aggregate(f1, f2, f3, f4, f5, f6).get().get();
+ } catch (Exception e) {
+ exception = e;
+ }
+ assertThat(exception).isNotNull();
+ assertThat(exception.getMessage()).contains("maximum number of aggregations");
+ }
+
+ @Test
+ public void aggregateQueriesSupportCollectionGroups() throws Exception {
+ assumeTrue(
+ "Skip this test when running against prod because it requires composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+ String collectionGroupId = "myColGroupId" + autoId();
+ Map data = map("x", 2);
+ // Setting documents at the following paths:
+ // `${collectionGroupId}/cg-doc1`,
+ // `abc/123/${collectionGroupId}/cg-doc2`,
+ // `zzz${collectionGroupId}/cg-doc3`,
+ // `abc/123/zzz${collectionGroupId}/cg-doc4`,
+ // `abc/123/zzz/${collectionGroupId}`
+ await(firestore.collection(collectionGroupId).document("cg-doc1").set(data));
+ await(
+ firestore
+ .collection("abc")
+ .document("123")
+ .collection(collectionGroupId)
+ .document("cg-doc2")
+ .set(data));
+ await(firestore.collection("zzz" + collectionGroupId).document("cg-doc3").set(data));
+ await(
+ firestore
+ .collection("abc")
+ .document("123")
+ .collection("zzz" + collectionGroupId)
+ .document("cg-doc4")
+ .set(data));
+ await(
+ firestore
+ .collection("abc")
+ .document("123")
+ .collection("zzz")
+ .document(collectionGroupId)
+ .set(data));
+ CollectionGroup collectionGroup = firestore.collectionGroup(collectionGroupId);
+ AggregateQuerySnapshot snapshot =
+ collectionGroup.aggregate(AggregateField.count(), sum("x"), average("x")).get().get();
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(2);
+ assertThat(snapshot.get(sum("x"))).isEqualTo(4);
+ assertThat(snapshot.get(average("x"))).isEqualTo(2);
+ }
+
+ @Test
+ public void performsAggregationsOnDocumentsWithAllAggregatedFields() throws Exception {
+ assumeTrue(
+ "Skip this test when running against prod because it requires composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+ Map> testDocs =
+ map(
+ "a",
+ map("author", "authorA", "title", "titleA", "pages", 100, "year", 1980),
+ "b",
+ map("author", "authorB", "title", "titleB", "pages", 50, "year", 2020),
+ "c",
+ map("author", "authorC", "title", "titleC", "pages", 150, "year", 2021),
+ "d",
+ map("author", "authorD", "title", "titleD", "pages", 50));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection
+ .aggregate(sum("pages"), average("pages"), average("year"), AggregateField.count())
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(300);
+ assertThat(snapshot.get(average("pages"))).isEqualTo(100);
+ assertThat(snapshot.get(average("year"))).isEqualTo(2007);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(3);
+ }
+
+ @Test
+ public void performsAggregationsWhenNaNExistsForSomeFieldValues() throws Exception {
+ assumeTrue(
+ "Skip this test when running against prod because it requires composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+ Map> testDocs =
+ map(
+ "a",
+ map(
+ "author", "authorA", "title", "titleA", "pages", 100, "year", 1980, "rating",
+ 5),
+ "b",
+ map("author", "authorB", "title", "titleB", "pages", 50, "year", 2020, "rating", 4),
+ "c",
+ map(
+ "author",
+ "authorC",
+ "title",
+ "titleC",
+ "pages",
+ 100,
+ "year",
+ 1980,
+ "rating",
+ Double.NaN),
+ "d",
+ map(
+ "author", "authorD", "title", "titleD", "pages", 50, "year", 2020, "rating",
+ 0));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection.aggregate(sum("rating"), sum("pages"), average("year")).get().get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(Double.NaN);
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(300);
+ assertThat(snapshot.get(average("year"))).isEqualTo(2000);
+ }
+
+ @Test
+ public void throwsAnErrorWhenGettingTheResultOfAnUnrequestedAggregation() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("pages")).get().get();
+ Exception exception = null;
+ try {
+ snapshot.get(average("pages"));
+ } catch (Exception e) {
+ exception = e;
+ }
+ assertThat(exception).isNotNull();
+ assertThat(exception.getMessage())
+ .isEqualTo("'average(pages)' was not requested in the aggregation query.");
+ exception = null;
+ try {
+ snapshot.get(sum("foo"));
+ } catch (RuntimeException e) {
+ exception = e;
+ }
+ assertThat(exception).isNotNull();
+ assertThat(exception.getMessage())
+ .isEqualTo("'sum(foo)' was not requested in the aggregation query.");
+ }
+
+ @Test
+ public void performsAggregationWhenUsingInOperator() throws Exception {
+ assumeTrue(
+ "Skip this test when running against prod because it requires composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+ Map> testDocs =
+ map(
+ "a",
+ map(
+ "author", "authorA", "title", "titleA", "pages", 100, "year", 1980, "rating",
+ 5),
+ "b",
+ map("author", "authorB", "title", "titleB", "pages", 50, "year", 2020, "rating", 4),
+ "c",
+ map(
+ "author", "authorC", "title", "titleC", "pages", 100, "year", 1980, "rating",
+ 3),
+ "d",
+ map(
+ "author", "authorD", "title", "titleD", "pages", 50, "year", 2020, "rating",
+ 0));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection
+ .whereIn("rating", asList(5, 3))
+ .aggregate(
+ sum("rating"),
+ average("rating"),
+ sum("pages"),
+ average("pages"),
+ AggregateField.count())
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(8);
+ assertThat(snapshot.get(average("rating"))).isEqualTo(4);
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(200);
+ assertThat(snapshot.get(average("pages"))).isEqualTo(100);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(2);
+ }
+
+ @Test
+ public void performsAggregationWhenUsingArrayContainsAnyOperator() throws Exception {
+ assumeTrue(
+ "Skip this test when running against prod because it requires composite index creation.",
+ isRunningAgainstFirestoreEmulator(firestore));
+ Map> testDocs =
+ map(
+ "a",
+ map(
+ "author",
+ "authorA",
+ "title",
+ "titleA",
+ "pages",
+ 100,
+ "year",
+ 1980,
+ "rating",
+ asList(5, 1000)),
+ "b",
+ map(
+ "author", "authorB", "title", "titleB", "pages", 50, "year", 2020, "rating",
+ asList(4)),
+ "c",
+ map(
+ "author",
+ "authorC",
+ "title",
+ "titleC",
+ "pages",
+ 100,
+ "year",
+ 1980,
+ "rating",
+ asList(2222, 3)),
+ "d",
+ map(
+ "author", "authorD", "title", "titleD", "pages", 50, "year", 2020, "rating",
+ asList(0)));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection
+ .whereArrayContainsAny("rating", asList(5, 3))
+ .aggregate(
+ sum("rating"),
+ average("rating"),
+ sum("pages"),
+ average("pages"),
+ AggregateField.count())
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(0);
+ assertThat(snapshot.get(average("rating"))).isEqualTo(null);
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(200);
+ assertThat(snapshot.get(average("pages"))).isEqualTo(100);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(2);
+ }
+
+ @Test
+ public void performsAggregationsOnNestedMapValues() throws Exception {
+ Map> testDocs =
+ map(
+ "a",
+ map(
+ "author",
+ "authorA",
+ "title",
+ "titleA",
+ "metadata",
+ map("pages", 100, "rating", map("critic", 2, "user", 5))),
+ "b",
+ map(
+ "author",
+ "authorB",
+ "title",
+ "titleB",
+ "metadata",
+ map("pages", 50, "rating", map("critic", 4, "user", 4))));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection
+ .aggregate(sum("metadata.pages"), average("metadata.pages"), AggregateField.count())
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("metadata.pages"))).isEqualTo(150);
+ assertThat(snapshot.get(average("metadata.pages"))).isEqualTo(75);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(2);
+ }
+
+ @Test
+ public void performsSumThatResultsInFloat() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 4.5),
+ "c", map("author", "authorC", "title", "titleC", "rating", 3));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum).isEqualTo(12.5);
+ }
+
+ @Test
+ public void performsSumOfIntsAndFloatsThatResultsInInt() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 4.5),
+ "c", map("author", "authorC", "title", "titleC", "rating", 3.5));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum).isEqualTo(13.0);
+ }
+
+ @Test
+ public void performsSumThatOverflowsMaxLong() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", Long.MAX_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", Long.MAX_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum).isEqualTo((double) Long.MAX_VALUE + (double) Long.MAX_VALUE);
+ }
+
+ @Test
+ public void performsSumThatCanOverflowIntegerValuesDuringAccumulation() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", Long.MAX_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", 1),
+ "c", map("author", "authorC", "title", "titleC", "rating", -101));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Long).isTrue();
+ assertThat(sum).isEqualTo(Long.MAX_VALUE - 100);
+ }
+
+ @Test
+ public void performsSumThatIsNegative() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", Long.MAX_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", -Long.MAX_VALUE),
+ "c", map("author", "authorC", "title", "titleC", "rating", -101),
+ "d", map("author", "authorD", "title", "titleD", "rating", -10000));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(-10101);
+ }
+
+ @Test
+ public void performsSumThatIsPositiveInfinity() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", Double.MAX_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", Double.MAX_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum).isEqualTo(Double.POSITIVE_INFINITY);
+ assertThat(snapshot.getDouble(sum("rating"))).isEqualTo(Double.POSITIVE_INFINITY);
+ assertThat(snapshot.getLong(sum("rating"))).isEqualTo(Long.MAX_VALUE);
+ }
+
+ @Test
+ public void performsSumThatIsNegativeInfinity() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", -Double.MAX_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", -Double.MAX_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum).isEqualTo(Double.NEGATIVE_INFINITY);
+ assertThat(snapshot.getDouble(sum("rating"))).isEqualTo(Double.NEGATIVE_INFINITY);
+ assertThat(snapshot.getLong(sum("rating"))).isEqualTo(Long.MIN_VALUE);
+ }
+
+ @Test
+ public void performsSumThatIsValidButCouldOverflowDuringAggregation() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", Double.MAX_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", Double.MAX_VALUE),
+ "c", map("author", "authorC", "title", "titleC", "rating", -Double.MAX_VALUE),
+ "d", map("author", "authorD", "title", "titleD", "rating", -Double.MAX_VALUE),
+ "e", map("author", "authorE", "title", "titleE", "rating", Double.MAX_VALUE),
+ "f", map("author", "authorF", "title", "titleF", "rating", -Double.MAX_VALUE),
+ "g", map("author", "authorG", "title", "titleG", "rating", -Double.MAX_VALUE),
+ "h", map("author", "authorH", "title", "titleH", "rating", Double.MAX_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ Object sum = snapshot.get(sum("rating"));
+ assertThat(sum instanceof Double).isTrue();
+ assertThat(sum).isAnyOf(0, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void performsSumThatIncludesNaN() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 4),
+ "c", map("author", "authorC", "title", "titleC", "rating", Double.NaN),
+ "d", map("author", "authorD", "title", "titleD", "rating", 0));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(Double.NaN);
+ }
+
+ @Test
+ public void performsSumOverResultSetOfZeroDocuments() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot =
+ collection.whereGreaterThan("pages", 200).aggregate(sum("pages")).get().get();
+ assertThat(snapshot.get(sum("pages"))).isEqualTo(0);
+ }
+
+ @Test
+ public void performsSumOnlyOnNumericFields() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 4),
+ "c", map("author", "authorC", "title", "titleC", "rating", "3"),
+ "d", map("author", "authorD", "title", "titleD", "rating", 1));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection.aggregate(sum("rating"), AggregateField.count()).get().get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(10);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(4);
+ }
+
+ @Test
+ public void performsSumOfMinIEEE754() throws Exception {
+ Map> testDocs =
+ map("a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get();
+ assertThat(snapshot.get(sum("rating"))).isEqualTo(Double.MIN_VALUE);
+ }
+
+ @Test
+ public void performsAverageOfIntsThatResultsInAnInt() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 10),
+ "b", map("author", "authorB", "title", "titleB", "rating", 5),
+ "c", map("author", "authorC", "title", "titleC", "rating", 0));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(5);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(5L);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(5.0);
+ }
+
+ @Test
+ public void performsAverageOfFloatsThatResultsInAnInt() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 10.5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 9.5));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating")) instanceof Double).isTrue();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(10);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(10L);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(10.0);
+ }
+
+ @Test
+ public void performsAverageOfFloatsAndIntsThatResultsInAnInt() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 10),
+ "b", map("author", "authorB", "title", "titleB", "rating", 9.5),
+ "c", map("author", "authorC", "title", "titleC", "rating", 10.5));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(10);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(10L);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(10.0);
+ }
+
+ @Test
+ public void performsAverageOfFloatsThatResultsInAFloat() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 5.5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 4.5),
+ "c", map("author", "authorC", "title", "titleC", "rating", 3.5));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(4.5);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(4.5);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(4L);
+ }
+
+ @Test
+ public void performsAverageOfFloatsAndIntsThatResultsInAFloat() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 8.6),
+ "b", map("author", "authorB", "title", "titleB", "rating", 9),
+ "c", map("author", "authorC", "title", "titleC", "rating", 10));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(27.6 / 3);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(27.6 / 3);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(9L);
+ }
+
+ @Test
+ public void performsAverageOfIntsThatResultsInAFloat() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 10),
+ "b", map("author", "authorB", "title", "titleB", "rating", 9));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(9.5);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(9.5d);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(9L);
+ }
+
+ @Test
+ public void performsAverageCausingUnderflow() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE),
+ "b", map("author", "authorB", "title", "titleB", "rating", 0));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(0);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(0.0d);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(0L);
+ }
+
+ @Test
+ public void performsAverageOfMinIEEE754() throws Exception {
+ Map> testDocs =
+ map("a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(Double.MIN_VALUE);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(Double.MIN_VALUE);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(0);
+ }
+
+ @Test
+ public void performsAverageThatCouldOverflowIEEE754DuringAccumulation() throws Exception {
+ Map> testDocs =
+ map(
+ "a",
+ map("author", "authorA", "title", "titleA", "rating", Double.MAX_VALUE),
+ "b",
+ map("author", "authorB", "title", "titleB", "rating", Double.MAX_VALUE));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(Double.POSITIVE_INFINITY);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(Double.POSITIVE_INFINITY);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(Long.MAX_VALUE);
+ }
+
+ @Test
+ public void performsAverageThatIncludesNaN() throws Exception {
+ Map> testDocs =
+ map(
+ "a",
+ map("author", "authorA", "title", "titleA", "rating", 5),
+ "b",
+ map("author", "authorB", "title", "titleB", "rating", 4),
+ "c",
+ map("author", "authorC", "title", "titleC", "rating", Double.NaN),
+ "d",
+ map("author", "authorD", "title", "titleD", "rating", 0));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(Double.NaN);
+ assertThat(snapshot.getDouble(average("rating"))).isEqualTo(Double.NaN);
+ assertThat(snapshot.getLong(average("rating"))).isEqualTo(0L);
+ }
+
+ @Test
+ public void performsAverageOverResultSetOfZeroDocuments() throws Exception {
+ CollectionReference collection = testCollectionWithDocs(testDocs1);
+ AggregateQuerySnapshot snapshot =
+ collection.whereGreaterThan("pages", 200).aggregate(average("pages")).get().get();
+ assertThat(snapshot.get(average("pages"))).isEqualTo(null);
+ assertThat(snapshot.getDouble(average("pages"))).isEqualTo(null);
+ assertThat(snapshot.getLong(average("pages"))).isEqualTo(null);
+ }
+
+ @Test
+ public void performsAverageOnlyOnNumericFields() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("author", "authorA", "title", "titleA", "rating", 5),
+ "b", map("author", "authorB", "title", "titleB", "rating", 4),
+ "c", map("author", "authorC", "title", "titleC", "rating", "3"),
+ "d", map("author", "authorD", "title", "titleD", "rating", 6));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+ AggregateQuerySnapshot snapshot =
+ collection.aggregate(average("rating"), AggregateField.count()).get().get();
+ assertThat(snapshot.get(average("rating"))).isEqualTo(5);
+ assertThat(snapshot.get(AggregateField.count())).isEqualTo(4);
+ }
+
+ // Currently not allowed because it requires __name__, num index.
+ @Ignore
+ @Test
+ public void aggregatesWithDocumentReferenceCursors() throws Exception {
+ Map> testDocs =
+ map(
+ "a", map("num", 1),
+ "b", map("num", 2),
+ "c", map("num", 3),
+ "d", map("num", 4),
+ "e", map("num", 5));
+ CollectionReference collection = testCollectionWithDocs(testDocs);
+
+ AggregateQuerySnapshot snapshot =
+ collection
+ .orderBy(FieldPath.documentId())
+ .startAfter(collection.document("c"))
+ .aggregate(sum("num"))
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(9);
+
+ snapshot =
+ collection
+ .orderBy(FieldPath.documentId())
+ .startAt(collection.document("c"))
+ .aggregate(sum("num"))
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(12);
+
+ snapshot =
+ collection
+ .orderBy(FieldPath.documentId())
+ .endBefore(collection.document("c"))
+ .aggregate(sum("num"))
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(3);
+
+ snapshot =
+ collection
+ .orderBy(FieldPath.documentId())
+ .endAt(collection.document("c"))
+ .aggregate(sum("num"))
+ .get()
+ .get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(6);
+ }
+
+ CollectionReference addTwoDocsForCursorTesting() throws InterruptedException {
+ Map> testDocs =
+ map(
+ "a", map("num", 5, "foo", 1),
+ "b", map("num", 7, "foo", 2));
+ return testCollectionWithDocs(testDocs);
+ }
+
+ @Test
+ public void aggregateWithNoFilterNoOrderByNoCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(12);
+ }
+
+ @Test
+ public void aggregateWithEqualityFilterNoOrderByNoCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.whereEqualTo("num", 5).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(5);
+ }
+
+ @Test
+ public void aggregateWithInequalityFilterNoOrderByNoCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.whereGreaterThan("num", 5).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ @Test
+ public void aggregateWithNoFilterExplicitOrderByNoCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.orderBy("num").aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(12);
+ }
+
+ @Test
+ public void aggregateWithEqualityFilterExplicitOrderByNoCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.whereEqualTo("num", 5).orderBy("num").aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(5);
+ }
+
+ @Test
+ public void aggregateWithInequalityFilterExplicitOrderByNoCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query =
+ collection.whereGreaterThan("num", 5).orderBy("num").aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ @Test
+ public void aggregateNoFilterExplicitOrderByFieldValueCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.orderBy("num").startAfter(5).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This is expected to fail because it requires the `__name__, num` index.
+ @Ignore
+ @Test
+ public void aggregateNoFilterExplicitOrderByDocumentReferenceCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query =
+ collection
+ .orderBy(FieldPath.documentId())
+ .startAfter(collection.document("a"))
+ .aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This is expected to fail because it requires the `__name__, num` index.
+ @Ignore
+ @Test
+ public void aggregateNoFilterNoOrderByDocumentReferenceCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query = collection.startAfter(collection.document("a")).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This is expected to fail because it requires the `foo, __name__, num` index.
+ @Ignore
+ @Test
+ public void aggregateNoFilterExplicitOrderByDocumentSnapshotCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ DocumentSnapshot docSnapshot = collection.document("a").get().get();
+ AggregateQuery query = collection.orderBy("foo").startAfter(docSnapshot).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This just happens to work because the orderBy field matches the aggregation field.
+ @Test
+ public void aggregateNoFilterExplicitOrderByDocumentSnapshotCursor2() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ DocumentSnapshot docSnapshot = collection.document("a").get().get();
+ AggregateQuery query = collection.orderBy("num").startAfter(docSnapshot).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ @Test
+ public void aggregateEqualityFilterExplicitOrderByFieldValueCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query =
+ collection.whereEqualTo("num", 5).orderBy("num").startAt(5).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(5);
+ }
+
+ @Test
+ public void aggregateInequalityFilterExplicitOrderByFieldValueCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query =
+ collection.whereGreaterThan("num", 5).orderBy("num").startAt(6).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This is expected to fail because it requires the `__name__, num` index.
+ @Ignore
+ @Test
+ public void aggregateEqualityFilterExplicitOrderByDocumentReferenceCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query =
+ collection
+ .whereEqualTo("num", 7)
+ .orderBy(FieldPath.documentId())
+ .startAfter(collection.document("a"))
+ .aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // Full orderBy is provided.
+ @Test
+ public void aggregateInequalityFilterExplicitOrderByDocumentReferenceCursor() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ AggregateQuery query =
+ collection
+ .whereGreaterThan("num", 0)
+ .orderBy("num")
+ .orderBy(FieldPath.documentId())
+ .startAfter(5, collection.document("a"))
+ .aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This is expected to fail because it requires the `__name__, num` index.
+ @Ignore
+ @Test
+ public void aggregateEqualityFilterNoOrderByDocumentSnapshotReference() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ DocumentSnapshot docSnapshot = collection.document("a").get().get();
+ AggregateQuery query =
+ collection.whereEqualTo("num", 7).startAfter(docSnapshot).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This just happens to work because the orderBy field matches the aggregation field.
+ @Test
+ public void aggregateInequalityFilterNoOrderByDocumentSnapshotReference() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ DocumentSnapshot docSnapshot = collection.document("a").get().get();
+ AggregateQuery query =
+ collection.whereGreaterThan("num", 0).startAfter(docSnapshot).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+
+ // This is expected to fail because it requires the `foo, __name__, num` index.
+ @Ignore
+ @Test
+ public void aggregateInequalityFilterNoOrderByDocumentSnapshotReference2() throws Exception {
+ CollectionReference collection = addTwoDocsForCursorTesting();
+ DocumentSnapshot docSnapshot = collection.document("a").get().get();
+ AggregateQuery query =
+ collection.whereGreaterThan("foo", 0).startAfter(docSnapshot).aggregate(sum("num"));
+ AggregateQuerySnapshot snapshot = query.get().get();
+ assertThat(snapshot.get(sum("num"))).isEqualTo(7);
+ }
+}
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 b825ac86ec..22e371ca22 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
@@ -17,6 +17,7 @@
package com.google.cloud.firestore.it;
import static com.google.cloud.firestore.LocalFirestoreHelper.autoId;
+import static com.google.cloud.firestore.it.TestHelper.await;
import static com.google.cloud.firestore.it.TestHelper.isRunningAgainstFirestoreEmulator;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Collections.singletonMap;
@@ -26,11 +27,13 @@
import com.google.api.core.ApiFuture;
import com.google.auto.value.AutoValue;
import com.google.cloud.Timestamp;
+import com.google.cloud.firestore.AggregateField;
import com.google.cloud.firestore.AggregateQuery;
import com.google.cloud.firestore.AggregateQuerySnapshot;
import com.google.cloud.firestore.CollectionGroup;
import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.DocumentReference;
+import com.google.cloud.firestore.FieldPath;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.QueryDocumentSnapshot;
import com.google.cloud.firestore.TransactionOptions;
@@ -40,9 +43,6 @@
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
@@ -104,7 +104,7 @@ public void countShouldRespectOrderBy() throws Exception {
}
@Test
- public void countShouldRespectStartAtAndEndAt() throws Exception {
+ public void countShouldRespectStartAtAndEndAtWithDocumentSnapshotCursor() throws Exception {
CollectionReference collection = createCollectionWithDocuments(10).collection();
List documentSnapshots = collection.get().get().getDocuments();
AggregateQuerySnapshot snapshot =
@@ -117,6 +117,52 @@ public void countShouldRespectStartAtAndEndAt() throws Exception {
assertThat(snapshot.getCount()).isEqualTo(6);
}
+ @Test
+ public void countShouldRespectStartAtAndEndAtWithDocumentReferenceCursor() throws Exception {
+ CollectionReference collection = createCollectionWithDocuments(10).collection();
+ List documentSnapshots = collection.get().get().getDocuments();
+ AggregateQuerySnapshot snapshot =
+ collection
+ .orderBy(FieldPath.documentId())
+ .startAt(documentSnapshots.get(2).getReference())
+ .endAt(documentSnapshots.get(7).getReference())
+ .count()
+ .get()
+ .get();
+ assertThat(snapshot.getCount()).isEqualTo(6);
+ }
+
+ @Test
+ public void countShouldRespectStartAfterAndEndBeforeWithDocumentSnapshotCursor()
+ throws Exception {
+ CollectionReference collection = createCollectionWithDocuments(10).collection();
+ List documentSnapshots = collection.get().get().getDocuments();
+ AggregateQuerySnapshot snapshot =
+ collection
+ .startAfter(documentSnapshots.get(2))
+ .endBefore(documentSnapshots.get(7))
+ .count()
+ .get()
+ .get();
+ assertThat(snapshot.getCount()).isEqualTo(4);
+ }
+
+ @Test
+ public void countShouldRespectStartAfterAndEndBeforeWithDocumentReferenceCursor()
+ throws Exception {
+ CollectionReference collection = createCollectionWithDocuments(10).collection();
+ List documentSnapshots = collection.get().get().getDocuments();
+ AggregateQuerySnapshot snapshot =
+ collection
+ .orderBy(FieldPath.documentId())
+ .startAfter(documentSnapshots.get(2).getReference())
+ .endBefore(documentSnapshots.get(7).getReference())
+ .count()
+ .get()
+ .get();
+ assertThat(snapshot.getCount()).isEqualTo(4);
+ }
+
@Test
public void countQueriesShouldFailIfCollectionNameIsInvalid() {
CollectionReference collection = createEmptyCollection().document().collection("__invalid__");
@@ -133,6 +179,13 @@ public void countShouldReturnNumberOfDocumentsForCollectionGroups() throws Excep
assertThat(snapshot.getCount()).isEqualTo(13);
}
+ @Test
+ public void aggregateQuerySupportsCollectionGroups() throws Exception {
+ CollectionGroup collectionGroup = createCollectionGroupWithDocuments(13);
+ AggregateQuerySnapshot snapshot = collectionGroup.aggregate(AggregateField.count()).get().get();
+ assertThat(snapshot.getCount()).isEqualTo(13);
+ }
+
@Test
public void countShouldReturnNumberOfDocumentsForPartitionQuery() throws Exception {
CollectionReference collection = createCollectionWithDocuments(3).collection();
@@ -150,6 +203,15 @@ public void inFlightCountQueriesShouldCompleteSuccessfullyWhenFirestoreIsClosed(
assertThat(task.get().getCount()).isEqualTo(20);
}
+ @Test
+ public void inFlightAggregateQueriesShouldCompleteSuccessfullyWhenFirestoreIsClosed()
+ throws Exception {
+ CollectionReference collection = createCollectionWithDocuments(20).collection();
+ ApiFuture task = collection.aggregate(AggregateField.count()).get();
+ collection.getFirestore().close();
+ assertThat(task.get().getCount()).isEqualTo(20);
+ }
+
@Test
public void inFlightCountQueriesShouldCompleteSuccessfullyWhenFirestoreIsShutDownGracefully()
throws Exception {
@@ -160,10 +222,20 @@ public void inFlightCountQueriesShouldCompleteSuccessfullyWhenFirestoreIsShutDow
}
@Test
- public void inFlightCountQueriesShouldRunToCompletionWhenFirestoreIsShutDownForcefully()
+ public void
+ inFlightAggregationQueriesShouldCompleteSuccessfullyWhenFirestoreIsShutDownGracefully()
+ throws Exception {
+ CollectionReference collection = createCollectionWithDocuments(20).collection();
+ ApiFuture task = collection.aggregate(AggregateField.count()).get();
+ collection.getFirestore().shutdown();
+ assertThat(task.get().getCount()).isEqualTo(20);
+ }
+
+ @Test
+ public void inFlightAggregateQueriesShouldRunToCompletionWhenFirestoreIsShutDownForcefully()
throws Exception {
CollectionReference collection = createCollectionWithDocuments(20).collection();
- ApiFuture task = collection.count().get();
+ ApiFuture task = collection.aggregate(AggregateField.count()).get();
collection.getFirestore().shutdownNow();
await(task);
}
@@ -176,6 +248,14 @@ public void countQueriesShouldFailIfStartedOnAClosedFirestoreInstance() throws E
assertThrows(IllegalStateException.class, aggregateQuery::get);
}
+ @Test
+ public void aggregateQueriesShouldFailIfStartedOnAClosedFirestoreInstance() throws Exception {
+ CollectionReference collection = createEmptyCollection();
+ AggregateQuery aggregateQuery = collection.aggregate(AggregateField.count());
+ collection.getFirestore().close();
+ assertThrows(IllegalStateException.class, aggregateQuery::get);
+ }
+
@Test
public void countQueriesShouldFailIfStartedOnAShutDownFirestoreInstance() throws Exception {
CollectionReference collection = createEmptyCollection();
@@ -371,31 +451,6 @@ private static long msFromTimestamp(Timestamp timestamp) {
return (timestamp.getSeconds() * 1_000) + (timestamp.getNanos() / 1_000_000);
}
- /**
- * Blocks the calling thread until the given future completes. Note that this method does not
- * check the success or failure of the future; it returns regardless of its success or failure.
- */
- private static void await(ApiFuture> future) throws InterruptedException {
- AtomicBoolean done = new AtomicBoolean(false);
- ExecutorService executor = Executors.newSingleThreadExecutor();
- future.addListener(
- () -> {
- synchronized (done) {
- done.set(true);
- done.notifyAll();
- }
- },
- executor);
-
- synchronized (done) {
- while (!done.get()) {
- done.wait();
- }
- }
-
- executor.shutdown();
- }
-
@AutoValue
abstract static class CreatedCollectionInfo {
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java
index e8439bde13..54cfd86f0f 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java
@@ -16,7 +16,11 @@
package com.google.cloud.firestore.it;
+import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.Firestore;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
public final class TestHelper {
/** Make constructor private to prevent creating instances. */
@@ -26,4 +30,29 @@ private TestHelper() {}
static boolean isRunningAgainstFirestoreEmulator(Firestore firestore) {
return firestore.getOptions().getHost().startsWith("localhost:");
}
+
+ /**
+ * Blocks the calling thread until the given future completes. Note that this method does not
+ * check the success or failure of the future; it returns regardless of its success or failure.
+ */
+ public static void await(ApiFuture> future) throws InterruptedException {
+ AtomicBoolean done = new AtomicBoolean(false);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ future.addListener(
+ () -> {
+ synchronized (done) {
+ done.set(true);
+ done.notifyAll();
+ }
+ },
+ executor);
+
+ synchronized (done) {
+ while (!done.get()) {
+ done.wait();
+ }
+ }
+
+ executor.shutdown();
+ }
}