diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index fbe940ff32b..9dfb14ef0da 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -3,8 +3,21 @@ by opting into a release at [go/firebase-android-release](http:go/firebase-android-release) (Googlers only). # Unreleased +- [feature] Added `Query.count()`, which fetches the number of documents in the + result set without actually downloading the documents (#4130). + +# 24.3.1 +- [changed] Updated dependency of `io.grpc.*` to its latest version (v1.48.1). + +# 24.3.0 +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). + +# 24.2.2 - [fixed] Fixed an issue `waitForPendingWrites()` could lead to NullPointerException. +# 24.2.1 +- [changed] Internal refactor and test improvements. + # 24.2.0 - [feature] Added `TransactionOptions` to control how many times a transaction will retry commits before failing. diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index d7a43041af9..edf6c6ed73b 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -19,6 +19,20 @@ package com.google.firebase { package com.google.firebase.firestore { + public class AggregateQuery { + method @NonNull public com.google.android.gms.tasks.Task get(@NonNull com.google.firebase.firestore.AggregateSource); + method @NonNull public com.google.firebase.firestore.Query getQuery(); + } + + public class AggregateQuerySnapshot { + method public long getCount(); + method @NonNull public com.google.firebase.firestore.AggregateQuery getQuery(); + } + + public enum AggregateSource { + enum_constant public static final com.google.firebase.firestore.AggregateSource SERVER; + } + public class Blob implements java.lang.Comparable { method public int compareTo(@NonNull com.google.firebase.firestore.Blob); method @NonNull public static com.google.firebase.firestore.Blob fromBytes(@NonNull byte[]); @@ -290,6 +304,7 @@ package com.google.firebase.firestore { method @NonNull public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(@NonNull com.google.firebase.firestore.MetadataChanges, @NonNull com.google.firebase.firestore.EventListener); method @NonNull public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(@NonNull java.util.concurrent.Executor, @NonNull com.google.firebase.firestore.MetadataChanges, @NonNull com.google.firebase.firestore.EventListener); method @NonNull public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(@NonNull android.app.Activity, @NonNull com.google.firebase.firestore.MetadataChanges, @NonNull com.google.firebase.firestore.EventListener); + method @NonNull public com.google.firebase.firestore.AggregateQuery count(); method @NonNull public com.google.firebase.firestore.Query endAt(@NonNull com.google.firebase.firestore.DocumentSnapshot); method @NonNull public com.google.firebase.firestore.Query endAt(java.lang.Object...); method @NonNull public com.google.firebase.firestore.Query endBefore(@NonNull com.google.firebase.firestore.DocumentSnapshot); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java index 5dffb4a70c4..155de0dc63a 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java @@ -88,9 +88,8 @@ public void testCanRunCount() { "b", map("k", "b"), "c", map("k", "c"))); - AggregateQuerySnapshot snapshot = - waitFor(collection.count().get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(3), snapshot.getCount()); + AggregateQuerySnapshot snapshot = waitFor(collection.count().get(AggregateSource.SERVER)); + assertEquals(3L, snapshot.getCount()); } @Test @@ -103,8 +102,8 @@ public void testCanRunCountWithFilters() { "c", map("k", "c"))); AggregateQuerySnapshot snapshot = - waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(1), snapshot.getCount()); + waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER)); + assertEquals(1L, snapshot.getCount()); } @Test @@ -118,9 +117,9 @@ public void testCanRunCountWithOrderBy() { "d", map("absent", "d"))); AggregateQuerySnapshot snapshot = - waitFor(collection.orderBy("k").count().get(AggregateSource.SERVER_DIRECT)); + waitFor(collection.orderBy("k").count().get(AggregateSource.SERVER)); // "d" is filtered out because it is ordered by "k". - assertEquals(Long.valueOf(3), snapshot.getCount()); + assertEquals(3L, snapshot.getCount()); } @Test @@ -132,7 +131,7 @@ public void testTerminateDoesNotCrashWithFlyingCountQuery() { "b", map("k", "b"), "c", map("k", "c"))); - collection.orderBy("k").count().get(AggregateSource.SERVER_DIRECT); + collection.orderBy("k").count().get(AggregateSource.SERVER); waitFor(collection.firestore.terminate()); } @@ -146,15 +145,15 @@ public void testSnapshotEquals() { "c", map("k", "c"))); AggregateQuerySnapshot snapshot1 = - waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER_DIRECT)); + waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER)); AggregateQuerySnapshot snapshot1_same = - waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER_DIRECT)); + waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER)); AggregateQuerySnapshot snapshot2 = - waitFor(collection.whereEqualTo("k", "a").count().get(AggregateSource.SERVER_DIRECT)); + waitFor(collection.whereEqualTo("k", "a").count().get(AggregateSource.SERVER)); waitFor(collection.document("d").set(map("k", "a"))); AggregateQuerySnapshot snapshot2_different = - waitFor(collection.whereEqualTo("k", "a").count().get(AggregateSource.SERVER_DIRECT)); + waitFor(collection.whereEqualTo("k", "a").count().get(AggregateSource.SERVER)); assertTrue(snapshot1.equals(snapshot1_same)); assertEquals(snapshot1.hashCode(), snapshot1_same.hashCode()); @@ -196,9 +195,9 @@ public void testCanRunCollectionGroupQuery() { waitFor(batch.commit()); AggregateQuerySnapshot snapshot = - waitFor(db.collectionGroup(collectionGroup).count().get(AggregateSource.SERVER_DIRECT)); + waitFor(db.collectionGroup(collectionGroup).count().get(AggregateSource.SERVER)); assertEquals( - Long.valueOf(5), // "cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5", + 5L, // "cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5", snapshot.getCount()); } @@ -213,18 +212,13 @@ public void testCanRunCountWithFiltersAndLimits() { "d", map("k", "d"))); AggregateQuerySnapshot snapshot = - waitFor( - collection.whereEqualTo("k", "a").limit(2).count().get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(2), snapshot.getCount()); + waitFor(collection.whereEqualTo("k", "a").limit(2).count().get(AggregateSource.SERVER)); + assertEquals(2L, snapshot.getCount()); snapshot = waitFor( - collection - .whereEqualTo("k", "a") - .limitToLast(2) - .count() - .get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(2), snapshot.getCount()); + collection.whereEqualTo("k", "a").limitToLast(2).count().get(AggregateSource.SERVER)); + assertEquals(2L, snapshot.getCount()); snapshot = waitFor( @@ -232,21 +226,19 @@ public void testCanRunCountWithFiltersAndLimits() { .whereEqualTo("k", "d") .limitToLast(1000) .count() - .get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(1), snapshot.getCount()); + .get(AggregateSource.SERVER)); + assertEquals(1L, snapshot.getCount()); } @Test public void testCanRunCountOnNonExistentCollection() { CollectionReference collection = testFirestore().collection("random-coll"); - AggregateQuerySnapshot snapshot = - waitFor(collection.count().get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(0), snapshot.getCount()); + AggregateQuerySnapshot snapshot = waitFor(collection.count().get(AggregateSource.SERVER)); + assertEquals(0L, snapshot.getCount()); - snapshot = - waitFor(collection.whereEqualTo("k", 100).count().get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(0), snapshot.getCount()); + snapshot = waitFor(collection.whereEqualTo("k", 100).count().get(AggregateSource.SERVER)); + assertEquals(0L, snapshot.getCount()); } @Test @@ -259,14 +251,13 @@ public void testFailWithoutNetwork() { "c", map("k", "c"))); waitFor(collection.getFirestore().disableNetwork()); - Exception e = waitForException(collection.count().get(AggregateSource.SERVER_DIRECT)); + Exception e = waitForException(collection.count().get(AggregateSource.SERVER)); assertThat(e, instanceOf(FirebaseFirestoreException.class)); assertEquals( FirebaseFirestoreException.Code.UNAVAILABLE, ((FirebaseFirestoreException) e).getCode()); waitFor(collection.getFirestore().enableNetwork()); - AggregateQuerySnapshot snapshot = - waitFor(collection.count().get(AggregateSource.SERVER_DIRECT)); - assertEquals(Long.valueOf(3), snapshot.getCount()); + AggregateQuerySnapshot snapshot = waitFor(collection.count().get(AggregateSource.SERVER)); + assertEquals(3L, snapshot.getCount()); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java index 179bc6e1b2d..cb7f1684759 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java @@ -21,32 +21,31 @@ import com.google.firebase.firestore.util.Preconditions; /** - * A {@code AggregateQuery} computes some aggregation statistics from the result set of a base - * {@link Query}. + * A query that calculates aggregations over an underlying query. * *

Subclassing Note: Cloud Firestore classes are not meant to be subclassed except for use * in test mocks. Subclassing is not supported in production code and new SDK releases may break * code that does so. */ -class AggregateQuery { - // The base query. +public class AggregateQuery { + private final Query query; AggregateQuery(@NonNull Query query) { this.query = query; } - /** Returns the base {@link Query} for this aggregate query. */ + /** Returns the query whose aggregations will be calculated by this object. */ @NonNull public Query getQuery() { return query; } /** - * Executes the aggregate query and returns the results as a {@code AggregateQuerySnapshot}. + * Executes this query. * - * @param source A value to configure the get behavior. - * @return A Task that will be resolved with the results of the {@code AggregateQuery}. + * @param source The source from which to acquire the aggregate results. + * @return A {@link Task} that will be resolved with the results of the query. */ @NonNull public Task get(@NonNull AggregateSource source) { @@ -70,14 +69,35 @@ public Task get(@NonNull AggregateSource source) { return tcs.getTask(); } + /** + * Compares this object with the given object for equality. + * + *

This object is considered "equal" to the other object if and only if all of the following + * conditions are satisfied: + * + *

    + *
  1. {@code object} is a non-null instance of {@link AggregateQuery}. + *
  2. {@code object} performs the same aggregations as this {@link AggregateQuery}. + *
  3. The underlying {@link Query} of {@code object} compares equal to that of this object. + *
+ * + * @param object The object to compare to this object for equality. + * @return {@code true} if this object is "equal" to the given object, as defined above, or {@code + * false} otherwise. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof AggregateQuery)) return false; - AggregateQuery that = (AggregateQuery) o; - return query.equals(that.query); + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof AggregateQuery)) return false; + AggregateQuery other = (AggregateQuery) object; + return query.equals(other.query); } + /** + * Calculates and returns the hash code for this object. + * + * @return the hash code for this object. + */ @Override public int hashCode() { return query.hashCode(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java index 0ec69134471..2d8ee7be8f0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java @@ -17,17 +17,16 @@ import static com.google.firebase.firestore.util.Preconditions.checkNotNull; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import java.util.Objects; /** - * A {@code AggregateQuerySnapshot} contains results of a {@link AggregateQuery}. + * The results of executing an {@link AggregateQuery}. * *

Subclassing Note: Cloud Firestore classes are not meant to be subclassed except for use * in test mocks. Subclassing is not supported in production code and new SDK releases may break * code that does so. */ -class AggregateQuerySnapshot { +public class AggregateQuerySnapshot { private final long count; private final AggregateQuery query; @@ -38,29 +37,46 @@ class AggregateQuerySnapshot { this.count = count; } - /** @return The original {@link AggregateQuery} this snapshot is a result of. */ + /** Returns the query that was executed to produce this result. */ @NonNull public AggregateQuery getQuery() { return query; } - /** - * @return The result of a document count aggregation. Returns null if no count aggregation is - * available in the result. - */ - @Nullable - public Long getCount() { + /** Returns the number of documents in the result set of the underlying query. */ + public long getCount() { return count; } + /** + * Compares this object with the given object for equality. + * + *

This object is considered "equal" to the other object if and only if all of the following + * conditions are satisfied: + * + *

    + *
  1. {@code object} is a non-null instance of {@link AggregateQuerySnapshot}. + *
  2. The {@link AggregateQuery} of {@code object} compares equal to that of this object. + *
  3. {@code object} has the same results as this object. + *
+ * + * @param object The object to compare to this object for equality. + * @return {@code true} if this object is "equal" to the given object, as defined above, or {@code + * false} otherwise. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof AggregateQuerySnapshot)) return false; - AggregateQuerySnapshot snapshot = (AggregateQuerySnapshot) o; - return count == snapshot.count && query.equals(snapshot.query); + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof AggregateQuerySnapshot)) return false; + AggregateQuerySnapshot other = (AggregateQuerySnapshot) object; + return count == other.count && query.equals(other.query); } + /** + * Calculates and returns the hash code for this object. + * + * @return the hash code for this object. + */ @Override public int hashCode() { return Objects.hash(count, query); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateSource.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateSource.java index afb235df184..31402ba99e4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateSource.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateSource.java @@ -14,13 +14,23 @@ package com.google.firebase.firestore; -/** Configures the behavior of {@link AggregateQuery#get}. */ -enum AggregateSource { +/** + * The sources from which an {@link AggregateQuery} can retrieve its results. + * + * @see AggregateQuery#get + */ +public enum AggregateSource { /** - * Reach to the Firestore backend and surface the result verbatim, that is no local documents or - * mutations in the SDK cache will be included in the surfaced result. + * Perform the aggregation on the server and download the result. * - *

Requires client to be online. + *

The result received from the server is presented, unaltered, without considering any local + * state. That is, documents in the local cache are not taken into consideration, neither are + * local modifications not yet synchronized with the server. Previously-downloaded results, if + * any, are not used: every request using this source necessarily involves a round trip to the + * server. + * + *

The {@link AggregateQuery} will fail if the server cannot be reached, such as if the client + * is offline. */ - SERVER_DIRECT, + SERVER, } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index 125b93c53a4..8db1e9ff8db 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -1224,13 +1224,19 @@ private void validateHasExplicitOrderByForLimitToLast() { } /** - * Creates an {@link AggregateQuery} counting the number of documents matching this query. + * Returns a query that counts the documents in the result set of this query. * - * @return An {@link AggregateQuery} object that can be used to count the number of documents in - * the result set of this query. + *

The returned query, when executed, counts the documents in the result set of this query + * without actually downloading the documents. + * + *

Using the returned query to count the documents is efficient because only the final count, + * not the documents' data, is downloaded. The returned query can even count the documents if the + * result set would be prohibitively large to download entirely (e.g. thousands of documents). + * + * @return a query that counts the documents in the result set of this query. */ @NonNull - AggregateQuery count() { + public AggregateQuery count() { return new AggregateQuery(this); }