FIRCompositeIndexQueryTests.mm 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  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 *)collectionRefwithTestDocs:
  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. // Asserts that the IDs in the query snapshot matches the expected Ids. The expected document
  109. // IDs are hashed to match the actual document IDs created by the test helper.
  110. - (void)assertSnapshotResultIdsMatch:(FIRQuerySnapshot *)snapshot
  111. expectedIds:(NSArray<NSString *> *)expectedIds {
  112. XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), [self toHashedIds:expectedIds]);
  113. }
  114. // Adds a filter on test id for a query.
  115. - (FIRQuery *)compositeIndexQuery:(FIRQuery *)query_ {
  116. return [query_ queryWhereField:TEST_ID_FIELD isEqualTo:self.testId];
  117. }
  118. // Get a document reference from a document key.
  119. - (FIRDocumentReference *)getDocRef:(FIRCollectionReference *)collection docId:(NSString *)docId {
  120. if (![docId containsString:@"test-id-"]) {
  121. docId = [self toHashedId:docId];
  122. }
  123. return [collection documentWithPath:docId];
  124. }
  125. // Adds a document to a Firestore collection with test-specific fields.
  126. - (FIRDocumentReference *)addDoc:(FIRCollectionReference *)collection
  127. data:(NSDictionary<NSString *, id> *)data {
  128. NSDictionary<NSString *, id> *updatedData = [self addTestSpecificFieldsToDoc:data];
  129. return [self addDocumentRef:collection data:updatedData];
  130. }
  131. // Sets a document in Firestore with test-specific fields.
  132. - (void)setDoc:(FIRDocumentReference *)document data:(NSDictionary<NSString *, id> *)data {
  133. NSDictionary<NSString *, id> *updatedData = [self addTestSpecificFieldsToDoc:data];
  134. return [self mergeDocumentRef:document data:updatedData];
  135. }
  136. - (void)updateDoc:(FIRDocumentReference *)document data:(NSDictionary<NSString *, id> *)data {
  137. [self updateDocumentRef:document data:data];
  138. }
  139. - (void)deleteDoc:(FIRDocumentReference *)document {
  140. [self deleteDocumentRef:document];
  141. }
  142. // Retrieve a single document from Firestore with test-specific fields removed.
  143. // TODO(composite-index-testing) Return sanitized DocumentSnapshot instead of its data.
  144. - (NSDictionary<NSString *, id> *)getSanitizedDocumentData:(FIRDocumentReference *)document {
  145. FIRDocumentSnapshot *docSnapshot = [self readDocumentForRef:document];
  146. return [self removeTestSpecificFieldsFromDoc:docSnapshot.data];
  147. }
  148. // Retrieve multiple documents from Firestore with test-specific fields removed.
  149. // TODO(composite-index-testing) Return sanitized QuerySnapshot instead of its data.
  150. - (NSArray<NSDictionary<NSString *, id> *> *)getSanitizedQueryData:(FIRQuery *)query {
  151. FIRQuerySnapshot *querySnapshot = [self readDocumentSetForRef:query];
  152. NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
  153. for (FIRDocumentSnapshot *doc in querySnapshot.documents) {
  154. [result addObject:[self removeTestSpecificFieldsFromDoc:doc.data]];
  155. }
  156. return result;
  157. }
  158. #pragma mark - Test Cases
  159. /*
  160. * Guidance for Creating Tests:
  161. * ----------------------------
  162. * When creating tests that require composite indexes, it is recommended to utilize the
  163. * test helpers in this class. This utility class provides methods for creating
  164. * and setting test documents and running queries with ease, ensuring proper data
  165. * isolation and query construction.
  166. *
  167. * To get started, please refer to the instructions provided in the README file. This will
  168. * guide you through setting up your local testing environment and updating the Terraform
  169. * configuration with any new composite indexes required for your testing scenarios.
  170. *
  171. * Note: Whenever feasible, make use of the current document fields (such as 'a,' 'b,' 'author,'
  172. * 'title') to avoid introducing new composite indexes and surpassing the limit. Refer to the
  173. * guidelines at https://firebase.google.com/docs/firestore/quotas#indexes for further information.
  174. */
  175. - (void)testOrQueriesWithCompositeIndexes {
  176. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  177. @"doc1" : @{@"a" : @1, @"b" : @0},
  178. @"doc2" : @{@"a" : @2, @"b" : @1},
  179. @"doc3" : @{@"a" : @3, @"b" : @2},
  180. @"doc4" : @{@"a" : @1, @"b" : @3},
  181. @"doc5" : @{@"a" : @1, @"b" : @1}
  182. }];
  183. // with one inequality: a>2 || b==1.
  184. FIRQuery *query1 = [collRef
  185. queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  186. [FIRFilter filterWhereField:@"a" isGreaterThan:@2], [FIRFilter filterWhereField:@"b"
  187. isEqualTo:@1]
  188. ]]];
  189. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query1]
  190. expectedDocs:@[ @"doc5", @"doc2", @"doc3" ]];
  191. // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2
  192. FIRQuery *query2 =
  193. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  194. [FIRFilter filterWhereField:@"a" isEqualTo:@1], [FIRFilter filterWhereField:@"b"
  195. isGreaterThan:@0]
  196. ]]];
  197. [self assertOnlineAndOfflineResultsMatch:[[self compositeIndexQuery:query2] queryLimitedTo:2]
  198. expectedDocs:@[ @"doc1", @"doc2" ]];
  199. // Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2
  200. // Note: The public query API does not allow implicit ordering when limitToLast is used.
  201. FIRQuery *query3 =
  202. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  203. [FIRFilter filterWhereField:@"a" isEqualTo:@1], [FIRFilter filterWhereField:@"b"
  204. isGreaterThan:@0]
  205. ]]];
  206. [self assertOnlineAndOfflineResultsMatch:[[[self compositeIndexQuery:query3] queryLimitedToLast:2]
  207. queryOrderedByField:@"b"]
  208. expectedDocs:@[ @"doc3", @"doc4" ]];
  209. // Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1
  210. FIRQuery *query4 =
  211. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  212. [FIRFilter filterWhereField:@"a" isEqualTo:@2], [FIRFilter filterWhereField:@"b"
  213. isEqualTo:@1]
  214. ]]];
  215. [self assertOnlineAndOfflineResultsMatch:[[[self compositeIndexQuery:query4] queryLimitedTo:1]
  216. queryOrderedByField:@"a"]
  217. expectedDocs:@[ @"doc5" ]];
  218. // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1
  219. FIRQuery *query5 =
  220. [collRef queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  221. [FIRFilter filterWhereField:@"a" isEqualTo:@2], [FIRFilter filterWhereField:@"b"
  222. isEqualTo:@1]
  223. ]]];
  224. [self assertOnlineAndOfflineResultsMatch:[[[self compositeIndexQuery:query5] queryLimitedToLast:1]
  225. queryOrderedByField:@"a"]
  226. expectedDocs:@[ @"doc2" ]];
  227. }
  228. - (void)testCanRunAggregateCollectionGroupQuery {
  229. NSString *collectionGroup = [[self testCollectionRef] collectionID];
  230. NSArray *docPathFormats = @[
  231. @"abc/123/%@/cg-doc1", @"abc/123/%@/cg-doc2", @"%@/cg-doc3", @"%@/cg-doc4",
  232. @"def/456/%@/cg-doc5", @"%@/virtual-doc/nested-coll/not-cg-doc", @"x%@/not-cg-doc",
  233. @"%@x/not-cg-doc", @"abc/123/%@x/not-cg-doc", @"abc/123/x%@/not-cg-doc", @"abc/%@"
  234. ];
  235. FIRWriteBatch *batch = self.db.batch;
  236. for (NSString *format in docPathFormats) {
  237. NSString *path = [NSString stringWithFormat:format, collectionGroup];
  238. [batch setData:[self addTestSpecificFieldsToDoc:@{@"a" : @2}]
  239. forDocument:[self.db documentWithPath:path]];
  240. }
  241. [self commitWriteBatch:batch];
  242. FIRAggregateQuerySnapshot *snapshot = [self
  243. readSnapshotForAggregate:[[self
  244. compositeIndexQuery:[self.db
  245. collectionGroupWithID:collectionGroup]]
  246. aggregate:@[
  247. [FIRAggregateField aggregateFieldForCount],
  248. [FIRAggregateField aggregateFieldForSumOfField:@"a"],
  249. [FIRAggregateField aggregateFieldForAverageOfField:@"a"]
  250. ]]];
  251. // "cg-doc1", "cg-doc2", "cg-doc3", "cg-doc4", "cg-doc5",
  252. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]],
  253. [NSNumber numberWithLong:5L]);
  254. XCTAssertEqual(
  255. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"a"]],
  256. [NSNumber numberWithLong:10L]);
  257. XCTAssertEqual(
  258. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"a"]],
  259. [NSNumber numberWithDouble:2.0]);
  260. }
  261. - (void)testCanPerformMaxAggregations {
  262. FIRCollectionReference *testCollection = [self collectionRefwithTestDocs:@{
  263. @"a" : @{
  264. @"author" : @"authorA",
  265. @"title" : @"titleA",
  266. @"pages" : @100,
  267. @"year" : @1980,
  268. @"rating" : @5.0,
  269. },
  270. @"b" : @{
  271. @"author" : @"authorB",
  272. @"title" : @"titleB",
  273. @"pages" : @50,
  274. @"year" : @2020,
  275. @"rating" : @4.0,
  276. }
  277. }];
  278. // Max is 5, do not exceed
  279. FIRAggregateQuerySnapshot *snapshot =
  280. [self readSnapshotForAggregate:[[self compositeIndexQuery:testCollection] aggregate:@[
  281. [FIRAggregateField aggregateFieldForCount],
  282. [FIRAggregateField aggregateFieldForSumOfField:@"pages"],
  283. [FIRAggregateField aggregateFieldForSumOfField:@"year"],
  284. [FIRAggregateField aggregateFieldForAverageOfField:@"pages"],
  285. [FIRAggregateField aggregateFieldForAverageOfField:@"rating"]
  286. ]]];
  287. // Assert
  288. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]],
  289. [NSNumber numberWithLong:2L]);
  290. XCTAssertEqual(
  291. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"pages"]],
  292. [NSNumber numberWithLong:150L]);
  293. XCTAssertEqual(
  294. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"year"]],
  295. [NSNumber numberWithLong:4000L]);
  296. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField
  297. aggregateFieldForAverageOfField:@"pages"]],
  298. [NSNumber numberWithDouble:75.0]);
  299. XCTAssertEqual([[snapshot valueForAggregateField:[FIRAggregateField
  300. aggregateFieldForAverageOfField:@"rating"]]
  301. doubleValue],
  302. 4.5);
  303. }
  304. - (void)testPerformsAggregationsWhenNaNExistsForSomeFieldValues {
  305. FIRCollectionReference *testCollection = [self collectionRefwithTestDocs:@{
  306. @"a" : @{
  307. @"author" : @"authorA",
  308. @"title" : @"titleA",
  309. @"pages" : @100,
  310. @"year" : @1980,
  311. @"rating" : @5
  312. },
  313. @"b" : @{
  314. @"author" : @"authorB",
  315. @"title" : @"titleB",
  316. @"pages" : @50,
  317. @"year" : @2020,
  318. @"rating" : @4
  319. },
  320. @"c" : @{
  321. @"author" : @"authorC",
  322. @"title" : @"titleC",
  323. @"pages" : @100,
  324. @"year" : @1980,
  325. @"rating" : [NSNumber numberWithFloat:NAN]
  326. },
  327. @"d" : @{
  328. @"author" : @"authorD",
  329. @"title" : @"titleD",
  330. @"pages" : @50,
  331. @"year" : @2020,
  332. @"rating" : @0
  333. }
  334. }];
  335. FIRAggregateQuerySnapshot *snapshot =
  336. [self readSnapshotForAggregate:[[self compositeIndexQuery:testCollection] aggregate:@[
  337. [FIRAggregateField aggregateFieldForSumOfField:@"rating"],
  338. [FIRAggregateField aggregateFieldForSumOfField:@"pages"],
  339. [FIRAggregateField aggregateFieldForAverageOfField:@"rating"],
  340. [FIRAggregateField aggregateFieldForAverageOfField:@"year"]
  341. ]]];
  342. // Sum
  343. XCTAssertEqual(
  344. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"rating"]],
  345. [NSNumber numberWithDouble:NAN]);
  346. XCTAssertEqual(
  347. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"pages"]]
  348. longValue],
  349. 300L);
  350. // Average
  351. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField
  352. aggregateFieldForAverageOfField:@"rating"]],
  353. [NSNumber numberWithDouble:NAN]);
  354. XCTAssertEqual(
  355. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"year"]]
  356. doubleValue],
  357. 2000.0);
  358. }
  359. - (void)testPerformsAggregationWhenUsingArrayContainsAnyOperator {
  360. FIRCollectionReference *testCollection = [self collectionRefwithTestDocs:@{
  361. @"a" : @{
  362. @"author" : @"authorA",
  363. @"title" : @"titleA",
  364. @"pages" : @100,
  365. @"year" : @1980,
  366. @"rating" : @[ @5, @1000 ]
  367. },
  368. @"b" : @{
  369. @"author" : @"authorB",
  370. @"title" : @"titleB",
  371. @"pages" : @50,
  372. @"year" : @2020,
  373. @"rating" : @[ @4 ]
  374. },
  375. @"c" : @{
  376. @"author" : @"authorC",
  377. @"title" : @"titleC",
  378. @"pages" : @100,
  379. @"year" : @1980,
  380. @"rating" : @[ @2222, @3 ]
  381. },
  382. @"d" : @{
  383. @"author" : @"authorD",
  384. @"title" : @"titleD",
  385. @"pages" : @50,
  386. @"year" : @2020,
  387. @"rating" : @[ @0 ]
  388. }
  389. }];
  390. FIRAggregateQuerySnapshot *snapshot = [self
  391. readSnapshotForAggregate:[[self
  392. compositeIndexQuery:[testCollection queryWhereField:@"rating"
  393. arrayContainsAny:@[ @5, @3 ]]]
  394. aggregate:@[
  395. [FIRAggregateField aggregateFieldForSumOfField:@"rating"],
  396. [FIRAggregateField aggregateFieldForSumOfField:@"pages"],
  397. [FIRAggregateField aggregateFieldForAverageOfField:@"rating"],
  398. [FIRAggregateField aggregateFieldForAverageOfField:@"pages"],
  399. [FIRAggregateField aggregateFieldForCount]
  400. ]]];
  401. // Count
  402. XCTAssertEqual(
  403. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]] longValue], 2L);
  404. // Sum
  405. XCTAssertEqual(
  406. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"rating"]]
  407. longValue],
  408. 0L);
  409. XCTAssertEqual(
  410. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"pages"]]
  411. longValue],
  412. 200L);
  413. // Average
  414. XCTAssertEqualObjects(
  415. [snapshot
  416. valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"rating"]],
  417. [NSNull null]);
  418. XCTAssertEqual(
  419. [[snapshot valueForAggregateField:[FIRAggregateField
  420. aggregateFieldForAverageOfField:@"pages"]] doubleValue],
  421. 100.0);
  422. }
  423. // Multiple Inequality
  424. - (void)testMultipleInequalityOnDifferentFields {
  425. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  426. @"doc1" : @{@"key" : @"a", @"sort" : @0, @"v" : @0},
  427. @"doc2" : @{@"key" : @"b", @"sort" : @3, @"v" : @1},
  428. @"doc3" : @{@"key" : @"c", @"sort" : @1, @"v" : @3},
  429. @"doc4" : @{@"key" : @"d", @"sort" : @2, @"v" : @2}
  430. }];
  431. // Multiple inequality fields
  432. FIRQuery *query = [[[collRef queryWhereField:@"key"
  433. isNotEqualTo:@"a"] queryWhereField:@"sort"
  434. isLessThanOrEqualTo:@2] queryWhereField:@"v"
  435. isGreaterThan:@2];
  436. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  437. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc3" ])];
  438. // Duplicate inequality fields
  439. query = [[[collRef queryWhereField:@"key"
  440. isNotEqualTo:@"a"] queryWhereField:@"sort"
  441. isLessThanOrEqualTo:@2] queryWhereField:@"sort"
  442. isGreaterThan:@1];
  443. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  444. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc4" ])];
  445. // With multiple IN
  446. query = [[[[collRef queryWhereField:@"key" isGreaterThanOrEqualTo:@"a"] queryWhereField:@"sort"
  447. isLessThanOrEqualTo:@2]
  448. queryWhereField:@"v"
  449. in:@[ @2, @3, @4 ]] queryWhereField:@"sort" in:@[ @2, @3 ]];
  450. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  451. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc4" ])];
  452. // With NOT-IN
  453. query = [[[collRef queryWhereField:@"key"
  454. isGreaterThanOrEqualTo:@"a"] queryWhereField:@"sort"
  455. isLessThanOrEqualTo:@2] queryWhereField:@"v"
  456. notIn:@[ @2, @4, @5 ]];
  457. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  458. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc1", @"doc3" ])];
  459. // With orderby
  460. query = [[[collRef queryWhereField:@"key"
  461. isGreaterThanOrEqualTo:@"a"] queryWhereField:@"sort"
  462. isLessThanOrEqualTo:@2] queryOrderedByField:@"v"
  463. descending:YES];
  464. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  465. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc3", @"doc4", @"doc1" ])];
  466. // With limit
  467. query = [[[[collRef queryWhereField:@"key" isGreaterThanOrEqualTo:@"a"]
  468. queryWhereField:@"sort"
  469. isLessThanOrEqualTo:@2] queryOrderedByField:@"v" descending:YES] queryLimitedTo:2];
  470. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  471. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc3", @"doc4" ])];
  472. // With limitedToLast
  473. query = [[[[collRef queryWhereField:@"key" isGreaterThanOrEqualTo:@"a"]
  474. queryWhereField:@"sort"
  475. isLessThanOrEqualTo:@2] queryOrderedByField:@"v" descending:YES] queryLimitedToLast:2];
  476. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  477. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc4", @"doc1" ])];
  478. }
  479. - (void)testMultipleInequalityOnSpecialValues {
  480. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  481. @"doc1" : @{@"key" : @"a", @"sort" : @0, @"v" : @0},
  482. @"doc2" : @{@"key" : @"b", @"sort" : @(NAN), @"v" : @1},
  483. @"doc3" : @{@"key" : @"c", @"sort" : [NSNull null], @"v" : @3},
  484. @"doc4" : @{@"key" : @"d", @"v" : @0},
  485. @"doc5" : @{@"key" : @"e", @"sort" : @1},
  486. @"doc6" : @{@"key" : @"f", @"sort" : @1, @"v" : @1}
  487. }];
  488. FIRQuery *query = [[collRef queryWhereField:@"key" isNotEqualTo:@"a"] queryWhereField:@"sort"
  489. isLessThanOrEqualTo:@2];
  490. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  491. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc5", @"doc6" ])];
  492. query = [[[collRef queryWhereField:@"key"
  493. isNotEqualTo:@"a"] queryWhereField:@"sort"
  494. isLessThanOrEqualTo:@2] queryWhereField:@"v"
  495. isLessThanOrEqualTo:@1];
  496. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  497. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc6" ])];
  498. }
  499. - (void)testMultipleInequalityWithArrayMembership {
  500. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  501. @"doc1" : @{@"key" : @"a", @"sort" : @0, @"v" : @[ @0 ]},
  502. @"doc2" : @{@"key" : @"b", @"sort" : @1, @"v" : @[ @0, @1, @3 ]},
  503. @"doc3" : @{@"key" : @"c", @"sort" : @1, @"v" : @[]},
  504. @"doc4" : @{@"key" : @"d", @"sort" : @2, @"v" : @[ @1 ]},
  505. @"doc5" : @{@"key" : @"e", @"sort" : @3, @"v" : @[ @2, @4 ]},
  506. @"doc6" : @{@"key" : @"f", @"sort" : @4, @"v" : @[ @(NAN) ]},
  507. @"doc7" : @{@"key" : @"g", @"sort" : @4, @"v" : @[ [NSNull null] ]}
  508. }];
  509. FIRQuery *query = [[[collRef queryWhereField:@"key"
  510. isNotEqualTo:@"a"] queryWhereField:@"sort"
  511. isGreaterThanOrEqualTo:@1] queryWhereField:@"v"
  512. arrayContains:@0];
  513. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  514. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc2" ])];
  515. query = [[[collRef queryWhereField:@"key"
  516. isNotEqualTo:@"a"] queryWhereField:@"sort"
  517. isGreaterThanOrEqualTo:@1] queryWhereField:@"v"
  518. arrayContainsAny:@[ @0, @1 ]];
  519. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  520. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc2", @"doc4" ])];
  521. }
  522. - (NSDictionary<NSString *, id> *)nestedData:(int)number {
  523. return @{
  524. @"name" : [NSString stringWithFormat:@"room %d", number],
  525. @"metadata" : @{@"createdAt" : @(number)},
  526. @"field" : [NSString stringWithFormat:@"field %d", number],
  527. @"field.dot" : @(number),
  528. @"field\\slash" : @(number)
  529. };
  530. }
  531. - (void)testMultipleInequalityWithNestedField {
  532. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  533. @"doc1" : [self nestedData:400],
  534. @"doc2" : [self nestedData:200],
  535. @"doc3" : [self nestedData:100],
  536. @"doc4" : [self nestedData:300]
  537. }];
  538. FIRQuery *query = [[[[collRef queryWhereField:@"metadata.createdAt" isLessThanOrEqualTo:@500]
  539. queryWhereField:@"metadata.createdAt"
  540. isGreaterThan:@100] queryWhereField:@"name"
  541. isNotEqualTo:@"room 200"] queryOrderedByField:@"name" descending:NO];
  542. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  543. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc4", @"doc1" ])];
  544. query = [[[[collRef queryWhereField:@"field" isGreaterThanOrEqualTo:@"field 100"]
  545. queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"field.dot" ]]
  546. isNotEqualTo:@300] queryWhereField:@"field\\slash"
  547. isLessThan:@400] queryOrderedByField:@"name" descending:YES];
  548. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  549. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc2", @"doc3" ])];
  550. }
  551. - (void)testMultipleInequalityWithCompositeFilters {
  552. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  553. @"doc1" : @{@"key" : @"a", @"sort" : @0, @"v" : @5},
  554. @"doc2" : @{@"key" : @"aa", @"sort" : @4, @"v" : @4},
  555. @"doc3" : @{@"key" : @"c", @"sort" : @3, @"v" : @3},
  556. @"doc4" : @{@"key" : @"b", @"sort" : @2, @"v" : @2},
  557. @"doc5" : @{@"key" : @"b", @"sort" : @2, @"v" : @1},
  558. @"doc6" : @{@"key" : @"b", @"sort" : @0, @"v" : @0}
  559. }];
  560. FIRQuery *query = [collRef
  561. queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  562. [FIRFilter andFilterWithFilters:@[
  563. [FIRFilter filterWhereField:@"key" isEqualTo:@"b"], [FIRFilter filterWhereField:@"sort"
  564. isLessThanOrEqualTo:@2]
  565. ]],
  566. [FIRFilter andFilterWithFilters:@[
  567. [FIRFilter filterWhereField:@"key" isNotEqualTo:@"b"], [FIRFilter filterWhereField:@"v"
  568. isGreaterThan:@4]
  569. ]]
  570. ]]];
  571. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  572. // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc
  573. [self assertSnapshotResultIdsMatch:snapshot
  574. expectedIds:(@[ @"doc1", @"doc6", @"doc5", @"doc4" ])];
  575. query = [[[collRef
  576. queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  577. [FIRFilter andFilterWithFilters:@[
  578. [FIRFilter filterWhereField:@"key" isEqualTo:@"b"], [FIRFilter filterWhereField:@"sort"
  579. isLessThanOrEqualTo:@2]
  580. ]],
  581. [FIRFilter andFilterWithFilters:@[
  582. [FIRFilter filterWhereField:@"key" isNotEqualTo:@"b"], [FIRFilter filterWhereField:@"v"
  583. isGreaterThan:@4]
  584. ]]
  585. ]]] queryOrderedByField:@"sort" descending:YES] queryOrderedByField:@"key"];
  586. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  587. // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc
  588. [self assertSnapshotResultIdsMatch:snapshot
  589. expectedIds:(@[ @"doc5", @"doc4", @"doc1", @"doc6" ])];
  590. query = [collRef
  591. queryWhereFilter:[FIRFilter andFilterWithFilters:@[
  592. [FIRFilter orFilterWithFilters:@[
  593. [FIRFilter andFilterWithFilters:@[
  594. [FIRFilter filterWhereField:@"key" isEqualTo:@"b"], [FIRFilter filterWhereField:@"sort"
  595. isLessThanOrEqualTo:@4]
  596. ]],
  597. [FIRFilter andFilterWithFilters:@[
  598. [FIRFilter filterWhereField:@"key" isNotEqualTo:@"b"], [FIRFilter filterWhereField:@"v"
  599. isGreaterThanOrEqualTo:@4]
  600. ]]
  601. ]],
  602. [FIRFilter orFilterWithFilters:@[
  603. [FIRFilter andFilterWithFilters:@[
  604. [FIRFilter filterWhereField:@"key" isGreaterThan:@"b"],
  605. [FIRFilter filterWhereField:@"sort" isGreaterThanOrEqualTo:@1]
  606. ]],
  607. [FIRFilter andFilterWithFilters:@[
  608. [FIRFilter filterWhereField:@"key" isLessThan:@"b"], [FIRFilter filterWhereField:@"v"
  609. isGreaterThan:@0]
  610. ]]
  611. ]]
  612. ]]];
  613. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  614. // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc
  615. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc1", @"doc2" ])];
  616. }
  617. - (void)testMultipleInequalityFieldsWillBeImplicitlyOrderedLexicographically {
  618. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  619. @"doc1" : @{@"key" : @"a", @"sort" : @0, @"v" : @5},
  620. @"doc2" : @{@"key" : @"aa", @"sort" : @4, @"v" : @4},
  621. @"doc3" : @{@"key" : @"b", @"sort" : @3, @"v" : @3},
  622. @"doc4" : @{@"key" : @"b", @"sort" : @2, @"v" : @2},
  623. @"doc5" : @{@"key" : @"b", @"sort" : @2, @"v" : @1},
  624. @"doc6" : @{@"key" : @"b", @"sort" : @0, @"v" : @0}
  625. }];
  626. FIRQuery *query = [[[collRef queryWhereField:@"key" isNotEqualTo:@"a"]
  627. queryWhereField:@"sort"
  628. isGreaterThan:@1] queryWhereField:@"v" in:@[ @1, @2, @3, @4 ]];
  629. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  630. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
  631. [self assertSnapshotResultIdsMatch:snapshot
  632. expectedIds:(@[ @"doc2", @"doc4", @"doc5", @"doc3" ])];
  633. query = [[[collRef queryWhereField:@"sort"
  634. isGreaterThan:@1] queryWhereField:@"key"
  635. isNotEqualTo:@"a"] queryWhereField:@"v"
  636. in:@[ @1, @2, @3, @4 ]];
  637. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  638. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
  639. [self assertSnapshotResultIdsMatch:snapshot
  640. expectedIds:(@[ @"doc2", @"doc4", @"doc5", @"doc3" ])];
  641. }
  642. - (void)testMultipleInequalityWithMultipleExplicitOrderBy {
  643. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  644. @"doc1" : @{@"key" : @"a", @"sort" : @5, @"v" : @0},
  645. @"doc2" : @{@"key" : @"aa", @"sort" : @4, @"v" : @0},
  646. @"doc3" : @{@"key" : @"b", @"sort" : @3, @"v" : @1},
  647. @"doc4" : @{@"key" : @"b", @"sort" : @2, @"v" : @1},
  648. @"doc5" : @{@"key" : @"bb", @"sort" : @1, @"v" : @1},
  649. @"doc6" : @{@"key" : @"c", @"sort" : @0, @"v" : @2}
  650. }];
  651. FIRQuery *query = [[[collRef queryWhereField:@"key"
  652. isGreaterThan:@"a"] queryWhereField:@"sort"
  653. isGreaterThanOrEqualTo:@1] queryOrderedByField:@"v"
  654. descending:NO];
  655. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  656. // Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc
  657. [self assertSnapshotResultIdsMatch:snapshot
  658. expectedIds:(@[ @"doc2", @"doc4", @"doc3", @"doc5" ])];
  659. query = [[[[collRef queryWhereField:@"key" isGreaterThan:@"a"] queryWhereField:@"sort"
  660. isGreaterThanOrEqualTo:@1]
  661. queryOrderedByField:@"v"
  662. descending:NO] queryOrderedByField:@"sort" descending:NO];
  663. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  664. // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc
  665. [self assertSnapshotResultIdsMatch:snapshot
  666. expectedIds:(@[ @"doc2", @"doc5", @"doc4", @"doc3" ])];
  667. query = [[[collRef queryWhereField:@"key"
  668. isGreaterThan:@"a"] queryWhereField:@"sort"
  669. isGreaterThanOrEqualTo:@1] queryOrderedByField:@"v"
  670. descending:YES];
  671. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  672. // Implicit order by matches the direction of last explicit order by.
  673. // Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc
  674. [self assertSnapshotResultIdsMatch:snapshot
  675. expectedIds:(@[ @"doc5", @"doc3", @"doc4", @"doc2" ])];
  676. query = [[[[collRef queryWhereField:@"key" isGreaterThan:@"a"] queryWhereField:@"sort"
  677. isGreaterThanOrEqualTo:@1]
  678. queryOrderedByField:@"v"
  679. descending:YES] queryOrderedByField:@"sort" descending:NO];
  680. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  681. // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc
  682. [self assertSnapshotResultIdsMatch:snapshot
  683. expectedIds:(@[ @"doc5", @"doc4", @"doc3", @"doc2" ])];
  684. }
  685. - (void)testMultipleInequalityInAggregateQuery {
  686. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  687. @"doc1" : @{@"key" : @"a", @"sort" : @5, @"v" : @0},
  688. @"doc2" : @{@"key" : @"aa", @"sort" : @4, @"v" : @0},
  689. @"doc3" : @{@"key" : @"b", @"sort" : @3, @"v" : @1},
  690. @"doc4" : @{@"key" : @"b", @"sort" : @2, @"v" : @1},
  691. @"doc5" : @{@"key" : @"bb", @"sort" : @1, @"v" : @1},
  692. }];
  693. FIRAggregateQuerySnapshot *snapshot =
  694. [self readSnapshotForAggregate:[[self compositeIndexQuery:[[[collRef queryWhereField:@"key"
  695. isGreaterThan:@"a"]
  696. queryWhereField:@"sort"
  697. isGreaterThanOrEqualTo:@1]
  698. queryOrderedByField:@"v"
  699. descending:NO]]
  700. aggregate:@[
  701. [FIRAggregateField aggregateFieldForCount],
  702. [FIRAggregateField aggregateFieldForSumOfField:@"sort"],
  703. [FIRAggregateField aggregateFieldForAverageOfField:@"v"]
  704. ]]];
  705. XCTAssertEqual([snapshot count], [NSNumber numberWithLong:4L]);
  706. snapshot =
  707. [self readSnapshotForAggregate:[[self compositeIndexQuery:[[[collRef queryWhereField:@"key"
  708. isGreaterThan:@"a"]
  709. queryWhereField:@"sort"
  710. isGreaterThanOrEqualTo:@1]
  711. queryWhereField:@"v"
  712. isNotEqualTo:@0]]
  713. aggregate:@[
  714. [FIRAggregateField aggregateFieldForCount],
  715. [FIRAggregateField aggregateFieldForSumOfField:@"sort"],
  716. [FIRAggregateField aggregateFieldForAverageOfField:@"v"],
  717. ]]];
  718. XCTAssertEqual([snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForCount]],
  719. [NSNumber numberWithLong:3L]);
  720. XCTAssertEqual(
  721. [[snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForSumOfField:@"sort"]]
  722. longValue],
  723. 6L);
  724. XCTAssertEqual(
  725. [snapshot valueForAggregateField:[FIRAggregateField aggregateFieldForAverageOfField:@"v"]],
  726. [NSNumber numberWithDouble:1.0]);
  727. }
  728. - (void)testMultipleInequalityFieldsWithDocumentKey {
  729. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  730. @"doc1" : @{@"key" : @"a", @"sort" : @5},
  731. @"doc2" : @{@"key" : @"aa", @"sort" : @4},
  732. @"doc3" : @{@"key" : @"b", @"sort" : @3},
  733. @"doc4" : @{@"key" : @"b", @"sort" : @2},
  734. @"doc5" : @{@"key" : @"bb", @"sort" : @1}
  735. }];
  736. FIRQuery *query = [[[collRef queryWhereField:@"sort" isGreaterThan:@1]
  737. queryWhereField:@"key"
  738. isNotEqualTo:@"a"] queryWhereFieldPath:[FIRFieldPath documentID] isLessThan:@"doc5"];
  739. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  740. // Document Key in inequality field will implicitly ordered to the last.
  741. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
  742. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc2", @"doc4", @"doc3" ])];
  743. query = [[[collRef queryWhereFieldPath:[FIRFieldPath documentID]
  744. isLessThan:@"doc5"] queryWhereField:@"sort"
  745. isGreaterThan:@1] queryWhereField:@"key"
  746. isNotEqualTo:@"a"];
  747. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  748. // Changing filters order will not effect implicit order.
  749. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
  750. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc2", @"doc4", @"doc3" ])];
  751. query = [[[[collRef queryWhereFieldPath:[FIRFieldPath documentID]
  752. isLessThan:@"doc5"] queryWhereField:@"sort" isGreaterThan:@1]
  753. queryWhereField:@"key"
  754. isNotEqualTo:@"a"] queryOrderedByField:@"sort" descending:YES];
  755. snapshot = [self readDocumentSetForRef:[self compositeIndexQuery:query]];
  756. // Ordered by: 'sort' desc,'key' desc, __name__ desc
  757. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc2", @"doc3", @"doc4" ])];
  758. }
  759. - (void)testMultipleInequalityReadFromCacheWhenOffline {
  760. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  761. @"doc1" : @{@"key" : @"a", @"sort" : @1},
  762. @"doc2" : @{@"key" : @"aa", @"sort" : @4},
  763. @"doc3" : @{@"key" : @"b", @"sort" : @3},
  764. @"doc4" : @{@"key" : @"b", @"sort" : @2},
  765. }];
  766. FIRQuery *query = [self compositeIndexQuery:[[collRef queryWhereField:@"key" isNotEqualTo:@"a"]
  767. queryWhereField:@"sort"
  768. isLessThanOrEqualTo:@3]];
  769. // populate the cache.
  770. FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query];
  771. XCTAssertEqual(snapshot.count, 2L);
  772. XCTAssertEqual(snapshot.metadata.isFromCache, NO);
  773. [self disableNetwork];
  774. snapshot = [self readDocumentSetForRef:query];
  775. XCTAssertEqual(snapshot.count, 2L);
  776. XCTAssertEqual(snapshot.metadata.isFromCache, YES);
  777. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
  778. [self assertSnapshotResultIdsMatch:snapshot expectedIds:(@[ @"doc4", @"doc3" ])];
  779. }
  780. - (void)testMultipleInequalityFromCacheAndFromServer {
  781. FIRCollectionReference *collRef = [self collectionRefwithTestDocs:@{
  782. @"doc1" : @{@"a" : @1, @"b" : @0},
  783. @"doc2" : @{@"a" : @2, @"b" : @1},
  784. @"doc3" : @{@"a" : @3, @"b" : @2},
  785. @"doc4" : @{@"a" : @1, @"b" : @3},
  786. @"doc5" : @{@"a" : @1, @"b" : @1},
  787. }];
  788. // implicit AND: a != 1 && b < 2
  789. FIRQuery *query = [[collRef queryWhereField:@"a" isNotEqualTo:@1] queryWhereField:@"b"
  790. isLessThan:@2];
  791. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query]
  792. expectedDocs:@[ @"doc2" ]];
  793. // explicit AND: a != 1 && b < 2
  794. query =
  795. [collRef queryWhereFilter:[FIRFilter andFilterWithFilters:@[
  796. [FIRFilter filterWhereField:@"a" isNotEqualTo:@1], [FIRFilter filterWhereField:@"b"
  797. isLessThan:@2]
  798. ]]];
  799. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query]
  800. expectedDocs:@[ @"doc2" ]];
  801. // explicit AND: a < 3 && b not-in [2, 3]
  802. // Implicitly ordered by: a asc, b asc, __name__ asc
  803. query = [collRef
  804. queryWhereFilter:[FIRFilter andFilterWithFilters:@[
  805. [FIRFilter filterWhereField:@"a" isLessThan:@3], [FIRFilter filterWhereField:@"b"
  806. notIn:@[ @2, @3 ]]
  807. ]]];
  808. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query]
  809. expectedDocs:@[ @"doc1", @"doc5", @"doc2" ]];
  810. // a <3 && b != 0, ordered by: b desc, a desc, __name__ desc
  811. query = [[[[collRef queryWhereField:@"a" isLessThan:@3] queryWhereField:@"b" isNotEqualTo:@0]
  812. queryOrderedByField:@"b"
  813. descending:YES] queryLimitedTo:2];
  814. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query]
  815. expectedDocs:@[ @"doc4", @"doc2" ]];
  816. // explicit OR: a>2 || b<1.
  817. query = [collRef
  818. queryWhereFilter:[FIRFilter orFilterWithFilters:@[
  819. [FIRFilter filterWhereField:@"a" isGreaterThan:@2], [FIRFilter filterWhereField:@"b"
  820. isLessThan:@1]
  821. ]]];
  822. [self assertOnlineAndOfflineResultsMatch:[self compositeIndexQuery:query]
  823. expectedDocs:@[ @"doc1", @"doc3" ]];
  824. }
  825. - (void)testMultipleInequalityRejectsIfDocumentKeyIsNotTheLastOrderByField {
  826. FIRCollectionReference *collRef = [self collectionRef];
  827. FIRQuery *query = [[collRef queryWhereField:@"key" isNotEqualTo:@42]
  828. queryOrderedByFieldPath:[FIRFieldPath documentID]];
  829. XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
  830. [query getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
  831. XCTAssertNil(results);
  832. XCTAssertNotNil(error);
  833. XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
  834. [queryCompletion fulfill];
  835. }];
  836. [self awaitExpectations];
  837. }
  838. - (void)testMultipleInequalityRejectsIfDocumentKeyAppearsOnlyInEqualityFilter {
  839. FIRCollectionReference *collRef = [self collectionRef];
  840. FIRQuery *query = [[collRef queryWhereField:@"key"
  841. isNotEqualTo:@42] queryWhereFieldPath:[FIRFieldPath documentID]
  842. isEqualTo:@"doc1"];
  843. XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
  844. [query getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
  845. XCTAssertNil(results);
  846. XCTAssertNotNil(error);
  847. XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument);
  848. [queryCompletion fulfill];
  849. }];
  850. [self awaitExpectations];
  851. }
  852. @end
  853. NS_ASSUME_NONNULL_END