diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 2921f9cffca..436edb55a59 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -140,7 +140,6 @@ 1145D70555D8CDC75183A88C /* leveldb_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */; }; 11627F3A48F710D654829807 /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 87DD1A65EBA9FFC1FFAAE657 /* comparison_test.cc */; }; 117AFA7934A52466633E12C1 /* FSTTestingHooks.mm in Sources */ = {isa = PBXBuildFile; fileRef = D85AC18C55650ED230A71B82 /* FSTTestingHooks.mm */; }; - 11A5189E73D954824F015424 /* pipeline_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */; }; 11BC867491A6631D37DE56A8 /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; 11EBD28DBD24063332433947 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 40F9D09063A07F710811A84F /* value_util_test.cc */; }; 11F8EE69182C9699E90A9E3D /* database_info_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D92E20235D22000A432D /* database_info_test.cc */; }; @@ -149,6 +148,9 @@ 121F0FB9DCCBFB7573C7AF48 /* bundle_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C2A94EE24E60543F62CC35 /* bundle_serializer_test.cc */; }; 124AAEE987451820F24EEA8E /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; }; 125B1048ECB755C2106802EB /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; + 128F2B012E254E2C0006327E /* QueryToPipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128F2B002E254E2C0006327E /* QueryToPipelineTests.swift */; }; + 128F2B022E254E2C0006327E /* QueryToPipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128F2B002E254E2C0006327E /* QueryToPipelineTests.swift */; }; + 128F2B032E254E2C0006327E /* QueryToPipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128F2B002E254E2C0006327E /* QueryToPipelineTests.swift */; }; 1290FA77A922B76503AE407C /* lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */; }; 1291D9F5300AFACD1FBD262D /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; 1296CECE2DEE97F5007F8552 /* RealtimePipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1296CECD2DEE97EF007F8552 /* RealtimePipelineTests.swift */; }; @@ -524,7 +526,6 @@ 48720B5768AFA2B2F3E14C04 /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D8E530B27D5641B9C26A452C /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json */; }; 48926FF55484E996B474D32F /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = DD990FD89C165F4064B4F608 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json */; }; 489D672CAA09B9BC66798E9F /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; - 48A9AD22B0601C52B0522CF7 /* pipeline_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */; }; 48BC5801432127A90CFF55E3 /* index.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 395E8B07639E69290A929695 /* index.pb.cc */; }; 48D1B38B93D34F1B82320577 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 48F44AA226FAD5DE4EAC3798 /* leveldb_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = DB1F1E1B1ED15E8D042144B1 /* leveldb_query_engine_test.cc */; }; @@ -723,7 +724,6 @@ 5556B648B9B1C2F79A706B4F /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; }; 55B9A6ACDF95D356EA501D92 /* Pods_Firestore_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB5A5E6DD07DA3EB7AD46CA7 /* Pods_Firestore_Example_iOS.framework */; }; 55E84644D385A70E607A0F91 /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; }; - 563FE05627C7E66469E99292 /* pipeline_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */; }; 568EC1C0F68A7B95E57C8C6C /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; }; 56D85436D3C864B804851B15 /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; }; 57171BD004A1691B19A76453 /* Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C939D1789E38C09F9A0C1157 /* Validation_BloomFilterTest_MD5_1_0001_membership_test_result.json */; }; @@ -962,7 +962,6 @@ 75C6CECF607CA94F56260BAB /* memory_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */; }; 75CC1D1F7F1093C2E09D9998 /* inequality_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A410E38FA5C3EB5AECDB6F1C /* inequality_test.cc */; }; 75D124966E727829A5F99249 /* FIRTypeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E071202154D600B64F25 /* FIRTypeTests.mm */; }; - 7676C06AF7FF67806747E4F0 /* pipeline_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */; }; 76A5447D76F060E996555109 /* task_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 899FC22684B0F7BEEAE13527 /* task_test.cc */; }; 76AD5862714F170251BDEACB /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = A5D9044B72061CAF284BC9E4 /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json */; }; 76C18D1BA96E4F5DF1BF7F4B /* Validation_BloomFilterTest_MD5_500_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AB49283E544497A9C5A0E59 /* Validation_BloomFilterTest_MD5_500_1_membership_test_result.json */; }; @@ -1650,7 +1649,6 @@ E1016ECF143B732E7821358E /* byte_stream_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7628664347B9C96462D4BF17 /* byte_stream_apple_test.mm */; }; E11DDA3DD75705F26245E295 /* FIRCollectionReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E045202154AA00B64F25 /* FIRCollectionReferenceTests.mm */; }; E1264B172412967A09993EC6 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; - E14DBE1D9FC94B5E7E391BEE /* pipeline_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */; }; E15A05789FF01F44BCAE75EF /* fields_array_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA4CBA48204C9E25B56993BC /* fields_array_test.cc */; }; E186D002520881AD2906ADDB /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; E1DB8E1A4CF3DCE2AE8454D8 /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; }; @@ -1676,7 +1674,6 @@ E54AC3EA240C05B3720A2FE9 /* Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 728F617782600536F2561463 /* Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json */; }; E56EEC9DAC455E2BE77D110A /* memory_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */; }; E59F597947D3E130A57E1B5E /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3369AC938F82A70685C5ED58 /* Validation_BloomFilterTest_MD5_1_1_membership_test_result.json */; }; - E5FE2BEECD70D59361B51540 /* pipeline_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */; }; E63342115B1DA65DB6F2C59A /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; }; E6357221227031DD77EE5265 /* index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AE4A9E38D65688EE000EE2A1 /* index_manager_test.cc */; }; E6603BA4B16C9E1422DD3A4B /* FSTTestingHooks.mm in Sources */ = {isa = PBXBuildFile; fileRef = D85AC18C55650ED230A71B82 /* FSTTestingHooks.mm */; }; @@ -1908,8 +1905,7 @@ 014C60628830D95031574D15 /* random_access_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = random_access_queue_test.cc; sourceTree = ""; }; 01D10113ECC5B446DB35E96D /* byte_stream_cpp_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = byte_stream_cpp_test.cc; sourceTree = ""; }; 03BD47161789F26754D3B958 /* Pods-Firestore_Benchmarks_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Benchmarks_iOS.release.xcconfig"; path = "Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS.release.xcconfig"; sourceTree = ""; }; - 0401C6FDE59C493BFBD5DFED /* pipeline_util_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = pipeline_util_test.cc; sourceTree = ""; }; - 0458BABD8F8738AD16F4A2FE /* array_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; name = array_test.cc; path = expressions/array_test.cc; sourceTree = ""; }; + 0458BABD8F8738AD16F4A2FE /* array_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = array_test.cc; path = expressions/array_test.cc; sourceTree = ""; }; 045D39C4A7D52AF58264240F /* remote_document_cache_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = remote_document_cache_test.h; sourceTree = ""; }; 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = ordered_code_benchmark.cc; sourceTree = ""; }; 062072B62773A055001655D7 /* AsyncAwaitIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAwaitIntegrationTests.swift; sourceTree = ""; }; @@ -1923,6 +1919,7 @@ 1235769122B7E915007DDFA9 /* EncodableFieldValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodableFieldValueTests.swift; sourceTree = ""; }; 1235769422B86E65007DDFA9 /* FirestoreEncoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirestoreEncoderTests.swift; sourceTree = ""; }; 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableIntegrationTests.swift; sourceTree = ""; }; + 128F2B002E254E2C0006327E /* QueryToPipelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryToPipelineTests.swift; sourceTree = ""; }; 1296CECD2DEE97EF007F8552 /* RealtimePipelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimePipelineTests.swift; sourceTree = ""; }; 129A369928CA555B005AE7E2 /* FIRCountTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRCountTests.mm; sourceTree = ""; }; 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; @@ -2504,6 +2501,7 @@ 62E54B832A9E910A003347C8 /* IndexingTests.swift */, 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */, 861684E49DAC993D153E60D0 /* PipelineTests.swift */, + 128F2B002E254E2C0006327E /* QueryToPipelineTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, 1296CECD2DEE97EF007F8552 /* RealtimePipelineTests.swift */, 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, @@ -5059,6 +5057,7 @@ C840AD39F7EC5524F1C0F5AE /* filter_test.cc in Sources */, A873EE3C8A97C90BA978B68A /* firebase_app_check_credentials_provider_test.mm in Sources */, F7EE3CCC821975B71E834453 /* firebase_auth_credentials_provider_test.mm in Sources */, + 128F2B022E254E2C0006327E /* QueryToPipelineTests.swift in Sources */, 8C602DAD4E8296AB5EFB962A /* firestore.pb.cc in Sources */, 6ABB82D43C0728EB095947AF /* geo_point_test.cc in Sources */, 5DE8F28A95F7CBD2B699D470 /* globals_cache_test.cc in Sources */, @@ -5340,6 +5339,7 @@ 0C10A73586C704EB8361D3BD /* filter_test.cc in Sources */, 992DD6779C7A166D3A22E749 /* firebase_app_check_credentials_provider_test.mm in Sources */, B6BEB7AF975FA31E169B7DD2 /* firebase_auth_credentials_provider_test.mm in Sources */, + 128F2B032E254E2C0006327E /* QueryToPipelineTests.swift in Sources */, D756A1A63E626572EE8DF592 /* firestore.pb.cc in Sources */, 8B31F63673F3B5238DE95AFB /* geo_point_test.cc in Sources */, FC6C9D1A8B24A5C9507272F7 /* globals_cache_test.cc in Sources */, @@ -5903,6 +5903,7 @@ 0AB8193385042B3DF56190B1 /* filter_test.cc in Sources */, F5B1F219E912F645FB79D08E /* firebase_app_check_credentials_provider_test.mm in Sources */, 58693C153EC597BC25EE9648 /* firebase_auth_credentials_provider_test.mm in Sources */, + 128F2B012E254E2C0006327E /* QueryToPipelineTests.swift in Sources */, 920B6ABF76FDB3547F1CCD84 /* firestore.pb.cc in Sources */, 5FE84472E5369DA866193C45 /* geo_point_test.cc in Sources */, C4D430E12F46F05416A66E0A /* globals_cache_test.cc in Sources */, diff --git a/Firestore/Source/API/FIRPipelineBridge+Internal.h b/Firestore/Source/API/FIRPipelineBridge+Internal.h index 48c2df15128..c1a11e64616 100644 --- a/Firestore/Source/API/FIRPipelineBridge+Internal.h +++ b/Firestore/Source/API/FIRPipelineBridge+Internal.h @@ -48,6 +48,39 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface FIRCollectionSourceStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + +@interface FIRDatabaseSourceStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + +@interface FIRCollectionGroupSourceStageBridge (Internal) +- (id)initWithCppStage: + (std::shared_ptr)stage; +@end + +@interface FIRDocumentsSourceStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + +@interface FIRWhereStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + +@interface FIRLimitStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + +@interface FIROffsetStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + +@interface FIRSorStageBridge (Internal) +- (id)initWithCppStage:(std::shared_ptr)stage; +@end + @interface __FIRPipelineSnapshotBridge (Internal) - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot; diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index d6d61ca2d0e..34f27cabc89 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -26,6 +26,7 @@ #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" #import "Firestore/Source/API/FIRPipelineBridge+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" #import "Firestore/Source/API/FSTUserDataWriter.h" @@ -256,6 +257,11 @@ - (Ordering)cppOrderingWithReader:(FSTUserDataReader *)reader { @end @implementation FIRStageBridge +- (NSString *)name { + [NSException raise:NSInternalInconsistencyException + format:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]; + return nil; +} @end @implementation FIRCollectionSourceStageBridge { @@ -283,6 +289,17 @@ - (id)initWithRef:(FIRCollectionReference *)ref firestore:(FIRFirestore *)db { return collection_source; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + collection_source = std::const_pointer_cast(stage); + } + return self; +} + +- (NSString *)name { + return @"collection"; +} @end @implementation FIRDatabaseSourceStageBridge { @@ -301,6 +318,17 @@ - (id)init { return cpp_database_source; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_database_source = std::const_pointer_cast(stage); + } + return self; +} + +- (NSString *)name { + return @"database"; +} @end @implementation FIRCollectionGroupSourceStageBridge { @@ -319,6 +347,17 @@ - (id)initWithCollectionId:(NSString *)id { return cpp_collection_group_source; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_collection_group_source = std::const_pointer_cast(stage); + } + return self; +} + +- (NSString *)name { + return @"collection_group"; +} @end @implementation FIRDocumentsSourceStageBridge { @@ -350,6 +389,17 @@ - (id)initWithDocuments:(NSArray *)documents firestore:( return cpp_document_source; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_document_source = std::const_pointer_cast(stage); + } + return self; +} + +- (NSString *)name { + return @"documents"; +} @end @implementation FIRWhereStageBridge { @@ -376,6 +426,18 @@ - (id)initWithExpr:(FIRExprBridge *)expr { return cpp_where; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_where = std::const_pointer_cast(stage); + isUserDataRead = YES; + } + return self; +} + +- (NSString *)name { + return @"where"; +} @end @implementation FIRLimitStageBridge { @@ -402,6 +464,18 @@ - (id)initWithLimit:(NSInteger)value { return cpp_limit_stage; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_limit_stage = std::const_pointer_cast(stage); + isUserDataRead = YES; + } + return self; +} + +- (NSString *)name { + return @"limit"; +} @end @implementation FIROffsetStageBridge { @@ -428,6 +502,18 @@ - (id)initWithOffset:(NSInteger)value { return cpp_offset_stage; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_offset_stage = std::const_pointer_cast(stage); + isUserDataRead = YES; + } + return self; +} + +- (NSString *)name { + return @"offset"; +} @end // TBD @@ -460,6 +546,9 @@ - (id)initWithFields:(NSDictionary *)fields { return cpp_add_fields; } +- (NSString *)name { + return @"add_fields"; +} @end @implementation FIRRemoveFieldsStageBridge { @@ -490,6 +579,9 @@ - (id)initWithFields:(NSArray *)fields { return cpp_remove_fields; } +- (NSString *)name { + return @"remove_fields"; +} @end @implementation FIRSelectStageBridge { @@ -520,6 +612,9 @@ - (id)initWithSelections:(NSDictionary *)selections return cpp_select; } +- (NSString *)name { + return @"select"; +} @end @implementation FIRDistinctStageBridge { @@ -550,6 +645,9 @@ - (id)initWithGroups:(NSDictionary *)groups { return cpp_distinct; } +- (NSString *)name { + return @"distinct"; +} @end @implementation FIRAggregateStageBridge { @@ -589,6 +687,9 @@ - (id)initWithAccumulators:(NSDictionary *)orderings { return cpp_sort; } +- (id)initWithCppStage:(std::shared_ptr)stage { + self = [super init]; + if (self) { + cpp_sort = std::const_pointer_cast(stage); + isUserDataRead = YES; + } + return self; +} + +- (NSString *)name { + return @"sort"; +} @end @implementation FIRReplaceWithStageBridge { @@ -706,6 +822,9 @@ - (id)initWithExpr:(FIRExprBridge *)expr { return cpp_replace_with; } +- (NSString *)name { + return @"replace_with"; +} @end @implementation FIRSampleStageBridge { @@ -753,6 +872,9 @@ - (id)initWithPercentage:(double)percentage { return cpp_sample; } +- (NSString *)name { + return @"sample"; +} @end @implementation FIRUnionStageBridge { @@ -779,6 +901,9 @@ - (id)initWithOther:(FIRPipelineBridge *)other { return cpp_union_stage; } +- (NSString *)name { + return @"union"; +} @end @implementation FIRUnnestStageBridge { @@ -818,6 +943,9 @@ - (id)initWithField:(FIRExprBridge *)field return cpp_unnest; } +- (NSString *)name { + return @"unnest"; +} @end @implementation FIRRawStageBridge { @@ -900,6 +1028,9 @@ - (id)initWithName:(NSString *)name return cpp_generic_stage; } +- (NSString *)name { + return _name; +} @end @interface __FIRPipelineSnapshotBridge () @@ -1118,6 +1249,39 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r return cpp_pipeline; } ++ (NSArray *)createStageBridgesFromQuery:(FIRQuery *)query { + std::vector> evaluable_stages = + firebase::firestore::core::ToPipelineStages(query.query); + std::vector> cpp_stages(evaluable_stages.begin(), + evaluable_stages.end()); + NSMutableArray *stageBridges = [NSMutableArray array]; + + for (const auto &cpp_stage_base : cpp_stages) { + if (auto cpp_stage = std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges addObject:[[FIRCollectionSourceStageBridge alloc] initWithCppStage:cpp_stage]]; + } else if (auto cpp_stage = + std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges + addObject:[[FIRCollectionGroupSourceStageBridge alloc] initWithCppStage:cpp_stage]]; + } else if (auto cpp_stage = std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges addObject:[[FIRDocumentsSourceStageBridge alloc] initWithCppStage:cpp_stage]]; + } else if (auto cpp_stage = std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges addObject:[[FIRWhereStageBridge alloc] initWithCppStage:cpp_stage]]; + } else if (auto cpp_stage = std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges addObject:[[FIRLimitStageBridge alloc] initWithCppStage:cpp_stage]]; + } else if (auto cpp_stage = std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges addObject:[[FIRSorStageBridge alloc] initWithCppStage:cpp_stage]]; + } else if (auto cpp_stage = std::dynamic_pointer_cast(cpp_stage_base)) { + [stageBridges addObject:[[FIROffsetStageBridge alloc] initWithCppStage:cpp_stage]]; + } else { + ThrowInvalidArgument( + "Unknown or unhandled stage type '%s' encountered when converting from FIRQuery.", + cpp_stage_base->name().c_str()); + } + } + return [stageBridges copy]; +} + @end @interface __FIRRealtimePipelineSnapshotBridge () @@ -1297,7 +1461,7 @@ - (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { wrapped_firestore->client()->user_executor(), std::move(view_listener)); std::shared_ptr query_listener = wrapped_firestore->client()->ListenToQuery( - *cpp_pipeline, ToListenOptions(options), async_listener); + core::QueryOrPipeline(*cpp_pipeline), ToListenOptions(options), async_listener); return [[FSTListenerRegistration alloc] initWithRegistration:absl::make_unique(wrapped_firestore->client(), diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index 209c1666c93..4c8d9a041ac 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -69,6 +69,7 @@ NS_SWIFT_NAME(OrderingBridge) NS_SWIFT_SENDABLE NS_SWIFT_NAME(StageBridge) @interface FIRStageBridge : NSObject +@property(nonatomic, readonly) NSString *name; @end NS_SWIFT_SENDABLE @@ -76,7 +77,6 @@ NS_SWIFT_NAME(CollectionSourceStageBridge) @interface FIRCollectionSourceStageBridge : FIRStageBridge - (id)initWithRef:(FIRCollectionReference *)ref firestore:(FIRFirestore *)db; - @end NS_SWIFT_SENDABLE @@ -84,7 +84,6 @@ NS_SWIFT_NAME(DatabaseSourceStageBridge) @interface FIRDatabaseSourceStageBridge : FIRStageBridge - (id)init; - @end NS_SWIFT_SENDABLE @@ -92,7 +91,6 @@ NS_SWIFT_NAME(CollectionGroupSourceStageBridge) @interface FIRCollectionGroupSourceStageBridge : FIRStageBridge - (id)initWithCollectionId:(NSString *)id; - @end NS_SWIFT_SENDABLE @@ -100,7 +98,6 @@ NS_SWIFT_NAME(DocumentsSourceStageBridge) @interface FIRDocumentsSourceStageBridge : FIRStageBridge - (id)initWithDocuments:(NSArray *)documents firestore:(FIRFirestore *)db; - @end NS_SWIFT_SENDABLE @@ -108,7 +105,6 @@ NS_SWIFT_NAME(WhereStageBridge) @interface FIRWhereStageBridge : FIRStageBridge - (id)initWithExpr:(FIRExprBridge *)expr; - @end NS_SWIFT_SENDABLE @@ -116,7 +112,6 @@ NS_SWIFT_NAME(LimitStageBridge) @interface FIRLimitStageBridge : FIRStageBridge - (id)initWithLimit:(NSInteger)value; - @end NS_SWIFT_SENDABLE @@ -124,7 +119,6 @@ NS_SWIFT_NAME(OffsetStageBridge) @interface FIROffsetStageBridge : FIRStageBridge - (id)initWithOffset:(NSInteger)value; - @end NS_SWIFT_SENDABLE @@ -269,6 +263,7 @@ NS_SWIFT_NAME(PipelineBridge) - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, NSError *_Nullable error))completion; ++ (NSArray *)createStageBridgesFromQuery:(FIRQuery *)query; @end NS_SWIFT_SENDABLE diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 90f906e2a6f..81e44cf1bdb 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -49,10 +49,30 @@ public struct PipelineSource

: @unchecked Sendable { } public func create(from query: Query) -> P { - return factory([QuerySource(query: query)], db) - } - - public func create(from aggregateQuery: AggregateQuery) -> P { - return factory([AggregateQuerySource(aggregateQuery: aggregateQuery)], db) + let stageBridges = PipelineBridge.createStageBridges(from: query) + let stages: [Stage] = stageBridges.map { bridge in + switch bridge.name { + case "collection": + return CollectionSource( + bridge: bridge as! CollectionSourceStageBridge, + db: query.firestore + ) + case "collection_group": + return CollectionGroupSource(bridge: bridge as! CollectionGroupSourceStageBridge) + case "documents": + return DocumentsSource(bridge: bridge as! DocumentsSourceStageBridge, db: query.firestore) + case "where": + return Where(bridge: bridge as! WhereStageBridge) + case "limit": + return Limit(bridge: bridge as! LimitStageBridge) + case "sort": + return Sort(bridge: bridge as! SortStageBridge) + case "offset": + return Offset(bridge: bridge as! OffsetStageBridge) + default: + fatalError("Unknown stage type \(bridge.name)") + } + } + return factory(stages, db) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index 9f6d071d9ff..9f43ab9641e 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -33,27 +33,32 @@ class CollectionSource: Stage { let name: String = "collection" let bridge: StageBridge - private var collection: CollectionReference private let db: Firestore init(collection: CollectionReference, db: Firestore) { - self.collection = collection self.db = db bridge = CollectionSourceStageBridge(ref: collection, firestore: db) } + + init(bridge: CollectionSourceStageBridge, db: Firestore) { + self.db = db + self.bridge = bridge + } } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class CollectionGroupSource: Stage { - let name: String = "collectionId" + let name: String = "collection_group" let bridge: StageBridge - private var collectionId: String init(collectionId: String) { - self.collectionId = collectionId bridge = CollectionGroupSourceStageBridge(collectionId: collectionId) } + + init(bridge: CollectionGroupSourceStageBridge) { + self.bridge = bridge + } } // Represents the entire database as a source. @@ -65,6 +70,10 @@ class DatabaseSource: Stage { init() { bridge = DatabaseSourceStageBridge() } + + init(bridge: DatabaseSourceStageBridge) { + self.bridge = bridge + } } // Represents a list of document references as a source. @@ -72,42 +81,17 @@ class DatabaseSource: Stage { class DocumentsSource: Stage { let name: String = "documents" let bridge: StageBridge - private var docs: [DocumentReference] private let db: Firestore // Initialize with an array of String paths init(docs: [DocumentReference], db: Firestore) { - self.docs = docs self.db = db bridge = DocumentsSourceStageBridge(documents: docs, firestore: db) } -} - -// Represents an existing Query as a source. -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -class QuerySource: Stage { - let name: String = "query" - let bridge: StageBridge - private var query: Query - - init(query: Query) { - self.query = query - bridge = DatabaseSourceStageBridge() - // TODO: bridge = QuerySourceStageBridge(query: query.query) - } -} -// Represents an existing AggregateQuery as a source. -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -class AggregateQuerySource: Stage { - let name: String = "aggregateQuery" - let bridge: StageBridge - private var aggregateQuery: AggregateQuery - - init(aggregateQuery: AggregateQuery) { - self.aggregateQuery = aggregateQuery - bridge = DatabaseSourceStageBridge() - // TODO: bridge = AggregateQuerySourceStageBridge(aggregateQuery: aggregateQuery.query) + init(bridge: DocumentsSourceStageBridge, db: Firestore) { + self.db = db + self.bridge = bridge } } @@ -116,12 +100,14 @@ class Where: Stage { let name: String = "where" let bridge: StageBridge - private var condition: BooleanExpr init(condition: BooleanExpr) { - self.condition = condition bridge = WhereStageBridge(expr: condition.toBridge()) } + + init(bridge: WhereStageBridge) { + self.bridge = bridge + } } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @@ -129,12 +115,14 @@ class Limit: Stage { let name: String = "limit" let bridge: StageBridge - private var limit: Int32 init(_ limit: Int32) { - self.limit = limit bridge = LimitStageBridge(limit: NSInteger(limit)) } + + init(bridge: LimitStageBridge) { + self.bridge = bridge + } } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @@ -142,17 +130,19 @@ class Offset: Stage { let name: String = "offset" let bridge: StageBridge - private var offset: Int32 init(_ offset: Int32) { - self.offset = offset bridge = OffsetStageBridge(offset: NSInteger(offset)) } + + init(bridge: OffsetStageBridge) { + self.bridge = bridge + } } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AddFields: Stage { - let name: String = "addFields" + let name: String = "add_fields" let bridge: StageBridge private var fields: [Selectable] @@ -171,7 +161,7 @@ class AddFields: Stage { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class RemoveFieldsStage: Stage { - let name: String = "removeFields" + let name: String = "remove_fields" let bridge: StageBridge private var fields: [String] @@ -190,10 +180,8 @@ class RemoveFieldsStage: Stage { class Select: Stage { let name: String = "select" let bridge: StageBridge - private var selections: [Selectable] init(selections: [Selectable]) { - self.selections = selections let map = Helper.selectablesToMap(selectables: selections) bridge = SelectStageBridge(selections: map .mapValues { Helper.sendableToExpr($0).toBridge() }) @@ -204,10 +192,8 @@ class Select: Stage { class Distinct: Stage { let name: String = "distinct" let bridge: StageBridge - private var groups: [Selectable] init(groups: [Selectable]) { - self.groups = groups let map = Helper.selectablesToMap(selectables: groups) bridge = DistinctStageBridge(groups: map .mapValues { Helper.sendableToExpr($0).toBridge() }) @@ -218,28 +204,26 @@ class Distinct: Stage { class Aggregate: Stage { let name: String = "aggregate" let bridge: StageBridge - private var accumulators: [AggregateWithAlias] - private var groups: [String: Expr] = [:] init(accumulators: [AggregateWithAlias], groups: [Selectable]?) { - self.accumulators = accumulators + var groupsMap: [String: Expr] = [:] if groups != nil { - self.groups = Helper.selectablesToMap(selectables: groups!) + groupsMap = Helper.selectablesToMap(selectables: groups!) } - let map = accumulators + let accumulatorsMap = accumulators .reduce(into: [String: AggregateFunctionBridge]()) { result, accumulator in result[accumulator.alias] = accumulator.aggregate.bridge } bridge = AggregateStageBridge( - accumulators: map, - groups: self.groups.mapValues { Helper.sendableToExpr($0).toBridge() } + accumulators: accumulatorsMap, + groups: groupsMap.mapValues { Helper.sendableToExpr($0).toBridge() } ) } } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class FindNearest: Stage { - let name: String = "findNearest" + let name: String = "find_nearest" let bridge: StageBridge private var field: Field private var vectorValue: [Double] @@ -271,17 +255,19 @@ class FindNearest: Stage { class Sort: Stage { let name: String = "sort" let bridge: StageBridge - private var orderings: [Ordering] init(orderings: [Ordering]) { - self.orderings = orderings bridge = SortStageBridge(orderings: orderings.map { $0.bridge }) } + + init(bridge: SortStageBridge) { + self.bridge = bridge + } } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class ReplaceWith: Stage { - let name: String = "replaceWith" + let name: String = "replace_with" let bridge: StageBridge private var expr: Expr diff --git a/Firestore/Swift/Tests/Integration/AggregationIntegrationTests.swift b/Firestore/Swift/Tests/Integration/AggregationIntegrationTests.swift index 85aab4d29a4..bbb00599b51 100644 --- a/Firestore/Swift/Tests/Integration/AggregationIntegrationTests.swift +++ b/Firestore/Swift/Tests/Integration/AggregationIntegrationTests.swift @@ -31,6 +31,7 @@ class AggregationIntegrationTests: FSTIntegrationTestCase { try await collection.addDocument(data: ["author": "authorA", "title": "titleA", "pages": 100, + "height": 24.5, "weight": 24.1, "foo": 1, diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index f712cceca1f..aecee67f538 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -36,9 +36,6 @@ final class PipelineTests: FSTIntegrationTestCase { let query: Query = db.collection("foo").limit(to: 2) let _: Pipeline = pipelineSource.create(from: query) - let aggregateQuery = db.collection("foo").count - let _: Pipeline = pipelineSource.create(from: aggregateQuery) - let _: PipelineSnapshot = try await pipeline.execute() } diff --git a/Firestore/Swift/Tests/Integration/QueryIntegrationTests.swift b/Firestore/Swift/Tests/Integration/QueryIntegrationTests.swift index bc71699774c..d17c58f14bc 100644 --- a/Firestore/Swift/Tests/Integration/QueryIntegrationTests.swift +++ b/Firestore/Swift/Tests/Integration/QueryIntegrationTests.swift @@ -18,7 +18,20 @@ import FirebaseFirestore import Foundation class QueryIntegrationTests: FSTIntegrationTestCase { - func testOrQueries() throws { + class var isRunningPipeline: Bool { + return false + } + + open func check(_ coll: CollectionReference, query: Query, + matchesResult expectedKeys: [String]) async throws { + checkOnlineAndOfflineCollection( + coll, + query: query, + matchesResult: expectedKeys + ) + } + + func testOrQueries() async throws { let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": 0], "doc2": ["a": 2, "b": 1], @@ -32,8 +45,8 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isEqualTo: 1), Filter.whereField("b", isEqualTo: 1)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter1), - matchesResult: ["doc1", "doc2", "doc4", "doc5"]) + try await check(collRef, query: collRef.whereFilter(filter1), + matchesResult: ["doc1", "doc2", "doc4", "doc5"]) // (a==1 && b==0) || (a==3 && b==2) let filter2 = Filter.orFilter( @@ -46,8 +59,8 @@ class QueryIntegrationTests: FSTIntegrationTestCase { Filter.whereField("b", isEqualTo: 2)] )] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter2), - matchesResult: ["doc1", "doc3"]) + try await check(collRef, query: collRef.whereFilter(filter2), + matchesResult: ["doc1", "doc3"]) // a==1 && (b==0 || b==3). let filter3 = Filter.andFilter( @@ -57,8 +70,8 @@ class QueryIntegrationTests: FSTIntegrationTestCase { Filter.whereField("b", isEqualTo: 3)] )] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter3), - matchesResult: ["doc1", "doc4"]) + try await check(collRef, query: collRef.whereFilter(filter3), + matchesResult: ["doc1", "doc4"]) // (a==2 || b==2) && (a==3 || b==3) let filter4 = Filter.andFilter( @@ -71,21 +84,21 @@ class QueryIntegrationTests: FSTIntegrationTestCase { Filter.whereField("b", isEqualTo: 3)] )] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter4), - matchesResult: ["doc3"]) + try await check(collRef, query: collRef.whereFilter(filter4), + matchesResult: ["doc3"]) // Test with limits without orderBy (the __name__ ordering is the tie breaker). let filter5 = Filter.orFilter( [Filter.whereField("a", isEqualTo: 2), Filter.whereField("b", isEqualTo: 1)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter5).limit(to: 1), - matchesResult: ["doc2"]) + try await check(collRef, query: collRef.whereFilter(filter5).limit(to: 1), + matchesResult: ["doc2"]) } - func testOrQueriesWithCompositeIndexes() throws { + func testOrQueriesWithCompositeIndexes() async throws { // TODO(orquery): Enable this test against production when possible. - try XCTSkipIf(!FSTIntegrationTestCase.isRunningAgainstEmulator(), + try XCTSkipIf(!(FSTIntegrationTestCase.isRunningAgainstEmulator()), "Skip this test if running against production because it results in" + "a 'missing index' error. The Firestore Emulator, however, does serve these queries.") @@ -102,16 +115,16 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isGreaterThan: 2), Filter.whereField("b", isEqualTo: 1)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter1), - matchesResult: ["doc5", "doc2", "doc3"]) + try await check(collRef, query: collRef.whereFilter(filter1), + matchesResult: ["doc5", "doc2", "doc3"]) // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2 let filter2 = Filter.orFilter( [Filter.whereField("a", isEqualTo: 1), Filter.whereField("b", isGreaterThan: 0)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter2).limit(to: 2), - matchesResult: ["doc1", "doc2"]) + try await check(collRef, query: collRef.whereFilter(filter2).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. @@ -119,7 +132,7 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isEqualTo: 1), Filter.whereField("b", isGreaterThan: 0)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter3) + try await check(collRef, query: collRef.whereFilter(filter3) .limit(toLast: 2) .order(by: "b"), matchesResult: ["doc3", "doc4"]) @@ -129,7 +142,7 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isEqualTo: 2), Filter.whereField("b", isEqualTo: 1)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter4).limit(to: 1) + try await check(collRef, query: collRef.whereFilter(filter4).limit(to: 1) .order(by: "a"), matchesResult: ["doc5"]) @@ -138,12 +151,12 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isEqualTo: 2), Filter.whereField("b", isEqualTo: 1)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter5).limit(toLast: 1) + try await check(collRef, query: collRef.whereFilter(filter5).limit(toLast: 1) .order(by: "a"), matchesResult: ["doc2"]) } - func testOrQueriesWithIn() throws { + func testOrQueriesWithIn() async throws { let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": 0], "doc2": ["b": 1], @@ -158,11 +171,11 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isEqualTo: 2), Filter.whereField("b", in: [2, 3])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter), - matchesResult: ["doc3", "doc4", "doc6"]) + try await check(collRef, query: collRef.whereFilter(filter), + matchesResult: ["doc3", "doc4", "doc6"]) } - func testOrQueriesWithArrayMembership() throws { + func testOrQueriesWithArrayMembership() async throws { let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": [0]], "doc2": ["b": 1], @@ -177,19 +190,19 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", isEqualTo: 2), Filter.whereField("b", arrayContains: 7)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter1), - matchesResult: ["doc3", "doc4", "doc6"]) + try await check(collRef, query: 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])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter2), - matchesResult: ["doc1", "doc4", "doc6"]) + try await check(collRef, query: collRef.whereFilter(filter2), + matchesResult: ["doc1", "doc4", "doc6"]) } - func testMultipleInOps() throws { + func testMultipleInOps() async throws { let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": 0], "doc2": ["b": 1], @@ -204,8 +217,8 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", in: [2, 3]), Filter.whereField("b", in: [0, 2])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter1).order(by: "a"), - matchesResult: ["doc1", "doc6", "doc3"]) + try await check(collRef, query: collRef.whereFilter(filter1).order(by: "a"), + matchesResult: ["doc1", "doc6", "doc3"]) // Two IN operations on same fields with disjunction. // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). @@ -213,11 +226,11 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", in: [0, 3]), Filter.whereField("a", in: [0, 2])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter2), - matchesResult: ["doc3", "doc6"]) + try await check(collRef, query: collRef.whereFilter(filter2), + matchesResult: ["doc3", "doc6"]) } - func testUsingInWithArrayContainsAny() throws { + func testUsingInWithArrayContainsAny() async throws { let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": [0]], "doc2": ["b": [1]], @@ -231,8 +244,8 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", in: [2, 3]), Filter.whereField("b", arrayContainsAny: [0, 7])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter1), - matchesResult: ["doc1", "doc3", "doc4", "doc6"]) + try await check(collRef, query: collRef.whereFilter(filter1), + matchesResult: ["doc1", "doc3", "doc4", "doc6"]) let filter2 = Filter.orFilter( [Filter.andFilter( @@ -241,11 +254,11 @@ class QueryIntegrationTests: FSTIntegrationTestCase { ), Filter.whereField("b", arrayContainsAny: [0, 7])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter2), - matchesResult: ["doc1", "doc3", "doc4"]) + try await check(collRef, query: collRef.whereFilter(filter2), + matchesResult: ["doc1", "doc3", "doc4"]) } - func testUseInWithArrayContains() throws { + func testUseInWithArrayContains() async throws { let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": [0]], "doc2": ["b": [1]], @@ -259,15 +272,15 @@ class QueryIntegrationTests: FSTIntegrationTestCase { [Filter.whereField("a", in: [2, 3]), Filter.whereField("b", arrayContainsAny: [3])] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter1), - matchesResult: ["doc3", "doc4", "doc6"]) + try await check(collRef, query: collRef.whereFilter(filter1), + matchesResult: ["doc3", "doc4", "doc6"]) let filter2 = Filter.andFilter( [Filter.whereField("a", in: [2, 3]), Filter.whereField("b", arrayContains: 7)] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter2), - matchesResult: ["doc3"]) + try await check(collRef, query: collRef.whereFilter(filter2), + matchesResult: ["doc3"]) let filter3 = Filter.orFilter( [Filter.whereField("a", in: [2, 3]), @@ -276,8 +289,8 @@ class QueryIntegrationTests: FSTIntegrationTestCase { Filter.whereField("a", isEqualTo: 1)] )] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter3), - matchesResult: ["doc3", "doc4", "doc6"]) + try await check(collRef, query: collRef.whereFilter(filter3), + matchesResult: ["doc3", "doc4", "doc6"]) let filter4 = Filter.andFilter( [Filter.whereField("a", in: [2, 3]), @@ -286,14 +299,16 @@ class QueryIntegrationTests: FSTIntegrationTestCase { Filter.whereField("a", isEqualTo: 1)] )] ) - checkOnlineAndOfflineCollection(collRef, query: collRef.whereFilter(filter4), - matchesResult: ["doc3"]) + try await check(collRef, query: collRef.whereFilter(filter4), + matchesResult: ["doc3"]) } - func testOrderByEquality() throws { + func testOrderByEquality() async throws { // TODO(orquery): Enable this test against production when possible. - try XCTSkipIf(!FSTIntegrationTestCase.isRunningAgainstEmulator(), - "Skip this test if running against production because order-by-equality is not supported yet.") + try XCTSkipIf( + !(FSTIntegrationTestCase.isRunningAgainstEmulator() || type(of: self).isRunningPipeline), + "Skip this test if running against production because order-by-equality is not supported yet." + ) let collRef = collectionRef( withDocuments: ["doc1": ["a": 1, "b": [0]], @@ -304,16 +319,54 @@ class QueryIntegrationTests: FSTIntegrationTestCase { "doc6": ["a": 2, "c": 20]] ) - checkOnlineAndOfflineCollection( + try await check( collRef, query: collRef.whereFilter(Filter.whereField("a", isEqualTo: 1)), matchesResult: ["doc1", "doc4", "doc5"] ) - checkOnlineAndOfflineCollection( + try await check( collRef, query: collRef.whereFilter(Filter.whereField("a", in: [2, 3])).order(by: "a"), matchesResult: ["doc6", "doc3"] ) } } + +class QueryAsPipelineIntegrationTests: QueryIntegrationTests { + override class var isRunningPipeline: Bool { + return true + } + + override func check(_ coll: CollectionReference, query: Query, + matchesResult expectedKeys: [String]) async throws { + let collPipeline = coll.firestore.realtimePipeline().create(from: coll) + var collIterator = collPipeline.snapshotStream().makeAsyncIterator() + var _ = try await collIterator.next() + + let pipeline = query.firestore.realtimePipeline().create(from: query) + + var cacheIterator = pipeline.snapshotStream(options: .init(source: .cache)).makeAsyncIterator() + let cacheSnapshot = try await cacheIterator.next() + let cacheResultIds = cacheSnapshot?.results().map { $0.id } + + var serverIterator = pipeline.snapshotStream(options: .init( + includeMetadataChanges: true, + source: .default + )).makeAsyncIterator() + var serverSnapshot = try await serverIterator.next() + if serverSnapshot?.metadata.isFromCache == true { + serverSnapshot = try await serverIterator.next() + } + let serverResultIds = serverSnapshot?.results().map { $0.id } + + var remoteKeysIterator = pipeline.snapshotStream(options: .init(source: .cache)) + .makeAsyncIterator() + let remoteKeysSnapshot = try await remoteKeysIterator.next() + let remoteKeysResultIds = remoteKeysSnapshot?.results().map { $0.id } + + XCTAssertEqual(cacheResultIds, serverResultIds) + XCTAssertEqual(serverResultIds, remoteKeysResultIds) + XCTAssertEqual(remoteKeysResultIds, expectedKeys) + } +} diff --git a/Firestore/Swift/Tests/Integration/QueryToPipelineTests.swift b/Firestore/Swift/Tests/Integration/QueryToPipelineTests.swift new file mode 100644 index 00000000000..38bcdd3a53d --- /dev/null +++ b/Firestore/Swift/Tests/Integration/QueryToPipelineTests.swift @@ -0,0 +1,727 @@ +// Copyright 2025 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 FirebaseCore +import FirebaseFirestore +import Foundation +import XCTest + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class QueryToPipelineTests: FSTIntegrationTestCase { + let testUnsupportedFeatures = false + + private func verifyResults(_ snapshot: PipelineSnapshot, + _ expected: [[String: AnyHashable?]], + enforceOrder: Bool = false, + file: StaticString = #file, + line: UInt = #line) { + let results = snapshot.results.map { $0.data as! [String: AnyHashable?] } + XCTAssertEqual(results.count, expected.count, "Result count mismatch.", file: file, line: line) + + if enforceOrder { + for i in 0 ..< expected.count { + XCTAssertEqual( + results[i], + expected[i], + "Document at index \(i) does not match.", + file: file, + line: line + ) + } + } else { + // For unordered comparison, convert to Sets of dictionaries. + XCTAssertEqual( + Set(results), + Set(expected), + "Result sets do not match.", + file: file, + line: line + ) + } + } + + func testSupportsDefaultQuery() async throws { + let collRef = collectionRef(withDocuments: ["1": ["foo": 1]]) + let db = collRef.firestore + + let pipeline = db.pipeline().create(from: collRef) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]]) + } + + func testSupportsFilteredQuery() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.whereField("foo", isEqualTo: 1) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]]) + } + + func testSupportsFilteredQueryWithFieldPath() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.whereField(FieldPath(["foo"]), isEqualTo: 1) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]]) + } + + func testSupportsOrderedQueryWithDefaultOrder() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo") + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1], ["foo": 2]], enforceOrder: true) + } + + func testSupportsOrderedQueryWithAsc() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo", descending: false) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1], ["foo": 2]], enforceOrder: true) + } + + func testSupportsOrderedQueryWithDesc() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo", descending: true) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2], ["foo": 1]], enforceOrder: true) + } + + func testSupportsLimitQuery() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").limit(to: 1) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]], enforceOrder: true) + } + + func testSupportsLimitToLastQuery() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + "3": ["foo": 3], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").limit(toLast: 2) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2], ["foo": 3]], enforceOrder: true) + } + + func testSupportsStartAt() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").start(at: [2]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2]], enforceOrder: true) + } + + func testSupportsStartAtWithLimitToLast() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + "3": ["foo": 3], + "4": ["foo": 4], + "5": ["foo": 5], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").start(at: [3]).limit(toLast: 4) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 3], ["foo": 4], ["foo": 5]], enforceOrder: true) + } + + func testSupportsEndAtWithLimitToLast() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + "3": ["foo": 3], + "4": ["foo": 4], + "5": ["foo": 5], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").end(at: [3]).limit(toLast: 2) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2], ["foo": 3]], enforceOrder: true) + } + + func testSupportsStartAfterWithDocumentSnapshot() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["id": 1, "foo": 1, "bar": 1, "baz": 1], + "2": ["id": 2, "foo": 1, "bar": 1, "baz": 2], + "3": ["id": 3, "foo": 1, "bar": 1, "baz": 2], + "4": ["id": 4, "foo": 1, "bar": 2, "baz": 1], + "5": ["id": 5, "foo": 1, "bar": 2, "baz": 2], + "6": ["id": 6, "foo": 1, "bar": 2, "baz": 2], + "7": ["id": 7, "foo": 2, "bar": 1, "baz": 1], + "8": ["id": 8, "foo": 2, "bar": 1, "baz": 2], + "9": ["id": 9, "foo": 2, "bar": 1, "baz": 2], + "10": ["id": 10, "foo": 2, "bar": 2, "baz": 1], + "11": ["id": 11, "foo": 2, "bar": 2, "baz": 2], + "12": ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ]) + let db = collRef.firestore + + var docRef = try await collRef.document("2").getDocument() + var query = collRef.order(by: "foo").order(by: "bar").order(by: "baz") + .start(afterDocument: docRef) + var pipeline = db.pipeline().create(from: query) + var snapshot = try await pipeline.execute() + + verifyResults( + snapshot, + [ + ["id": 3, "foo": 1, "bar": 1, "baz": 2], + ["id": 4, "foo": 1, "bar": 2, "baz": 1], + ["id": 5, "foo": 1, "bar": 2, "baz": 2], + ["id": 6, "foo": 1, "bar": 2, "baz": 2], + ["id": 7, "foo": 2, "bar": 1, "baz": 1], + ["id": 8, "foo": 2, "bar": 1, "baz": 2], + ["id": 9, "foo": 2, "bar": 1, "baz": 2], + ["id": 10, "foo": 2, "bar": 2, "baz": 1], + ["id": 11, "foo": 2, "bar": 2, "baz": 2], + ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ], + enforceOrder: true + ) + + docRef = try await collRef.document("3").getDocument() + query = collRef.order(by: "foo").order(by: "bar").order(by: "baz").start(afterDocument: docRef) + pipeline = db.pipeline().create(from: query) + snapshot = try await pipeline.execute() + verifyResults( + snapshot, + [ + ["id": 4, "foo": 1, "bar": 2, "baz": 1], + ["id": 5, "foo": 1, "bar": 2, "baz": 2], + ["id": 6, "foo": 1, "bar": 2, "baz": 2], + ["id": 7, "foo": 2, "bar": 1, "baz": 1], + ["id": 8, "foo": 2, "bar": 1, "baz": 2], + ["id": 9, "foo": 2, "bar": 1, "baz": 2], + ["id": 10, "foo": 2, "bar": 2, "baz": 1], + ["id": 11, "foo": 2, "bar": 2, "baz": 2], + ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ], + enforceOrder: true + ) + } + + func testSupportsStartAtWithDocumentSnapshot() async throws { + try XCTSkipIf(true, "Unsupported feature: sort on __name__ is not working") + let collRef = collectionRef(withDocuments: [ + "1": ["id": 1, "foo": 1, "bar": 1, "baz": 1], + "2": ["id": 2, "foo": 1, "bar": 1, "baz": 2], + "3": ["id": 3, "foo": 1, "bar": 1, "baz": 2], + "4": ["id": 4, "foo": 1, "bar": 2, "baz": 1], + "5": ["id": 5, "foo": 1, "bar": 2, "baz": 2], + "6": ["id": 6, "foo": 1, "bar": 2, "baz": 2], + "7": ["id": 7, "foo": 2, "bar": 1, "baz": 1], + "8": ["id": 8, "foo": 2, "bar": 1, "baz": 2], + "9": ["id": 9, "foo": 2, "bar": 1, "baz": 2], + "10": ["id": 10, "foo": 2, "bar": 2, "baz": 1], + "11": ["id": 11, "foo": 2, "bar": 2, "baz": 2], + "12": ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ]) + let db = collRef.firestore + + var docRef = try await collRef.document("2").getDocument() + var query = collRef.order(by: "foo").order(by: "bar").order(by: "baz").start(atDocument: docRef) + var pipeline = db.pipeline().create(from: query) + var snapshot = try await pipeline.execute() + + verifyResults( + snapshot, + [ + ["id": 2, "foo": 1, "bar": 1, "baz": 2], + ["id": 3, "foo": 1, "bar": 1, "baz": 2], + ["id": 4, "foo": 1, "bar": 2, "baz": 1], + ["id": 5, "foo": 1, "bar": 2, "baz": 2], + ["id": 6, "foo": 1, "bar": 2, "baz": 2], + ["id": 7, "foo": 2, "bar": 1, "baz": 1], + ["id": 8, "foo": 2, "bar": 1, "baz": 2], + ["id": 9, "foo": 2, "bar": 1, "baz": 2], + ["id": 10, "foo": 2, "bar": 2, "baz": 1], + ["id": 11, "foo": 2, "bar": 2, "baz": 2], + ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ], + enforceOrder: true + ) + + docRef = try await collRef.document("3").getDocument() + query = collRef.order(by: "foo").order(by: "bar").order(by: "baz").start(atDocument: docRef) + pipeline = db.pipeline().create(from: query) + snapshot = try await pipeline.execute() + verifyResults( + snapshot, + [ + ["id": 3, "foo": 1, "bar": 1, "baz": 2], + ["id": 4, "foo": 1, "bar": 2, "baz": 1], + ["id": 5, "foo": 1, "bar": 2, "baz": 2], + ["id": 6, "foo": 1, "bar": 2, "baz": 2], + ["id": 7, "foo": 2, "bar": 1, "baz": 1], + ["id": 8, "foo": 2, "bar": 1, "baz": 2], + ["id": 9, "foo": 2, "bar": 1, "baz": 2], + ["id": 10, "foo": 2, "bar": 2, "baz": 1], + ["id": 11, "foo": 2, "bar": 2, "baz": 2], + ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ], + enforceOrder: true + ) + } + + func testSupportsStartAfter() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").start(after: [1]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2]], enforceOrder: true) + } + + func testSupportsEndAt() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").end(at: [1]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]], enforceOrder: true) + } + + func testSupportsEndBefore() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + let query = collRef.order(by: "foo").end(before: [2]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]], enforceOrder: true) + } + + func testSupportsPagination() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + var query = collRef.order(by: "foo").limit(to: 1) + var pipeline = db.pipeline().create(from: query) + var snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]], enforceOrder: true) + + let lastFoo = snapshot.results.first!.get("foo")! + query = query.start(after: [lastFoo]) + pipeline = db.pipeline().create(from: query) + snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2]], enforceOrder: true) + } + + func testSupportsPaginationOnDocumentIds() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1], + "2": ["foo": 2], + ]) + let db = collRef.firestore + + var query = collRef.order(by: "foo").order(by: FieldPath.documentID()).limit(to: 1) + var pipeline = db.pipeline().create(from: query) + var snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]], enforceOrder: true) + + let lastSnapshot = snapshot.results.first! + query = query.start(after: [lastSnapshot.get("foo")!, lastSnapshot.ref!.documentID]) + pipeline = db.pipeline().create(from: query) + snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2]], enforceOrder: true) + } + + func testSupportsCollectionGroups() async throws { + let db = firestore() + let collRef = collectionRef() + let collectionGroupId = "\(collRef.collectionID)group" + + let fooDoc = db.document("\(collRef.path)/foo/\(collectionGroupId)/doc1") + let barDoc = db.document("\(collRef.path)/bar/baz/boo/\(collectionGroupId)/doc2") + + try await fooDoc.setData(["foo": 1]) + try await barDoc.setData(["bar": 1]) + + let query = db.collectionGroup(collectionGroupId) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["bar": 1], ["foo": 1]]) + } + + func testSupportsQueryOverCollectionPathWithSpecialCharacters() async throws { + let collRef = collectionRef() + let db = collRef.firestore + + let docWithSpecials = collRef.document("so! @#$%^&*()_+special") + let collectionWithSpecials = docWithSpecials.collection("so! @#$%^&*()_+special") + + try await collectionWithSpecials.addDocument(data: ["foo": 1]) + try await collectionWithSpecials.addDocument(data: ["foo": 2]) + + let query = collectionWithSpecials.order(by: "foo", descending: false) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1], ["foo": 2]], enforceOrder: true) + } + + func testSupportsMultipleInequalityOnSameField() async throws { + let collRef = collectionRef(withDocuments: [ + "01": ["id": 1, "foo": 1, "bar": 1, "baz": 1], + "02": ["id": 2, "foo": 1, "bar": 1, "baz": 2], + "03": ["id": 3, "foo": 1, "bar": 1, "baz": 2], + "04": ["id": 4, "foo": 1, "bar": 2, "baz": 1], + "05": ["id": 5, "foo": 1, "bar": 2, "baz": 2], + "06": ["id": 6, "foo": 1, "bar": 2, "baz": 2], + "07": ["id": 7, "foo": 2, "bar": 1, "baz": 1], + "08": ["id": 8, "foo": 2, "bar": 1, "baz": 2], + "09": ["id": 9, "foo": 2, "bar": 1, "baz": 2], + "10": ["id": 10, "foo": 2, "bar": 2, "baz": 1], + "11": ["id": 11, "foo": 2, "bar": 2, "baz": 2], + "12": ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ]) + let db = collRef.firestore + + let query = collRef.whereField("id", isGreaterThan: 2).whereField("id", isLessThanOrEqualTo: 10) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults( + snapshot, + [ + ["id": 3, "foo": 1, "bar": 1, "baz": 2], + ["id": 4, "foo": 1, "bar": 2, "baz": 1], + ["id": 5, "foo": 1, "bar": 2, "baz": 2], + ["id": 6, "foo": 1, "bar": 2, "baz": 2], + ["id": 7, "foo": 2, "bar": 1, "baz": 1], + ["id": 8, "foo": 2, "bar": 1, "baz": 2], + ["id": 9, "foo": 2, "bar": 1, "baz": 2], + ["id": 10, "foo": 2, "bar": 2, "baz": 1], + ], + enforceOrder: false + ) + } + + func testSupportsMultipleInequalityOnDifferentFields() async throws { + let collRef = collectionRef(withDocuments: [ + "01": ["id": 1, "foo": 1, "bar": 1, "baz": 1], + "02": ["id": 2, "foo": 1, "bar": 1, "baz": 2], + "03": ["id": 3, "foo": 1, "bar": 1, "baz": 2], + "04": ["id": 4, "foo": 1, "bar": 2, "baz": 1], + "05": ["id": 5, "foo": 1, "bar": 2, "baz": 2], + "06": ["id": 6, "foo": 1, "bar": 2, "baz": 2], + "07": ["id": 7, "foo": 2, "bar": 1, "baz": 1], + "08": ["id": 8, "foo": 2, "bar": 1, "baz": 2], + "09": ["id": 9, "foo": 2, "bar": 1, "baz": 2], + "10": ["id": 10, "foo": 2, "bar": 2, "baz": 1], + "11": ["id": 11, "foo": 2, "bar": 2, "baz": 2], + "12": ["id": 12, "foo": 2, "bar": 2, "baz": 2], + ]) + let db = collRef.firestore + + let query = collRef.whereField("id", isGreaterThanOrEqualTo: 2) + .whereField("baz", isLessThan: 2) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults( + snapshot, + [ + ["id": 4, "foo": 1, "bar": 2, "baz": 1], + ["id": 7, "foo": 2, "bar": 1, "baz": 1], + ["id": 10, "foo": 2, "bar": 2, "baz": 1], + ], + enforceOrder: false + ) + } + + func testSupportsCollectionGroupQuery() async throws { + let collRef = collectionRef(withDocuments: ["1": ["foo": 1]]) + let db = collRef.firestore + + let query = db.collectionGroup(collRef.collectionID) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1]]) + } + + func testSupportsEqNan() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": Double.nan], + "2": ["foo": 2, "bar": 1], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", isEqualTo: Double.nan) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1) + let data = snapshot.results.first!.data + XCTAssertEqual(data["foo"] as? Int, 1) + XCTAssertTrue((data["bar"] as? Double)?.isNaN ?? false) + } + + func testSupportsNeqNan() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": Double.nan], + "2": ["foo": 2, "bar": 1], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", isNotEqualTo: Double.nan) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2, "bar": 1]]) + } + + func testSupportsEqNull() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": NSNull()], + "2": ["foo": 2, "bar": 1], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", isEqualTo: NSNull()) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1, "bar": nil]]) + } + + func testSupportsNeqNull() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": NSNull()], + "2": ["foo": 2, "bar": 1], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", isNotEqualTo: NSNull()) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2, "bar": 1]]) + } + + func testSupportsNeq() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": 0], + "2": ["foo": 2, "bar": 1], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", isNotEqualTo: 0) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 2, "bar": 1]]) + } + + func testSupportsArrayContains() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": [0, 2, 4, 6]], + "2": ["foo": 2, "bar": [1, 3, 5, 7]], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", arrayContains: 4) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1, "bar": [0, 2, 4, 6]]]) + } + + func testSupportsArrayContainsAny() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": [0, 2, 4, 6]], + "2": ["foo": 2, "bar": [1, 3, 5, 7]], + "3": ["foo": 3, "bar": [10, 20, 30, 40]], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", arrayContainsAny: [4, 5]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults( + snapshot, + [ + ["foo": 1, "bar": [0, 2, 4, 6]], + ["foo": 2, "bar": [1, 3, 5, 7]], + ] + ) + } + + func testSupportsIn() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": 2], + "2": ["foo": 2], + "3": ["foo": 3, "bar": 10], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", in: [0, 10, 20]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 3, "bar": 10]]) + } + + func testSupportsInWith1() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": 2], + "2": ["foo": 2], + "3": ["foo": 3, "bar": 10], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", in: [2]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1, "bar": 2]]) + } + + func testSupportsNotIn() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": 2], + "2": ["foo": 2, "bar": 1], + "3": ["foo": 3, "bar": 10], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", notIn: [0, 10, 20]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 1, "bar": 2], ["foo": 2, "bar": 1]]) + } + + func testSupportsNotInWith1() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": 2], + "2": ["foo": 2], + "3": ["foo": 3, "bar": 10], + ]) + let db = collRef.firestore + + let query = collRef.whereField("bar", notIn: [2]) + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults(snapshot, [["foo": 3, "bar": 10]]) + } + + func testSupportsOrOperator() async throws { + let collRef = collectionRef(withDocuments: [ + "1": ["foo": 1, "bar": 2], + "2": ["foo": 2, "bar": 0], + "3": ["foo": 3, "bar": 10], + ]) + let db = collRef.firestore + + let query = collRef.whereFilter(Filter.orFilter([ + Filter.whereField("bar", isEqualTo: 2), + Filter.whereField("foo", isEqualTo: 3), + ])).order(by: "foo") + let pipeline = db.pipeline().create(from: query) + let snapshot = try await pipeline.execute() + + verifyResults( + snapshot, + [ + ["foo": 1, "bar": 2], + ["foo": 3, "bar": 10], + ], + enforceOrder: true + ) + } +} diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index bbcfee737f4..7b24604e23a 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -50,7 +50,7 @@ CollectionSource::CollectionSource(std::string path) google_firestore_v1_Pipeline_Stage CollectionSource::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("collection"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -68,7 +68,7 @@ google_firestore_v1_Pipeline_Stage CollectionSource::to_proto() const { google_firestore_v1_Pipeline_Stage DatabaseSource::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("database"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 0; result.args = nullptr; result.options_count = 0; @@ -80,7 +80,7 @@ google_firestore_v1_Pipeline_Stage DatabaseSource::to_proto() const { google_firestore_v1_Pipeline_Stage CollectionGroupSource::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("collection_group"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 2; result.args = nanopb::MakeArray(2); @@ -102,7 +102,7 @@ google_firestore_v1_Pipeline_Stage CollectionGroupSource::to_proto() const { google_firestore_v1_Pipeline_Stage DocumentsSource::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("documents"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = static_cast(documents_.size()); result.args = nanopb::MakeArray(result.args_count); @@ -123,7 +123,7 @@ google_firestore_v1_Pipeline_Stage DocumentsSource::to_proto() const { google_firestore_v1_Pipeline_Stage AddFields::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("add_fields"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -143,7 +143,7 @@ google_firestore_v1_Pipeline_Stage AddFields::to_proto() const { google_firestore_v1_Pipeline_Stage AggregateStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("aggregate"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 2; result.args = nanopb::MakeArray(2); @@ -177,7 +177,7 @@ google_firestore_v1_Pipeline_Stage AggregateStage::to_proto() const { google_firestore_v1_Pipeline_Stage Where::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("where"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -208,7 +208,7 @@ google_firestore_v1_Value FindNearestStage::DistanceMeasure::proto() const { google_firestore_v1_Pipeline_Stage FindNearestStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("find_nearest"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 3; result.args = nanopb::MakeArray(3); @@ -228,7 +228,7 @@ google_firestore_v1_Pipeline_Stage FindNearestStage::to_proto() const { google_firestore_v1_Pipeline_Stage LimitStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("limit"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -242,7 +242,7 @@ google_firestore_v1_Pipeline_Stage LimitStage::to_proto() const { google_firestore_v1_Pipeline_Stage OffsetStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("offset"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -256,7 +256,7 @@ google_firestore_v1_Pipeline_Stage OffsetStage::to_proto() const { google_firestore_v1_Pipeline_Stage SelectStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("select"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -276,7 +276,7 @@ google_firestore_v1_Pipeline_Stage SelectStage::to_proto() const { google_firestore_v1_Pipeline_Stage SortStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("sort"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = static_cast(orders_.size()); result.args = nanopb::MakeArray(result.args_count); @@ -292,7 +292,7 @@ google_firestore_v1_Pipeline_Stage SortStage::to_proto() const { google_firestore_v1_Pipeline_Stage DistinctStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("distinct"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -312,7 +312,7 @@ google_firestore_v1_Pipeline_Stage DistinctStage::to_proto() const { google_firestore_v1_Pipeline_Stage RemoveFieldsStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("remove_fields"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = static_cast(fields_.size()); result.args = nanopb::MakeArray(result.args_count); @@ -342,7 +342,7 @@ google_firestore_v1_Value ReplaceWith::ReplaceMode::to_proto() const { google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("replace_with"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 2; result.args = nanopb::MakeArray(2); @@ -379,7 +379,7 @@ Sample::Sample(SampleMode mode, int64_t count, double percentage) google_firestore_v1_Pipeline_Stage Sample::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("sample"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 2; result.args = nanopb::MakeArray(2); @@ -409,7 +409,7 @@ Union::Union(std::shared_ptr other) : other_(std::move(other)) { google_firestore_v1_Pipeline_Stage Union::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("union"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 1; result.args = nanopb::MakeArray(1); @@ -430,7 +430,7 @@ Unnest::Unnest(std::shared_ptr field, google_firestore_v1_Pipeline_Stage Unnest::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray("unnest"); + result.name = nanopb::MakeBytesArray(name()); result.args_count = 2; result.args = nanopb::MakeArray(2); @@ -462,7 +462,7 @@ RawStage::RawStage( google_firestore_v1_Pipeline_Stage RawStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; - result.name = nanopb::MakeBytesArray(name_); + result.name = nanopb::MakeBytesArray(name()); result.args_count = static_cast(params_.size()); result.args = nanopb::MakeArray(result.args_count); diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index 110fd5d6b91..2b914c895a2 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -49,6 +49,7 @@ class Stage { Stage() = default; virtual ~Stage() = default; + virtual const std::string& name() const = 0; virtual google_firestore_v1_Pipeline_Stage to_proto() const = 0; }; @@ -75,9 +76,8 @@ class EvaluateContext { class EvaluableStage : public Stage { public: EvaluableStage() = default; - virtual ~EvaluableStage() = default; + ~EvaluableStage() override = default; - virtual absl::string_view name() const = 0; virtual model::PipelineInputOutputVector Evaluate( const EvaluateContext& context, const model::PipelineInputOutputVector& inputs) const = 0; @@ -90,8 +90,9 @@ class CollectionSource : public EvaluableStage { google_firestore_v1_Pipeline_Stage to_proto() const override; - absl::string_view name() const override { - return "collection"; + const std::string& name() const override { + static const std::string kName = "collection"; + return kName; } std::string path() const { @@ -113,8 +114,9 @@ class DatabaseSource : public EvaluableStage { google_firestore_v1_Pipeline_Stage to_proto() const override; - absl::string_view name() const override { - return "database"; + const std::string& name() const override { + static const std::string kName = "database"; + return kName; } model::PipelineInputOutputVector Evaluate( @@ -131,8 +133,9 @@ class CollectionGroupSource : public EvaluableStage { google_firestore_v1_Pipeline_Stage to_proto() const override; - absl::string_view name() const override { - return "collection_group"; + const std::string& name() const override { + static const std::string kName = "collection_group"; + return kName; } absl::string_view collection_id() const { @@ -160,8 +163,9 @@ class DocumentsSource : public EvaluableStage { const EvaluateContext& context, const model::PipelineInputOutputVector& inputs) const override; - absl::string_view name() const override { - return "documents"; + const std::string& name() const override { + static const std::string kName = "documents"; + return kName; } std::vector documents() const { @@ -182,6 +186,11 @@ class AddFields : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "add_fields"; + return kName; + } + private: std::unordered_map> fields_; }; @@ -197,6 +206,11 @@ class AggregateStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "aggregate"; + return kName; + } + private: std::unordered_map> accumulators_; @@ -211,8 +225,9 @@ class Where : public EvaluableStage { google_firestore_v1_Pipeline_Stage to_proto() const override; - absl::string_view name() const override { - return "where"; + const std::string& name() const override { + static const std::string kName = "where"; + return kName; } const Expr* expr() const { @@ -256,6 +271,11 @@ class FindNearestStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "find_nearest"; + return kName; + } + private: std::shared_ptr property_; nanopb::SharedMessage vector_; @@ -271,8 +291,9 @@ class LimitStage : public EvaluableStage { google_firestore_v1_Pipeline_Stage to_proto() const override; - absl::string_view name() const override { - return "limit"; + const std::string& name() const override { + static const std::string kName = "limit"; + return kName; } int64_t limit() const { @@ -295,6 +316,11 @@ class OffsetStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "offset"; + return kName; + } + private: int64_t offset_; }; @@ -309,6 +335,11 @@ class SelectStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "select"; + return kName; + } + private: std::unordered_map> fields_; }; @@ -322,8 +353,9 @@ class SortStage : public EvaluableStage { google_firestore_v1_Pipeline_Stage to_proto() const override; - absl::string_view name() const override { - return "sort"; + const std::string& name() const override { + static const std::string kName = "sort"; + return kName; } model::PipelineInputOutputVector Evaluate( @@ -348,6 +380,11 @@ class DistinctStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "distinct"; + return kName; + } + private: std::unordered_map> groups_; }; @@ -361,6 +398,11 @@ class RemoveFieldsStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "remove_fields"; + return kName; + } + private: std::vector fields_; }; @@ -389,6 +431,11 @@ class ReplaceWith : public Stage { ~ReplaceWith() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "replace_with"; + return kName; + } + private: std::shared_ptr expr_; ReplaceMode mode_; @@ -417,6 +464,11 @@ class Sample : public Stage { ~Sample() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "sample"; + return kName; + } + private: SampleMode mode_; int64_t count_; @@ -429,6 +481,11 @@ class Union : public Stage { ~Union() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "union"; + return kName; + } + private: std::shared_ptr other_; }; @@ -441,6 +498,11 @@ class Unnest : public Stage { ~Unnest() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + static const std::string kName = "unnest"; + return kName; + } + private: std::shared_ptr field_; std::shared_ptr alias_; @@ -455,6 +517,10 @@ class RawStage : public Stage { ~RawStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; + const std::string& name() const override { + return name_; + } + private: std::string name_; std::vector params_; diff --git a/Firestore/core/src/core/pipeline_util.cc b/Firestore/core/src/core/pipeline_util.cc index 57b94592473..f2ec09e1053 100644 --- a/Firestore/core/src/core/pipeline_util.cc +++ b/Firestore/core/src/core/pipeline_util.cc @@ -680,7 +680,7 @@ std::shared_ptr WhereConditionsFromCursor( std::string func_inclusive_name = is_before ? "lte" : "gte"; std::vector> or_conditions; - for (size_t sub_end = 1; sub_end <= orderings.size(); ++sub_end) { + for (size_t sub_end = 1; sub_end <= cursors.size(); ++sub_end) { std::vector> conditions; for (size_t index = 0; index < sub_end; ++index) { if (index < sub_end - 1) { @@ -794,11 +794,11 @@ std::vector> ToPipelineStages( stages.push_back(std::make_shared(api_orderings)); if (query.start_at()) { stages.push_back(std::make_shared(WhereConditionsFromCursor( - *query.start_at(), api_orderings, /*is_before=*/true))); + *query.start_at(), api_orderings, /*is_before=*/false))); } if (query.end_at()) { stages.push_back(std::make_shared(WhereConditionsFromCursor( - *query.end_at(), api_orderings, /*is_before=*/false))); + *query.end_at(), api_orderings, /*is_before=*/true))); } if (query.limit_type() == LimitType::First && query.limit()) { stages.push_back(std::make_shared(query.limit()));