Skip to content

Commit 30bf7bb

Browse files
cherylEnkiduncooke3yyyu-googlejscud
authored
Add subquery support in pipeline (#15963)
Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Yvonne Yu <yyyu@google.com> Co-authored-by: Jeff Scudder <jscudder@google.com>
1 parent 701e8a3 commit 30bf7bb

25 files changed

+1870
-404
lines changed

Firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [feature] Added support for Firestore Pipeline Subqueries and scope bridging with `Subcollection`, `define()`, `toArrayExpression()`, `toScalarExpression()`, `Variable`, and `CurrentDocument` APIs.
3+
24
- [feature] Added support for Pipeline expressions `nor` and `switchOn`. (#15943)
35

46
# 12.11.0

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 171 additions & 163 deletions
Large diffs are not rendered by default.

Firestore/Source/API/FIRPipelineBridge+Internal.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ NS_ASSUME_NONNULL_BEGIN
5252
- (id)initWithCppStage:(std::shared_ptr<const firebase::firestore::api::CollectionSource>)stage;
5353
@end
5454

55+
@interface FIRSubcollectionSourceStageBridge (Internal)
56+
- (id)initWithCppStage:(std::shared_ptr<const firebase::firestore::api::SubcollectionSource>)stage;
57+
@end
58+
5559
@interface FIRDatabaseSourceStageBridge (Internal)
5660
- (id)initWithCppStage:(std::shared_ptr<const firebase::firestore::api::DatabaseSource>)stage;
5761
@end

Firestore/Source/API/FIRPipelineBridge.mm

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
using firebase::firestore::api::CollectionSource;
6565
using firebase::firestore::api::Constant;
6666
using firebase::firestore::api::DatabaseSource;
67+
using firebase::firestore::api::DefineStage;
6768
using firebase::firestore::api::DistinctStage;
6869
using firebase::firestore::api::DocumentChange;
6970
using firebase::firestore::api::DocumentReference;
@@ -88,8 +89,10 @@
8889
using firebase::firestore::api::SelectStage;
8990
using firebase::firestore::api::SnapshotMetadata;
9091
using firebase::firestore::api::SortStage;
92+
using firebase::firestore::api::SubcollectionSource;
9193
using firebase::firestore::api::Union;
9294
using firebase::firestore::api::Unnest;
95+
using firebase::firestore::api::Variable;
9396
using firebase::firestore::api::Where;
9497
using firebase::firestore::core::EventListener;
9598
using firebase::firestore::core::ViewSnapshot;
@@ -148,6 +151,30 @@ - (NSString *)field_name {
148151

149152
@end
150153

154+
@implementation FIRVariableBridge {
155+
std::shared_ptr<Variable> cpp_variable;
156+
NSString *_name;
157+
Boolean isUserDataRead;
158+
}
159+
160+
- (id)initWithName:(NSString *)name {
161+
self = [super init];
162+
if (self) {
163+
_name = name;
164+
isUserDataRead = NO;
165+
}
166+
return self;
167+
}
168+
169+
- (std::shared_ptr<api::Expr>)cppExprWithReader:(FSTUserDataReader *)reader {
170+
if (!isUserDataRead) {
171+
cpp_variable = std::make_shared<Variable>(MakeString(_name));
172+
}
173+
isUserDataRead = YES;
174+
return cpp_variable;
175+
}
176+
@end
177+
151178
@implementation FIRConstantBridge {
152179
std::shared_ptr<Constant> cpp_constant;
153180
id _input;
@@ -302,6 +329,27 @@ - (NSString *)name {
302329
}
303330
@end
304331

332+
@implementation FIRSubcollectionSourceStageBridge {
333+
std::shared_ptr<SubcollectionSource> cpp_subcollection_source;
334+
}
335+
336+
- (id)initWithPath:(NSString *)path {
337+
self = [super init];
338+
if (self) {
339+
cpp_subcollection_source = std::make_shared<SubcollectionSource>(MakeString(path));
340+
}
341+
return self;
342+
}
343+
344+
- (std::shared_ptr<api::Stage>)cppStageWithReader:(FSTUserDataReader *)reader {
345+
return cpp_subcollection_source;
346+
}
347+
348+
- (NSString *)name {
349+
return @"subcollection";
350+
}
351+
@end
352+
305353
@implementation FIRDatabaseSourceStageBridge {
306354
std::shared_ptr<DatabaseSource> cpp_database_source;
307355
}
@@ -617,6 +665,38 @@ - (NSString *)name {
617665
}
618666
@end
619667

668+
@implementation FIRDefineStageBridge {
669+
NSDictionary<NSString *, FIRExprBridge *> *_variables;
670+
Boolean isUserDataRead;
671+
std::shared_ptr<DefineStage> cpp_define;
672+
}
673+
674+
- (id)initWithVariables:(NSDictionary<NSString *, FIRExprBridge *> *)variables {
675+
self = [super init];
676+
if (self) {
677+
_variables = variables;
678+
isUserDataRead = NO;
679+
}
680+
return self;
681+
}
682+
683+
- (std::shared_ptr<api::Stage>)cppStageWithReader:(FSTUserDataReader *)reader {
684+
if (!isUserDataRead) {
685+
std::unordered_map<std::string, std::shared_ptr<Expr>> cpp_fields;
686+
for (NSString *key in _variables) {
687+
cpp_fields[MakeString(key)] = [_variables[key] cppExprWithReader:reader];
688+
}
689+
cpp_define = std::make_shared<DefineStage>(std::move(cpp_fields));
690+
}
691+
isUserDataRead = YES;
692+
return cpp_define;
693+
}
694+
695+
- (NSString *)name {
696+
return @"let";
697+
}
698+
@end
699+
620700
@implementation FIRDistinctStageBridge {
621701
NSDictionary<NSString *, FIRExprBridge *> *_groups;
622702
Boolean isUserDataRead;
@@ -1205,7 +1285,30 @@ - (id)initWithCppChange:(api::PipelineResultChange)change db:(std::shared_ptr<ap
12051285

12061286
return self;
12071287
}
1288+
@end
12081289

1290+
@implementation FIRPipelineExprBridge {
1291+
NSArray<FIRStageBridge *> *_stages;
1292+
Boolean isUserDataRead;
1293+
std::vector<std::shared_ptr<api::Stage>> cpp_stages;
1294+
}
1295+
- (id)initWithStages:(NSArray<FIRStageBridge *> *)stages {
1296+
self = [super init];
1297+
if (self) {
1298+
_stages = stages;
1299+
isUserDataRead = NO;
1300+
}
1301+
return self;
1302+
}
1303+
- (std::shared_ptr<api::Expr>)cppExprWithReader:(FSTUserDataReader *)reader {
1304+
if (!isUserDataRead) {
1305+
for (FIRStageBridge *stage in _stages) {
1306+
cpp_stages.push_back([stage cppStageWithReader:reader]);
1307+
}
1308+
isUserDataRead = YES;
1309+
}
1310+
return std::make_shared<api::PipelineExpr>(cpp_stages);
1311+
}
12091312
@end
12101313

12111314
@implementation FIRPipelineBridge {
@@ -1240,7 +1343,7 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r
12401343
if (!isUserDataRead) {
12411344
std::vector<std::shared_ptr<firebase::firestore::api::Stage>> cpp_stages;
12421345
for (FIRStageBridge *stage in _stages) {
1243-
cpp_stages.push_back([stage cppStageWithReader:firestore.dataReader]);
1346+
cpp_stages.push_back([stage cppStageWithReader:reader]);
12441347
}
12451348
cpp_pipeline = std::make_shared<Pipeline>(cpp_stages, firestore.wrapped);
12461349
}

Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ NS_SWIFT_NAME(FieldBridge)
4242
- (NSString *)field_name;
4343
@end
4444

45+
NS_SWIFT_SENDABLE
46+
NS_SWIFT_NAME(VariableBridge)
47+
@interface FIRVariableBridge : FIRExprBridge
48+
- (id)initWithName:(NSString *)name;
49+
@end
50+
4551
NS_SWIFT_SENDABLE
4652
NS_SWIFT_NAME(ConstantBridge)
4753
@interface FIRConstantBridge : FIRExprBridge
@@ -79,6 +85,13 @@ NS_SWIFT_NAME(CollectionSourceStageBridge)
7985
- (id)initWithRef:(FIRCollectionReference *)ref firestore:(FIRFirestore *)db;
8086
@end
8187

88+
NS_SWIFT_SENDABLE
89+
NS_SWIFT_NAME(SubcollectionSourceStageBridge)
90+
@interface FIRSubcollectionSourceStageBridge : FIRStageBridge
91+
92+
- (id)initWithPath:(NSString *)path;
93+
@end
94+
8295
NS_SWIFT_SENDABLE
8396
NS_SWIFT_NAME(DatabaseSourceStageBridge)
8497
@interface FIRDatabaseSourceStageBridge : FIRStageBridge
@@ -139,6 +152,12 @@ NS_SWIFT_NAME(SelectStageBridge)
139152
- (id)initWithSelections:(NSDictionary<NSString *, FIRExprBridge *> *)selections;
140153
@end
141154

155+
NS_SWIFT_SENDABLE
156+
NS_SWIFT_NAME(DefineStageBridge)
157+
@interface FIRDefineStageBridge : FIRStageBridge
158+
- (id)initWithVariables:(NSDictionary<NSString *, FIRExprBridge *> *)variables;
159+
@end
160+
142161
NS_SWIFT_SENDABLE
143162
NS_SWIFT_NAME(DistinctStageBridge)
144163
@interface FIRDistinctStageBridge : FIRStageBridge
@@ -266,6 +285,12 @@ NS_SWIFT_NAME(PipelineBridge)
266285
+ (NSArray<FIRStageBridge *> *)createStageBridgesFromQuery:(FIRQuery *)query;
267286
@end
268287

288+
NS_SWIFT_SENDABLE
289+
NS_SWIFT_NAME(PipelineExprBridge)
290+
@interface FIRPipelineExprBridge : FIRExprBridge
291+
- (id)initWithStages:(NSArray<FIRStageBridge *> *)stages;
292+
@end
293+
269294
NS_SWIFT_SENDABLE
270295
NS_SWIFT_NAME(__RealtimePipelineSnapshotBridge)
271296
@interface __FIRRealtimePipelineSnapshotBridge : NSObject

Firestore/Swift/Source/ExpressionImplementation.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14-
1514
extension Expression {
16-
/// Returns the internal error message. It is overridden in specific expression implementations
17-
/// (like FunctionExpression) but defaults to `nil` for others.
15+
/// Returns the internal error message. It is dynamically dispatched
16+
/// to specific expression implementations (like FunctionExpression), and returns `nil` for
17+
/// others.
1818
/// This design is to support pipeline conversion to expression.
1919
var errorMessage: String? {
20-
return nil
20+
return Helper.errorMessage(for: self)
2121
}
2222

2323
func toBridge() -> ExprBridge {
@@ -1190,4 +1190,14 @@ public extension Expression {
11901190
func type() -> FunctionExpression {
11911191
return FunctionExpression(functionName: "type", args: [self])
11921192
}
1193+
1194+
/// Creates an expression that accesses a field on this expression using a string key.
1195+
func getField(_ key: String) -> FunctionExpression {
1196+
return FunctionExpression(functionName: "get_field", args: [self, Constant(key)])
1197+
}
1198+
1199+
/// Creates an expression that accesses a field on this expression using a dynamic key expression.
1200+
func getField(_ expression: Expression) -> FunctionExpression {
1201+
return FunctionExpression(functionName: "get_field", args: [self, expression])
1202+
}
11931203
}

Firestore/Swift/Source/Helper/PipelineHelper.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ enum Helper {
2626
}
2727
}
2828

29+
static func errorMessage(for expr: Expression) -> String? {
30+
if let funcExpr = expr as? FunctionExpression {
31+
return funcExpr.errorMessage
32+
} else if let pipelineExpr = expr as? PipelineExpression {
33+
return pipelineExpr.errorMessage
34+
} else if let boolFuncExpr = expr as? BooleanFunctionExpression {
35+
return boolFuncExpr.errorMessage
36+
}
37+
return nil
38+
}
39+
2940
static func sendableToExpr(_ value: Sendable?) -> Expression {
3041
guard let value else {
3142
return Constant.nil

Firestore/Swift/Source/Stages.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ class CollectionSource: Stage {
5555
}
5656
}
5757

58+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
59+
class SubcollectionStage: Stage {
60+
let name: String = "subcollection"
61+
let bridge: StageBridge
62+
63+
init(path: String) {
64+
bridge = SubcollectionSourceStageBridge(path: path)
65+
}
66+
}
67+
5868
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
5969
class CollectionGroupSource: Stage {
6070
let name: String = "collection_group"
@@ -192,6 +202,25 @@ class RemoveFieldsStage: Stage {
192202
}
193203
}
194204

205+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
206+
class Define: Stage {
207+
let name: String = "let"
208+
let bridge: StageBridge
209+
let errorMessage: String?
210+
211+
init(variables: [Selectable]) {
212+
let (exprMap, error) = Helper.selectablesToMap(selectables: variables)
213+
if let error = error {
214+
errorMessage = error.localizedDescription
215+
bridge = DefineStageBridge(variables: [:])
216+
} else {
217+
errorMessage = nil
218+
let bridgeVariables = exprMap.mapValues { $0.toBridge() }
219+
bridge = DefineStageBridge(variables: bridgeVariables)
220+
}
221+
}
222+
}
223+
195224
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
196225
class Select: Stage {
197226
let name: String = "select"
@@ -355,7 +384,7 @@ class Union: Stage {
355384

356385
init(other: Pipeline) {
357386
self.other = other
358-
bridge = UnionStageBridge(other: other.bridge)
387+
bridge = UnionStageBridge(other: other.pipelineBridge)
359388
errorMessage = other.errorMessage
360389
}
361390
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/// An expression that represents the current document being processed.
16+
///
17+
/// Example:
18+
/// ```swift
19+
/// // Define the current document as a variable "doc"
20+
/// firestore.pipeline().collection("books")
21+
/// .define([CurrentDocument().as("doc")])
22+
/// // Access a field from the defined document variable
23+
/// .select([Variable("doc").getField("title")])
24+
/// ```
25+
public struct CurrentDocument: Expression, BridgeWrapper {
26+
let bridge: ExprBridge
27+
28+
public init() {
29+
bridge = FunctionExprBridge(name: "current_document", args: [])
30+
}
31+
}

Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,18 @@ public protocol Expression: Sendable {
13601360

13611361
// MARK: Map Operations
13621362

1363+
/// Creates an expression that accesses a field on this expression using a string key.
1364+
///
1365+
/// - Parameter key: The string key to access.
1366+
/// - Returns: A new `FunctionExpression` representing the field access.
1367+
func getField(_ key: String) -> FunctionExpression
1368+
1369+
/// Creates an expression that accesses a field on this expression using a dynamic key expression.
1370+
///
1371+
/// - Parameter expression: The expression evaluating to a key to access.
1372+
/// - Returns: A new `FunctionExpression` representing the field access.
1373+
func getField(_ expression: Expression) -> FunctionExpression
1374+
13631375
/// Accesses a value from a map (object) field using the provided literal string key.
13641376
/// Assumes `self` evaluates to a Map.
13651377
///

0 commit comments

Comments
 (0)