diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index affae20a4f7..3dc774aa982 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [feature] Added `Query.count()`, which fetches the number of documents in the + result set without actually downloading the documents (#10246). + # 10.0.0 - [fixed] Fixed compiler warning about `@param comparator` (#10226). diff --git a/Firestore/Example/Tests/Integration/API/FIRCountTests.mm b/Firestore/Example/Tests/Integration/API/FIRCountTests.mm index 5a89aeb49dc..d3d644b4bc4 100644 --- a/Firestore/Example/Tests/Integration/API/FIRCountTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRCountTests.mm @@ -19,15 +19,47 @@ #import #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" -#import "Firestore/Source/API/FIRAggregateQuery+Internal.h" -#import "Firestore/Source/API/FIRAggregateQuerySnapshot+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h" +#import "Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h" +#import "Firestore/Source/Public/FirebaseFirestore/FIRAggregateSource.h" @interface FIRCountTests : FSTIntegrationTestCase @end @implementation FIRCountTests +- (void)testAggregateQueryEquals { + FIRCollectionReference* coll1 = [self collectionRefWithDocuments:@{}]; + FIRCollectionReference* coll1Same = [[coll1 firestore] collectionWithPath:[coll1 path]]; + FIRAggregateQuery* query1 = [coll1 count]; + FIRAggregateQuery* query1Same = [coll1Same count]; + + FIRCollectionReference* sub = [[coll1 documentWithPath:@"bar"] collectionWithPath:@"baz"]; + FIRAggregateQuery* query2 = [[[sub queryWhereField:@"a" isEqualTo:@1] queryLimitedTo:100] count]; + FIRAggregateQuery* query2Same = [[[sub queryWhereField:@"a" + isEqualTo:@1] queryLimitedTo:100] count]; + FIRAggregateQuery* query3 = [[[sub queryWhereField:@"b" + isEqualTo:@1] queryOrderedByField:@"c"] count]; + FIRAggregateQuery* query3Same = [[[sub queryWhereField:@"b" + isEqualTo:@1] queryOrderedByField:@"c"] count]; + + XCTAssertEqualObjects(query1, query1Same); + XCTAssertEqualObjects(query2, query2Same); + XCTAssertEqualObjects(query3, query3Same); + + XCTAssertEqual([query1 hash], [query1Same hash]); + XCTAssertEqual([query2 hash], [query2Same hash]); + XCTAssertEqual([query3 hash], [query3Same hash]); + + XCTAssertFalse([query1 isEqual:nil]); + XCTAssertFalse([query1 isEqual:@"string"]); + XCTAssertFalse([query1 isEqual:query2]); + XCTAssertFalse([query2 isEqual:query3]); + + XCTAssertNotEqual([query1 hash], [query2 hash]); + XCTAssertNotEqual([query2 hash], [query3 hash]); +} + - (void)testCanRunCountQuery { // TODO(b/246758022): Remove this (and below) once COUNT is release for the backend. if (![FSTIntegrationTestCase isRunningAgainstEmulator]) { @@ -77,6 +109,48 @@ - (void)testCanRunCountWithOrderBys { XCTAssertEqual(snapshot.count, [NSNumber numberWithLong:3L]); } +- (void)testSnapshotEquals { + if (![FSTIntegrationTestCase isRunningAgainstEmulator]) { + return; + } + + FIRCollectionReference* testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"} + }]; + + FIRAggregateQuerySnapshot* snapshot1 = + [self readSnapshotForAggregate:[[testCollection queryWhereField:@"k" isEqualTo:@"b"] count]]; + FIRAggregateQuerySnapshot* snapshot1Same = + [self readSnapshotForAggregate:[[testCollection queryWhereField:@"k" isEqualTo:@"b"] count]]; + + FIRAggregateQuerySnapshot* snapshot2 = + [self readSnapshotForAggregate:[[testCollection queryWhereField:@"k" isEqualTo:@"a"] count]]; + [self writeDocumentRef:[testCollection documentWithPath:@"d"] data:@{@"k" : @"a"}]; + FIRAggregateQuerySnapshot* snapshot2Different = + [self readSnapshotForAggregate:[[testCollection queryWhereField:@"k" isEqualTo:@"a"] count]]; + + FIRAggregateQuerySnapshot* snapshot3 = + [self readSnapshotForAggregate:[[testCollection queryWhereField:@"k" isEqualTo:@"b"] count]]; + FIRAggregateQuerySnapshot* snapshot3Different = + [self readSnapshotForAggregate:[[testCollection queryWhereField:@"k" isEqualTo:@"c"] count]]; + + XCTAssertEqualObjects(snapshot1, snapshot1Same); + XCTAssertEqual([snapshot1 hash], [snapshot1Same hash]); + XCTAssertEqualObjects([snapshot1 query], [[testCollection queryWhereField:@"k" + isEqualTo:@"b"] count]); + + XCTAssertNotEqualObjects(snapshot1, nil); + XCTAssertNotEqualObjects(snapshot1, @"string"); + XCTAssertNotEqualObjects(snapshot1, snapshot2); + XCTAssertNotEqual([snapshot1 hash], [snapshot2 hash]); + XCTAssertNotEqualObjects(snapshot2, snapshot2Different); + XCTAssertNotEqual([snapshot2 hash], [snapshot2Different hash]); + XCTAssertNotEqualObjects(snapshot3, snapshot3Different); + XCTAssertNotEqual([snapshot3 hash], [snapshot3Different hash]); +} + - (void)testTerminateDoesNotCrashWithFlyingCountQuery { if (![FSTIntegrationTestCase isRunningAgainstEmulator]) { return; diff --git a/Firestore/Source/API/FIRAggregateQuery+Internal.h b/Firestore/Source/API/FIRAggregateQuery+Internal.h index 1627abf7b5b..1c010269b37 100644 --- a/Firestore/Source/API/FIRAggregateQuery+Internal.h +++ b/Firestore/Source/API/FIRAggregateQuery+Internal.h @@ -14,35 +14,16 @@ * limitations under the License. */ -// TODO(b/246760853): Move FIRAggregateQuery to public headers to release it. - -#import "FIRAggregateSource+Internal.h" +#import "FIRAggregateQuery.h" #import "FIRQuery.h" -@class FIRAggregateQuerySnapshot; - -/** - * An `AggregateQuery` computes some aggregation statistics from the result set of a base - * `Query`. - */ -NS_SWIFT_NAME(AggregateQuery) -@interface FIRAggregateQuery : NSObject +NS_ASSUME_NONNULL_BEGIN -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithQuery:(FIRQuery *_Nonnull)query NS_DESIGNATED_INITIALIZER; +@interface FIRAggregateQuery (/* init */) -/** The base `Query` for this aggregate query. */ -@property(nonatomic, readonly) FIRQuery *_Nonnull query; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithQuery:(FIRQuery *)query NS_DESIGNATED_INITIALIZER; -/** - * Executes the aggregate query and reads back the results as a `FIRAggregateQuerySnapshot`. - * - * @param source indicates where the results should be fetched from. - * @param completion a block to execute once the results have been successfully read. - * snapshot will be `nil` only if error is `non-nil`. - */ -- (void)aggregationWithSource:(FIRAggregateSource)source - completion:(void (^_Nonnull)(FIRAggregateQuerySnapshot *_Nullable snapshot, - NSError *_Nullable error))completion - NS_SWIFT_NAME(aggregation(source:completion:)); @end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRAggregateQuery.mm b/Firestore/Source/API/FIRAggregateQuery.mm index 68756e5a85d..0ab5fbddc8a 100644 --- a/Firestore/Source/API/FIRAggregateQuery.mm +++ b/Firestore/Source/API/FIRAggregateQuery.mm @@ -24,12 +24,16 @@ #include "Firestore/core/src/util/error_apple.h" #include "Firestore/core/src/util/statusor.h" +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRAggregateQuery + @implementation FIRAggregateQuery { FIRQuery *_query; std::unique_ptr _aggregation; } -- (instancetype _Nonnull)initWithQuery:(FIRQuery *)query { +- (instancetype)initWithQuery:(FIRQuery *)query { if (self = [super init]) { _query = query; _aggregation = absl::make_unique(query.apiQuery.Count()); @@ -37,6 +41,22 @@ - (instancetype _Nonnull)initWithQuery:(FIRQuery *)query { return self; } +#pragma mark - NSObject Methods + +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + auto otherQuery = static_cast(other); + return [_query isEqual:otherQuery->_query]; +} + +- (NSUInteger)hash { + return [_query hash]; +} + +#pragma mark - Public Methods + - (FIRQuery *)query { return _query; } @@ -46,7 +66,7 @@ - (void)aggregationWithSource:(FIRAggregateSource)source NSError *_Nullable error))completion { _aggregation->Get([self, completion](const firebase::firestore::util::StatusOr &result) { if (result.ok()) { - completion([[FIRAggregateQuerySnapshot alloc] initWithCount:result.ValueOrDie() Query:self], + completion([[FIRAggregateQuerySnapshot alloc] initWithCount:result.ValueOrDie() query:self], nil); } else { completion(nil, MakeNSError(result.status())); @@ -55,3 +75,5 @@ - (void)aggregationWithSource:(FIRAggregateSource)source } @end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRAggregateQuerySnapshot+Internal.h b/Firestore/Source/API/FIRAggregateQuerySnapshot+Internal.h index 7f6a197b31c..868b0c29cc5 100644 --- a/Firestore/Source/API/FIRAggregateQuerySnapshot+Internal.h +++ b/Firestore/Source/API/FIRAggregateQuerySnapshot+Internal.h @@ -14,29 +14,18 @@ * limitations under the License. */ -// TODO(b/246760853): Move FIRAggregateQuerySnapshot to public headers to release it. - -#import "FIRAggregateQuery+Internal.h" +#import "FIRAggregateQuerySnapshot.h" @class FIRAggregateQuery; -/** - * An `AggregateQuerySnapshot` contains results of a `AggregateQuery`. - */ -NS_SWIFT_NAME(AggregateQuerySnapshot) -@interface FIRAggregateQuerySnapshot : NSObject +NS_ASSUME_NONNULL_BEGIN -- (instancetype _Nonnull)init NS_UNAVAILABLE; -- (instancetype _Nonnull)initWithCount:(int64_t)result - Query:(FIRAggregateQuery* _Nonnull)query NS_DESIGNATED_INITIALIZER; +@interface FIRAggregateQuerySnapshot (/* init */) -/** The original `AggregateQuery` this snapshot is a result of. */ -@property(nonatomic, readonly) FIRAggregateQuery* _Nonnull query; - -/** - * The result of a document count aggregation. Null if no count aggregation is - * available in the result. - */ -@property(nonatomic, readonly) NSNumber* _Nullable count; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCount:(int64_t)result + query:(FIRAggregateQuery *)query NS_DESIGNATED_INITIALIZER; @end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRAggregateQuerySnapshot.mm b/Firestore/Source/API/FIRAggregateQuerySnapshot.mm index faec3fe4fb1..f7462c262b6 100644 --- a/Firestore/Source/API/FIRAggregateQuerySnapshot.mm +++ b/Firestore/Source/API/FIRAggregateQuerySnapshot.mm @@ -16,12 +16,16 @@ #import "FIRAggregateQuerySnapshot+Internal.h" +#import "FIRAggregateQuery.h" + +NS_ASSUME_NONNULL_BEGIN + @implementation FIRAggregateQuerySnapshot { int64_t _result; FIRAggregateQuery* _query; } -- (instancetype _Nonnull)initWithCount:(int64_t)count Query:(FIRAggregateQuery*)query { +- (instancetype)initWithCount:(int64_t)count query:(FIRAggregateQuery*)query { if (self = [super init]) { _result = count; _query = query; @@ -29,6 +33,24 @@ - (instancetype _Nonnull)initWithCount:(int64_t)count Query:(FIRAggregateQuery*) return self; } +#pragma mark - NSObject Methods + +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + auto otherSnap = static_cast(other); + return _result == otherSnap->_result && [_query isEqual:otherSnap->_query]; +} + +- (NSUInteger)hash { + NSUInteger result = [_query hash]; + result = 31 * result + [[self count] hash]; + return result; +} + +#pragma mark - Public Methods + - (NSNumber*)count { return [NSNumber numberWithLongLong:_result]; } @@ -38,3 +60,5 @@ - (FIRAggregateQuery*)query { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery+Internal.h b/Firestore/Source/API/FIRQuery+Internal.h index 052e9b920db..1d2e74a36e7 100644 --- a/Firestore/Source/API/FIRQuery+Internal.h +++ b/Firestore/Source/API/FIRQuery+Internal.h @@ -14,7 +14,6 @@ * limitations under the License. */ -#import "FIRAggregateQuery+Internal.h" #import "FIRQuery.h" #include @@ -48,12 +47,6 @@ NS_ASSUME_NONNULL_BEGIN // TODO(orquery): This method will become public API. Change visibility and add documentation. - (FIRQuery *)queryWhereFilter:(FIRFilter *)filter; -// TODO(b/246760853): This property will become public API. -/** - * An `AggregateQuery` counting the number of documents matching this query. - */ -@property(nonatomic, readonly) FIRAggregateQuery *count; - @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index eecf7bdd324..330ff3b73bc 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -20,6 +20,7 @@ #include #include +#import "FIRAggregateQuery+Internal.h" #import "FIRDocumentReference.h" #import "FIRFirestoreErrors.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h new file mode 100644 index 00000000000..8170d5caa53 --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "FIRAggregateSource.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRQuery; +@class FIRAggregateQuerySnapshot; + +/** + * A query that calculates aggregations over an underlying query. + */ +NS_SWIFT_NAME(AggregateQuery) +@interface FIRAggregateQuery : NSObject + +/** :nodoc: */ +- (instancetype)init __attribute__((unavailable("FIRAggregateQuery cannot be created directly."))); + +/** The query whose aggregations will be calculated by this object. */ +@property(nonatomic, readonly) FIRQuery *query; + +/** + * Executes this query. + * + * @param source The source from which to acquire the aggregate results. + * @param completion a block to execute once the results have been successfully read. + * snapshot will be `nil` only if error is `non-nil`. + */ +- (void)aggregationWithSource:(FIRAggregateSource)source + completion:(void (^)(FIRAggregateQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion + NS_SWIFT_NAME(getAggregation(source:completion:)); +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRAggregateSource+Internal.h b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h similarity index 51% rename from Firestore/Source/API/FIRAggregateSource+Internal.h rename to Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h index 7a65035bc4d..8d41ba7c687 100644 --- a/Firestore/Source/API/FIRAggregateSource+Internal.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h @@ -14,17 +14,28 @@ * limitations under the License. */ -// TODO(b/246760853): Move FIRAggregateSource to public headers to release it. - #import -/** Configures the behavior of `AggregateQuery.aggregateWithSource(source:completion:)`. */ -typedef NS_ENUM(NSUInteger, FIRAggregateSource) { - /** - * Reach to the Firestore backend and surface the result verbatim, that is no local documents or - * mutations in the SDK cache will be included in the surfaced result. - * - * NOTE: Requires client to be online. - */ - FIRAggregateSourceServer, -} NS_SWIFT_NAME(AggregateSource); +NS_ASSUME_NONNULL_BEGIN + +@class FIRAggregateQuery; + +/** + * The results of executing an `AggregateQuery`. + */ +NS_SWIFT_NAME(AggregateQuerySnapshot) +@interface FIRAggregateQuerySnapshot : NSObject + +/** :nodoc: */ +- (instancetype)init + __attribute__((unavailable("FIRAggregateQuerySnapshot cannot be created directly."))); + +/** The query that was executed to produce this result. */ +@property(nonatomic, readonly) FIRAggregateQuery* query; + +/** The number of documents in the result set of the underlying query. */ +@property(nonatomic, readonly) NSNumber* count; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateSource.h b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateSource.h new file mode 100644 index 00000000000..365aec30c49 --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateSource.h @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The sources from which an `AggregateQuery` can retrieve its results. + * + * See `AggregateQuery.getAggregation(source:completion:)`. + */ +typedef NS_ENUM(NSUInteger, FIRAggregateSource) { + /** + * Perform the aggregation on the server and download the result. + * + * The result received from the server is presented, unaltered, without considering any local + * state. That is, documents in the local cache are not taken into consideration, neither are + * local modifications not yet synchronized with the server. Previously-downloaded results, if + * any, are not used: every request using this source necessarily involves a round trip to the + * server. + * + * The `AggregateQuery` will fail if the server cannot be reached, such as if the client is + * offline. + */ + FIRAggregateSourceServer, +} NS_SWIFT_NAME(AggregateSource); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h index e01f6af4105..b2e0dd29b9f 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h @@ -19,6 +19,7 @@ #import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" +@class FIRAggregateQuery; @class FIRFieldPath; @class FIRFirestore; @class FIRQuerySnapshot; @@ -542,6 +543,20 @@ NS_SWIFT_NAME(Query) */ - (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues NS_SWIFT_NAME(end(at:)); +#pragma mark - Aggregation + +/** + * A query that counts the documents in the result set of this query. + * + * The `AggregateQuery` query, when executed, counts the documents in the result set of this `Query` + * without actually downloading the documents. + * + * Using the returned query to count the documents is efficient because only the final count, not + * the documents' data, is downloaded. The returned query can even count the documents if the result + * set would be prohibitively large to download entirely (e.g. thousands of documents). + */ +@property(nonatomic, readonly) FIRAggregateQuery *count; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Swift/Tests/Integration/AsyncAwaitIntegrationTests.swift b/Firestore/Swift/Tests/Integration/AsyncAwaitIntegrationTests.swift index 5f7da2c53a8..309e0276e81 100644 --- a/Firestore/Swift/Tests/Integration/AsyncAwaitIntegrationTests.swift +++ b/Firestore/Swift/Tests/Integration/AsyncAwaitIntegrationTests.swift @@ -73,5 +73,12 @@ let emptyBundle = """ XCTAssertNil(value, "value should be nil on success") } + + func testCount() async throws { + let collection = collectionRef() + try await collection.addDocument(data: [:]) + let snapshot = try await collection.count.getAggregation(source: .server) + XCTAssertEqual(snapshot.count, 1) + } } #endif