Skip to content

Commit a50da9f

Browse files
authored
Online Count with tentative package private api(#3847)
1 parent 1beb195 commit a50da9f

File tree

13 files changed

+754
-0
lines changed

13 files changed

+754
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore;
16+
17+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection;
18+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs;
19+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore;
20+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
21+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitForException;
22+
import static com.google.firebase.firestore.testutil.TestUtil.map;
23+
import static org.hamcrest.CoreMatchers.instanceOf;
24+
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.junit.Assert.assertEquals;
26+
import static org.junit.Assert.assertFalse;
27+
import static org.junit.Assert.assertNotEquals;
28+
import static org.junit.Assert.assertTrue;
29+
30+
import androidx.test.ext.junit.runners.AndroidJUnit4;
31+
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
32+
import org.junit.After;
33+
import org.junit.Before;
34+
import org.junit.Test;
35+
import org.junit.runner.RunWith;
36+
37+
@RunWith(AndroidJUnit4.class)
38+
public class CountTest {
39+
40+
@Before
41+
public void setUp() {
42+
// TODO(b/243368243): Remove this once backend is ready to support count.
43+
org.junit.Assume.assumeTrue(BuildConfig.USE_EMULATOR_FOR_TESTS);
44+
}
45+
46+
@After
47+
public void tearDown() {
48+
IntegrationTestUtil.tearDown();
49+
}
50+
51+
@Test
52+
public void testCountQueryEquals() {
53+
CollectionReference coll1 = testCollection("foo");
54+
CollectionReference coll1_same = coll1.firestore.collection(coll1.getPath());
55+
AggregateQuery query1 = coll1.count();
56+
AggregateQuery query1_same = coll1_same.count();
57+
AggregateQuery query2 =
58+
coll1.document("bar").collection("baz").whereEqualTo("a", 1).limit(100).count();
59+
AggregateQuery query2_same =
60+
coll1.document("bar").collection("baz").whereEqualTo("a", 1).limit(100).count();
61+
AggregateQuery query3 =
62+
coll1.document("bar").collection("baz").whereEqualTo("b", 1).orderBy("c").count();
63+
AggregateQuery query3_same =
64+
coll1.document("bar").collection("baz").whereEqualTo("b", 1).orderBy("c").count();
65+
66+
assertTrue(query1.equals(query1_same));
67+
assertTrue(query2.equals(query2_same));
68+
assertTrue(query3.equals(query3_same));
69+
70+
assertEquals(query1.hashCode(), query1_same.hashCode());
71+
assertEquals(query2.hashCode(), query2_same.hashCode());
72+
assertEquals(query3.hashCode(), query3_same.hashCode());
73+
74+
assertFalse(query1.equals(null));
75+
assertFalse(query1.equals("string"));
76+
assertFalse(query1.equals(query2));
77+
assertFalse(query2.equals(query3));
78+
assertNotEquals(query1.hashCode(), query2.hashCode());
79+
assertNotEquals(query2.hashCode(), query3.hashCode());
80+
}
81+
82+
@Test
83+
public void testCanRunCount() {
84+
CollectionReference collection =
85+
testCollectionWithDocs(
86+
map(
87+
"a", map("k", "a"),
88+
"b", map("k", "b"),
89+
"c", map("k", "c")));
90+
91+
AggregateQuerySnapshot snapshot =
92+
waitFor(collection.count().get(AggregateSource.SERVER_DIRECT));
93+
assertEquals(Long.valueOf(3), snapshot.getCount());
94+
}
95+
96+
@Test
97+
public void testCanRunCountWithFilters() {
98+
CollectionReference collection =
99+
testCollectionWithDocs(
100+
map(
101+
"a", map("k", "a"),
102+
"b", map("k", "b"),
103+
"c", map("k", "c")));
104+
105+
AggregateQuerySnapshot snapshot =
106+
waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER_DIRECT));
107+
assertEquals(Long.valueOf(1), snapshot.getCount());
108+
}
109+
110+
@Test
111+
public void testCanRunCountWithOrderBy() {
112+
CollectionReference collection =
113+
testCollectionWithDocs(
114+
map(
115+
"a", map("k", "a"),
116+
"b", map("k", "b"),
117+
"c", map("k", "c"),
118+
"d", map("absent", "d")));
119+
120+
AggregateQuerySnapshot snapshot =
121+
waitFor(collection.orderBy("k").count().get(AggregateSource.SERVER_DIRECT));
122+
// "d" is filtered out because it is ordered by "k".
123+
assertEquals(Long.valueOf(3), snapshot.getCount());
124+
}
125+
126+
@Test
127+
public void testTerminateDoesNotCrashWithFlyingCountQuery() {
128+
CollectionReference collection =
129+
testCollectionWithDocs(
130+
map(
131+
"a", map("k", "a"),
132+
"b", map("k", "b"),
133+
"c", map("k", "c")));
134+
135+
collection.orderBy("k").count().get(AggregateSource.SERVER_DIRECT);
136+
waitFor(collection.firestore.terminate());
137+
}
138+
139+
@Test
140+
public void testSnapshotEquals() {
141+
CollectionReference collection =
142+
testCollectionWithDocs(
143+
map(
144+
"a", map("k", "a"),
145+
"b", map("k", "b"),
146+
"c", map("k", "c")));
147+
148+
AggregateQuerySnapshot snapshot1 =
149+
waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER_DIRECT));
150+
AggregateQuerySnapshot snapshot1_same =
151+
waitFor(collection.whereEqualTo("k", "b").count().get(AggregateSource.SERVER_DIRECT));
152+
153+
AggregateQuerySnapshot snapshot2 =
154+
waitFor(collection.whereEqualTo("k", "a").count().get(AggregateSource.SERVER_DIRECT));
155+
waitFor(collection.document("d").set(map("k", "a")));
156+
AggregateQuerySnapshot snapshot2_different =
157+
waitFor(collection.whereEqualTo("k", "a").count().get(AggregateSource.SERVER_DIRECT));
158+
159+
assertTrue(snapshot1.equals(snapshot1_same));
160+
assertEquals(snapshot1.hashCode(), snapshot1_same.hashCode());
161+
assertTrue(snapshot1.getQuery().equals(collection.whereEqualTo("k", "b").count()));
162+
163+
assertFalse(snapshot1.equals(null));
164+
assertFalse(snapshot1.equals("string"));
165+
assertFalse(snapshot1.equals(snapshot2));
166+
assertNotEquals(snapshot1.hashCode(), snapshot2.hashCode());
167+
assertFalse(snapshot2.equals(snapshot2_different));
168+
assertNotEquals(snapshot2.hashCode(), snapshot2_different.hashCode());
169+
}
170+
171+
@Test
172+
public void testCanRunCollectionGroupQuery() {
173+
FirebaseFirestore db = testFirestore();
174+
// Use .document() to get a random collection group name to use but ensure it starts with 'b'
175+
// for predictable ordering.
176+
String collectionGroup = "b" + db.collection("foo").document().getId();
177+
178+
String[] docPaths =
179+
new String[] {
180+
"abc/123/${collectionGroup}/cg-doc1",
181+
"abc/123/${collectionGroup}/cg-doc2",
182+
"${collectionGroup}/cg-doc3",
183+
"${collectionGroup}/cg-doc4",
184+
"def/456/${collectionGroup}/cg-doc5",
185+
"${collectionGroup}/virtual-doc/nested-coll/not-cg-doc",
186+
"x${collectionGroup}/not-cg-doc",
187+
"${collectionGroup}x/not-cg-doc",
188+
"abc/123/${collectionGroup}x/not-cg-doc",
189+
"abc/123/x${collectionGroup}/not-cg-doc",
190+
"abc/${collectionGroup}"
191+
};
192+
WriteBatch batch = db.batch();
193+
for (String path : docPaths) {
194+
batch.set(db.document(path.replace("${collectionGroup}", collectionGroup)), map("x", 1));
195+
}
196+
waitFor(batch.commit());
197+
198+
AggregateQuerySnapshot snapshot =
199+
waitFor(db.collectionGroup(collectionGroup).count().get(AggregateSource.SERVER_DIRECT));
200+
assertEquals(
201+
Long.valueOf(5), // "cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5",
202+
snapshot.getCount());
203+
}
204+
205+
@Test
206+
public void testCanRunCountWithFiltersAndLimits() {
207+
CollectionReference collection =
208+
testCollectionWithDocs(
209+
map(
210+
"a", map("k", "a"),
211+
"b", map("k", "a"),
212+
"c", map("k", "a"),
213+
"d", map("k", "d")));
214+
215+
AggregateQuerySnapshot snapshot =
216+
waitFor(
217+
collection.whereEqualTo("k", "a").limit(2).count().get(AggregateSource.SERVER_DIRECT));
218+
assertEquals(Long.valueOf(2), snapshot.getCount());
219+
220+
snapshot =
221+
waitFor(
222+
collection
223+
.whereEqualTo("k", "a")
224+
.limitToLast(2)
225+
.count()
226+
.get(AggregateSource.SERVER_DIRECT));
227+
assertEquals(Long.valueOf(2), snapshot.getCount());
228+
229+
snapshot =
230+
waitFor(
231+
collection
232+
.whereEqualTo("k", "d")
233+
.limitToLast(1000)
234+
.count()
235+
.get(AggregateSource.SERVER_DIRECT));
236+
assertEquals(Long.valueOf(1), snapshot.getCount());
237+
}
238+
239+
@Test
240+
public void testCanRunCountOnNonExistentCollection() {
241+
CollectionReference collection = testFirestore().collection("random-coll");
242+
243+
AggregateQuerySnapshot snapshot =
244+
waitFor(collection.count().get(AggregateSource.SERVER_DIRECT));
245+
assertEquals(Long.valueOf(0), snapshot.getCount());
246+
247+
snapshot =
248+
waitFor(collection.whereEqualTo("k", 100).count().get(AggregateSource.SERVER_DIRECT));
249+
assertEquals(Long.valueOf(0), snapshot.getCount());
250+
}
251+
252+
@Test
253+
public void testFailWithoutNetwork() {
254+
CollectionReference collection =
255+
testCollectionWithDocs(
256+
map(
257+
"a", map("k", "a"),
258+
"b", map("k", "b"),
259+
"c", map("k", "c")));
260+
waitFor(collection.getFirestore().disableNetwork());
261+
262+
Exception e = waitForException(collection.count().get(AggregateSource.SERVER_DIRECT));
263+
assertThat(e, instanceOf(FirebaseFirestoreException.class));
264+
assertEquals(
265+
FirebaseFirestoreException.Code.UNAVAILABLE, ((FirebaseFirestoreException) e).getCode());
266+
267+
waitFor(collection.getFirestore().enableNetwork());
268+
AggregateQuerySnapshot snapshot =
269+
waitFor(collection.count().get(AggregateSource.SERVER_DIRECT));
270+
assertEquals(Long.valueOf(3), snapshot.getCount());
271+
}
272+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.android.gms.tasks.Task;
19+
import com.google.android.gms.tasks.TaskCompletionSource;
20+
import com.google.firebase.firestore.util.Executors;
21+
import com.google.firebase.firestore.util.Preconditions;
22+
23+
/**
24+
* A {@code AggregateQuery} computes some aggregation statistics from the result set of a base
25+
* {@link Query}.
26+
*
27+
* <p><b>Subclassing Note</b>: Cloud Firestore classes are not meant to be subclassed except for use
28+
* in test mocks. Subclassing is not supported in production code and new SDK releases may break
29+
* code that does so.
30+
*/
31+
class AggregateQuery {
32+
// The base query.
33+
private final Query query;
34+
35+
AggregateQuery(@NonNull Query query) {
36+
this.query = query;
37+
}
38+
39+
/** Returns the base {@link Query} for this aggregate query. */
40+
@NonNull
41+
public Query getQuery() {
42+
return query;
43+
}
44+
45+
/**
46+
* Executes the aggregate query and returns the results as a {@code AggregateQuerySnapshot}.
47+
*
48+
* @param source A value to configure the get behavior.
49+
* @return A Task that will be resolved with the results of the {@code AggregateQuery}.
50+
*/
51+
@NonNull
52+
public Task<AggregateQuerySnapshot> get(@NonNull AggregateSource source) {
53+
Preconditions.checkNotNull(source, "AggregateSource must not be null");
54+
TaskCompletionSource<AggregateQuerySnapshot> tcs = new TaskCompletionSource<>();
55+
query
56+
.firestore
57+
.getClient()
58+
.runCountQuery(query.query)
59+
.continueWith(
60+
Executors.DIRECT_EXECUTOR,
61+
(task) -> {
62+
if (task.isSuccessful()) {
63+
tcs.setResult(new AggregateQuerySnapshot(this, task.getResult()));
64+
} else {
65+
tcs.setException(task.getException());
66+
}
67+
return null;
68+
});
69+
70+
return tcs.getTask();
71+
}
72+
73+
@Override
74+
public boolean equals(Object o) {
75+
if (this == o) return true;
76+
if (!(o instanceof AggregateQuery)) return false;
77+
AggregateQuery that = (AggregateQuery) o;
78+
return query.equals(that.query);
79+
}
80+
81+
@Override
82+
public int hashCode() {
83+
return query.hashCode();
84+
}
85+
}

0 commit comments

Comments
 (0)