Skip to content

Commit 2827026

Browse files
authored
Release limitToLast and query/target split. (#994)
* [1/3] Split Query and Target and make RemoteStore and LocalStore work with Targets (#963) * Split Query and Target and make RemoteStore and LocalStore work with Targets mostly. * Address comments 1 * [2/3] Make SyncEngine able to work with both Query and Target (#965) * Make sync engine work with both Query and Target * stopListening handles query:target mapping * Address comments * Merge limitToLast to correct remote branch (#984) * Make sync engine work with both Query and Target * stopListening handles query:target mapping * Add converting for limit to last * Implement limitToLast and added tests * Address comments * Address comments * Disallow double-releasing. * [3/3] Add spec tests for limitToLast and proper sync state update for new queries. (#990) * Add spec test to verify mirror queries being rejected. Also, applying sync state to new query if there are already active queries to the same target. * remove debug code. * Add change log * fix api.txt * Fix type
1 parent 09eaa70 commit 2827026

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+90745
-73643
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# Unreleased (21.3.0)
1+
# Unreleased (21.3.1)
2+
- [feature] Added `Query.limitToLast(n: long)`, which returns the last `n`
3+
documents as the result.
4+
5+
# 21.3.0
26
- [feature] Added `Query.whereIn()` and `Query.whereArrayContainsAny()` query
37
operators. `Query.whereIn()` finds documents where a specified field’s value
48
is IN a specified array. `Query.whereArrayContainsAny()` finds documents

firebase-firestore/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ package com.google.firebase.firestore {
268268
method @NonNull public com.google.firebase.firestore.FirebaseFirestore getFirestore();
269269
method public int hashCode();
270270
method @NonNull public com.google.firebase.firestore.Query limit(long);
271+
method @NonNull public com.google.firebase.firestore.Query limitToLast(long);
271272
method @NonNull public com.google.firebase.firestore.Query orderBy(@NonNull String);
272273
method @NonNull public com.google.firebase.firestore.Query orderBy(@NonNull com.google.firebase.firestore.FieldPath);
273274
method @NonNull public com.google.firebase.firestore.Query orderBy(@NonNull String, @NonNull com.google.firebase.firestore.Query.Direction);

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs;
2121
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore;
2222
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
23+
import static com.google.firebase.firestore.testutil.TestUtil.expectError;
2324
import static com.google.firebase.firestore.testutil.TestUtil.map;
2425
import static java.util.Arrays.asList;
2526
import static java.util.Collections.singletonList;
@@ -80,6 +81,78 @@ public void testLimitQueriesUsingDescendingSortOrder() {
8081
assertEquals(asList(map("k", "d", "sort", 2L), map("k", "c", "sort", 1L)), data);
8182
}
8283

84+
@Test
85+
public void testLimitToLastMustAlsoHaveExplicitOrderBy() {
86+
CollectionReference collection = testCollectionWithDocs(map());
87+
88+
Query query = collection.limitToLast(2);
89+
expectError(
90+
() -> waitFor(query.get()),
91+
"limitToLast() queries require specifying at least one orderBy() clause");
92+
}
93+
94+
// Two queries that mapped to the same target ID are referred to as
95+
// "mirror queries". An example for a mirror query is a limitToLast()
96+
// query and a limit() query that share the same backend Target ID.
97+
// Since limitToLast() queries are sent to the backend with a modified
98+
// orderBy() clause, they can map to the same target representation as
99+
// limit() query, even if both queries appear separate to the user.
100+
@Test
101+
public void testListenUnlistenRelistenSequenceOfMirrorQueries() {
102+
CollectionReference collection =
103+
testCollectionWithDocs(
104+
map(
105+
"a", map("k", "a", "sort", 0),
106+
"b", map("k", "b", "sort", 1),
107+
"c", map("k", "c", "sort", 1),
108+
"d", map("k", "d", "sort", 2)));
109+
110+
// Setup `limit` query.
111+
Query limit = collection.limit(2).orderBy("sort", Direction.ASCENDING);
112+
EventAccumulator<QuerySnapshot> limitAccumulator = new EventAccumulator<>();
113+
ListenerRegistration limitRegistration = limit.addSnapshotListener(limitAccumulator.listener());
114+
115+
// Setup mirroring `limitToLast` query.
116+
Query limitToLast = collection.limitToLast(2).orderBy("sort", Direction.DESCENDING);
117+
EventAccumulator<QuerySnapshot> limitToLastAccumulator = new EventAccumulator<>();
118+
ListenerRegistration limitToLastRegistration =
119+
limitToLast.addSnapshotListener(limitToLastAccumulator.listener());
120+
121+
// Verify both query get expected result.
122+
List<Map<String, Object>> data = querySnapshotToValues(limitAccumulator.await());
123+
assertEquals(asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L)), data);
124+
data = querySnapshotToValues(limitToLastAccumulator.await());
125+
assertEquals(asList(map("k", "b", "sort", 1L), map("k", "a", "sort", 0L)), data);
126+
127+
// Unlisten then re-listen limit query.
128+
limitRegistration.remove();
129+
limit.addSnapshotListener(limitAccumulator.listener());
130+
131+
// Verify `limit` query still works.
132+
data = querySnapshotToValues(limitAccumulator.await());
133+
assertEquals(asList(map("k", "a", "sort", 0L), map("k", "b", "sort", 1L)), data);
134+
135+
// Add a document that would change the result set.
136+
waitFor(collection.add(map("k", "e", "sort", -1)));
137+
138+
// Verify both query get expected result.
139+
data = querySnapshotToValues(limitAccumulator.await());
140+
assertEquals(asList(map("k", "e", "sort", -1L), map("k", "a", "sort", 0L)), data);
141+
data = querySnapshotToValues(limitToLastAccumulator.await());
142+
assertEquals(asList(map("k", "a", "sort", 0L), map("k", "e", "sort", -1L)), data);
143+
144+
// Unlisten to limitToLast, update a doc, then relisten to limitToLast
145+
limitToLastRegistration.remove();
146+
waitFor(collection.document("a").update(map("k", "a", "sort", -2)));
147+
limitToLast.addSnapshotListener(limitToLastAccumulator.listener());
148+
149+
// Verify both query get expected result.
150+
data = querySnapshotToValues(limitAccumulator.await());
151+
assertEquals(asList(map("k", "a", "sort", -2L), map("k", "e", "sort", -1L)), data);
152+
data = querySnapshotToValues(limitToLastAccumulator.await());
153+
assertEquals(asList(map("k", "e", "sort", -1L), map("k", "a", "sort", -2L)), data);
154+
}
155+
83156
@Test
84157
public void testKeyOrderIsDescendingForDescendingInequality() {
85158
CollectionReference collection =

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,12 @@ public void queriesWithNonPositiveLimitFail() {
401401
expectError(
402402
() -> collection.limit(-1),
403403
"Invalid Query. Query limit (-1) is invalid. Limit must be positive.");
404+
expectError(
405+
() -> collection.limitToLast(0),
406+
"Invalid Query. Query limitToLast (0) is invalid. Limit must be positive.");
407+
expectError(
408+
() -> collection.limitToLast(-1),
409+
"Invalid Query. Query limitToLast (-1) is invalid. Limit must be positive.");
404410
}
405411

406412
@Test

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -574,8 +574,8 @@ private Query orderBy(
574574
}
575575

576576
/**
577-
* Creates and returns a new {@code Query} that's additionally limited to only return up to the
578-
* specified number of documents.
577+
* Creates and returns a new {@code Query} that only returns the first matching documents up to
578+
* the specified number.
579579
*
580580
* @param limit The maximum number of items to return.
581581
* @return The created {@code Query}.
@@ -586,7 +586,26 @@ public Query limit(long limit) {
586586
throw new IllegalArgumentException(
587587
"Invalid Query. Query limit (" + limit + ") is invalid. Limit must be positive.");
588588
}
589-
return new Query(query.limit(limit), firestore);
589+
return new Query(query.limitToFirst(limit), firestore);
590+
}
591+
592+
/**
593+
* Creates and returns a new {@code Query} that only returns the last matching documents up to the
594+
* specified number.
595+
*
596+
* <p>You must specify at least one {@code orderBy} clause for {@code limitToLast} queries,
597+
* otherwise an exception will be thrown during execution.
598+
*
599+
* @param limit The maximum number of items to return.
600+
* @return The created {@code Query}.
601+
*/
602+
@NonNull
603+
public Query limitToLast(long limit) {
604+
if (limit <= 0) {
605+
throw new IllegalArgumentException(
606+
"Invalid Query. Query limitToLast (" + limit + ") is invalid. Limit must be positive.");
607+
}
608+
return new Query(query.limitToLast(limit), firestore);
590609
}
591610

592611
/**
@@ -832,6 +851,7 @@ public Task<QuerySnapshot> get() {
832851
*/
833852
@NonNull
834853
public Task<QuerySnapshot> get(@NonNull Source source) {
854+
validateHasExplicitOrderByForLimitToLast();
835855
if (source == Source.CACHE) {
836856
return firestore
837857
.getClient()
@@ -1010,6 +1030,7 @@ private ListenerRegistration addSnapshotListenerInternal(
10101030
ListenOptions options,
10111031
@Nullable Activity activity,
10121032
EventListener<QuerySnapshot> userListener) {
1033+
validateHasExplicitOrderByForLimitToLast();
10131034

10141035
// Convert from ViewSnapshots to QuerySnapshots.
10151036
EventListener<ViewSnapshot> viewListener =
@@ -1035,6 +1056,13 @@ private ListenerRegistration addSnapshotListenerInternal(
10351056
new ListenerRegistrationImpl(firestore.getClient(), queryListener, asyncListener));
10361057
}
10371058

1059+
private void validateHasExplicitOrderByForLimitToLast() {
1060+
if (query.hasLimitToLast() && query.getExplicitOrderBy().isEmpty()) {
1061+
throw new IllegalStateException(
1062+
"limitToLast() queries require specifying at least one orderBy() clause");
1063+
}
1064+
}
1065+
10381066
@Override
10391067
public boolean equals(Object o) {
10401068
if (this == o) {

0 commit comments

Comments
 (0)