Преглед на файлове

Add Public API for Or Query (#10352)

* Add Public API for Or Query

* Add Or query swift tests

* Add Swift tests for IN

* Adding back FIRFirestore+Internal as include file

* Add Documentation

* Address feedback

* Update the changelog.

---------

Co-authored-by: Ehsan Nasiri <ehsann@google.com>
cherylEnkidu преди 3 години
родител
ревизия
c0e6ac1cf2

+ 2 - 0
Firestore/CHANGELOG.md

@@ -1,3 +1,5 @@
+# Unreleased
+- [feature] Add support for disjunctions in queries (`OR` queries).
 
 # 10.6.0
 - [fixed] Fix a potential high memory usage issue.

+ 9 - 1
Firestore/Example/Firestore.xcodeproj/project.pbxproj

@@ -638,6 +638,9 @@
 		61D35E0DE04E70D3BC243A65 /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; };
 		61ECC7CE18700CBD73D0D810 /* leveldb_migrations_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */; };
 		61F72C5620BC48FD001A68CB /* serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 61F72C5520BC48FD001A68CB /* serializer_test.cc */; };
+		621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */; };
+		621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */; };
+		621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */; };
 		623AA12C3481646B0715006D /* string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0EE5300F8233D14025EF0456 /* string_apple_test.mm */; };
 		627253FDEC6BB5549FE77F4E /* tree_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */; };
 		62B1C1100A8C68D94565916C /* document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FFCA39825D9678A03D1845D0 /* document_overlay_cache_test.cc */; };
@@ -1457,7 +1460,7 @@
 		2220F583583EFC28DE792ABE /* Pods_Firestore_IntegrationTests_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		2286F308EFB0534B1BDE05B9 /* memory_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_target_cache_test.cc; sourceTree = "<group>"; };
 		277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = lru_garbage_collector_test.cc; sourceTree = "<group>"; };
-		28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = logic_utils_test.cc; sourceTree = "<group>"; };
+		28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = logic_utils_test.cc; sourceTree = "<group>"; };
 		29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_document_overlay_cache_test.cc; sourceTree = "<group>"; };
 		2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = objc_type_traits_apple_test.mm; sourceTree = "<group>"; };
 		2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1636,6 +1639,7 @@
 		618BBE9A20B89AAC00B5BCE7 /* status.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = status.pb.h; sourceTree = "<group>"; };
 		61F72C5520BC48FD001A68CB /* serializer_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = serializer_test.cc; sourceTree = "<group>"; };
 		620C1427763BA5D3CCFB5A1F /* BridgingHeader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
+		621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryIntegrationTests.swift; sourceTree = "<group>"; };
 		62E103B28B48A81D682A0DE9 /* Pods_Firestore_Example_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_index_matcher_test.cc; sourceTree = "<group>"; };
 		64AA92CFA356A2360F3C5646 /* filesystem_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = filesystem_testing.h; sourceTree = "<group>"; };
@@ -1937,6 +1941,7 @@
 			children = (
 				062072B62773A055001655D7 /* AsyncAwaitIntegrationTests.swift */,
 				124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */,
+				621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */,
 			);
 			path = Integration;
 			sourceTree = "<group>";
@@ -4057,6 +4062,7 @@
 				D77941FD93DBE862AEF1F623 /* FSTTransactionTests.mm in Sources */,
 				432056C4D1259F76C80FC2A8 /* FSTUserDataReaderTests.mm in Sources */,
 				3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */,
+				621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */,
 				4D42E5C756229C08560DD731 /* XCTestCase+Await.mm in Sources */,
 				0EC3921AE220410F7394729B /* aggregation_result.pb.cc in Sources */,
 				276A563D546698B6AAC20164 /* annotations.pb.cc in Sources */,
@@ -4280,6 +4286,7 @@
 				5E5B3B8B3A41C8EB70035A6B /* FSTTransactionTests.mm in Sources */,
 				75A176239B37354588769206 /* FSTUserDataReaderTests.mm in Sources */,
 				5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */,
+				621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */,
 				736C4E82689F1CA1859C4A3F /* XCTestCase+Await.mm in Sources */,
 				DF983A9C1FBF758AF3AF110D /* aggregation_result.pb.cc in Sources */,
 				EA46611779C3EEF12822508C /* annotations.pb.cc in Sources */,
@@ -4739,6 +4746,7 @@
 				5492E07F202154EC00B64F25 /* FSTTransactionTests.mm in Sources */,
 				F5BDECEB3B43BD1591EEADBD /* FSTUserDataReaderTests.mm in Sources */,
 				6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */,
+				621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */,
 				5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */,
 				1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */,
 				02EB33CC2590E1484D462912 /* annotations.pb.cc in Sources */,

+ 0 - 3
Firestore/Example/Tests/Integration/API/FIRQueryTests.mm

@@ -18,9 +18,6 @@
 
 #import <XCTest/XCTest.h>
 
-#import "Firestore/Source/API/FIRFilter+Internal.h"
-#import "Firestore/Source/API/FIRQuery+Internal.h"
-
 #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h"
 #import "Firestore/Example/Tests/Util/FSTHelpers.h"
 #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"

+ 0 - 3
Firestore/Example/Tests/Integration/API/FIRValidationTests.mm

@@ -23,10 +23,7 @@
 #import "FirebaseCore/Extension/FIROptionsInternal.h"
 #import "Firestore/Example/Tests/Util/FSTHelpers.h"
 #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
-#import "Firestore/Source/API/FIRFieldValue+Internal.h"
-#import "Firestore/Source/API/FIRFilter+Internal.h"
 #import "Firestore/Source/API/FIRFirestore+Internal.h"
-#import "Firestore/Source/API/FIRQuery+Internal.h"
 #include "Firestore/core/test/unit/testutil/app_testing.h"
 
 using firebase::firestore::testutil::AppForUnitTesting;

+ 3 - 80
Firestore/Source/API/FIRFilter+Internal.h

@@ -15,92 +15,15 @@
  */
 
 #import <Foundation/Foundation.h>
-#include "Firestore/Protos/nanopb/google/firestore/v1/query.nanopb.h"
-#import "Firestore/core/src/core/composite_filter.h"
-#import "Firestore/core/src/core/field_filter.h"
 
-// TODO(orquery): This class will become public API. Change visibility and add documentation.
+#import "FIRFilter.h"
+#include "Firestore/core/src/core/composite_filter.h"
+#include "Firestore/core/src/core/field_filter.h"
 
 @class FIRFieldPath;
 
 NS_ASSUME_NONNULL_BEGIN
 
-NS_SWIFT_NAME(Filter)
-@interface FIRFilter : NSObject
-
-#pragma mark - Public Methods
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                      isEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isEqualTo:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                          isEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isEqualTo:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                   isNotEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isNotEqualTo:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                       isNotEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isNotEqualTo:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                  isGreaterThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isGreaterThan:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                      isGreaterThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isGreaterThan:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-         isGreaterThanOrEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isGreaterOrEqualTo:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-             isGreaterThanOrEqualTo:(nonnull id)value
-    NS_SWIFT_NAME(whereField(_:isGreaterOrEqualTo:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                     isLessThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isLessThan:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                         isLessThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isLessThan:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-            isLessThanOrEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                isLessThanOrEqualTo:(nonnull id)value
-    NS_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                  arrayContains:(nonnull id)value NS_SWIFT_NAME(whereField(_:arrayContains:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                      arrayContains:(nonnull id)value NS_SWIFT_NAME(whereField(_:arrayContains:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-               arrayContainsAny:(nonnull NSArray<id> *)values
-    NS_SWIFT_NAME(whereField(_:arrayContainsAny:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                   arrayContainsAny:(nonnull NSArray<id> *)values
-    NS_SWIFT_NAME(whereField(_:arrayContainsAny:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                             in:(nonnull NSArray<id> *)values NS_SWIFT_NAME(whereField(_:in:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                                 in:(nonnull NSArray<id> *)values NS_SWIFT_NAME(whereField(_:in:));
-
-+ (FIRFilter *)filterWhereField:(nonnull NSString *)field
-                          notIn:(nonnull NSArray<id> *)values NS_SWIFT_NAME(whereField(_:notIn:));
-
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
-                              notIn:(nonnull NSArray<id> *)values
-    NS_SWIFT_NAME(whereField(_:notIn:));
-
-+ (FIRFilter *)orFilterWithFilters:(NSArray<FIRFilter *> *)filters NS_SWIFT_NAME(orFilter(_:));
-
-+ (FIRFilter *)andFilterWithFilters:(NSArray<FIRFilter *> *)filters NS_SWIFT_NAME(andFilter(_:));
-
-@end
-
 /** Exposed internally */
 @interface FSTUnaryFilter : FIRFilter
 

+ 22 - 22
Firestore/Source/API/FIRFilter.mm

@@ -42,11 +42,11 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
 
 @implementation FSTUnaryFilter
 
-- (instancetype)initWithFIRFieldPath:(nonnull FIRFieldPath *)fieldPath
+- (instancetype)initWithFIRFieldPath:(nonnull FIRFieldPath *)path
                                   op:(FieldFilter::Operator)op
                                value:(nonnull id)value {
   if (self = [super init]) {
-    self.fieldPath = fieldPath;
+    self.fieldPath = path;
     self.unaryOp = op;
     self.value = value;
   }
@@ -83,8 +83,8 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) isEqualTo:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field isEqualTo:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path isEqualTo:(nonnull id)value {
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::Equal
                                                 value:value];
 }
@@ -93,8 +93,8 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) isNotEqualTo:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field isNotEqualTo:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path isNotEqualTo:(nonnull id)value {
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::NotEqual
                                                 value:value];
 }
@@ -103,8 +103,8 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) isGreaterThan:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field isGreaterThan:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path isGreaterThan:(nonnull id)value {
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::GreaterThan
                                                 value:value];
 }
@@ -113,9 +113,9 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) isGreaterThanOrEqualTo:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
              isGreaterThanOrEqualTo:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::GreaterThanOrEqual
                                                 value:value];
 }
@@ -124,8 +124,8 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) isLessThan:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field isLessThan:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path isLessThan:(nonnull id)value {
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::LessThan
                                                 value:value];
 }
@@ -134,9 +134,9 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) isLessThanOrEqualTo:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
                 isLessThanOrEqualTo:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::LessThanOrEqual
                                                 value:value];
 }
@@ -145,8 +145,8 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) arrayContains:value];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field arrayContains:(nonnull id)value {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path arrayContains:(nonnull id)value {
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::ArrayContains
                                                 value:value];
 }
@@ -156,9 +156,9 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) arrayContainsAny:values];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
                    arrayContainsAny:(nonnull NSArray<id> *)values {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::ArrayContainsAny
                                                 value:values];
 }
@@ -167,8 +167,8 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) in:values];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field in:(nonnull NSArray<id> *)values {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path in:(nonnull NSArray<id> *)values {
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::In
                                                 value:values];
 }
@@ -177,9 +177,9 @@ FIRFieldPath *MakeFIRFieldPath(NSString *field) {
   return [self filterWhereFieldPath:MakeFIRFieldPath(field) notIn:values];
 }
 
-+ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)field
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
                               notIn:(nonnull NSArray<id> *)values {
-  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:field
+  return [[FSTUnaryFilter alloc] initWithFIRFieldPath:path
                                                    op:FieldFilter::Operator::NotIn
                                                 value:values];
 }

+ 0 - 3
Firestore/Source/API/FIRQuery+Internal.h

@@ -44,9 +44,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (const api::Query &)apiQuery;
 
-// TODO(orquery): This method will become public API. Change visibility and add documentation.
-- (FIRQuery *)queryWhereFilter:(FIRFilter *)filter;
-
 @end
 
 NS_ASSUME_NONNULL_END

+ 9 - 9
Firestore/Source/API/FIRQuery.mm

@@ -228,6 +228,15 @@ int32_t SaturatedLimitValue(NSInteger limit) {
                                                                         std::move(query_listener))];
 }
 
+- (FIRQuery *)queryWhereFilter:(FIRFilter *)filter {
+  Filter parsedFilter = [self parseFilter:filter];
+  if (parsedFilter.IsEmpty()) {
+    // Return the existing query if not adding any more filters (e.g. an empty composite filter).
+    return self;
+  }
+  return Wrap(_query.AddNewFilter(std::move(parsedFilter)));
+}
+
 - (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value {
   return [self queryWhereFilter:[FIRFilter filterWhereField:field isEqualTo:value]];
 }
@@ -668,15 +677,6 @@ int32_t SaturatedLimitValue(NSInteger limit) {
   return _query;
 }
 
-- (FIRQuery *)queryWhereFilter:(FIRFilter *)filter {
-  Filter parsedFilter = [self parseFilter:filter];
-  if (parsedFilter.IsEmpty()) {
-    // Return the existing query if not adding any more filters (e.g. an empty composite filter).
-    return self;
-  }
-  return Wrap(_query.AddNewFilter(std::move(parsedFilter)));
-}
-
 @end
 
 NS_ASSUME_NONNULL_END

+ 261 - 0
Firestore/Source/Public/FirebaseFirestore/FIRFilter.h

@@ -0,0 +1,261 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRFieldPath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A Filter represents a restriction on one or more field values and can be used to refine
+ * the results of a Query.
+ */
+NS_SWIFT_NAME(Filter)
+@interface FIRFilter : NSObject
+
+#pragma mark - Create Filter
+
+/**
+ * Creates a new filter for checking that the given field is equal to the given value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                      isEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is equal to the given value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                          isEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is not equal to the given value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                   isNotEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isNotEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is not equal to the given value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                       isNotEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isNotEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is greater than the given value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                  isGreaterThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isGreaterThan:));
+
+/**
+ * Creates a new filter for checking that the given field is greater than the given value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                      isGreaterThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isGreaterThan:));
+
+/**
+ * Creates a new filter for checking that the given field is greater than or equal to the given
+ * value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+         isGreaterThanOrEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isGreaterOrEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is greater than or equal to the given
+ * value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+             isGreaterThanOrEqualTo:(nonnull id)value
+    NS_SWIFT_NAME(whereField(_:isGreaterOrEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is less than the given value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                     isLessThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isLessThan:));
+
+/**
+ * Creates a new filter for checking that the given field is less than the given value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                         isLessThan:(nonnull id)value NS_SWIFT_NAME(whereField(_:isLessThan:));
+
+/**
+ * Creates a new filter for checking that the given field is less than or equal to the given
+ * value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+            isLessThanOrEqualTo:(nonnull id)value NS_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given field is less than or equal to the given
+ * value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                isLessThanOrEqualTo:(nonnull id)value
+    NS_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
+
+/**
+ * Creates a new filter for checking that the given array field contains the given value.
+ *
+ * @param field The field used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                  arrayContains:(nonnull id)value NS_SWIFT_NAME(whereField(_:arrayContains:));
+
+/**
+ * Creates a new filter for checking that the given array field contains the given value.
+ *
+ * @param path The field path used for the filter.
+ * @param value The value used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                      arrayContains:(nonnull id)value NS_SWIFT_NAME(whereField(_:arrayContains:));
+
+/**
+ * Creates a new filter for checking that the given array field contains any of the given values.
+ *
+ * @param field The field used for the filter.
+ * @param values The list of values used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+               arrayContainsAny:(nonnull NSArray<id> *)values
+    NS_SWIFT_NAME(whereField(_:arrayContainsAny:));
+
+/**
+ * Creates a new filter for checking that the given array field contains any of the given values.
+ *
+ * @param path The field path used for the filter.
+ * @param values The list of values used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                   arrayContainsAny:(nonnull NSArray<id> *)values
+    NS_SWIFT_NAME(whereField(_:arrayContainsAny:));
+
+/**
+ * Creates a new filter for checking that the given field equals any of the given values.
+ *
+ * @param field The field used for the filter.
+ * @param values The list of values used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                             in:(nonnull NSArray<id> *)values NS_SWIFT_NAME(whereField(_:in:));
+
+/**
+ * Creates a new filter for checking that the given field equals any of the given values.
+ *
+ * @param path The field path used for the filter.
+ * @param values The list of values used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                                 in:(nonnull NSArray<id> *)values NS_SWIFT_NAME(whereField(_:in:));
+
+/**
+ * Creates a new filter for checking that the given field does not equal any of the given values.
+ *
+ * @param field The field path used for the filter.
+ * @param values The list of values used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereField:(nonnull NSString *)field
+                          notIn:(nonnull NSArray<id> *)values NS_SWIFT_NAME(whereField(_:notIn:));
+
+/**
+ * Creates a new filter for checking that the given field does not equal any of the given values.
+ *
+ * @param path The field path used for the filter.
+ * @param values The list of values used for the filter.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)filterWhereFieldPath:(nonnull FIRFieldPath *)path
+                              notIn:(nonnull NSArray<id> *)values
+    NS_SWIFT_NAME(whereField(_:notIn:));
+
+/**
+ * Creates a new filter that is a disjunction of the given filters. A disjunction filter includes
+ * a document if it satisfies any of the given filters.
+ *
+ * @param filters The list of filters to perform a disjunction for.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)orFilterWithFilters:(NSArray<FIRFilter *> *)filters NS_SWIFT_NAME(orFilter(_:));
+
+/**
+ * Creates a new filter that is a conjunction of the given filters. A conjunction filter includes
+ * a document if it satisfies all of the given filters.
+ *
+ * @param filters The list of filters to perform a disjunction for.
+ * @return The newly created filter.
+ */
++ (FIRFilter *)andFilterWithFilters:(NSArray<FIRFilter *> *)filters NS_SWIFT_NAME(andFilter(_:));
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 9 - 0
Firestore/Source/Public/FirebaseFirestore/FIRQuery.h

@@ -22,6 +22,7 @@
 @class FIRAggregateQuery;
 @class FIRFieldPath;
 @class FIRFirestore;
+@class FIRFilter;
 @class FIRQuerySnapshot;
 @class FIRDocumentSnapshot;
 
@@ -103,6 +104,14 @@ NS_SWIFT_NAME(Query)
     NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:));
 
 #pragma mark - Filtering Data
+/**
+ * Creates and returns a new Query with the additional filter.
+ *
+ * @param filter The new filter to apply to the existing query.
+ * @return The newly created Query.
+ */
+- (FIRQuery *)queryWhereFilter:(FIRFilter *)filter NS_SWIFT_NAME(whereFilter(_:));
+
 /**
  * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be equal to the specified value.

+ 1 - 0
Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h

@@ -23,6 +23,7 @@
 #import "FIRDocumentSnapshot.h"
 #import "FIRFieldPath.h"
 #import "FIRFieldValue.h"
+#import "FIRFilter.h"
 #import "FIRFirestore.h"
 #import "FIRFirestoreErrors.h"
 #import "FIRFirestoreSettings.h"

+ 20 - 0
Firestore/Swift/Tests/Integration/AsyncAwaitIntegrationTests.swift

@@ -80,5 +80,25 @@ let emptyBundle = """
       let snapshot = try await collection.count.getAggregation(source: .server)
       XCTAssertEqual(snapshot.count, 1)
     }
+
+    func testQuery() async throws {
+      let collRef = collectionRef(
+        withDocuments: ["doc1": ["a": 1, "b": 0],
+                        "doc2": ["a": 2, "b": 1],
+                        "doc3": ["a": 3, "b": 2],
+                        "doc4": ["a": 1, "b": 3],
+                        "doc5": ["a": 1, "b": 1]]
+      )
+
+      // Two equalities: a==1 || b==1.
+      let filter = Filter.orFilter(
+        [Filter.whereField("a", isEqualTo: 1),
+         Filter.whereField("b", isEqualTo: 1)]
+      )
+      let query = collRef.whereFilter(filter)
+      let snapshot = try await query.getDocuments(source: FirestoreSource.server)
+      XCTAssertEqual(FIRQuerySnapshotGetIDs(snapshot),
+                     ["doc1", "doc2", "doc4", "doc5"])
+    }
   }
 #endif

+ 397 - 0
Firestore/Swift/Tests/Integration/QueryIntegrationTests.swift

@@ -0,0 +1,397 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import FirebaseFirestore
+import FirebaseFirestoreSwift
+import Foundation
+
+class QueryIntegrationTests: FSTIntegrationTestCase {
+  /**
+   * Checks that running the query while online (against the backend/emulator) results in the same
+   * documents as running the query while offline. If expectedDocs is provided, it also checks
+   * that both online and offline query result is equal to the expected documents.
+   *
+   * @param query The query to check.
+   * @param expectedDocs Ordered list of document keys that are expected to match the query.
+   */
+  private func checkOnlineAndOfflineQuery(_ query: Query, matchesResult expectedDocs: [String]?) {
+    let docsFromServer = readDocumentSet(forRef: query,
+                                         source: FirestoreSource.server)
+
+    let docsFromCache = readDocumentSet(forRef: query,
+                                        source: FirestoreSource.cache)
+
+    XCTAssertEqual(FIRQuerySnapshotGetIDs(docsFromServer),
+                   FIRQuerySnapshotGetIDs(docsFromCache))
+    if expectedDocs != nil {
+      XCTAssertEqual(FIRQuerySnapshotGetIDs(docsFromCache), expectedDocs)
+    }
+  }
+
+  func testOrQueries() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": 0],
+                      "doc2": ["a": 2, "b": 1],
+                      "doc3": ["a": 3, "b": 2],
+                      "doc4": ["a": 1, "b": 3],
+                      "doc5": ["a": 1, "b": 1]]
+    )
+
+    // Two equalities: a==1 || b==1.
+    let filter1 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 1),
+       Filter.whereField("b", isEqualTo: 1)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter1),
+                               matchesResult: ["doc1", "doc2", "doc4", "doc5"])
+
+    // with one inequality: a>2 || b==1.
+    let filter2 = Filter.orFilter(
+      [Filter.whereField("a", isGreaterThan: 2),
+       Filter.whereField("b", isEqualTo: 1)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter2),
+                               matchesResult: ["doc5", "doc2", "doc3"])
+
+    // (a==1 && b==0) || (a==3 && b==2)
+    let filter3 = Filter.orFilter(
+      [Filter.andFilter(
+        [Filter.whereField("a", isEqualTo: 1),
+         Filter.whereField("b", isEqualTo: 0)]
+      ),
+      Filter.andFilter(
+        [Filter.whereField("a", isEqualTo: 3),
+         Filter.whereField("b", isEqualTo: 2)]
+      )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter3),
+                               matchesResult: ["doc1", "doc3"])
+
+    // a==1 && (b==0 || b==3).
+    let filter4 = Filter.andFilter(
+      [Filter.whereField("a", isEqualTo: 1),
+       Filter.orFilter(
+         [Filter.whereField("b", isEqualTo: 0),
+          Filter.whereField("b", isEqualTo: 3)]
+       )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter4),
+                               matchesResult: ["doc1", "doc4"])
+
+    // (a==2 || b==2) && (a==3 || b==3)
+    let filter5 = Filter.andFilter(
+      [Filter.orFilter(
+        [Filter.whereField("a", isEqualTo: 2),
+         Filter.whereField("b", isEqualTo: 2)]
+      ),
+      Filter.orFilter(
+        [Filter.whereField("a", isEqualTo: 3),
+         Filter.whereField("b", isEqualTo: 3)]
+      )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter5),
+                               matchesResult: ["doc3"])
+
+    // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2
+    let filter6 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 1),
+       Filter.whereField("b", isGreaterThan: 0)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter6).limit(to: 2),
+                               matchesResult: ["doc1", "doc2"])
+
+    // Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2
+    // Note: The public query API does not allow implicit ordering when limitToLast is used.
+    let filter7 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 1),
+       Filter.whereField("b", isGreaterThan: 0)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter7)
+      .limit(toLast: 2)
+      .order(by: "b"),
+      matchesResult: ["doc3", "doc4"])
+
+    // Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1
+    let filter8 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", isEqualTo: 1)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter8).limit(to: 1)
+      .order(by: "a"),
+      matchesResult: ["doc5"])
+
+    // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1
+    let filter9 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", isEqualTo: 1)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter9).limit(toLast: 1)
+      .order(by: "a"),
+      matchesResult: ["doc2"])
+
+    // Test with limits without orderBy (the __name__ ordering is the tie breaker).
+    let filter10 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", isEqualTo: 1)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter10).limit(to: 1),
+                               matchesResult: ["doc2"])
+  }
+
+  func testOrQueriesWithInAndNotIn() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": 0],
+                      "doc2": ["b": 1],
+                      "doc3": ["a": 3, "b": 2],
+                      "doc4": ["a": 1, "b": 3],
+                      "doc5": ["a": 1],
+                      "doc6": ["a": 2]]
+    )
+
+    // a==2 || b in [2,3]
+    let filter1 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", in: [2, 3])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter1),
+                               matchesResult: ["doc3", "doc4", "doc6"])
+
+    // a==2 || b not-in [2,3]
+    // Has implicit orderBy b.
+    let filter2 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", notIn: [2, 3])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter2),
+                               matchesResult: ["doc1", "doc2"])
+  }
+
+  func testOrQueriesWithArrayMembership() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": [0]],
+                      "doc2": ["b": 1],
+                      "doc3": ["a": 3, "b": [2, 7]],
+                      "doc4": ["a": 1, "b": [3, 7]],
+                      "doc5": ["a": 1],
+                      "doc6": ["a": 2]]
+    )
+
+    // a==2 || b array-contains 7
+    let filter1 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", arrayContains: 7)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter1),
+                               matchesResult: ["doc3", "doc4", "doc6"])
+
+    // a==2 || b array-contains-any [0, 3]
+    let filter2 = Filter.orFilter(
+      [Filter.whereField("a", isEqualTo: 2),
+       Filter.whereField("b", arrayContainsAny: [0, 3])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter2),
+                               matchesResult: ["doc1", "doc4", "doc6"])
+  }
+
+  func testMultipleInOps() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": 0],
+                      "doc2": ["b": 1],
+                      "doc3": ["a": 3, "b": 2],
+                      "doc4": ["a": 1, "b": 3],
+                      "doc5": ["a": 1],
+                      "doc6": ["a": 2]]
+    )
+
+    // Two IN operations on different fields with disjunction.
+    let filter1 = Filter.orFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("b", in: [0, 2])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter1).order(by: "a"),
+                               matchesResult: ["doc1", "doc6", "doc3"])
+
+    // Two IN operations on different fields with conjunction.
+    let filter2 = Filter.andFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("b", in: [0, 2])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter2).order(by: "a"),
+                               matchesResult: ["doc3"])
+
+    // Two IN operations on the same field.
+    // a IN [1,2,3] && a IN [0,1,4] should result in "a==1".
+    let filter3 = Filter.andFilter(
+      [Filter.whereField("a", in: [1, 2, 3]),
+       Filter.whereField("a", in: [0, 1, 4])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter3),
+                               matchesResult: ["doc1", "doc4", "doc5"])
+
+    // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an empty set.
+    let filter4 = Filter.andFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("a", in: [0, 1, 4])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter4),
+                               matchesResult: [])
+
+    // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]).
+    let filter5 = Filter.orFilter(
+      [Filter.whereField("a", in: [0, 3]),
+       Filter.whereField("a", in: [0, 2])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter5),
+                               matchesResult: ["doc3", "doc6"])
+
+    // Nested composite filter on the same field.
+    let filter6 = Filter.andFilter(
+      [Filter.whereField("a", in: [1, 3]),
+       Filter.orFilter(
+         [Filter.whereField("a", in: [0, 2]),
+          Filter.andFilter(
+            [Filter.whereField("b", isGreaterOrEqualTo: 1),
+             Filter.whereField("a", in: [1, 3])]
+          )]
+       )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter6),
+                               matchesResult: ["doc3", "doc4"])
+
+    // Nested composite filter on different fields.
+    let filter7 = Filter.andFilter(
+      [Filter.whereField("b", in: [0, 3]),
+       Filter.orFilter(
+         [Filter.whereField("b", in: [1]),
+          Filter.andFilter(
+            [Filter.whereField("b", in: [2, 3]),
+             Filter.whereField("a", in: [1, 3])]
+          )]
+       )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter7),
+                               matchesResult: ["doc4"])
+  }
+
+  func testUseInWithArrayContainsAny() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": [0]],
+                      "doc2": ["b": [1]],
+                      "doc3": ["a": 3, "b": [2, 7], "c": 10],
+                      "doc4": ["a": 1, "b": [3, 7]],
+                      "doc5": ["a": 1],
+                      "doc6": ["a": 2, "c": 20]]
+    )
+
+    let filter1 = Filter.orFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("b", arrayContainsAny: [0, 7])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter1),
+                               matchesResult: ["doc1", "doc3", "doc4", "doc6"])
+
+    let filter2 = Filter.andFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("b", arrayContainsAny: [0, 7])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter2),
+                               matchesResult: ["doc3"])
+
+    let filter3 = Filter.orFilter(
+      [Filter.andFilter(
+        [Filter.whereField("a", in: [2, 3]),
+         Filter.whereField("c", isEqualTo: 10)]
+      ),
+      Filter.whereField("b", arrayContainsAny: [0, 7])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter3),
+                               matchesResult: ["doc1", "doc3", "doc4"])
+
+    let filter4 = Filter.andFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.orFilter(
+         [Filter.whereField("b", arrayContainsAny: [0, 7]),
+          Filter.whereField("c", isEqualTo: 20)]
+       )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter4),
+                               matchesResult: ["doc3", "doc6"])
+  }
+
+  func testUseInWithArrayContains() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": [0]],
+                      "doc2": ["b": [1]],
+                      "doc3": ["a": 3, "b": [2, 7]],
+                      "doc4": ["a": 1, "b": [3, 7]],
+                      "doc5": ["a": 1],
+                      "doc6": ["a": 2]]
+    )
+
+    let filter1 = Filter.orFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("b", arrayContainsAny: [3])]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter1),
+                               matchesResult: ["doc3", "doc4", "doc6"])
+
+    let filter2 = Filter.andFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.whereField("b", arrayContains: 7)]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter2),
+                               matchesResult: ["doc3"])
+
+    let filter3 = Filter.orFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.andFilter(
+         [Filter.whereField("b", arrayContains: 3),
+          Filter.whereField("a", isEqualTo: 1)]
+       )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter3),
+                               matchesResult: ["doc3", "doc4", "doc6"])
+
+    let filter4 = Filter.andFilter(
+      [Filter.whereField("a", in: [2, 3]),
+       Filter.orFilter(
+         [Filter.whereField("b", arrayContains: 7),
+          Filter.whereField("a", isEqualTo: 1)]
+       )]
+    )
+    checkOnlineAndOfflineQuery(collRef.whereFilter(filter4),
+                               matchesResult: ["doc3"])
+  }
+
+  func testOrderByEquality() throws {
+    let collRef = collectionRef(
+      withDocuments: ["doc1": ["a": 1, "b": [0]],
+                      "doc2": ["b": [1]],
+                      "doc3": ["a": 3, "b": [2, 7], "c": 10],
+                      "doc4": ["a": 1, "b": [3, 7]],
+                      "doc5": ["a": 1],
+                      "doc6": ["a": 2, "c": 20]]
+    )
+
+    checkOnlineAndOfflineQuery(collRef.whereFilter(Filter.whereField("a", isEqualTo: 1)),
+                               matchesResult: ["doc1", "doc4", "doc5"])
+
+    checkOnlineAndOfflineQuery(
+      collRef.whereFilter(Filter.whereField("a", in: [2, 3])).order(by: "a"),
+      matchesResult: ["doc6", "doc3"]
+    )
+  }
+}

+ 12 - 2
Firestore/core/src/core/composite_filter.cc

@@ -94,7 +94,18 @@ bool CompositeFilter::Rep::Matches(const model::Document& doc) const {
 }
 
 std::string CompositeFilter::Rep::CanonicalId() const {
-  // TODO(orquery): Add special case for flat AND filters.
+  // Older SDK versions use an implicit AND operation between their filters. In
+  // the new SDK versions, the developer may use an explicit AND filter. To stay
+  // consistent with the old usages, we add a special case to ensure the
+  // canonical ID for these two are the same. For example: `col.whereEquals("a",
+  // 1).whereEquals("b", 2)` should have the same canonical ID as
+  // `col.where(and(equals("a",1), equals("b",2)))`.
+  if (IsFlatConjunction()) {
+    return absl::StrJoin(filters_, "", [](std::string* out, const Filter& f) {
+      return absl::StrAppend(out, f.CanonicalId());
+    });
+  }
+
   return util::StringFormat(
       "%s(%s)", CanonicalName(op_),
       absl::StrJoin(filters_, ",", [](std::string* out, const Filter& f) {
@@ -108,7 +119,6 @@ bool CompositeFilter::Rep::Equals(const Filter::Rep& other) const {
   // Note: This comparison requires order of filters in the list to be the same,
   // and it does not remove duplicate subfilters from each composite filter.
   // It is therefore way less expensive.
-  // TODO(orquery): Consider removing duplicates and ignoring order of filters
   // in the list.
   return op_ == other_rep.op_ && filters_ == other_rep.filters_;
 }

+ 5 - 1
Firestore/core/src/core/composite_filter.h

@@ -69,7 +69,7 @@ class CompositeFilter : public Filter {
    * false otherwise.
    */
   bool IsFlatConjunction() const {
-    return IsFlat() && IsConjunction();
+    return composite_filter_rep().IsFlatConjunction();
   }
 
   /**
@@ -116,6 +116,10 @@ class CompositeFilter : public Filter {
 
     bool IsFlat() const;
 
+    bool IsFlatConjunction() const {
+      return IsFlat() && IsConjunction();
+    }
+
     bool IsACompositeFilter() const override {
       return true;
     }

+ 0 - 2
Firestore/core/src/remote/serializer.cc

@@ -813,8 +813,6 @@ std::vector<Filter> Serializer::DecodeFilters(
 
   // Instead of a singletonList containing AND(F1, F2, ...), we can return
   // a list containing F1, F2, ...
-  // TODO(orquery): Once proper support for composite filters has been
-  // completed, we can remove this flattening from here.
   if (decoded_filter.IsACompositeFilter()) {
     CompositeFilter composite_filter(decoded_filter);
     if (composite_filter.IsFlatConjunction()) {

+ 8 - 0
Firestore/core/test/unit/core/filter_test.cc

@@ -17,6 +17,7 @@
 #include "Firestore/core/src/core/filter.h"
 #include "Firestore/core/src/core/composite_filter.h"
 #include "Firestore/core/src/core/field_filter.h"
+#include "Firestore/core/src/core/query.h"
 #include "Firestore/core/test/unit/testutil/testutil.h"
 #include "gtest/gtest.h"
 
@@ -27,6 +28,7 @@ namespace core {
 using testutil::AndFilters;
 using testutil::Field;
 using testutil::OrFilters;
+using testutil::Query;
 using testutil::Resource;
 using testutil::Value;
 
@@ -99,6 +101,12 @@ TEST(FilterTest, CompositeFilterNestedChecks) {
   EXPECT_FALSE(or_filter2.IsFlatConjunction());
 }
 
+TEST(FilterTest, CanonicalIdOfFlatConjunctions) {
+  auto query1 = Query("col").AddingFilter(A).AddingFilter(B).AddingFilter(C);
+  auto query2 = Query("col").AddingFilter(AndFilters({A, B, C}));
+  EXPECT_EQ(query1.CanonicalId(), query2.CanonicalId());
+}
+
 }  // namespace core
 }  // namespace firestore
 }  // namespace firebase