diff --git a/FirebaseFirestoreInternal/FirebaseFirestore/FIRPipelineBridge.h b/FirebaseFirestoreInternal/FirebaseFirestore/FIRPipelineBridge.h new file mode 100644 index 00000000000..35e39ab7904 --- /dev/null +++ b/FirebaseFirestoreInternal/FirebaseFirestore/FIRPipelineBridge.h @@ -0,0 +1,17 @@ +/* + * 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 diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 5cf594fd314..1146b8a5dde 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -127,6 +127,9 @@ 11F8EE69182C9699E90A9E3D /* database_info_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D92E20235D22000A432D /* database_info_test.cc */; }; 12158DFCEE09D24B7988A340 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */; }; 121F0FB9DCCBFB7573C7AF48 /* bundle_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C2A94EE24E60543F62CC35 /* bundle_serializer_test.cc */; }; + 12260A2A2D56A3CE001766EB /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12260A292D56A3CE001766EB /* PipelineTests.swift */; }; + 12260A2B2D56A3CE001766EB /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12260A292D56A3CE001766EB /* PipelineTests.swift */; }; + 12260A2C2D56A3CE001766EB /* PipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12260A292D56A3CE001766EB /* PipelineTests.swift */; }; 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 */; }; 1290FA77A922B76503AE407C /* lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */; }; @@ -1735,6 +1738,7 @@ 0D964D4936953635AC7E0834 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json; sourceTree = ""; }; 0EE5300F8233D14025EF0456 /* string_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_test.mm; sourceTree = ""; }; 11984BA0A99D7A7ABA5B0D90 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = ""; }; + 12260A292D56A3CE001766EB /* PipelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineTests.swift; sourceTree = ""; }; 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 = ""; }; @@ -1797,7 +1801,7 @@ 4334F87873015E3763954578 /* status_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = status_testing.h; sourceTree = ""; }; 4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json; sourceTree = ""; }; 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = hard_assert_test.cc; sourceTree = ""; }; - 4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = globals_cache_test.cc; sourceTree = ""; }; + 4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = globals_cache_test.cc; sourceTree = ""; }; 478DC75A0DCA6249A616DD30 /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json; sourceTree = ""; }; 48D0915834C3D234E5A875A9 /* grpc_stream_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = grpc_stream_tester.h; sourceTree = ""; }; 4B3E4A77493524333133C5DC /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json; sourceTree = ""; }; @@ -1915,7 +1919,7 @@ 5B5414D28802BC76FDADABD6 /* stream_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = stream_test.cc; sourceTree = ""; }; 5B96CC29E9946508F022859C /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json; sourceTree = ""; }; 5C68EE4CB94C0DD6E333F546 /* Validation_BloomFilterTest_MD5_1_01_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_01_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_01_membership_test_result.json; sourceTree = ""; }; - 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = memory_globals_cache_test.cc; sourceTree = ""; }; + 5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_globals_cache_test.cc; sourceTree = ""; }; 5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_mutation_queue_test.cc; sourceTree = ""; }; 5CAE131920FFFED600BE9A4A /* Firestore_Benchmarks_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Benchmarks_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5CAE131D20FFFED600BE9A4A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2296,6 +2300,7 @@ 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */, 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */, 62E54B832A9E910A003347C8 /* IndexingTests.swift */, + 12260A292D56A3CE001766EB /* PipelineTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */, @@ -4685,6 +4690,7 @@ 432056C4D1259F76C80FC2A8 /* FSTUserDataReaderTests.mm in Sources */, 3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */, 62E54B862A9E910B003347C8 /* IndexingTests.swift in Sources */, + 12260A2C2D56A3CE001766EB /* PipelineTests.swift in Sources */, 621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAC2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -4935,6 +4941,7 @@ 75A176239B37354588769206 /* FSTUserDataReaderTests.mm in Sources */, 5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */, 62E54B852A9E910B003347C8 /* IndexingTests.swift in Sources */, + 12260A2B2D56A3CE001766EB /* PipelineTests.swift in Sources */, 621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAB2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -5440,6 +5447,7 @@ F5BDECEB3B43BD1591EEADBD /* FSTUserDataReaderTests.mm in Sources */, 6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */, 62E54B842A9E910B003347C8 /* IndexingTests.swift in Sources */, + 12260A2A2D56A3CE001766EB /* PipelineTests.swift in Sources */, 621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h index 5c5da4c525d..eecc1160a5f 100644 --- a/Firestore/Source/API/FIRFirestore+Internal.h +++ b/Firestore/Source/API/FIRFirestore+Internal.h @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google + * 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. diff --git a/Firestore/Source/API/FIRPipelineBridge+Internal.h b/Firestore/Source/API/FIRPipelineBridge+Internal.h new file mode 100644 index 00000000000..bfe7befe923 --- /dev/null +++ b/Firestore/Source/API/FIRPipelineBridge+Internal.h @@ -0,0 +1,49 @@ +/* + * 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 "FIRPipelineBridge.h" + +#include + +#include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/api/pipeline.h" +#include "Firestore/core/src/api/stages.h" + +@class FIRFilter; + +namespace api = firebase::firestore::api; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRExprBridge (Internal) + +- (std::shared_ptr)cpp_expr; + +@end + +@interface FIRStageBridge (Internal) + +- (std::shared_ptr)cpp_stage; + +@end + +@interface __FIRPipelineSnapshotBridge (Internal) + +- (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm new file mode 100644 index 00000000000..013cedcccab --- /dev/null +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -0,0 +1,191 @@ +/* + * 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 "FIRPipelineBridge.h" + +#include + +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRPipelineBridge+Internal.h" + +#include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/api/pipeline.h" +#include "Firestore/core/src/api/pipeline_result.h" +#include "Firestore/core/src/api/pipeline_snapshot.h" +#include "Firestore/core/src/api/stages.h" +#include "Firestore/core/src/util/error_apple.h" +#include "Firestore/core/src/util/status.h" +#include "Firestore/core/src/util/string_apple.h" + +using firebase::firestore::api::CollectionSource; +using firebase::firestore::api::Constant; +using firebase::firestore::api::Expr; +using firebase::firestore::api::Field; +using firebase::firestore::api::FunctionExpr; +using firebase::firestore::api::Pipeline; +using firebase::firestore::api::Where; +using firebase::firestore::util::MakeCallback; +using firebase::firestore::util::MakeString; + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRExprBridge +@end + +@implementation FIRFieldBridge { + std::shared_ptr field; +} + +- (id)init:(NSString *)name { + self = [super init]; + if (self) { + field = std::make_shared(MakeString(name)); + } + return self; +} + +- (std::shared_ptr)cpp_expr { + return field; +} + +@end + +@implementation FIRConstantBridge { + std::shared_ptr constant; +} +- (id)init:(NSNumber *)value { + self = [super init]; + if (self) { + constant = std::make_shared(value.doubleValue); + } + return self; +} + +- (std::shared_ptr)cpp_expr { + return constant; +} + +@end + +@implementation FIRFunctionExprBridge { + std::shared_ptr eq; +} + +- (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray *)args { + self = [super init]; + if (self) { + std::vector> cpp_args; + for (FIRExprBridge *arg in args) { + cpp_args.push_back(arg.cpp_expr); + } + + eq = std::make_shared(MakeString(name), std::move(cpp_args)); + } + return self; +} + +- (std::shared_ptr)cpp_expr { + return eq; +} + +@end + +@implementation FIRStageBridge +@end + +@implementation FIRCollectionSourceStageBridge { + std::shared_ptr collection_source; +} + +- (id)initWithPath:(NSString *)path { + self = [super init]; + if (self) { + collection_source = std::make_shared(MakeString(path)); + } + return self; +} + +- (std::shared_ptr)cpp_stage { + return collection_source; +} + +@end + +@implementation FIRWhereStageBridge { + std::shared_ptr where; +} + +- (id)initWithExpr:(FIRExprBridge *)expr { + self = [super init]; + if (self) { + where = std::make_shared(expr.cpp_expr); + } + return self; +} + +- (std::shared_ptr)cpp_stage { + return where; +} + +@end + +@implementation __FIRPipelineSnapshotBridge { + absl::optional pipeline; +} + +- (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { + self = [super init]; + if (self) { + pipeline = std::move(snapshot); + } + + return self; +} + +@end + +@implementation FIRPipelineBridge { + std::shared_ptr pipeline; +} + +- (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { + self = [super init]; + if (self) { + std::vector> cpp_stages; + for (FIRStageBridge *stage in stages) { + cpp_stages.push_back(stage.cpp_stage); + } + pipeline = std::make_shared(cpp_stages, db.wrapped); + } + return self; +} + +- (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, + NSError *_Nullable error))completion { + pipeline->execute([completion](StatusOr maybe_value) { + if (maybe_value.ok()) { + __FIRPipelineSnapshotBridge *bridge = [[__FIRPipelineSnapshotBridge alloc] + initWithCppSnapshot:std::move(maybe_value).ValueOrDie()]; + completion(bridge, nil); + } else { + completion(nil, MakeNSError(std::move(maybe_value).status())); + } + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h new file mode 100644 index 00000000000..fa7472e3292 --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -0,0 +1,89 @@ +/* + * 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 "FIRFirestore.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(ExprBridge) +@interface FIRExprBridge : NSObject +@end + +NS_SWIFT_NAME(FieldBridge) +@interface FIRFieldBridge : FIRExprBridge +- (id)init:(NSString *)name; +@end + +NS_SWIFT_NAME(ConstantBridge) +@interface FIRConstantBridge : FIRExprBridge +- (id)init:(NSNumber *)value; +@end + +NS_SWIFT_NAME(FunctionExprBridge) +@interface FIRFunctionExprBridge : FIRExprBridge +- (id)initWithName:(NSString *)name Args:(NSArray *)args; +@end + +NS_SWIFT_NAME(StageBridge) +@interface FIRStageBridge : NSObject +@end + +NS_SWIFT_NAME(CollectionSourceStageBridge) +@interface FIRCollectionSourceStageBridge : FIRStageBridge + +- (id)initWithPath:(NSString *)path; + +@end + +NS_SWIFT_NAME(WhereStageBridge) +@interface FIRWhereStageBridge : FIRStageBridge + +- (id)initWithExpr:(FIRExprBridge *)expr; + +@end + +NS_SWIFT_NAME(__PipelineSnapshotBridge) +@interface __FIRPipelineSnapshotBridge : NSObject + +@property(nonatomic, strong, readonly) NSArray<__FIRPipelineSnapshotBridge *> *results; + +@end + +NS_SWIFT_NAME(__PipelineResultBridge) +@interface __FIRPipelineResultBridge : NSObject + +@property(nonatomic, strong, readonly) FIRDocumentReference *reference; + +@property(nonatomic, copy, readonly) NSString *documentID; + +- (nullable NSDictionary *)data; + +@end + +NS_SWIFT_NAME(PipelineBridge) +@interface FIRPipelineBridge : NSObject + +/** :nodoc: */ +- (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db; + +- (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, + NSError *_Nullable error))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h b/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h index 7fabad323c8..0f10968565a 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h +++ b/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h @@ -32,6 +32,7 @@ #import "FIRListenerRegistration.h" #import "FIRLoadBundleTask.h" #import "FIRLocalCacheSettings.h" +#import "FIRPipelineBridge.h" #import "FIRQuery.h" #import "FIRQuerySnapshot.h" #import "FIRSnapshotListenOptions.h" diff --git a/Firestore/Swift/Source/SwiftAPI/Expressions.swift b/Firestore/Swift/Source/SwiftAPI/Expressions.swift new file mode 100644 index 00000000000..729b5c9fb67 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Expressions.swift @@ -0,0 +1,70 @@ +/* + * 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 Foundation + +public protocol Expr { + var bridge: ExprBridge { get } +} + +public struct Constant: Expr { + public var bridge: ExprBridge + + var value: any Numeric + init(value: any Numeric) { + self.value = value + bridge = ConstantBridge(value as! NSNumber) + } +} + +public func constant(_ number: any Numeric) -> Constant { + return Constant(value: number) +} + +public struct Field: Expr { + public var bridge: ExprBridge + + var name: String + init(name: String) { + self.name = name + bridge = FieldBridge(name) + } +} + +public func field(_ name: String) -> Field { + return Field(name: name) +} + +protocol Function: Expr { + var name: String { get } +} + +public struct FunctionExpr: Function { + public var bridge: ExprBridge + + var name: String + private var args: [Expr] + + init(name: String, args: [Expr]) { + self.name = name + self.args = args + bridge = FunctionExprBridge(name: name, args: args.map { $0.bridge }) + } +} + +public func eq(_ left: Expr, _ right: Expr) -> FunctionExpr { + return FunctionExpr(name: "eq", args: [left, right]) +} diff --git a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift new file mode 100644 index 00000000000..0179ece4e04 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift @@ -0,0 +1,23 @@ +/* + * 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 Foundation + +@objc public extension Firestore { + @nonobjc func pipeline() -> PipelineSource { + return PipelineSource(db: self) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline.swift new file mode 100644 index 00000000000..8c8a4364d30 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline.swift @@ -0,0 +1,45 @@ +/* + * 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 Foundation + +public struct Pipeline { + private var stages: [Stage] + private var bridge: PipelineBridge + private let db: Firestore + + init(stages: [Stage], db: Firestore) { + self.stages = stages + self.db = db + bridge = PipelineBridge(stages: stages.map { $0.bridge }, db: db) + } + + public func `where`(_ condition: Expr) -> Pipeline { + return Pipeline(stages: stages + [Where(condition: condition)], db: db) + } + + public func execute() async throws -> PipelineSnapshot { + return try await withCheckedThrowingContinuation { continuation in + self.bridge.execute { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: PipelineSnapshot(result!)) + } + } + } + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift new file mode 100644 index 00000000000..00386d0c6dc --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift @@ -0,0 +1,25 @@ +/* + * 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 Foundation + +public struct PipelineSnapshot { + private let bridge: __PipelineSnapshotBridge + + init(_ bridge: __PipelineSnapshotBridge) { + self.bridge = bridge + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift new file mode 100644 index 00000000000..ce84c0356ac --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift @@ -0,0 +1,28 @@ +/* + * 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 Foundation + +public class PipelineSource { + private let db: Firestore + public init(db: Firestore) { + self.db = db + } + + public func collection(path: String) -> Pipeline { + return Pipeline(stages: [CollectionSource(collection: path)], db: db) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift new file mode 100644 index 00000000000..df3c163e803 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -0,0 +1,47 @@ +/* + * 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 FirebaseFirestoreInternal +import Foundation + +protocol Stage { + var name: String { get } + var bridge: StageBridge { get } +} + +class CollectionSource: Stage { + var name: String = "collection" + + var bridge: StageBridge + private var collection: String + + init(collection: String) { + self.collection = collection + bridge = CollectionSourceStageBridge(path: collection) + } +} + +class Where: Stage { + var name: String = "where" + + var bridge: StageBridge + private var condition: Expr // TODO: should be FilterCondition + + init(condition: Expr) { + self.condition = condition + bridge = WhereStageBridge(expr: condition.bridge) + } +} diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift new file mode 100644 index 00000000000..79185762b91 --- /dev/null +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -0,0 +1,31 @@ +/* + * 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 FirebaseFirestore +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class PipelineIntegrationTests: FSTIntegrationTestCase { + func testCount() async throws { + let snapshot = try await firestore() + .pipeline() + .collection(path: "foo") + .where(eq(field("foo"), constant(42))) + .execute() + + print(snapshot) + } +} diff --git a/Firestore/core/src/api/api_fwd.h b/Firestore/core/src/api/api_fwd.h index 0c35c567157..ded3bfb76af 100644 --- a/Firestore/core/src/api/api_fwd.h +++ b/Firestore/core/src/api/api_fwd.h @@ -44,6 +44,8 @@ class DocumentReference; class DocumentSnapshot; class Firestore; class ListenerRegistration; +class Pipeline; +class PipelineSnapshot; class Query; class QuerySnapshot; class Settings; diff --git a/Firestore/core/src/api/expressions.cc b/Firestore/core/src/api/expressions.cc new file mode 100644 index 00000000000..07e99b1e848 --- /dev/null +++ b/Firestore/core/src/api/expressions.cc @@ -0,0 +1,61 @@ +/* + * 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. + */ + +#include "Firestore/core/src/api/expressions.h" + +#include + +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/nanopb/nanopb_util.h" + +namespace firebase { +namespace firestore { +namespace api { + +google_firestore_v1_Value Field::to_proto() const { + google_firestore_v1_Value result; + + result.which_value_type = google_firestore_v1_Value_field_reference_value_tag; + result.field_reference_value = nanopb::MakeBytesArray(this->name_); + + return result; +} + +google_firestore_v1_Value Constant::to_proto() const { + google_firestore_v1_Value result; + + result.which_value_type = google_firestore_v1_Value_double_value_tag; + result.double_value = this->value_; + + return result; +} + +google_firestore_v1_Value FunctionExpr::to_proto() const { + google_firestore_v1_Value result; + + result.which_value_type = google_firestore_v1_Value_function_value_tag; + result.function_value = google_firestore_v1_Function{}; + result.function_value.name = nanopb::MakeBytesArray(name_); + nanopb::SetRepeatedField( + &result.function_value.args, &result.function_value.args_count, args_, + [](const std::shared_ptr& arg) { return arg->to_proto(); }); + + return result; +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h new file mode 100644 index 00000000000..2ab134249cf --- /dev/null +++ b/Firestore/core/src/api/expressions.h @@ -0,0 +1,75 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_EXPRESSIONS_H_ +#define FIRESTORE_CORE_SRC_API_EXPRESSIONS_H_ + +#include +#include +#include +#include + +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" + +namespace firebase { +namespace firestore { +namespace api { + +class Expr { + public: + Expr() = default; + virtual ~Expr() = default; + virtual google_firestore_v1_Value to_proto() const = 0; +}; + +class Field : public Expr { + public: + explicit Field(std::string name) : name_(std::move(name)) { + } + google_firestore_v1_Value to_proto() const override; + + private: + std::string name_; +}; + +class Constant : public Expr { + public: + explicit Constant(double value) : value_(value) { + } + google_firestore_v1_Value to_proto() const override; + + private: + double value_; +}; + +class FunctionExpr : public Expr { + public: + FunctionExpr(std::string name, std::vector> args) + : name_(std::move(name)), args_(std::move(args)) { + } + + google_firestore_v1_Value to_proto() const override; + + private: + std::string name_; + std::vector> args_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_EXPRESSIONS_H_ diff --git a/Firestore/core/src/api/firestore.cc b/Firestore/core/src/api/firestore.cc index 70cb975cc71..1484f3a27fc 100644 --- a/Firestore/core/src/api/firestore.cc +++ b/Firestore/core/src/api/firestore.cc @@ -179,6 +179,13 @@ void Firestore::RunTransaction(core::TransactionUpdateCallback update_callback, std::move(result_callback)); } +void Firestore::RunPipeline(const api::Pipeline& pipeline, + util::StatusOrCallback callback) { + EnsureClientConfigured(); + + client_->RunPipeline(pipeline, std::move(callback)); +} + void Firestore::Terminate(util::StatusCallback callback) { // The client must be initialized to ensure that all subsequent API usage // throws an exception. diff --git a/Firestore/core/src/api/firestore.h b/Firestore/core/src/api/firestore.h index 0300f6c61f2..de22a87c14c 100644 --- a/Firestore/core/src/api/firestore.h +++ b/Firestore/core/src/api/firestore.h @@ -102,6 +102,8 @@ class Firestore : public std::enable_shared_from_this { void RunTransaction(core::TransactionUpdateCallback update_callback, core::TransactionResultCallback result_callback, int max_attempts); + void RunPipeline(const api::Pipeline& pipeline, + util::StatusOrCallback callback); void Terminate(util::StatusCallback callback); void ClearPersistence(util::StatusCallback callback); diff --git a/Firestore/core/src/api/pipeline.cc b/Firestore/core/src/api/pipeline.cc new file mode 100644 index 00000000000..24c5109bd95 --- /dev/null +++ b/Firestore/core/src/api/pipeline.cc @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#include "Firestore/core/src/api/pipeline.h" + +#include +#include + +#include "Firestore/core/src/core/firestore_client.h" + +namespace firebase { +namespace firestore { +namespace api { + +Pipeline Pipeline::AddingStage(std::shared_ptr stage) { + auto copy = std::vector>(this->stages_); + copy.push_back(stage); + + return {copy, this->firestore_}; +} + +const std::vector>& Pipeline::stages() const { + return this->stages_; +} + +void Pipeline::execute(util::StatusOrCallback callback) { + this->firestore_->RunPipeline(*this, std::move(callback)); +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/pipeline.h b/Firestore/core/src/api/pipeline.h new file mode 100644 index 00000000000..6103f366eda --- /dev/null +++ b/Firestore/core/src/api/pipeline.h @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_PIPELINE_H_ +#define FIRESTORE_CORE_SRC_API_PIPELINE_H_ + +#include +#include +#include + +#include "Firestore/core/src/api/firestore.h" +#include "Firestore/core/src/api/pipeline_snapshot.h" +#include "Firestore/core/src/api/stages.h" +#include "Firestore/core/src/util/status_fwd.h" + +namespace firebase { +namespace firestore { +namespace api { + +class Pipeline { + public: + Pipeline(std::vector> stages, + std::shared_ptr firestore) + : stages_(std::move(stages)), firestore_(firestore) { + } + + const std::shared_ptr& firestore() const { + return firestore_; + } + + Pipeline AddingStage(std::shared_ptr stage); + + const std::vector>& stages() const; + + void execute(util::StatusOrCallback callback); + + private: + std::vector> stages_; + std::shared_ptr firestore_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_PIPELINE_H_ diff --git a/Firestore/core/src/api/pipeline_result.cc b/Firestore/core/src/api/pipeline_result.cc new file mode 100644 index 00000000000..655fd7b4132 --- /dev/null +++ b/Firestore/core/src/api/pipeline_result.cc @@ -0,0 +1,52 @@ +/* + * 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. + */ + +#include "Firestore/core/src/api/pipeline_result.h" + +#include + +#include "Firestore/core/src/api/document_reference.h" +#include "Firestore/core/src/model/document.h" +#include "Firestore/core/src/model/document_key.h" +#include "Firestore/core/src/model/field_path.h" +#include "Firestore/core/src/model/object_value.h" +#include "Firestore/core/src/model/resource_path.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace api { + +using model::Document; +using model::DocumentKey; +using model::FieldPath; +using model::ObjectValue; + +std::shared_ptr PipelineResult::internal_value() const { + return value_; +} + +absl::optional PipelineResult::document_id() const { + if (!internal_key_.has_value()) { + return absl::nullopt; + } + return internal_key_.value().path().last_segment(); +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/pipeline_result.h b/Firestore/core/src/api/pipeline_result.h new file mode 100644 index 00000000000..4680d058c7b --- /dev/null +++ b/Firestore/core/src/api/pipeline_result.h @@ -0,0 +1,71 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_PIPELINE_RESULT_H_ +#define FIRESTORE_CORE_SRC_API_PIPELINE_RESULT_H_ + +#include +#include +#include + +#include "Firestore/core/src/model/document.h" +#include "Firestore/core/src/model/document_key.h" +#include "Firestore/core/src/model/model_fwd.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace api { + +class DocumentReference; +class Firestore; + +class PipelineResult { + public: + PipelineResult(absl::optional document_key, + std::shared_ptr value, + absl::optional create_time, + absl::optional update_time, + absl::optional execution_time) + : internal_key_{std::move(document_key)}, + value_{std::move(value)}, + create_time_{create_time}, + update_time_{update_time}, + execution_time_{execution_time} { + } + + PipelineResult() = default; + + std::shared_ptr internal_value() const; + absl::optional document_id() const; + + private: + absl::optional internal_key_; + // Using a shared pointer to ObjectValue makes PipelineResult copy-assignable + // without having to manually create a deep clone of its Protobuf contents. + std::shared_ptr value_ = + std::make_shared(); + absl::optional create_time_; + absl::optional update_time_; + absl::optional execution_time_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_PIPELINE_RESULT_H_ diff --git a/Firestore/core/src/api/pipeline_snapshot.h b/Firestore/core/src/api/pipeline_snapshot.h new file mode 100644 index 00000000000..a19e76138a7 --- /dev/null +++ b/Firestore/core/src/api/pipeline_snapshot.h @@ -0,0 +1,53 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_PIPELINE_SNAPSHOT_H_ +#define FIRESTORE_CORE_SRC_API_PIPELINE_SNAPSHOT_H_ + +#include +#include +#include +#include + +#include "Firestore/core/src/api/api_fwd.h" +#include "Firestore/core/src/api/pipeline_result.h" +#include "Firestore/core/src/model/snapshot_version.h" + +namespace firebase { +namespace firestore { +namespace api { + +class PipelineSnapshot { + public: + explicit PipelineSnapshot(std::vector&& results, + model::SnapshotVersion execution_time) + : results_(std::move(results)), execution_time_(execution_time) { + } + + const std::vector& results() const { + return results_; + } + + private: + std::vector results_; + model::SnapshotVersion execution_time_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_PIPELINE_SNAPSHOT_H_ diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc new file mode 100644 index 00000000000..6843a1b4ce5 --- /dev/null +++ b/Firestore/core/src/api/stages.cc @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#include "Firestore/core/src/api/stages.h" + +#include "Firestore/core/src/nanopb/nanopb_util.h" + +namespace firebase { +namespace firestore { +namespace api { + +google_firestore_v1_Pipeline_Stage CollectionSource::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + + result.name = nanopb::MakeBytesArray("collection"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0].which_value_type = + google_firestore_v1_Value_reference_value_tag; + result.args[0].reference_value = nanopb::MakeBytesArray(this->path_); + + result.options_count = 0; + result.options = nullptr; + + return result; +} + +google_firestore_v1_Pipeline_Stage Where::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + + result.name = nanopb::MakeBytesArray("where"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0] = this->expr_->to_proto(); + + result.options_count = 0; + result.options = nullptr; + + return result; +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h new file mode 100644 index 00000000000..f037a70408e --- /dev/null +++ b/Firestore/core/src/api/stages.h @@ -0,0 +1,66 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_STAGES_H_ +#define FIRESTORE_CORE_SRC_API_STAGES_H_ + +#include +#include + +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/api/expressions.h" + +namespace firebase { +namespace firestore { +namespace api { + +class Stage { + public: + Stage() = default; + virtual ~Stage() = default; + + virtual google_firestore_v1_Pipeline_Stage to_proto() const = 0; +}; + +class CollectionSource : public Stage { + public: + explicit CollectionSource(std::string path) : path_(path) { + } + ~CollectionSource() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::string path_; +}; + +class Where : public Stage { + public: + explicit Where(std::shared_ptr expr) : expr_(expr) { + } + ~Where() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr expr_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_STAGES_H_ diff --git a/Firestore/core/src/core/firestore_client.cc b/Firestore/core/src/core/firestore_client.cc index c6dd50634c1..0c6ac315dea 100644 --- a/Firestore/core/src/core/firestore_client.cc +++ b/Firestore/core/src/core/firestore_client.cc @@ -575,6 +575,25 @@ void FirestoreClient::RunAggregateQuery( }); } +void FirestoreClient::RunPipeline( + const api::Pipeline& pipeline, + util::StatusOrCallback callback) { + VerifyNotTerminated(); + + // Dispatch the result back onto the user dispatch queue. + auto async_callback = + [this, callback](const StatusOr& status) { + if (callback) { + user_executor_->Execute([=] { callback(std::move(status)); }); + } + }; + + worker_queue_->Enqueue( + [this, pipeline, async_callback = std::move(async_callback)] { + remote_store_->RunPipeline(pipeline, async_callback); + }); +} + void FirestoreClient::AddSnapshotsInSyncListener( const std::shared_ptr>& user_listener) { worker_queue_->Enqueue([this, user_listener] { diff --git a/Firestore/core/src/core/firestore_client.h b/Firestore/core/src/core/firestore_client.h index d752deff66a..24c0e8c396a 100644 --- a/Firestore/core/src/core/firestore_client.h +++ b/Firestore/core/src/core/firestore_client.h @@ -23,6 +23,7 @@ #include "Firestore/core/src/api/api_fwd.h" #include "Firestore/core/src/api/load_bundle_task.h" +#include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/bundle/bundle_serializer.h" #include "Firestore/core/src/core/core_fwd.h" #include "Firestore/core/src/core/database_info.h" @@ -159,6 +160,9 @@ class FirestoreClient : public std::enable_shared_from_this { const std::vector& aggregates, api::AggregateQueryCallback&& result_callback); + void RunPipeline(const api::Pipeline& pipeline, + util::StatusOrCallback callback); + /** * Adds a listener to be called when a snapshots-in-sync event fires. */ diff --git a/Firestore/core/src/nanopb/fields_array.h b/Firestore/core/src/nanopb/fields_array.h index 3b89b2ecd06..29fdc1c66dd 100644 --- a/Firestore/core/src/nanopb/fields_array.h +++ b/Firestore/core/src/nanopb/fields_array.h @@ -227,6 +227,18 @@ inline const pb_field_t* FieldsArray< return google_firestore_v1_StructuredAggregationQuery_Aggregation_Count_fields; } +template <> +inline const pb_field_t* +FieldsArray() { + return google_firestore_v1_ExecutePipelineRequest_fields; +} + +template <> +inline const pb_field_t* +FieldsArray() { + return google_firestore_v1_ExecutePipelineResponse_fields; +} + template <> inline const pb_field_t* FieldsArray() { return google_firestore_v1_ExistenceFilter_fields; diff --git a/Firestore/core/src/remote/datastore.cc b/Firestore/core/src/remote/datastore.cc index 47a8fb716b5..d5950ca09c6 100644 --- a/Firestore/core/src/remote/datastore.cc +++ b/Firestore/core/src/remote/datastore.cc @@ -63,6 +63,8 @@ const auto kRpcNameCommit = "/google.firestore.v1.Firestore/Commit"; const auto kRpcNameLookup = "/google.firestore.v1.Firestore/BatchGetDocuments"; const auto kRpcNameRunAggregationQuery = "/google.firestore.v1.Firestore/RunAggregationQuery"; +const auto kRpcNameExecutePipeline = + "/google.firestore.v1.Firestore/ExecutePipeline"; std::unique_ptr CreateExecutor() { return Executor::CreateSerial("com.google.firebase.firestore.rpc"); @@ -308,6 +310,52 @@ void Datastore::RunAggregateQueryWithCredentials( }); } +void Datastore::RunPipeline( + const api::Pipeline& pipeline, + util::StatusOrCallback&& result_callback) { + ResumeRpcWithCredentials( + [this, pipeline, result_callback = std::move(result_callback)]( + const StatusOr& auth_token, + const std::string& app_check_token) mutable { + if (!auth_token.ok()) { + // result_callback(auth_token.status()); + return; + } + RunPipelineWithCredentials(auth_token.ValueOrDie(), app_check_token, + pipeline, std::move(result_callback)); + }); +} + +void Datastore::RunPipelineWithCredentials( + const credentials::AuthToken& auth_token, + const std::string& app_check_token, + const api::Pipeline& pipeline, + util::StatusOrCallback&& callback) { + auto request = datastore_serializer_.EncodeExecutePipelineRequest(pipeline); + LOG_DEBUG("Run Pipeline: %s", request.ToString()); + + grpc::ByteBuffer message = MakeByteBuffer(request); + std::unique_ptr call_owning = grpc_connection_.CreateUnaryCall( + kRpcNameExecutePipeline, auth_token, app_check_token, std::move(message)); + GrpcUnaryCall* call = call_owning.get(); + active_calls_.push_back(std::move(call_owning)); + + call->Start([this, call, callback = std::move(callback)]( + const StatusOr& result) { + LogGrpcCallFinished("ExecutePipeline", call, result.status()); + HandleCallStatus(result.status()); + + if (result.ok()) { + callback(datastore_serializer_.DecodeExecutePipelineResponse( + result.ValueOrDie())); + } else { + callback(result.status()); + } + + RemoveGrpcCall(call); + }); +} + void Datastore::ResumeRpcWithCredentials(const OnCredentials& on_credentials) { // Auth/AppCheck may outlive Firestore std::weak_ptr weak_this{shared_from_this()}; diff --git a/Firestore/core/src/remote/datastore.h b/Firestore/core/src/remote/datastore.h index 7de64663d11..df912159eef 100644 --- a/Firestore/core/src/remote/datastore.h +++ b/Firestore/core/src/remote/datastore.h @@ -23,6 +23,7 @@ #include #include "Firestore/core/src/api/api_fwd.h" +#include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/core/core_fwd.h" #include "Firestore/core/src/credentials/auth_token.h" #include "Firestore/core/src/credentials/credentials_fwd.h" @@ -112,6 +113,10 @@ class Datastore : public std::enable_shared_from_this { const std::vector& aggregates, api::AggregateQueryCallback&& result_callback); + void RunPipeline( + const api::Pipeline& pipeline, + util::StatusOrCallback&& result_callback); + /** Returns true if the given error is a gRPC ABORTED error. */ static bool IsAbortedError(const util::Status& error); @@ -195,6 +200,12 @@ class Datastore : public std::enable_shared_from_this { const std::vector& aggregates, api::AggregateQueryCallback&& callback); + void RunPipelineWithCredentials( + const credentials::AuthToken& auth_token, + const std::string& app_check_token, + const api::Pipeline& pipeline, + util::StatusOrCallback&& result_callback); + using OnCredentials = std::function&, const std::string&)>; void ResumeRpcWithCredentials(const OnCredentials& on_credentials); diff --git a/Firestore/core/src/remote/remote_objc_bridge.cc b/Firestore/core/src/remote/remote_objc_bridge.cc index ae7fcb246ea..466ed1229cc 100644 --- a/Firestore/core/src/remote/remote_objc_bridge.cc +++ b/Firestore/core/src/remote/remote_objc_bridge.cc @@ -383,6 +383,32 @@ util::StatusOr DatastoreSerializer::DecodeAggregateQueryResponse( aliasMap); } +Message +DatastoreSerializer::EncodeExecutePipelineRequest( + const firebase::firestore::api::Pipeline& pipeline) const { + Message result; + result->database = serializer_.EncodeDatabaseName(); + result->which_pipeline_type = + google_firestore_v1_ExecutePipelineRequest_structured_pipeline_tag; + result->pipeline_type.structured_pipeline = + serializer_.EncodePipeline(pipeline); + + return result; +} + +util::StatusOr +DatastoreSerializer::DecodeExecutePipelineResponse( + const grpc::ByteBuffer& response) const { + ByteBufferReader reader{response}; + auto message = + Message::TryParse(&reader); + if (!reader.ok()) { + return reader.status(); + } + + return serializer_.DecodePipelineResponse(reader.context(), message); +} + } // namespace remote } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/remote/remote_objc_bridge.h b/Firestore/core/src/remote/remote_objc_bridge.h index 0aa2b93f988..f6615003eed 100644 --- a/Firestore/core/src/remote/remote_objc_bridge.h +++ b/Firestore/core/src/remote/remote_objc_bridge.h @@ -32,6 +32,7 @@ #include "Firestore/core/src/util/status_fwd.h" #include "grpcpp/support/byte_buffer.h" +#include "Firestore/core/src/api/pipeline.h" #include "absl/container/flat_hash_map.h" namespace firebase { @@ -150,6 +151,13 @@ class DatastoreSerializer { return serializer_; } + nanopb::Message + EncodeExecutePipelineRequest( + const firebase::firestore::api::Pipeline& pipeline) const; + + util::StatusOr DecodeExecutePipelineResponse( + const grpc::ByteBuffer& response) const; + private: Serializer serializer_; }; diff --git a/Firestore/core/src/remote/remote_store.cc b/Firestore/core/src/remote/remote_store.cc index 86455fcd15e..1bf4370240b 100644 --- a/Firestore/core/src/remote/remote_store.cc +++ b/Firestore/core/src/remote/remote_store.cc @@ -390,6 +390,17 @@ void RemoteStore::RunAggregateQuery( } } +void RemoteStore::RunPipeline( + const api::Pipeline& pipeline, + util::StatusOrCallback result_callback) { + if (CanUseNetwork()) { + datastore_->RunPipeline(pipeline, std::move(result_callback)); + } else { + result_callback(Status::FromErrno(Error::kErrorUnavailable, + "Failed to get result from server.")); + } +} + // Write Stream void RemoteStore::FillWritePipeline() { diff --git a/Firestore/core/src/remote/remote_store.h b/Firestore/core/src/remote/remote_store.h index ae6bd7023bc..cd0d7b8e7ca 100644 --- a/Firestore/core/src/remote/remote_store.h +++ b/Firestore/core/src/remote/remote_store.h @@ -21,6 +21,7 @@ #include #include +#include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/core/transaction.h" #include "Firestore/core/src/local/target_data.h" #include "Firestore/core/src/model/model_fwd.h" @@ -203,6 +204,9 @@ class RemoteStore : public TargetMetadataProvider, const std::vector& aggregates, api::AggregateQueryCallback&& result_callback); + void RunPipeline(const api::Pipeline& pipeline, + util::StatusOrCallback callback); + void OnWatchStreamOpen() override; void OnWatchStreamChange( const WatchChange& change, diff --git a/Firestore/core/src/remote/serializer.cc b/Firestore/core/src/remote/serializer.cc index f301a65e37f..2985f86d623 100644 --- a/Firestore/core/src/remote/serializer.cc +++ b/Firestore/core/src/remote/serializer.cc @@ -175,6 +175,15 @@ FieldPath InvalidFieldPath() { return FieldPath::EmptyPath(); } +absl::optional NotNoneVersionOrNullOpt( + const SnapshotVersion& version) { + if (version == SnapshotVersion::None()) { + return absl::nullopt; + } else { + return version; + } +} + } // namespace Serializer::Serializer(DatabaseId database_id) @@ -1197,6 +1206,26 @@ Serializer::DecodeCursorValue(google_firestore_v1_Cursor& cursor) const { return index_components; } +google_firestore_v1_StructuredPipeline Serializer::EncodePipeline( + const api::Pipeline& pipeline) const { + google_firestore_v1_StructuredPipeline result; + auto* stages = + MakeArray(pipeline.stages().size()); + + size_t i = 0; + for (const auto& stage : pipeline.stages()) { + stages[i++] = stage->to_proto(); + } + + result.pipeline.stages_count = pipeline.stages().size(); + result.pipeline.stages = stages; + + result.options_count = 0; + result.options = nullptr; + + return result; +} + /* static */ pb_bytes_array_t* Serializer::EncodeFieldPath(const FieldPath& field_path) { return EncodeString(field_path.CanonicalString()); @@ -1479,6 +1508,35 @@ bool Serializer::IsLocalDocumentKey(absl::string_view path) const { DocumentKey::IsDocumentKey(resource.PopFirst(5)); } +api::PipelineSnapshot Serializer::DecodePipelineResponse( + util::ReadContext* context, + const nanopb::Message& message) + const { + auto execution_time = DecodeVersion(context, message->execution_time); + + std::vector results(message->results_count); + + for (pb_size_t i = 0; i < message->results_count; ++i) { + absl::optional key; + if (message->results[i].name != nullptr) { + key = DecodeKey(context, message->results[i].name); + } + + auto create_time = DecodeVersion(context, message->results[i].create_time); + auto update_time = DecodeVersion(context, message->results[i].update_time); + + auto value = ObjectValue::FromFieldsEntry(message->results[i].fields, + message->results[i].fields_count); + results.push_back({std::move(key), + std::make_shared(std::move(value)), + NotNoneVersionOrNullOpt(create_time), + NotNoneVersionOrNullOpt(update_time), + NotNoneVersionOrNullOpt(execution_time)}); + } + + return api::PipelineSnapshot(std::move(results), execution_time); +} + } // namespace remote } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/remote/serializer.h b/Firestore/core/src/remote/serializer.h index c42c6c3ac1b..2105b7eb754 100644 --- a/Firestore/core/src/remote/serializer.h +++ b/Firestore/core/src/remote/serializer.h @@ -28,6 +28,7 @@ #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.h" #include "Firestore/Protos/nanopb/google/type/latlng.nanopb.h" +#include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/core/composite_filter.h" #include "Firestore/core/src/core/core_fwd.h" #include "Firestore/core/src/core/field_filter.h" @@ -204,6 +205,9 @@ class Serializer { pb_bytes_array_t* parent, google_firestore_v1_StructuredQuery& query) const; + google_firestore_v1_StructuredPipeline EncodePipeline( + const api::Pipeline& pipeline) const; + /** * Decodes the watch change. Modifies the provided proto to release * ownership of any Value messages. @@ -241,6 +245,11 @@ class Serializer { return database_id_; } + api::PipelineSnapshot DecodePipelineResponse( + util::ReadContext* context, + const nanopb::Message& + message) const; + private: friend class SerializerTest;