FIRCompositeIndexQueryTests.mm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. /*
  2. * Copyright 2023 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <FirebaseFirestore/FirebaseFirestore.h>
  17. #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
  18. #include "Firestore/core/src/util/autoid.h"
  19. using firebase::firestore::util::CreateAutoId;
  20. NS_ASSUME_NONNULL_BEGIN
  21. static NSString *const TEST_ID_FIELD = @"testId";
  22. static NSString *const TTL_FIELD = @"expireAt";
  23. static NSString *const COMPOSITE_INDEX_TEST_COLLECTION = @"composite-index-test-collection";
  24. /**
  25. * This FIRCompositeIndexQueryTests class is designed to facilitate integration
  26. * testing of Firestore queries that require composite indexes within a
  27. * controlled testing environment.
  28. *
  29. * Key Features:
  30. * <ul>
  31. * <li>Runs tests against the dedicated test collection with predefined composite indexes.
  32. * <li>Automatically associates a test ID with documents for data isolation.
  33. * <li>Utilizes TTL policy for automatic test data cleanup.
  34. * <li>Constructs Firestore queries with test ID filters.
  35. * </ul>
  36. */
  37. @interface FIRCompositeIndexQueryTests : FSTIntegrationTestCase
  38. // Creates a new unique identifier for each test case to ensure data isolation.
  39. @property(nonatomic, strong) NSString *testId;
  40. @end
  41. @implementation FIRCompositeIndexQueryTests
  42. - (void)setUp {
  43. [super setUp];
  44. _testId = [NSString stringWithFormat:@"test-id-%s", CreateAutoId().c_str()];
  45. }
  46. #pragma mark - Test Helpers
  47. // Return reference to the static test collection: composite-index-test-collection
  48. - (FIRCollectionReference *)testCollectionRef {
  49. return [self.db collectionWithPath:COMPOSITE_INDEX_TEST_COLLECTION];
  50. }
  51. // Runs a test with specified documents in the COMPOSITE_INDEX_TEST_COLLECTION.
  52. - (FIRCollectionReference *)withTestDocs:
  53. (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)docs {
  54. FIRCollectionReference *writer = [self testCollectionRef];
  55. // Use a different instance to write the documents
  56. [self writeAllDocuments:[self prepareTestDocuments:docs]
  57. toCollection:[self.firestore collectionWithPath:writer.path]];
  58. return self.testCollectionRef;
  59. }
  60. // Hash the document key with testId.
  61. - (NSString *)toHashedId:(NSString *)docId {
  62. return [NSString stringWithFormat:@"%@-%@", docId, self.testId];
  63. }
  64. - (NSArray<NSString *> *)toHashedIds:(NSArray<NSString *> *)docs {
  65. NSMutableArray<NSString *> *hashedIds = [NSMutableArray arrayWithCapacity:docs.count];
  66. for (NSString *doc in docs) {
  67. [hashedIds addObject:[self toHashedId:doc]];
  68. }
  69. return hashedIds;
  70. }
  71. // Adds test-specific fields to a document, including the testId and expiration date.
  72. - (NSDictionary<NSString *, id> *)addTestSpecificFieldsToDoc:(NSDictionary<NSString *, id> *)doc {
  73. NSMutableDictionary<NSString *, id> *updatedDoc = [doc mutableCopy];
  74. updatedDoc[TEST_ID_FIELD] = self.testId;
  75. int64_t expirationTime =
  76. [[FIRTimestamp timestamp] seconds] + 24 * 60 * 60; // Expire test data after 24 hours
  77. updatedDoc[TTL_FIELD] = [FIRTimestamp timestampWithSeconds:expirationTime nanoseconds:0];
  78. return [updatedDoc copy];
  79. }
  80. // Remove test-specific fields from a Firestore document.
  81. - (NSDictionary<NSString *, id> *)removeTestSpecificFieldsFromDoc:
  82. (NSDictionary<NSString *, id> *)doc {
  83. NSMutableDictionary<NSString *, id> *mutableDoc = [doc mutableCopy];
  84. [mutableDoc removeObjectForKey:TEST_ID_FIELD];
  85. [mutableDoc removeObjectForKey:TTL_FIELD];
  86. // Update the document with the modified data.
  87. return [mutableDoc copy];
  88. }
  89. // Helper method to hash document keys and add test-specific fields for the provided documents.
  90. - (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)prepareTestDocuments:
  91. (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)docs {
  92. NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *result =
  93. [NSMutableDictionary dictionaryWithCapacity:docs.count];
  94. for (NSString *key in docs.allKeys) {
  95. NSDictionary<NSString *, id> *doc = docs[key];
  96. NSDictionary<NSString *, id> *updatedDoc = [self addTestSpecificFieldsToDoc:doc];
  97. result[[self toHashedId:key]] = updatedDoc;
  98. }
  99. return [result copy];
  100. }
  101. // Asserts that the result of running the query while online (against the backend/emulator) is
  102. // the same as running it while offline. The expected document Ids are hashed to match the
  103. // actual document IDs created by the test helper.
  104. - (void)assertOnlineAndOfflineResultsMatch:(FIRQuery *)query
  105. expectedDocs:(NSArray<NSString *> *)expectedDocs {
  106. [self checkOnlineAndOfflineQuery:query matchesResult:[self toHashedIds:expectedDocs]];
  107. }
  108. // Adds a filter on test id for a query.
  109. - (FIRQuery *)compositeIndexQuery:(FIRQuery *)query_ {
  110. return [query_ queryWhereField:TEST_ID_FIELD isEqualTo:self.testId];
  111. }
  112. // Get a document reference from a document key.
  113. - (FIRDocumentReference *)getDocRef:(FIRCollectionReference *)collection docId:(NSString *)docId {
  114. if (![docId containsString:@"test-id-"]) {
  115. docId = [self toHashedId:docId];
  116. }
  117. return [collection documentWithPath:docId];
  118. }
  119. // Adds a document to a Firestore collection with test-specific fields.
  120. - (FIRDocumentReference *)addDoc:(FIRCollectionReference *)collection
  121. data:(NSDictionary<NSString *, id> *)data {
  122. NSDictionary<NSString *, id> *updatedData = [self addTestSpecificFieldsToDoc:data];
  123. return [self addDocumentRef:collection data:updatedData];
  124. }
  125. // Sets a document in Firestore with test-specific fields.
  126. - (void)setDoc:(FIRDocumentReference *)document data:(NSDictionary<NSString *, id> *)data {
  127. NSDictionary<NSString *, id> *updatedData = [self addTestSpecificFieldsToDoc:data];
  128. return [self mergeDocumentRef:document data:updatedData];
  129. }
  130. - (void)updateDoc:(FIRDocumentReference *)document data:(NSDictionary<NSString *, id> *)data {
  131. [self updateDocumentRef:document data:data];
  132. }
  133. - (void)deleteDoc:(FIRDocumentReference *)document {
  134. [self deleteDocumentRef:document];
  135. }
  136. // Retrieve a single document from Firestore with test-specific fields removed.
  137. // TODO(composite-index-testing) Return sanitized DocumentSnapshot instead of its data.
  138. - (NSDictionary<NSString *, id> *)getSanitizedDocumentData:(FIRDocumentReference *)document {
  139. FIRDocumentSnapshot *docSnapshot = [self readDocumentForRef:document];
  140. return [self removeTestSpecificFieldsFromDoc:docSnapshot.data];
  141. }
  142. // Retrieve multiple documents from Firestore with test-specific fields removed.
  143. // TODO(composite-index-testing) Return sanitized QuerySnapshot instead of its data.
  144. - (NSArray<NSDictionary<NSString *, id> *> *)getSanitizedQueryData:(FIRQuery *)query {
  145. FIRQuerySnapshot *querySnapshot = [self readDocumentSetForRef:query];
  146. NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
  147. for (FIRDocumentSnapshot *doc in querySnapshot.documents) {
  148. [result addObject:[self removeTestSpecificFieldsFromDoc:doc.data]];
  149. }
  150. return result;
  151. }
  152. #pragma mark - Test Cases
  153. /*
  154. * Guidance for Creating Tests:
  155. * ----------------------------
  156. * When creating tests that require composite indexes, it is recommended to utilize the
  157. * test helpers in this class. This utility class provides methods for creating
  158. * and setting test documents and running queries with ease, ensuring proper data
  159. * isolation and query construction.
  160. *
  161. * To get started, please refer to the instructions provided in the README file. This will
  162. * guide you through setting up your local testing environment and updating the Terraform
  163. * configuration with any new composite indexes required for your testing scenarios.
  164. *
  165. * Note: Whenever feasible, make use of the current document fields (such as 'a,' 'b,' 'author,'
  166. * 'title') to avoid introducing new composite indexes and surpassing the limit. Refer to the
  167. * guidelines at https://firebase.google.com/docs/firestore/quotas#indexes for further information.
  168. */
  169. - (void)testOrQueriesWithCompositeIndexes {
  170. FIRCollectionReference *collRef = [self withTestDocs:@{
  171. @"doc1" : @{@"a" : @1, @"b" : @0},
  172. @"doc2" : @{@"a" : @2, @"b" : @1},
  173. @"doc3" : @{@"a" : @3, @"b" : @2},
  174. @"doc4" : @{@"a" : @1, @"b" : @3},
  175. @"doc5" : @{@"a" : @1, @"b" : @1}
  176. }];
  177. // with one inequality: a>2 || b==1.
  178. FIRQuery *query1 = [collRef
  179. queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  180. [FIRFilter filterWhereField:@"a" isGreaterThan:@2], [FIRFilter filterWhereField:@"b"
  181. isEqualTo:@1]
  182. ]]];
  183. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query1]
  184. expectedDocs:@[ @"doc5", @"doc2", @"doc3" ]];
  185. // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2
  186. FIRQuery *query2 =
  187. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  188. [FIRFilter filterWhereField:@"a" isEqualTo:@1], [FIRFilter filterWhereField:@"b"
  189. isGreaterThan:@0]
  190. ]]];
  191. [self assertOnlineAndOfflineResultsMatch:[[self compositeIndexQuery:query2] queryLimitedTo:2]
  192. expectedDocs:@[ @"doc1", @"doc2" ]];
  193. // Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2
  194. // Note: The public query API does not allow implicit ordering when limitToLast is used.
  195. FIRQuery *query3 =
  196. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  197. [FIRFilter filterWhereField:@"a" isEqualTo:@1], [FIRFilter filterWhereField:@"b"
  198. isGreaterThan:@0]
  199. ]]];
  200. [self assertOnlineAndOfflineResultsMatch:[[[self compositeIndexQuery:query3] queryLimitedToLast:2]
  201. queryOrderedByField:@"b"]
  202. expectedDocs:@[ @"doc3", @"doc4" ]];
  203. // Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1
  204. FIRQuery *query4 =
  205. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  206. [FIRFilter filterWhereField:@"a" isEqualTo:@2], [FIRFilter filterWhereField:@"b"
  207. isEqualTo:@1]
  208. ]]];
  209. [self assertOnlineAndOfflineResultsMatch:[[[self compositeIndexQuery:query4] queryLimitedTo:1]
  210. queryOrderedByField:@"a"]
  211. expectedDocs:@[ @"doc5" ]];
  212. // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1
  213. FIRQuery *query5 =
  214. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  215. [FIRFilter filterWhereField:@"a" isEqualTo:@2], [FIRFilter filterWhereField:@"b"
  216. isEqualTo:@1]
  217. ]]];
  218. [self assertOnlineAndOfflineResultsMatch:[[[self compositeIndexQuery:query5] queryLimitedToLast:1]
  219. queryOrderedByField:@"a"]
  220. expectedDocs:@[ @"doc2" ]];
  221. }
  222. - (void)testCanRunAggregateCollectionGroupQuery {
  223. NSString *collectionGroup = [[self testCollectionRef] collectionID];
  224. NSArray *docPathFormats = @[
  225. @"abc/123/%@/cg-doc1", @"abc/123/%@/cg-doc2", @"%@/cg-doc3", @"%@/cg-doc4",
  226. @"def/456/%@/cg-doc5", @"%@/virtual-doc/nested-coll/not-cg-doc", @"x%@/not-cg-doc",
  227. @"%@x/not-cg-doc", @"abc/123/%@x/not-cg-doc", @"abc/123/x%@/not-cg-doc", @"abc/%@"
  228. ];
  229. FIRWriteBatch *batch = self.db.batch;
  230. for (NSString *format in docPathFormats) {
  231. NSString *path = [NSString stringWithFormat:format, collectionGroup];
  232. [batch setData:[self addTestSpecificFieldsToDoc:@{@"a" : @2}]
  233. forDocument:[self.db documentWithPath:path]];
  234. }
  235. [self commitWriteBatch:batch];
  236. FIRAggregateQuerySnapshot *snapshot = [self
  237. readSnapshotForAggregate:[[self
  238. compositeIndexQuery:[self.db
  239. collectionGroupWithID:collectionGroup]]
  240. aggregate:@[
  241. [FIRAggregateField aggregateFieldForCount],
  242. [FIRAggregateField aggregateFieldForSumOfField:@"a"],
  243. [FIRAggregateField aggregateFieldForAverageOfField:@"a"]
  244. ]]];
  245. // "cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5",
  246. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]],
  247. [NSNumber numberWithLong:5L]);
  248. XCTAssertEqual(
  249. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"a"]],
  250. [NSNumber numberWithLong:10L]);
  251. XCTAssertEqual(
  252. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"a"]],
  253. [NSNumber numberWithDouble:2.0]);
  254. }
  255. - (void)testCanPerformMaxAggregations {
  256. FIRCollectionReference *testCollection = [self withTestDocs:@{
  257. @"a" : @{
  258. @"author" : @"authorA",
  259. @"title" : @"titleA",
  260. @"pages" : @100,
  261. @"year" : @1980,
  262. @"rating" : @5.0,
  263. },
  264. @"b" : @{
  265. @"author" : @"authorB",
  266. @"title" : @"titleB",
  267. @"pages" : @50,
  268. @"year" : @2020,
  269. @"rating" : @4.0,
  270. }
  271. }];
  272. // Max is 5, do not exceed
  273. FIRAggregateQuerySnapshot *snapshot =
  274. [self readSnapshotForAggregate:[[self compositeIndexQuery:testCollection] aggregate:@[
  275. [FIRAggregateField aggregateFieldForCount],
  276. [FIRAggregateField aggregateFieldForSumOfField:@"pages"],
  277. [FIRAggregateField aggregateFieldForSumOfField:@"year"],
  278. [FIRAggregateField aggregateFieldForAverageOfField:@"pages"],
  279. [FIRAggregateField aggregateFieldForAverageOfField:@"rating"]
  280. ]]];
  281. // Assert
  282. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]],
  283. [NSNumber numberWithLong:2L]);
  284. XCTAssertEqual(
  285. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"pages"]],
  286. [NSNumber numberWithLong:150L]);
  287. XCTAssertEqual(
  288. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"year"]],
  289. [NSNumber numberWithLong:4000L]);
  290. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField
  291. aggregateFieldForAverageOfField:@"pages"]],
  292. [NSNumber numberWithDouble:75.0]);
  293. XCTAssertEqual([[snapshot valueForAggregateField:[FIRAggregateField
  294. aggregateFieldForAverageOfField:@"rating"]]
  295. doubleValue],
  296. 4.5);
  297. }
  298. - (void)testPerformsAggregationsWhenNaNExistsForSomeFieldValues {
  299. FIRCollectionReference *testCollection = [self withTestDocs:@{
  300. @"a" : @{
  301. @"author" : @"authorA",
  302. @"title" : @"titleA",
  303. @"pages" : @100,
  304. @"year" : @1980,
  305. @"rating" : @5
  306. },
  307. @"b" : @{
  308. @"author" : @"authorB",
  309. @"title" : @"titleB",
  310. @"pages" : @50,
  311. @"year" : @2020,
  312. @"rating" : @4
  313. },
  314. @"c" : @{
  315. @"author" : @"authorC",
  316. @"title" : @"titleC",
  317. @"pages" : @100,
  318. @"year" : @1980,
  319. @"rating" : [NSNumber numberWithFloat:NAN]
  320. },
  321. @"d" : @{
  322. @"author" : @"authorD",
  323. @"title" : @"titleD",
  324. @"pages" : @50,
  325. @"year" : @2020,
  326. @"rating" : @0
  327. }
  328. }];
  329. FIRAggregateQuerySnapshot *snapshot =
  330. [self readSnapshotForAggregate:[[self compositeIndexQuery:testCollection] aggregate:@[
  331. [FIRAggregateField aggregateFieldForSumOfField:@"rating"],
  332. [FIRAggregateField aggregateFieldForSumOfField:@"pages"],
  333. [FIRAggregateField aggregateFieldForAverageOfField:@"rating"],
  334. [FIRAggregateField aggregateFieldForAverageOfField:@"year"]
  335. ]]];
  336. // Sum
  337. XCTAssertEqual(
  338. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"rating"]],
  339. [NSNumber numberWithDouble:NAN]);
  340. XCTAssertEqual(
  341. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"pages"]]
  342. longValue],
  343. 300L);
  344. // Average
  345. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField
  346. aggregateFieldForAverageOfField:@"rating"]],
  347. [NSNumber numberWithDouble:NAN]);
  348. XCTAssertEqual(
  349. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"year"]]
  350. doubleValue],
  351. 2000.0);
  352. }
  353. - (void)testPerformsAggregationWhenUsingArrayContainsAnyOperator {
  354. FIRCollectionReference *testCollection = [self withTestDocs:@{
  355. @"a" : @{
  356. @"author" : @"authorA",
  357. @"title" : @"titleA",
  358. @"pages" : @100,
  359. @"year" : @1980,
  360. @"rating" : @[ @5, @1000 ]
  361. },
  362. @"b" : @{
  363. @"author" : @"authorB",
  364. @"title" : @"titleB",
  365. @"pages" : @50,
  366. @"year" : @2020,
  367. @"rating" : @[ @4 ]
  368. },
  369. @"c" : @{
  370. @"author" : @"authorC",
  371. @"title" : @"titleC",
  372. @"pages" : @100,
  373. @"year" : @1980,
  374. @"rating" : @[ @2222, @3 ]
  375. },
  376. @"d" : @{
  377. @"author" : @"authorD",
  378. @"title" : @"titleD",
  379. @"pages" : @50,
  380. @"year" : @2020,
  381. @"rating" : @[ @0 ]
  382. }
  383. }];
  384. FIRAggregateQuerySnapshot *snapshot = [self
  385. readSnapshotForAggregate:[[self
  386. compositeIndexQuery:[testCollection queryWhereField:@"rating"
  387. arrayContainsAny:@[ @5, @3 ]]]
  388. aggregate:@[
  389. [FIRAggregateField aggregateFieldForSumOfField:@"rating"],
  390. [FIRAggregateField aggregateFieldForSumOfField:@"pages"],
  391. [FIRAggregateField aggregateFieldForAverageOfField:@"rating"],
  392. [FIRAggregateField aggregateFieldForAverageOfField:@"pages"],
  393. [FIRAggregateField aggregateFieldForCount]
  394. ]]];
  395. // Count
  396. XCTAssertEqual(
  397. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]] longValue], 2L);
  398. // Sum
  399. XCTAssertEqual(
  400. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"rating"]]
  401. longValue],
  402. 0L);
  403. XCTAssertEqual(
  404. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"pages"]]
  405. longValue],
  406. 200L);
  407. // Average
  408. XCTAssertEqualObjects(
  409. [snapshot
  410. valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"rating"]],
  411. [NSNull null]);
  412. XCTAssertEqual(
  413. [[snapshot valueForAggregateField:[FIRAggregateField
  414. aggregateFieldForAverageOfField:@"pages"]] doubleValue],
  415. 100.0);
  416. }
  417. @end
  418. NS_ASSUME_NONNULL_END