Skip to content

Commit 1c8d242

Browse files
authored
feat: Make count queries publicly available for use (#1042)
1 parent 9b4c26e commit 1c8d242

File tree

6 files changed

+115
-35
lines changed

6 files changed

+115
-35
lines changed

google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
import javax.annotation.Nonnull;
3434
import javax.annotation.Nullable;
3535

36-
// TODO(count) Make this class public
36+
/** A query that calculates aggregations over an underlying query. */
3737
@InternalExtensionOnly
38-
class AggregateQuery {
38+
public class AggregateQuery {
3939

4040
/**
4141
* The "alias" to specify in the {@link RunAggregationQueryRequest} proto when running a count
@@ -50,11 +50,17 @@ class AggregateQuery {
5050
this.query = query;
5151
}
5252

53+
/** Returns the query whose aggregations will be calculated by this object. */
5354
@Nonnull
5455
public Query getQuery() {
5556
return query;
5657
}
5758

59+
/**
60+
* Executes this query.
61+
*
62+
* @return An {@link ApiFuture} that will be resolved with the results of the query.
63+
*/
5864
@Nonnull
5965
public ApiFuture<AggregateQuerySnapshot> get() {
6066
return get(null);
@@ -131,6 +137,12 @@ public void onError(Throwable throwable) {
131137
public void onComplete() {}
132138
}
133139

140+
/**
141+
* Returns the {@link RunAggregationQueryRequest} that this AggregateQuery instance represents.
142+
* The request contain the serialized form of all aggregations and Query constraints.
143+
*
144+
* @return the serialized RunAggregationQueryRequest
145+
*/
134146
@Nonnull
135147
public RunAggregationQueryRequest toProto() {
136148
return toProto(null);
@@ -159,6 +171,17 @@ RunAggregationQueryRequest toProto(@Nullable final ByteString transactionId) {
159171
return request.build();
160172
}
161173

174+
/**
175+
* Returns an AggregateQuery instance that can be used to execute the provided {@link
176+
* RunAggregationQueryRequest}.
177+
*
178+
* <p>Only RunAggregationQueryRequests that pertain to the same project as the Firestore instance
179+
* can be deserialized.
180+
*
181+
* @param firestore a Firestore instance to apply the query to.
182+
* @param proto the serialized RunAggregationQueryRequest.
183+
* @return a AggregateQuery instance that can be used to execute the RunAggregationQueryRequest.
184+
*/
162185
@Nonnull
163186
public static AggregateQuery fromProto(Firestore firestore, RunAggregationQueryRequest proto) {
164187
RunQueryRequest runQueryRequest =
@@ -170,19 +193,40 @@ public static AggregateQuery fromProto(Firestore firestore, RunAggregationQueryR
170193
return new AggregateQuery(query);
171194
}
172195

196+
/**
197+
* Calculates and returns the hash code for this object.
198+
*
199+
* @return the hash code for this object.
200+
*/
173201
@Override
174202
public int hashCode() {
175203
return query.hashCode();
176204
}
177205

206+
/**
207+
* Compares this object with the given object for equality.
208+
*
209+
* <p>This object is considered "equal" to the other object if and only if all of the following
210+
* conditions are satisfied:
211+
*
212+
* <ol>
213+
* <li>{@code object} is a non-null instance of {@link AggregateQuery}.
214+
* <li>{@code object} performs the same aggregations as this {@link AggregateQuery}.
215+
* <li>The underlying {@link Query} of {@code object} compares equal to that of this object.
216+
* </ol>
217+
*
218+
* @param object The object to compare to this object for equality.
219+
* @return {@code true} if this object is "equal" to the given object, as defined above, or {@code
220+
* false} otherwise.
221+
*/
178222
@Override
179-
public boolean equals(Object obj) {
180-
if (obj == this) {
223+
public boolean equals(Object object) {
224+
if (object == this) {
181225
return true;
182-
} else if (!(obj instanceof AggregateQuery)) {
226+
} else if (!(object instanceof AggregateQuery)) {
183227
return false;
184228
}
185-
AggregateQuery other = (AggregateQuery) obj;
229+
AggregateQuery other = (AggregateQuery) object;
186230
return query.equals(other.query);
187231
}
188232
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import java.util.Objects;
2222
import javax.annotation.Nonnull;
2323

24-
// TODO(count) Make this class public
24+
/** The results of executing an {@link AggregateQuery}. */
2525
@InternalExtensionOnly
26-
class AggregateQuerySnapshot {
26+
public class AggregateQuerySnapshot {
2727

2828
@Nonnull private final AggregateQuery query;
2929
@Nonnull private final Timestamp readTime;
@@ -35,34 +35,60 @@ class AggregateQuerySnapshot {
3535
this.count = count;
3636
}
3737

38+
/** Returns the query that was executed to produce this result. */
3839
@Nonnull
3940
public AggregateQuery getQuery() {
4041
return query;
4142
}
4243

44+
/** Returns the time at which this snapshot was read. */
4345
@Nonnull
4446
public Timestamp getReadTime() {
4547
return readTime;
4648
}
4749

50+
/** Returns the number of documents in the result set of the underlying query. */
4851
public long getCount() {
4952
return count;
5053
}
5154

55+
/**
56+
* Compares this object with the given object for equality.
57+
*
58+
* <p>This object is considered "equal" to the other object if and only if all of the following
59+
* conditions are satisfied:
60+
*
61+
* <ol>
62+
* <li>{@code object} is a non-null instance of {@link AggregateQuerySnapshot}.
63+
* <li>The {@link AggregateQuery} of {@code object} compares equal to that of this object.
64+
* <li>{@code object} has the same results as this object.
65+
* </ol>
66+
*
67+
* @param object The object to compare to this object for equality.
68+
* @return {@code true} if this object is "equal" to the given object, as defined above, or {@code
69+
* false} otherwise.
70+
*/
5271
@Override
53-
public boolean equals(Object obj) {
54-
if (obj == this) {
72+
public boolean equals(Object object) {
73+
if (object == this) {
5574
return true;
56-
} else if (!(obj instanceof AggregateQuerySnapshot)) {
75+
} else if (!(object instanceof AggregateQuerySnapshot)) {
5776
return false;
5877
}
5978

60-
AggregateQuerySnapshot other = (AggregateQuerySnapshot) obj;
61-
return query.equals(other.query) && readTime.equals(other.readTime) && count == other.count;
79+
AggregateQuerySnapshot other = (AggregateQuerySnapshot) object;
80+
81+
// Don't check `readTime`, because `DocumentSnapshot.equals()` doesn't either.
82+
return query.equals(other.query) && count == other.count;
6283
}
6384

85+
/**
86+
* Calculates and returns the hash code for this object.
87+
*
88+
* @return the hash code for this object.
89+
*/
6490
@Override
6591
public int hashCode() {
66-
return Objects.hash(query, readTime, count);
92+
return Objects.hash(query, count);
6793
}
6894
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,9 +1846,20 @@ private boolean isRetryableError(Throwable throwable) {
18461846
return false;
18471847
}
18481848

1849-
// TODO(count) Make this method public
1849+
/**
1850+
* Returns a query that counts the documents in the result set of this query.
1851+
*
1852+
* <p>The returned query, when executed, counts the documents in the result set of this query
1853+
* <em>without actually downloading the documents</em>.
1854+
*
1855+
* <p>Using the returned query to count the documents is efficient because only the final count,
1856+
* not the documents' data, is downloaded. The returned query can even count the documents if the
1857+
* result set would be prohibitively large to download entirely (e.g. thousands of documents).
1858+
*
1859+
* @return a query that counts the documents in the result set of this query.
1860+
*/
18501861
@Nonnull
1851-
AggregateQuery count() {
1862+
public AggregateQuery count() {
18521863
return new AggregateQuery(this);
18531864
}
18541865

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,14 @@ public ApiFuture<QuerySnapshot> get(@Nonnull Query query) {
191191
return query.get(transactionId);
192192
}
193193

194-
// TODO(count) Make this method public
195194
/**
196195
* Returns the result from the provided aggregate query. Holds a pessimistic lock on all accessed
197196
* documents.
198197
*
199198
* @return The result of the aggregation.
200199
*/
201200
@Nonnull
202-
ApiFuture<AggregateQuerySnapshot> get(@Nonnull AggregateQuery query) {
201+
public ApiFuture<AggregateQuerySnapshot> get(@Nonnull AggregateQuery query) {
203202
Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG);
204203

205204
return query.get(transactionId);

google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQuerySnapshotTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ public void hashCodeShouldReturnDifferentHashCodeWhenConstructedDifferentAggrega
8484
}
8585

8686
@Test
87-
public void hashCodeShouldReturnDifferentHashCodeWhenConstructedDifferentTimestamp() {
87+
public void hashCodeShouldReturnSameHashCodeWhenConstructedDifferentTimestamp() {
8888
AggregateQuerySnapshot snapshot1 =
8989
new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
9090
AggregateQuerySnapshot snapshot2 =
9191
new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp2, 42);
92-
assertThat(snapshot1.hashCode()).isNotEqualTo(snapshot2.hashCode());
92+
assertThat(snapshot1.hashCode()).isEqualTo(snapshot2.hashCode());
9393
}
9494

9595
@Test
@@ -125,12 +125,12 @@ public void equalsShouldReturnFalseWhenGivenAnAggregateQuerySnapshotWithADiffere
125125
}
126126

127127
@Test
128-
public void equalsShouldReturnFalseWhenGivenAnAggregateQuerySnapshotWithADifferentReadTime() {
128+
public void equalsShouldReturnTrueWhenGivenAnAggregateQuerySnapshotWithADifferentReadTime() {
129129
AggregateQuerySnapshot snapshot1 =
130130
new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp, 42);
131131
AggregateQuerySnapshot snapshot2 =
132132
new AggregateQuerySnapshot(sampleAggregateQuery, sampleTimestamp2, 42);
133-
assertThat(snapshot1.equals(snapshot2)).isFalse();
133+
assertThat(snapshot1.equals(snapshot2)).isTrue();
134134
}
135135

136136
@Test

google-cloud-firestore/src/test/java/com/google/cloud/firestore/ITQueryCountTest.java renamed to google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryCountTest.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,29 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.google.cloud.firestore;
17+
package com.google.cloud.firestore.it;
1818

1919
import static com.google.cloud.firestore.LocalFirestoreHelper.autoId;
2020
import static com.google.common.truth.Truth.assertThat;
2121
import static java.util.Collections.singletonMap;
2222
import static org.junit.Assert.assertThrows;
23-
import static org.junit.Assume.assumeFalse;
2423
import static org.junit.Assume.assumeTrue;
2524

2625
import com.google.api.core.ApiFuture;
2726
import com.google.auto.value.AutoValue;
2827
import com.google.cloud.Timestamp;
28+
import com.google.cloud.firestore.AggregateQuery;
29+
import com.google.cloud.firestore.AggregateQuerySnapshot;
30+
import com.google.cloud.firestore.CollectionGroup;
31+
import com.google.cloud.firestore.CollectionReference;
32+
import com.google.cloud.firestore.DocumentReference;
33+
import com.google.cloud.firestore.Firestore;
34+
import com.google.cloud.firestore.FirestoreOptions;
35+
import com.google.cloud.firestore.Query;
36+
import com.google.cloud.firestore.QueryDocumentSnapshot;
37+
import com.google.cloud.firestore.TransactionOptions;
38+
import com.google.cloud.firestore.WriteBatch;
39+
import com.google.cloud.firestore.WriteResult;
2940
import com.google.common.base.Preconditions;
3041
import java.util.ArrayList;
3142
import java.util.List;
@@ -42,7 +53,6 @@
4253
import org.junit.runner.RunWith;
4354
import org.junit.runners.JUnit4;
4455

45-
// TODO(count) Move this class back into the "it" subdirectory.
4656
@RunWith(JUnit4.class)
4757
public class ITQueryCountTest {
4858

@@ -289,16 +299,6 @@ public void aggregateQueryInATransactionShouldRespectReadTime() throws Exception
289299
.build());
290300

291301
Long transactionCount = transactionFuture.get();
292-
293-
// Put this assumption as close to the end of this method as possible, so that at least the code
294-
// above can be _executed_, even if we end up skipping the assertThat() check below.
295-
assumeFalse(
296-
"Snapshot reads are not yet implemented in the Firestore emulator (b/220918135). "
297-
+ "As a result, this test will fail when run against the Firestore emulator "
298-
+ "because it will incorrectly ignore the read time and return the count "
299-
+ "of the documents at the current time.",
300-
isRunningAgainstFirestoreEmulator());
301-
302302
assertThat(transactionCount).isEqualTo(5);
303303
}
304304

0 commit comments

Comments
 (0)