Skip to content

Public Count #4130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions firebase-firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions firebase-firestore/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<com.google.firebase.firestore.AggregateQuerySnapshot> 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<com.google.firebase.firestore.Blob> {
method public int compareTo(@NonNull com.google.firebase.firestore.Blob);
method @NonNull public static com.google.firebase.firestore.Blob fromBytes(@NonNull byte[]);
Expand Down Expand Up @@ -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<com.google.firebase.firestore.QuerySnapshot>);
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<com.google.firebase.firestore.QuerySnapshot>);
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<com.google.firebase.firestore.QuerySnapshot>);
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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());
}

Expand All @@ -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());
Expand Down Expand Up @@ -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());
}

Expand All @@ -213,40 +212,33 @@ 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(
collection
.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
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p><b>Subclassing Note</b>: 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<AggregateQuerySnapshot> get(@NonNull AggregateSource source) {
Expand All @@ -70,14 +69,35 @@ public Task<AggregateQuerySnapshot> get(@NonNull AggregateSource source) {
return tcs.getTask();
}

/**
* Compares this object with the given object for equality.
*
* <p>This object is considered "equal" to the other object if and only if all of the following
* conditions are satisfied:
*
* <ol>
* <li>{@code object} is a non-null instance of {@link AggregateQuery}.
* <li>{@code object} performs the same aggregations as this {@link AggregateQuery}.
* <li>The underlying {@link Query} of {@code object} compares equal to that of this object.
* </ol>
*
* @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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
* <p><b>Subclassing Note</b>: 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;
Expand All @@ -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.
*
* <p>This object is considered "equal" to the other object if and only if all of the following
* conditions are satisfied:
*
* <ol>
* <li>{@code object} is a non-null instance of {@link AggregateQuerySnapshot}.
* <li>The {@link AggregateQuery} of {@code object} compares equal to that of this object.
* <li>{@code object} has the same results as this object.
* </ol>
*
* @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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Requires client to be online.
* <p>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.
*
* <p>The {@link AggregateQuery} will fail if the server cannot be reached, such as if the client
* is offline.
*/
SERVER_DIRECT,
SERVER,
}
Loading