FIRCompositeIndexQueryTests.mm 45 KB

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