Skip to content

Commit 14e2812

Browse files
wu-huiwilhuff
authored andcommitted
Release LimitToLast (#4616)
* [1/4] Split Query and Target, and make LocalStore/RemoteStore work with Target. (#4413) * [2/4]LimitToLast: Make SyncEngine work with Target (#4491) * [3/4] Implement LimitToLast (#4505)
1 parent 7a86bef commit 14e2812

Some content is hidden

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

65 files changed

+91674
-74142
lines changed

Firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [feature] Added a `limit(toLast:)` query operator, which returns the last
3+
matching documents up to the given limit.
24

35
# v1.8.3
46
- [changed] Internal improvements.

Firestore/Example/Tests/Integration/API/FIRQueryTests.mm

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#import "Firestore/Source/API/FIRQuery+Internal.h"
2222

2323
#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h"
24+
#import "Firestore/Example/Tests/Util/FSTHelpers.h"
2425
#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
2526

2627
@interface FIRQueryTests : FSTIntegrationTestCase
@@ -56,6 +57,84 @@ - (void)testLimitQueriesWithDescendingSortOrder {
5657
(@[ @{@"k" : @"d", @"sort" : @2}, @{@"k" : @"c", @"sort" : @1} ]));
5758
}
5859

60+
- (void)testLimitToLastMustAlsoHaveExplicitOrderBy {
61+
FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{}];
62+
FIRQuery *query = [collRef queryLimitedToLast:2];
63+
FSTAssertThrows(
64+
[query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error){
65+
}],
66+
@"limit(toLast:) queries require specifying at least one OrderBy() clause.");
67+
}
68+
69+
// Two queries that mapped to the same target ID are referred to as
70+
// "mirror queries". An example for a mirror query is a limitToLast()
71+
// query and a limit() query that share the same backend Target ID.
72+
// Since limitToLast() queries are sent to the backend with a modified
73+
// orderBy() clause, they can map to the same target representation as
74+
// limit() query, even if both queries appear separate to the user.
75+
- (void)testListenUnlistenRelistenSequenceOfMirrorQueries {
76+
FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
77+
@"a" : @{@"k" : @"a", @"sort" : @0},
78+
@"b" : @{@"k" : @"b", @"sort" : @1},
79+
@"c" : @{@"k" : @"c", @"sort" : @1},
80+
@"d" : @{@"k" : @"d", @"sort" : @2},
81+
}];
82+
83+
// Setup a `limit` query.
84+
FIRQuery *limit = [[collRef queryOrderedByField:@"sort" descending:NO] queryLimitedTo:2];
85+
FSTEventAccumulator *limitAccumulator = [FSTEventAccumulator accumulatorForTest:self];
86+
id<FIRListenerRegistration> limitRegistration =
87+
[limit addSnapshotListener:limitAccumulator.valueEventHandler];
88+
89+
// Setup a mirroring `limitToLast` query.
90+
FIRQuery *limitToLast = [[collRef queryOrderedByField:@"sort"
91+
descending:YES] queryLimitedToLast:2];
92+
FSTEventAccumulator *limitToLastAccumulator = [FSTEventAccumulator accumulatorForTest:self];
93+
id<FIRListenerRegistration> limitToLastRegistration =
94+
[limitToLast addSnapshotListener:limitToLastAccumulator.valueEventHandler];
95+
96+
// Verify both queries get expected result.
97+
FIRQuerySnapshot *snapshot = [limitAccumulator awaitEventWithName:@"Snapshot"];
98+
NSArray *expected = @[ @{@"k" : @"a", @"sort" : @0}, @{@"k" : @"b", @"sort" : @1} ];
99+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
100+
snapshot = [limitToLastAccumulator awaitEventWithName:@"Snapshot"];
101+
expected = @[ @{@"k" : @"b", @"sort" : @1}, @{@"k" : @"a", @"sort" : @0} ];
102+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
103+
104+
// Unlisten then re-listen limit query.
105+
[limitRegistration remove];
106+
[limit addSnapshotListener:[limitAccumulator valueEventHandler]];
107+
108+
// Verify limit query still works.
109+
snapshot = [limitAccumulator awaitEventWithName:@"Snapshot"];
110+
expected = @[ @{@"k" : @"a", @"sort" : @0}, @{@"k" : @"b", @"sort" : @1} ];
111+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
112+
113+
// Add a document that would change the result set.
114+
[self addDocumentRef:collRef data:@{@"k" : @"e", @"sort" : @-1}];
115+
116+
// Verify both queries get expected result.
117+
snapshot = [limitAccumulator awaitEventWithName:@"Snapshot"];
118+
expected = @[ @{@"k" : @"e", @"sort" : @-1}, @{@"k" : @"a", @"sort" : @0} ];
119+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
120+
snapshot = [limitToLastAccumulator awaitEventWithName:@"Snapshot"];
121+
expected = @[ @{@"k" : @"a", @"sort" : @0}, @{@"k" : @"e", @"sort" : @-1} ];
122+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
123+
124+
// Unlisten to limitToLast, update a doc, then relisten to limitToLast
125+
[limitToLastRegistration remove];
126+
[self updateDocumentRef:[collRef documentWithPath:@"a"] data:@{@"k" : @"a", @"sort" : @-2}];
127+
[limitToLast addSnapshotListener:[limitToLastAccumulator valueEventHandler]];
128+
129+
// Verify both queries get expected result.
130+
snapshot = [limitAccumulator awaitEventWithName:@"Snapshot"];
131+
expected = @[ @{@"k" : @"a", @"sort" : @-2}, @{@"k" : @"e", @"sort" : @-1} ];
132+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
133+
snapshot = [limitToLastAccumulator awaitEventWithName:@"Snapshot"];
134+
expected = @[ @{@"k" : @"e", @"sort" : @-1}, @{@"k" : @"a", @"sort" : @-2} ];
135+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), expected);
136+
}
137+
59138
- (void)testKeyOrderIsDescendingForDescendingInequality {
60139
FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
61140
@"a" : @{@"foo" : @42},

Firestore/Example/Tests/Integration/API/FIRValidationTests.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ - (void)testQueryWithNonPositiveLimitFails {
410410
@"Invalid Query. Query limit (0) is invalid. Limit must be positive.");
411411
FSTAssertThrows([[self collectionRef] queryLimitedTo:-1],
412412
@"Invalid Query. Query limit (-1) is invalid. Limit must be positive.");
413+
FSTAssertThrows([[self collectionRef] queryLimitedToLast:0],
414+
@"Invalid Query. Query limit (0) is invalid. Limit must be positive.");
415+
FSTAssertThrows([[self collectionRef] queryLimitedToLast:-1],
416+
@"Invalid Query. Query limit (-1) is invalid. Limit must be positive.");
413417
}
414418

415419
- (void)testNonEqualityQueriesOnNullOrNaNFail {

Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ bool IsOpen() const override {
102102
}
103103

104104
void WatchQuery(const QueryData& query) override {
105-
LOG_DEBUG("WatchQuery: %s: %s, %s", query.target_id(), query.query().ToString(),
105+
LOG_DEBUG("WatchQuery: %s: %s, %s", query.target_id(), query.target().ToString(),
106106
query.resume_token().ToString());
107107

108108
// Snapshot version is ignored on the wire

Firestore/Example/Tests/SpecTests/FSTSpecTests.mm

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@
8686
using firebase::firestore::remote::ExistenceFilter;
8787
using firebase::firestore::remote::DocumentWatchChange;
8888
using firebase::firestore::remote::ExistenceFilterWatchChange;
89-
using firebase::firestore::remote::InvalidQuery;
9089
using firebase::firestore::remote::WatchTargetChange;
9190
using firebase::firestore::remote::WatchTargetChangeState;
9291
using firebase::firestore::util::MakeString;
@@ -214,11 +213,18 @@ - (Query)parseQuery:(id)querySpec {
214213
std::shared_ptr<const std::string> collectionGroup =
215214
util::MakeStringPtr(queryDict[@"collectionGroup"]);
216215
Query query(std::move(resource_path), std::move(collectionGroup));
216+
217217
if (queryDict[@"limit"]) {
218218
NSNumber *limitNumber = queryDict[@"limit"];
219219
auto limit = static_cast<int32_t>(limitNumber.integerValue);
220-
query = query.WithLimit(limit);
220+
NSString *limitType = queryDict[@"limitType"];
221+
if ([limitType isEqualToString:@"LimitToFirst"]) {
222+
query = query.WithLimitToFirst(limit);
223+
} else {
224+
query = query.WithLimitToLast(limit);
225+
}
221226
}
227+
222228
if (queryDict[@"filters"]) {
223229
NSArray<NSArray<id> *> *filters = queryDict[@"filters"];
224230
for (NSArray<id> *filter in filters) {
@@ -228,6 +234,7 @@ - (Query)parseQuery:(id)querySpec {
228234
query = query.AddingFilter(Filter(key, op, value));
229235
}
230236
}
237+
231238
if (queryDict[@"orderBys"]) {
232239
NSArray *orderBys = queryDict[@"orderBys"];
233240
for (NSArray<NSString *> *orderBy in orderBys) {
@@ -239,7 +246,7 @@ - (Query)parseQuery:(id)querySpec {
239246
return query;
240247
} else {
241248
XCTFail(@"Invalid query: %@", querySpec);
242-
return InvalidQuery();
249+
return Query();
243250
}
244251
}
245252

@@ -651,21 +658,26 @@ - (void)validateExpectedState:(nullable NSDictionary *)expectedState {
651658
[self.driver setExpectedLimboDocuments:std::move(expectedLimboDocuments)];
652659
}
653660
if (expectedState[@"activeTargets"]) {
654-
__block std::unordered_map<TargetId, QueryData> expectedActiveTargets;
661+
__block ActiveTargetMap expectedActiveTargets;
655662
[expectedState[@"activeTargets"]
656663
enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, NSDictionary *queryData,
657664
BOOL *stop) {
658665
TargetId targetID = [targetIDString intValue];
659-
Query query = [self parseQuery:queryData[@"query"]];
660666
ByteString resumeToken = MakeResumeToken(queryData[@"resumeToken"]);
661-
// TODO(mcg): populate the purpose of the target once it's possible to encode that in
662-
// the spec tests. For now, hard-code that it's a listen despite the fact that it's not
663-
// always the right value.
664-
expectedActiveTargets[targetID] =
665-
QueryData(std::move(query), targetID, 0, QueryPurpose::Listen,
666-
SnapshotVersion::None(), SnapshotVersion::None(), std::move(resumeToken));
667+
NSArray *queriesJson = queryData[@"queries"];
668+
std::vector<QueryData> queries;
669+
for (id queryJson in queriesJson) {
670+
Query query = [self parseQuery:queryJson];
671+
// TODO(mcg): populate the purpose of the target once it's possible to encode that in
672+
// the spec tests. For now, hard-code that it's a listen despite the fact that it's
673+
// not always the right value.
674+
queries.push_back(QueryData(query.ToTarget(), targetID, 0, QueryPurpose::Listen,
675+
SnapshotVersion::None(), SnapshotVersion::None(),
676+
std::move(resumeToken)));
677+
}
678+
expectedActiveTargets[targetID] = std::make_pair(std::move(queries), resumeToken);
667679
}];
668-
[self.driver setExpectedActiveTargets:expectedActiveTargets];
680+
[self.driver setExpectedActiveTargets:std::move(expectedActiveTargets)];
669681
}
670682
}
671683

@@ -726,9 +738,10 @@ - (void)validateActiveTargets {
726738
// Create a copy so we can modify it below
727739
std::unordered_map<TargetId, QueryData> actualTargets = [self.driver activeTargets];
728740

729-
for (const auto &kv : [self.driver activeTargets]) {
741+
for (const auto &kv : [self.driver expectedActiveTargets]) {
730742
TargetId targetID = kv.first;
731-
const QueryData &queryData = kv.second;
743+
const std::pair<std::vector<QueryData>, ByteString> &queries = kv.second;
744+
const QueryData &queryData = queries.first[0];
732745

733746
auto found = actualTargets.find(targetID);
734747
XCTAssertNotEqual(found, actualTargets.end(), @"Expected active target not found: %s",
@@ -739,7 +752,7 @@ - (void)validateActiveTargets {
739752
// XCTAssertEqualObjects(actualTargets[targetID], queryData);
740753

741754
const QueryData &actual = found->second;
742-
XCTAssertEqual(actual.query(), queryData.query());
755+
XCTAssertEqual(actual.target(), queryData.target());
743756
XCTAssertEqual(actual.target_id(), queryData.target_id());
744757
XCTAssertEqual(actual.snapshot_version(), queryData.snapshot_version());
745758
XCTAssertEqual(actual.resume_token(), queryData.resume_token());

Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <map>
2020
#include <memory>
2121
#include <unordered_map>
22+
#include <utility>
2223
#include <vector>
2324

2425
#include "Firestore/core/src/firebase/firestore/auth/user.h"
@@ -31,6 +32,8 @@
3132
#include "Firestore/core/src/firebase/firestore/model/mutation.h"
3233
#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h"
3334
#include "Firestore/core/src/firebase/firestore/model/types.h"
35+
#include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h"
36+
#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h"
3437
#include "Firestore/core/src/firebase/firestore/remote/watch_change.h"
3538
#include "Firestore/core/src/firebase/firestore/util/async_queue.h"
3639
#include "Firestore/core/src/firebase/firestore/util/empty.h"
@@ -48,6 +51,14 @@ class Persistence;
4851
namespace core = firebase::firestore::core;
4952
namespace local = firebase::firestore::local;
5053
namespace model = firebase::firestore::model;
54+
namespace nanopb = firebase::firestore::nanopb;
55+
56+
// A map holds expected information about currently active targets. The keys are
57+
// target ID, and the values are a vector of `QueryData`s mapped to the target and
58+
// the target's resume token.
59+
using ActiveTargetMap =
60+
std::unordered_map<model::TargetId,
61+
std::pair<std::vector<local::QueryData>, nanopb::ByteString>>;
5162

5263
NS_ASSUME_NONNULL_BEGIN
5364

@@ -337,11 +348,9 @@ typedef std::unordered_map<firebase::firestore::auth::User,
337348
- (const std::unordered_map<firebase::firestore::model::TargetId, local::QueryData> &)activeTargets;
338349

339350
/** The expected set of active targets, keyed by target ID. */
340-
- (const std::unordered_map<firebase::firestore::model::TargetId, local::QueryData> &)
341-
expectedActiveTargets;
351+
- (const ActiveTargetMap &)expectedActiveTargets;
342352

343-
- (void)setExpectedActiveTargets:
344-
(const std::unordered_map<firebase::firestore::model::TargetId, local::QueryData> &)targets;
353+
- (void)setExpectedActiveTargets:(ActiveTargetMap)targets;
345354

346355
@end
347356

Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ @implementation FSTSyncEngineTestDriver {
165165

166166
DelayedConstructor<EventManager> _eventManager;
167167

168-
std::unordered_map<TargetId, QueryData> _expectedActiveTargets;
168+
// Set of active targets, keyed by target Id, mapped to corresponding resume token,
169+
// and list of `QueryData`.
170+
ActiveTargetMap _expectedActiveTargets;
169171

170172
// ivar is declared as mutable.
171173
std::unordered_map<User, NSMutableArray<FSTOutstandingWrite *> *, HashUser> _outstandingWrites;
@@ -478,12 +480,12 @@ - (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *
478480
return _datastore->ActiveTargets();
479481
}
480482

481-
- (const std::unordered_map<TargetId, QueryData> &)expectedActiveTargets {
483+
- (const ActiveTargetMap &)expectedActiveTargets {
482484
return _expectedActiveTargets;
483485
}
484486

485-
- (void)setExpectedActiveTargets:(const std::unordered_map<TargetId, QueryData> &)targets {
486-
_expectedActiveTargets = targets;
487+
- (void)setExpectedActiveTargets:(ActiveTargetMap)targets {
488+
_expectedActiveTargets = std::move(targets);
487489
}
488490

489491
#pragma mark - Helper Methods

Firestore/Example/Tests/SpecTests/json/collection_spec_test.json

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
"expectedState": {
2121
"activeTargets": {
2222
"2": {
23-
"query": {
24-
"path": "collection",
25-
"filters": [],
26-
"orderBys": []
27-
},
23+
"queries": [
24+
{
25+
"path": "collection",
26+
"filters": [],
27+
"orderBys": []
28+
}
29+
],
2830
"resumeToken": ""
2931
}
3032
}
@@ -117,11 +119,13 @@
117119
"expectedState": {
118120
"activeTargets": {
119121
"2": {
120-
"query": {
121-
"path": "collection",
122-
"filters": [],
123-
"orderBys": []
124-
},
122+
"queries": [
123+
{
124+
"path": "collection",
125+
"filters": [],
126+
"orderBys": []
127+
}
128+
],
125129
"resumeToken": ""
126130
}
127131
}

0 commit comments

Comments
 (0)