Skip to content

Commit 2fc6e4c

Browse files
author
Michael Lehenbauer
committed
Collection Group queries.
Port of firebase/firebase-js-sdk#1440.
1 parent 1d8d86e commit 2fc6e4c

39 files changed

+2195
-65
lines changed

Firestore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Unreleased
2+
- [feature] You can now query across all collections in your database with a
3+
given collection ID using the `Firestore.collectionGroup()` method.
4+
25

36
# v1.0.1
47
- [changed] Internal improvements.

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,11 @@
172172
6EDD3B6020BF25AE00C33877 /* FSTFuzzTestsPrincipal.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */; };
173173
6F3CAC76D918D6B0917EDF92 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; };
174174
71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; };
175+
731541612214AFFA0037F4DC /* query_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 731541602214AFFA0037F4DC /* query_spec_test.json */; };
175176
73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */; };
177+
73F1F73C2210F3D800E1F692 /* memory_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */; };
178+
73F1F73D2210F3D800E1F692 /* index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F73B2210F3D800E1F692 /* index_manager_test.mm */; };
179+
73F1F7412211FEF300E1F692 /* leveldb_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */; };
176180
73FE5066020EF9B2892C86BF /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; };
177181
84DBE646DCB49305879D3500 /* nanopb_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 353EEE078EF3F39A9B7279F6 /* nanopb_string_test.cc */; };
178182
873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; };
@@ -485,7 +489,12 @@
485489
6EDD3B5C20BF247500C33877 /* Firestore_FuzzTests_iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore_FuzzTests_iOS-Info.plist"; sourceTree = "<group>"; };
486490
6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestsPrincipal.mm; sourceTree = "<group>"; };
487491
71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
492+
731541602214AFFA0037F4DC /* query_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = query_spec_test.json; sourceTree = "<group>"; };
488493
73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRArrayTransformTests.mm; sourceTree = "<group>"; };
494+
73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = memory_index_manager_test.mm; sourceTree = "<group>"; };
495+
73F1F73A2210F3D800E1F692 /* index_manager_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = index_manager_test.h; sourceTree = "<group>"; };
496+
73F1F73B2210F3D800E1F692 /* index_manager_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = index_manager_test.mm; sourceTree = "<group>"; };
497+
73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = leveldb_index_manager_test.mm; sourceTree = "<group>"; };
489498
79507DF8378D3C42F5B36268 /* string_win_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = string_win_test.cc; sourceTree = "<group>"; };
490499
84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; sourceTree = "<group>"; };
491500
873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@@ -788,6 +797,10 @@
788797
54995F70205B6E1A004EFFA0 /* local */ = {
789798
isa = PBXGroup;
790799
children = (
800+
73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */,
801+
73F1F73A2210F3D800E1F692 /* index_manager_test.h */,
802+
73F1F73B2210F3D800E1F692 /* index_manager_test.mm */,
803+
73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */,
791804
54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */,
792805
332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */,
793806
F8043813A5D16963EC02B182 /* local_serializer_test.cc */,
@@ -1238,6 +1251,7 @@
12381251
DE51B19C1F0D48AC0013853F /* json */ = {
12391252
isa = PBXGroup;
12401253
children = (
1254+
731541602214AFFA0037F4DC /* query_spec_test.json */,
12411255
DE51B1A71F0D48AC0013853F /* README.md */,
12421256
54DA129C1F315EE100DD57A1 /* collection_spec_test.json */,
12431257
54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */,
@@ -1566,6 +1580,7 @@
15661580
3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */,
15671581
54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */,
15681582
54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */,
1583+
731541612214AFFA0037F4DC /* query_spec_test.json in Resources */,
15691584
);
15701585
runOnlyForDeploymentPostprocessing = 0;
15711586
};
@@ -1979,9 +1994,11 @@
19791994
DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */,
19801995
5492E0B92021555100B64F25 /* FSTDocumentKeyTests.mm in Sources */,
19811996
5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */,
1997+
73F1F73C2210F3D800E1F692 /* memory_index_manager_test.mm in Sources */,
19821998
5492E0BD2021555100B64F25 /* FSTDocumentTests.mm in Sources */,
19831999
5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */,
19842000
B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */,
2001+
73F1F7412211FEF300E1F692 /* leveldb_index_manager_test.mm in Sources */,
19852002
5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */,
19862003
5492E0BF2021555100B64F25 /* FSTFieldValueTests.mm in Sources */,
19872004
54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */,
@@ -1996,6 +2013,7 @@
19962013
5492E0A02021552D00B64F25 /* FSTLevelDBMutationQueueTests.mm in Sources */,
19972014
5492E0AE2021552D00B64F25 /* FSTLevelDBQueryCacheTests.mm in Sources */,
19982015
5492E0AA2021552D00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */,
2016+
73F1F73D2210F3D800E1F692 /* index_manager_test.mm in Sources */,
19992017
5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */,
20002018
132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */,
20012019
5492E0A32021552D00B64F25 /* FSTLocalSerializerTests.mm in Sources */,

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,112 @@ - (void)testArrayContainsQueries {
294294
// of anything else interesting to test.
295295
}
296296

297+
- (void)testCollectionGroupQueries {
298+
// Use .document() to get a random collection group name to use but ensure it starts with 'b'
299+
// for predictable ordering.
300+
NSString *collectionGroup = [NSString
301+
stringWithFormat:@"b%@", [[self.db collectionWithPath:@"foo"] documentWithAutoID].documentID];
302+
303+
NSArray *docPaths = @[
304+
@"abc/123/${collectionGroup}/cg-doc1", @"abc/123/${collectionGroup}/cg-doc2",
305+
@"${collectionGroup}/cg-doc3", @"${collectionGroup}/cg-doc4",
306+
@"def/456/${collectionGroup}/cg-doc5", @"${collectionGroup}/virtual-doc/nested-coll/not-cg-doc",
307+
@"x${collectionGroup}/not-cg-doc", @"${collectionGroup}x/not-cg-doc",
308+
@"abc/123/${collectionGroup}x/not-cg-doc", @"abc/123/x${collectionGroup}/not-cg-doc",
309+
@"abc/${collectionGroup}"
310+
];
311+
312+
FIRWriteBatch *batch = [self.db batch];
313+
for (NSString *docPath in docPaths) {
314+
NSString *path = [docPath stringByReplacingOccurrencesOfString:@"${collectionGroup}"
315+
withString:collectionGroup];
316+
[batch setData:@{@"x" : @1} forDocument:[self.db documentWithPath:path]];
317+
}
318+
XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
319+
[batch commitWithCompletion:^(NSError *error) {
320+
XCTAssertNil(error);
321+
[expectation fulfill];
322+
}];
323+
[self awaitExpectations];
324+
325+
FIRQuerySnapshot *querySnapshot =
326+
[self readDocumentSetForRef:[self.db collectionGroupWithID:collectionGroup]];
327+
NSArray<NSString *> *ids = FIRQuerySnapshotGetIDs(querySnapshot);
328+
XCTAssertEqualObjects(ids, (@[ @"cg-doc1", @"cg-doc2", @"cg-doc3", @"cg-doc4", @"cg-doc5" ]));
329+
}
330+
331+
- (void)testCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIDs {
332+
// Use .document() to get a random collection group name to use but ensure it starts with 'b'
333+
// for predictable ordering.
334+
NSString *collectionGroup = [NSString
335+
stringWithFormat:@"b%@", [[self.db collectionWithPath:@"foo"] documentWithAutoID].documentID];
336+
337+
NSArray *docPaths = @[
338+
@"a/a/${collectionGroup}/cg-doc1", @"a/b/a/b/${collectionGroup}/cg-doc2",
339+
@"a/b/${collectionGroup}/cg-doc3", @"a/b/c/d/${collectionGroup}/cg-doc4",
340+
@"a/c/${collectionGroup}/cg-doc5", @"${collectionGroup}/cg-doc6", @"a/b/nope/nope"
341+
];
342+
343+
FIRWriteBatch *batch = [self.db batch];
344+
for (NSString *docPath in docPaths) {
345+
NSString *path = [docPath stringByReplacingOccurrencesOfString:@"${collectionGroup}"
346+
withString:collectionGroup];
347+
[batch setData:@{@"x" : @1} forDocument:[self.db documentWithPath:path]];
348+
}
349+
XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
350+
[batch commitWithCompletion:^(NSError *error) {
351+
XCTAssertNil(error);
352+
[expectation fulfill];
353+
}];
354+
[self awaitExpectations];
355+
356+
FIRQuerySnapshot *querySnapshot = [self
357+
readDocumentSetForRef:[[[[self.db collectionGroupWithID:collectionGroup]
358+
queryOrderedByFieldPath:[FIRFieldPath documentID]]
359+
queryStartingAfterValues:@[ @"a/b" ]]
360+
queryEndingBeforeValues:@[
361+
[NSString stringWithFormat:@"a/b/%@/cg-doc3", collectionGroup]
362+
]]];
363+
364+
NSArray<NSString *> *ids = FIRQuerySnapshotGetIDs(querySnapshot);
365+
XCTAssertEqualObjects(ids, (@[ @"cg-doc2" ]));
366+
}
367+
368+
- (void)testCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIDs {
369+
// Use .document() to get a random collection group name to use but ensure it starts with 'b'
370+
// for predictable ordering.
371+
NSString *collectionGroup = [NSString
372+
stringWithFormat:@"b%@", [[self.db collectionWithPath:@"foo"] documentWithAutoID].documentID];
373+
374+
NSArray *docPaths = @[
375+
@"a/a/${collectionGroup}/cg-doc1", @"a/b/a/b/${collectionGroup}/cg-doc2",
376+
@"a/b/${collectionGroup}/cg-doc3", @"a/b/c/d/${collectionGroup}/cg-doc4",
377+
@"a/c/${collectionGroup}/cg-doc5", @"${collectionGroup}/cg-doc6", @"a/b/nope/nope"
378+
];
379+
380+
FIRWriteBatch *batch = [self.db batch];
381+
for (NSString *docPath in docPaths) {
382+
NSString *path = [docPath stringByReplacingOccurrencesOfString:@"${collectionGroup}"
383+
withString:collectionGroup];
384+
[batch setData:@{@"x" : @1} forDocument:[self.db documentWithPath:path]];
385+
}
386+
XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
387+
[batch commitWithCompletion:^(NSError *error) {
388+
XCTAssertNil(error);
389+
[expectation fulfill];
390+
}];
391+
[self awaitExpectations];
392+
393+
FIRQuerySnapshot *querySnapshot = [self
394+
readDocumentSetForRef:[[[self.db collectionGroupWithID:collectionGroup]
395+
queryWhereFieldPath:[FIRFieldPath documentID]
396+
isGreaterThanOrEqualTo:@"a/b"]
397+
queryWhereFieldPath:[FIRFieldPath documentID]
398+
isLessThan:[NSString stringWithFormat:@"a/b/%@/cg-doc3",
399+
collectionGroup]]];
400+
401+
NSArray<NSString *> *ids = FIRQuerySnapshotGetIDs(querySnapshot);
402+
XCTAssertEqualObjects(ids, (@[ @"cg-doc2" ]));
403+
}
404+
297405
@end

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -482,12 +482,19 @@ - (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders {
482482
}
483483

484484
- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes {
485-
FIRCollectionReference *testCollection = [self collectionRef];
486-
FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]];
485+
FIRQuery *query = [[self.db collectionWithPath:@"collection"]
486+
queryOrderedByFieldPath:[FIRFieldPath documentID]];
487+
FIRQuery *cgQuery = [[self.db collectionGroupWithID:@"collection"]
488+
queryOrderedByFieldPath:[FIRFieldPath documentID]];
487489
FSTAssertThrows([query queryStartingAtValues:@[ @1 ]],
488490
@"Invalid query. Expected a string for the document ID.");
489491
FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]],
490-
@"Invalid query. Document ID 'foo/bar' contains a slash.");
492+
@"Invalid query. When querying a collection and ordering by document "
493+
"ID, you must pass a plain document ID, but 'foo/bar' contains a slash.");
494+
FSTAssertThrows([cgQuery queryStartingAtValues:@[ @"foo" ]],
495+
@"Invalid query. When querying a collection group and ordering by "
496+
"document ID, you must pass a value that results in a valid document path, "
497+
"but 'foo' is not because it contains an odd number of segments.");
491498
}
492499

493500
- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder {
@@ -508,15 +515,23 @@ - (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences {
508515
"document ID, but it was an empty string.";
509516
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason);
510517

511-
reason = @"Invalid query. When querying by document ID you must provide a valid document ID, "
512-
"but 'foo/bar/baz' contains a '/' character.";
518+
reason = @"Invalid query. When querying a collection by document ID you must provide a "
519+
"plain document ID, but 'foo/bar/baz' contains a '/' character.";
513520
FSTAssertThrows(
514521
[collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason);
515522

516523
reason = @"Invalid query. When querying by document ID you must provide a valid string or "
517524
"DocumentReference, but it was of type: __NSCFNumber";
518525
FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason);
519526

527+
reason = @"Invalid query. When querying a collection group by document ID, the value "
528+
"provided must result in a valid document path, but 'foo/bar/baz' is not because it "
529+
"has an odd number of segments.";
530+
FSTAssertThrows(
531+
[[self.db collectionGroupWithID:@"collection"] queryWhereFieldPath:[FIRFieldPath documentID]
532+
isEqualTo:@"foo/bar/baz"],
533+
reason);
534+
520535
reason =
521536
@"Invalid query. You can't perform arrayContains queries on document ID since document IDs "
522537
"are not arrays.";

Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ - (void)setUp {
4444

4545
self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence];
4646
HARD_ASSERT(!_cache, "Previous cache not torn down");
47-
_cache = absl::make_unique<MemoryRemoteDocumentCache>();
47+
_cache = absl::make_unique<MemoryRemoteDocumentCache>(self.persistence);
4848
}
4949

5050
- (RemoteDocumentCache *)remoteDocumentCache {

Firestore/Example/Tests/SpecTests/FSTSpecTests.mm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "Firestore/core/src/firebase/firestore/auth/user.h"
4040
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
4141
#include "Firestore/core/src/firebase/firestore/model/document_key_set.h"
42+
#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
4243
#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h"
4344
#include "Firestore/core/src/firebase/firestore/model/types.h"
4445
#include "Firestore/core/src/firebase/firestore/remote/existence_filter.h"
@@ -57,6 +58,7 @@
5758
using firebase::firestore::core::DocumentViewChangeType;
5859
using firebase::firestore::model::DocumentKey;
5960
using firebase::firestore::model::DocumentKeySet;
61+
using firebase::firestore::model::ResourcePath;
6062
using firebase::firestore::model::SnapshotVersion;
6163
using firebase::firestore::model::TargetId;
6264
using firebase::firestore::remote::ExistenceFilter;
@@ -168,7 +170,10 @@ - (nullable FSTQuery *)parseQuery:(id)querySpec {
168170
} else if ([querySpec isKindOfClass:[NSDictionary class]]) {
169171
NSDictionary *queryDict = (NSDictionary *)querySpec;
170172
NSString *path = queryDict[@"path"];
171-
__block FSTQuery *query = FSTTestQuery(util::MakeString(path));
173+
ResourcePath resource_path = ResourcePath::FromString(util::MakeString(path));
174+
NSString *_Nullable collectionGroup = queryDict[@"collectionGroup"];
175+
__block FSTQuery *query = [FSTQuery queryWithPath:resource_path
176+
collectionGroup:collectionGroup];
172177
if (queryDict[@"limit"]) {
173178
NSNumber *limit = queryDict[@"limit"];
174179
query = [query queryBySettingLimit:limit.integerValue];

0 commit comments

Comments
 (0)