Skip to content

Commit 3b49631

Browse files
authored
Implement collection groups in C++ Query (#3327)
* Add a test that verifies canonical IDs don't change * Remove Query::AtPath This has no parallel in Typescript or Java implementations. * Add util::Equals ... for comparing pointers by the thing to which they point. * Use std::shared_ptr<const std::string> for collection group IDs * Reorder/Cleanup C++ Query to match Android * Add collection group to C++ Query
1 parent 3d0c97e commit 3b49631

File tree

19 files changed

+249
-78
lines changed

19 files changed

+249
-78
lines changed

Firestore/Example/Tests/Core/FSTQueryTests.mm

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
2929
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
3030
#include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
31+
#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h"
3132
#include "absl/strings/string_view.h"
3233

3334
namespace testutil = firebase::firestore::testutil;
@@ -39,6 +40,8 @@
3940
using firebase::firestore::model::ResourcePath;
4041
using firebase::firestore::util::ComparisonResult;
4142

43+
using testutil::Value;
44+
4245
NS_ASSUME_NONNULL_BEGIN
4346

4447
/** Convenience methods for building test queries. */
@@ -628,6 +631,54 @@ - (void)testImplicitOrderBy {
628631
]));
629632
}
630633

634+
MATCHER_P(HasCanonicalId, expected, "") {
635+
std::string actual = util::MakeString([arg canonicalID]);
636+
*result_listener << "which has canonicalID " << actual;
637+
return actual == expected;
638+
}
639+
640+
- (void)testCanonicalIDs {
641+
FSTQuery *query = FSTTestQuery("coll");
642+
XC_ASSERT_THAT(query, HasCanonicalId("coll|f:|ob:__name__asc"));
643+
644+
FSTQuery *cg = [FSTQuery queryWithPath:ResourcePath::Empty()
645+
collectionGroup:std::make_shared<const std::string>("foo")];
646+
XC_ASSERT_THAT(cg, HasCanonicalId("|cg:foo|f:|ob:__name__asc"));
647+
648+
FSTQuery *subcoll = FSTTestQuery("foo/bar/baz");
649+
XC_ASSERT_THAT(subcoll, HasCanonicalId("foo/bar/baz|f:|ob:__name__asc"));
650+
651+
FSTQuery *filters = FSTTestQuery("coll");
652+
filters = [filters queryByAddingFilter:FSTTestFilter("str", @"==", @"foo")];
653+
XC_ASSERT_THAT(filters, HasCanonicalId("coll|f:str==foo|ob:__name__asc"));
654+
655+
// Inequality filters end up in the order by too
656+
filters = [filters queryByAddingFilter:FSTTestFilter("int", @"<", @42)];
657+
XC_ASSERT_THAT(filters, HasCanonicalId("coll|f:str==fooint<42|ob:intasc__name__asc"));
658+
659+
FSTQuery *orderBys = FSTTestQuery("coll");
660+
orderBys = [orderBys queryByAddingSortBy:"up" ascending:true];
661+
XC_ASSERT_THAT(orderBys, HasCanonicalId("coll|f:|ob:upasc__name__asc"));
662+
663+
// __name__'s order matches the trailing component
664+
orderBys = [orderBys queryByAddingSortBy:"down" ascending:false];
665+
XC_ASSERT_THAT(orderBys, HasCanonicalId("coll|f:|ob:upascdowndesc__name__desc"));
666+
667+
FSTQuery *limit = [FSTTestQuery("coll") queryBySettingLimit:25];
668+
XC_ASSERT_THAT(limit, HasCanonicalId("coll|f:|ob:__name__asc|l:25"));
669+
670+
FSTQuery *bounds = FSTTestQuery("airports");
671+
bounds = [bounds queryByAddingSortBy:"name" ascending:YES];
672+
bounds = [bounds queryByAddingSortBy:"score" ascending:NO];
673+
bounds = [bounds queryByAddingStartAt:[FSTBound boundWithPosition:{Value("OAK"), Value(1000)}
674+
isBefore:true]];
675+
bounds = [bounds queryByAddingEndAt:[FSTBound boundWithPosition:{Value("SFO"), Value(2000)}
676+
isBefore:false]];
677+
XC_ASSERT_THAT(
678+
bounds,
679+
HasCanonicalId("airports|f:|ob:nameascscoredesc__name__desc|lb:b:OAK1000|ub:a:SFO2000"));
680+
}
681+
631682
@end
632683

633684
NS_ASSUME_NONNULL_END

Firestore/Example/Tests/SpecTests/FSTSpecTests.mm

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#import <FirebaseFirestore/FIRFirestoreErrors.h>
2020

2121
#include <map>
22+
#include <memory>
23+
#include <string>
2224
#include <unordered_map>
2325
#include <utility>
2426
#include <vector>
@@ -176,7 +178,8 @@ - (nullable FSTQuery *)parseQuery:(id)querySpec {
176178
NSDictionary *queryDict = (NSDictionary *)querySpec;
177179
NSString *path = queryDict[@"path"];
178180
ResourcePath resource_path = ResourcePath::FromString(util::MakeString(path));
179-
NSString *_Nullable collectionGroup = queryDict[@"collectionGroup"];
181+
std::shared_ptr<const std::string> collectionGroup =
182+
util::MakeStringPtr(queryDict[@"collectionGroup"]);
180183
__block FSTQuery *query = [FSTQuery queryWithPath:resource_path
181184
collectionGroup:collectionGroup];
182185
if (queryDict[@"limit"]) {

Firestore/Source/API/FIRFirestore.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ - (FIRQuery *)collectionGroupWithID:(NSString *)collectionID {
183183
collectionID);
184184
}
185185

186-
return _firestore->GetCollectionGroup(collectionID);
186+
return _firestore->GetCollectionGroup(util::MakeString(collectionID));
187187
}
188188

189189
- (FIRWriteBatch *)batch {

Firestore/Source/Core/FSTQuery.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
#import <Foundation/Foundation.h>
1818

19+
#include <memory>
20+
#include <string>
1921
#include <vector>
22+
2023
#include "Firestore/core/src/firebase/firestore/core/filter.h"
2124
#include "Firestore/core/src/firebase/firestore/model/document_set.h"
2225
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
@@ -166,7 +169,7 @@ NS_ASSUME_NONNULL_BEGIN
166169
* Initializes a query with all of its components directly.
167170
*/
168171
- (instancetype)initWithPath:(model::ResourcePath)path
169-
collectionGroup:(nullable NSString *)collectionGroup
172+
collectionGroup:(std::shared_ptr<const std::string>)collectionGroup
170173
filterBy:(NSArray<FSTFilter *> *)filters
171174
orderBy:(NSArray<FSTSortOrder *> *)sortOrders
172175
limit:(int32_t)limit
@@ -186,12 +189,12 @@ NS_ASSUME_NONNULL_BEGIN
186189
*
187190
* @param path The path to the location to be queried over. Must currently be
188191
* empty in the case of a collection group query.
189-
* @param collectionGroup The collection group to be queried over. nil if this
192+
* @param collectionGroup The collection group to be queried over. nullptr if this
190193
* is not a collection group query.
191194
* @return A new instance of FSTQuery.
192195
*/
193196
+ (instancetype)queryWithPath:(model::ResourcePath)path
194-
collectionGroup:(nullable NSString *)collectionGroup;
197+
collectionGroup:(std::shared_ptr<const std::string>)collectionGroup;
195198

196199
/**
197200
* Returns the list of ordering constraints that were explicitly requested on the query by the
@@ -282,7 +285,7 @@ NS_ASSUME_NONNULL_BEGIN
282285
- (const model::ResourcePath &)path;
283286

284287
/** The collection group of the query. */
285-
@property(nonatomic, nullable, strong, readonly) NSString *collectionGroup;
288+
- (const std::shared_ptr<const std::string> &)collectionGroup;
286289

287290
/** The filters on the documents returned by the query. */
288291
@property(nonatomic, strong, readonly) NSArray<FSTFilter *> *filters;

Firestore/Source/Core/FSTQuery.mm

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
3636
#include "Firestore/core/src/firebase/firestore/objc/objc_compatibility.h"
3737
#include "Firestore/core/src/firebase/firestore/util/comparison.h"
38+
#include "Firestore/core/src/firebase/firestore/util/equality.h"
3839
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
3940
#include "Firestore/core/src/firebase/firestore/util/hashing.h"
4041
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
@@ -544,8 +545,10 @@ - (instancetype)copyWithZone:(nullable NSZone *)zone {
544545
@interface FSTQuery () {
545546
// Cached value of the canonicalID property.
546547
NSString *_canonicalID;
547-
/** The base path of the query. */
548+
// The base path of the query.
548549
ResourcePath _path;
550+
// The collection group of the query, if any.
551+
std::shared_ptr<const std::string> _collectionGroup;
549552
}
550553

551554
/** A list of fields given to sort by. This does not include the implicit key sort at the end. */
@@ -561,13 +564,13 @@ @implementation FSTQuery
561564
#pragma mark - Constructors
562565

563566
+ (instancetype)queryWithPath:(ResourcePath)path {
564-
return [FSTQuery queryWithPath:std::move(path) collectionGroup:nil];
567+
return [FSTQuery queryWithPath:std::move(path) collectionGroup:nullptr];
565568
}
566569

567570
+ (instancetype)queryWithPath:(ResourcePath)path
568-
collectionGroup:(nullable NSString *)collectionGroup {
571+
collectionGroup:(std::shared_ptr<const std::string>)collectionGroup {
569572
return [[FSTQuery alloc] initWithPath:std::move(path)
570-
collectionGroup:collectionGroup
573+
collectionGroup:std::move(collectionGroup)
571574
filterBy:@[]
572575
orderBy:@[]
573576
limit:Query::kNoLimit
@@ -576,15 +579,15 @@ + (instancetype)queryWithPath:(ResourcePath)path
576579
}
577580

578581
- (instancetype)initWithPath:(ResourcePath)path
579-
collectionGroup:(nullable NSString *)collectionGroup
582+
collectionGroup:(std::shared_ptr<const std::string>)collectionGroup
580583
filterBy:(NSArray<FSTFilter *> *)filters
581584
orderBy:(NSArray<FSTSortOrder *> *)sortOrders
582585
limit:(int32_t)limit
583586
startAt:(nullable FSTBound *)startAtBound
584587
endAt:(nullable FSTBound *)endAtBound {
585588
if (self = [super init]) {
586589
_path = std::move(path);
587-
_collectionGroup = collectionGroup;
590+
_collectionGroup = std::move(collectionGroup);
588591
_filters = filters;
589592
_explicitSortOrders = sortOrders;
590593
_limit = limit;
@@ -804,6 +807,10 @@ - (nullable const FieldPath *)firstSortOrderField {
804807
return _path;
805808
}
806809

810+
- (const std::shared_ptr<const std::string> &)collectionGroup {
811+
return _collectionGroup;
812+
}
813+
807814
#pragma mark - Private properties
808815

809816
- (NSString *)canonicalID {
@@ -815,7 +822,7 @@ - (NSString *)canonicalID {
815822
[canonicalID appendFormat:@"%s", _path.CanonicalString().c_str()];
816823

817824
if (self.collectionGroup) {
818-
[canonicalID appendFormat:@"|cg:%@", self.collectionGroup];
825+
[canonicalID appendFormat:@"|cg:%s", self.collectionGroup->c_str()];
819826
}
820827

821828
// Add filters.
@@ -850,7 +857,7 @@ - (NSString *)canonicalID {
850857
#pragma mark - Private methods
851858

852859
- (BOOL)isEqualToQuery:(FSTQuery *)other {
853-
return self.path == other.path && objc::Equals(self.collectionGroup, other.collectionGroup) &&
860+
return self.path == other.path && util::Equals(self.collectionGroup, other.collectionGroup) &&
854861
self.limit == other.limit && objc::Equals(self.filters, other.filters) &&
855862
objc::Equals(self.sortOrders, other.sortOrders) &&
856863
objc::Equals(self.startAt, other.startAt) && objc::Equals(self.endAt, other.endAt);
@@ -862,7 +869,7 @@ - (BOOL)pathAndCollectionGroupMatchDocument:(FSTDocument *)document {
862869
if (self.collectionGroup) {
863870
// NOTE: self.path is currently always empty since we don't expose Collection Group queries
864871
// rooted at a document path yet.
865-
return document.key.HasCollectionId(util::MakeString(self.collectionGroup)) &&
872+
return document.key.HasCollectionId(*self.collectionGroup) &&
866873
self.path.IsPrefixOf(documentPath);
867874
} else if (DocumentKey::IsDocumentKey(_path)) {
868875
// Exact match for document queries.

Firestore/Source/Remote/FSTSerializerBeta.mm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ - (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query {
786786
"Collection group queries should be within a document path or root.");
787787
queryTarget.parent = [self encodedQueryPath:path];
788788
GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
789-
from.collectionId = query.collectionGroup;
789+
from.collectionId = util::MakeNSString(query.collectionGroup);
790790
from.allDescendants = YES;
791791
[queryTarget.structuredQuery.fromArray addObject:from];
792792
} else {
@@ -827,15 +827,15 @@ - (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target {
827827
ResourcePath path = [self decodedQueryPath:target.parent];
828828

829829
GCFSStructuredQuery *query = target.structuredQuery;
830-
NSString *collectionGroup;
830+
std::shared_ptr<const std::string> collectionGroup;
831831
NSUInteger fromCount = query.fromArray_Count;
832832
if (fromCount > 0) {
833833
HARD_ASSERT(fromCount == 1,
834834
"StructuredQuery.from with more than one collection is not supported.");
835835

836836
GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0];
837837
if (from.allDescendants) {
838-
collectionGroup = from.collectionId;
838+
collectionGroup = util::MakeStringPtr(from.collectionId);
839839
} else {
840840
path = path.Append(util::MakeString(from.collectionId));
841841
}

Firestore/core/src/firebase/firestore/api/firestore.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class Firestore : public std::enable_shared_from_this<Firestore> {
8484
CollectionReference GetCollection(absl::string_view collection_path);
8585
DocumentReference GetDocument(absl::string_view document_path);
8686
WriteBatch GetBatch();
87-
FIRQuery* GetCollectionGroup(NSString* collection_id);
87+
FIRQuery* GetCollectionGroup(std::string collection_id);
8888

8989
void RunTransaction(core::TransactionUpdateCallback update_callback,
9090
core::TransactionResultCallback result_callback);

Firestore/core/src/firebase/firestore/api/firestore.mm

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,12 @@
117117
return WriteBatch(shared_from_this());
118118
}
119119

120-
FIRQuery* Firestore::GetCollectionGroup(NSString* collection_id) {
120+
FIRQuery* Firestore::GetCollectionGroup(std::string collection_id) {
121121
EnsureClientConfigured();
122122

123123
FSTQuery* query = [FSTQuery queryWithPath:ResourcePath::Empty()
124-
collectionGroup:collection_id];
124+
collectionGroup:std::make_shared<const std::string>(
125+
std::move(collection_id))];
125126
return [[FIRQuery alloc] initWithQuery:query firestore:shared_from_this()];
126127
}
127128

Firestore/core/src/firebase/firestore/core/query.cc

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
2222
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
2323
#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
24+
#include "Firestore/core/src/firebase/firestore/util/equality.h"
2425
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
2526

2627
namespace firebase {
@@ -31,13 +32,46 @@ using model::Document;
3132
using model::DocumentKey;
3233
using model::ResourcePath;
3334

35+
Query::Query(ResourcePath path, std::string collection_group)
36+
: path_(std::move(path)),
37+
collection_group_(
38+
std::make_shared<const std::string>(std::move(collection_group))) {
39+
}
40+
41+
// MARK: - Accessors
42+
43+
bool Query::IsDocumentQuery() const {
44+
return DocumentKey::IsDocumentKey(path_) && !collection_group_ &&
45+
filters_.empty();
46+
}
47+
48+
// MARK: - Builder methods
49+
50+
Query Query::Filter(std::shared_ptr<core::Filter> filter) const {
51+
HARD_ASSERT(!DocumentKey::IsDocumentKey(path_),
52+
"No filter is allowed for document query");
53+
54+
// TODO(rsgowman): ensure only one inequality field
55+
// TODO(rsgowman): ensure first orderby must match inequality field
56+
57+
auto updated_filters = filters_;
58+
updated_filters.push_back(std::move(filter));
59+
return Query(path_, collection_group_, std::move(updated_filters));
60+
}
61+
62+
Query Query::AsCollectionQueryAtPath(ResourcePath path) const {
63+
return Query(path, /*collection_group=*/nullptr, filters_);
64+
}
65+
66+
// MARK: - Matching
67+
3468
bool Query::Matches(const Document& doc) const {
3569
return MatchesPath(doc) && MatchesOrderBy(doc) && MatchesFilters(doc) &&
3670
MatchesBounds(doc);
3771
}
3872

3973
bool Query::MatchesPath(const Document& doc) const {
40-
ResourcePath doc_path = doc.key().path();
74+
const ResourcePath& doc_path = doc.key().path();
4175
if (DocumentKey::IsDocumentKey(path_)) {
4276
return path_ == doc_path;
4377
} else {
@@ -62,16 +96,14 @@ bool Query::MatchesBounds(const Document&) const {
6296
return true;
6397
}
6498

65-
Query Query::Filter(std::shared_ptr<core::Filter> filter) const {
66-
HARD_ASSERT(!DocumentKey::IsDocumentKey(path_),
67-
"No filter is allowed for document query");
68-
69-
// TODO(rsgowman): ensure only one inequality field
70-
// TODO(rsgowman): ensure first orderby must match inequality field
71-
72-
std::vector<std::shared_ptr<core::Filter>> updated_filters = filters_;
73-
updated_filters.push_back(std::move(filter));
74-
return Query(path_, std::move(updated_filters));
99+
bool operator==(const Query& lhs, const Query& rhs) {
100+
// TODO(rsgowman): check limit (once it exists)
101+
// TODO(rsgowman): check orderby (once it exists)
102+
// TODO(rsgowman): check startat (once it exists)
103+
// TODO(rsgowman): check endat (once it exists)
104+
return lhs.path() == rhs.path() &&
105+
util::Equals(lhs.collection_group(), rhs.collection_group()) &&
106+
lhs.filters() == rhs.filters();
75107
}
76108

77109
} // namespace core

0 commit comments

Comments
 (0)