From bd0ce8f80a1c62148e3581eee49272dc227d79c1 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 27 Feb 2025 12:25:18 -0500 Subject: [PATCH 01/43] create main branch of ppl --- Firestore/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index a006d39ab7b..a533789f01a 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [feature] Add `Pipeline` support. + # 11.9.0 - [fixed] Fixed memory leak in `Query.whereField()`. (#13978) From 84691ab976073ac08b8b7172dbbc82b7158c2318 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 27 Feb 2025 15:47:19 -0500 Subject: [PATCH 02/43] Add stage API --- .../SwiftAPI/Pipeline/AccumulatorExpr.swift | 15 + .../Pipeline/AccumulatorWithAlias.swift | 17 ++ .../Swift/Source/SwiftAPI/Pipeline/Add.swift | 15 + .../SwiftAPI/Pipeline/AggregateOption.swift | 23 ++ .../SwiftAPI/Pipeline/BooleanExpr.swift | 15 + .../Source/SwiftAPI/Pipeline/Constant.swift | 15 + .../Source/SwiftAPI/Pipeline/Count.swift | 13 + .../Swift/Source/SwiftAPI/Pipeline/Eq.swift | 15 + .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 35 +++ .../SwiftAPI/Pipeline/ExprWithAlias.swift | 17 ++ .../Source/SwiftAPI/Pipeline/Field.swift | 18 ++ .../Pipeline/FindNearestOptions.swift | 63 ++++ .../SwiftAPI/Pipeline/FunctionExpr.swift | 15 + .../Source/SwiftAPI/Pipeline/LogicalMin.swift | 15 + .../Swift/Source/SwiftAPI/Pipeline/Min.swift | 13 + .../Source/SwiftAPI/Pipeline/Ordering.swift | 47 +++ .../Source/SwiftAPI/Pipeline/Pipeline.swift | 288 ++++++++++++++++++ .../SwiftAPI/Pipeline/PipelineResult.swift | 82 +++++ .../SwiftAPI/Pipeline/PipelineType.swift | 15 + .../SwiftAPI/Pipeline/RealtimePipeline.swift | 16 + .../SwiftAPI/Pipeline/SampleOptions.swift | 31 ++ .../Pipeline/SelectableOrFieldName.swift | 41 +++ .../Swift/Source/SwiftAPI/Pipeline/Sum.swift | 15 + 23 files changed, 839 insertions(+) create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift new file mode 100644 index 00000000000..7d29433fe61 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift @@ -0,0 +1,15 @@ +// 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. + +public protocol AccumulatorExpr: Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift new file mode 100644 index 00000000000..c227de4d1c4 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift @@ -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. + +public struct AccumulatorWithAlias { + init(_ expr: some AccumulatorExpr, _ alias: String) {} +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift new file mode 100644 index 00000000000..69a39ee901c --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift @@ -0,0 +1,15 @@ +// 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. + +public struct Add {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift new file mode 100644 index 00000000000..d6743852692 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.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. + +public struct AggregateOption { + public let accumulators: [AccumulatorWithAlias] + public let groups: [SelectableOrFieldName]? + + public init(accumulators: [AccumulatorWithAlias], groups: [SelectableOrFieldName]? = nil) { + self.accumulators = accumulators + self.groups = groups + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift new file mode 100644 index 00000000000..587c0093c7d --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift @@ -0,0 +1,15 @@ +// 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. + +public struct BooleanExpr: FunctionExpr, Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift new file mode 100644 index 00000000000..ea567bae58c --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift @@ -0,0 +1,15 @@ +// 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. + +public struct Constant: Expr, Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift new file mode 100644 index 00000000000..470cd3d1d5a --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift @@ -0,0 +1,13 @@ +// 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. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift new file mode 100644 index 00000000000..12fa745aea1 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift @@ -0,0 +1,15 @@ +// 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. + +public struct Eq {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift new file mode 100644 index 00000000000..b4718f6fdce --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -0,0 +1,35 @@ +// 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. + +public protocol Expr: Sendable, Equatable { + func alias(_ alias: String) -> ExprWithAlias + + func eq(_ expr: BooleanExpr) + + func add(_ expr: some FunctionExpr) + + func add(_ expr: Constant) +} + +public extension Expr { + func alias(_ alias: String) -> ExprWithAlias { + return ExprWithAlias(self, alias) + } + + func eq(_ expr: some Expr) {} + + func add(_ expr: some Expr) {} + + func add(_ expr: Constant) {} +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift new file mode 100644 index 00000000000..0f32d09a345 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -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. + +public struct ExprWithAlias { + init(_ expr: some Expr, _ alias: String) {} +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift new file mode 100644 index 00000000000..22236b67005 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift @@ -0,0 +1,18 @@ +// 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. + +public struct Field: Expr, Sendable { + public var alias: String + public let fieldName: String +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift new file mode 100644 index 00000000000..50383debf7e --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift @@ -0,0 +1,63 @@ +// 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +import Foundation + +public struct FindNearestOptions { + let field: Field + let vectorValue: [VectorValue] + let distanceMeasure: DistanceMeasure + let limit: Int? + let distanceField: String? +} + +public struct DistanceMeasure: Sendable, Equatable, Hashable { + enum Kind: String { + case euclidean + case cosine + case dotProduct = "dot_product" + } + + public static var euclidean: DistanceMeasure { + return self.init(kind: .euclidean) + } + + public static var cosine: DistanceMeasure { + return self.init(kind: .cosine) + } + + public static var dotProduct: DistanceMeasure { + return self.init(kind: .dotProduct) + } + + /// Returns the raw string representation of the `DistanceMeasure` value. + public let rawValue: String + + init(kind: Kind) { + rawValue = kind.rawValue + } + + public init(rawValue: String) { + if Kind(rawValue: rawValue) == nil { + // impl + } + self.rawValue = rawValue + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift new file mode 100644 index 00000000000..7f78feebcd5 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift @@ -0,0 +1,15 @@ +// 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. + +public protocol FunctionExpr: Expr, Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift new file mode 100644 index 00000000000..c565ea79bb2 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift @@ -0,0 +1,15 @@ +// 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. + +public struct LogicalMin: FunctionExpr, Sendable, Equatable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift new file mode 100644 index 00000000000..470cd3d1d5a --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift @@ -0,0 +1,13 @@ +// 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. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift new file mode 100644 index 00000000000..27e9ee048af --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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. + */ + +public struct Ordering { + let direction: Direction +} + +public struct Direction: Sendable, Equatable, Hashable { + enum Kind: String { + case ascending + case descending + } + + public static var ascending: Direction { + return self.init(kind: .ascending) + } + + public static var descending: Direction { + return self.init(kind: .descending) + } + + public let rawValue: String + + init(kind: Kind) { + rawValue = kind.rawValue + } + + public init(rawValue: String) { + if Kind(rawValue: rawValue) == nil { + // impl + } + self.rawValue = rawValue + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift new file mode 100644 index 00000000000..677421777aa --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -0,0 +1,288 @@ +// 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct Pipeline: PipelineType { + var cppObj: firebase.firestore.api.Pipeline? = nil + + init(_ cppSource: firebase.firestore.api.Pipeline) { + cppObj = cppSource + } + + init() {} + + public func execute() async throws -> PipelineResult { + return try await withCheckedThrowingContinuation { continuation in + let listener = CallbackWrapper.wrapPipelineCallback(firestore: cppObj!.GetFirestore()) { + result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: PipelineResult(result!.pointee)) + } + } + cppObj!.Execute(listener) + } + } + + /// Adds new fields to outputs from previous stages. + /// + /// This stage allows you to compute values on-the-fly based on existing data from previous + /// stages or constants. You can use this to create new fields or overwrite existing ones. + /// + /// The added fields are defined using `Selectable`s, which can be: + /// + /// - `Field`: References an existing document field. + /// - `Function`: Performs a calculation using functions like `add`, `multiply` with + /// assigned aliases using `Expr.as`. + /// + /// - Parameter fields: The fields to add to the documents, specified as `Selectable`s. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func addFields(_ fields: Selectable...) -> Pipeline { + return self + } + + /// Selects or creates a set of fields from the outputs of previous stages. + /// + /// The selected fields are defined using `Selectable` expressions, which can be: + /// + /// - `String`: Name of an existing field. + /// - `Field`: References an existing field. + /// - `Function`: Represents the result of a function with an assigned alias name using `Expr#as`. + /// + /// If no selections are provided, the output of this stage is empty. Use `addFields` instead if + /// only additions are desired. + /// + /// - Parameter selections: The fields to include in the output documents, specified as + /// `Selectable` expressions or `String` values representing field names. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func select(_ selections: SelectableOrFieldName...) -> Pipeline { + // Implementation + return self + } + + /// Filters the documents from previous stages to only include those matching the specified + /// `BooleanExpr`. + /// + /// This stage allows you to apply conditions to the data, similar to a "WHERE" clause + /// in SQL. + /// You can filter documents based on their field values, using implementations of + /// `BooleanExpr`, typically including but not limited to: + /// + /// - field comparators: `Function.eq`, `Function.lt` (less than), `Function.gt` (greater than), + /// etc. + /// - logical operators: `Function.and`, `Function.or`, `Function.not`, + /// etc. + /// - advanced functions: `Function.regexMatch`, `Function.arrayContains`, etc. + /// + /// - Parameter condition: The `BooleanExpr` to apply. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func `where`(condition: BooleanExpr) -> Pipeline { + return self + } + + /// Skips the first `offset` number of documents from the results of previous stages. + /// + /// This stage is useful for implementing pagination in your pipelines, allowing you to + /// retrieve results in chunks. It is typically used in conjunction with `limit` to control the + /// size of each page. + /// + /// - Parameter offset: The number of documents to skip. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func offset(_ offset: Int32) -> Pipeline { + return self + } + + /// Limits the maximum number of documents returned by previous stages to `limit`. + /// + /// This stage is particularly useful when you want to retrieve a controlled + /// subset of data from a potentially large result set. It's often used for: + /// + /// - **Pagination:** In combination with `skip` to retrieve specific pages of results. + /// - **Limiting Data Retrieval:** To prevent excessive data transfer and improve + /// performance, especially when dealing with large collections. + /// + /// - Parameter limit: The maximum number of documents to return. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func limit(_ limit: Int32) -> Pipeline { + return self + } + + /// Returns a set of distinct `Expr` values from the inputs to this stage. + /// + /// This stage processes the results from previous stages, ensuring that only unique + /// combinations of `Expr` values (such as `Field` and `Function`) are included. + /// + /// The parameters to this stage are defined using `Selectable` expressions or field names: + /// + /// - `String`: The name of an existing field. + /// - `Field`: A reference to an existing document field. + /// - `Function`: Represents the result of a function with an assigned alias using + /// `Expr.alias(_:)`. + /// + /// - Parameter selections: The fields to include in the output documents, specified as + /// `Selectable` expressions or `String` values representing field names. + func distinct(_ groups: SelectableOrFieldName...) -> Pipeline { + return self + } + + /// Performs aggregation operations on the documents from previous stages. + /// + /// This stage allows you to compute aggregate values over a set of documents. + /// Aggregations are defined using `AccumulatorWithAlias`, which wraps an `Accumulator` + /// and provides a name for the accumulated results. These expressions are typically + /// created by calling `alias(_:)` on `Accumulator` instances. + /// + /// - Parameter accumulators: The `AccumulatorWithAlias` expressions, each wrapping an + /// `Accumulator` and assigning a name to the accumulated results. + func aggregate(_ aggregates: AccumulatorWithAlias...) -> Pipeline { + return self + } + + /// Performs optionally grouped aggregation operations on the documents from previous stages. + /// + /// This stage calculates aggregate values over a set of documents, optionally grouped by + /// one or more fields or computed expressions. + /// + /// - **Grouping Fields or Expressions:** Defines how documents are grouped. For each + /// unique combination of values in the specified fields or expressions, a separate group + /// is created. If no grouping fields are provided, all documents are placed into a single + /// group. + /// - **Accumulators:** Defines the accumulation operations to perform within each group. + /// These are provided as `AccumulatorWithAlias` expressions, typically created by + /// calling `alias(_:)` on `Accumulator` instances. Each aggregation computes a + /// value (e.g., sum, average, count) based on the documents in its group. + /// + /// - Parameters: + /// - accumulators: A list of `AccumulatorWithAlias` expressions defining the aggregation + /// calculations. + /// - groups: An optional list of grouping fields or expressions. + /// - Returns: A new `Pipeline` object with this stage appended. + func aggregate(option: AggregateOption) -> Pipeline { + return self + } + + /// Performs a vector similarity search, ordering the result set by most similar to least + /// similar, and returning the first N documents in the result set. + func findNearest(options: FindNearestOptions) -> Pipeline { + return self + } + + /// Sorts the documents from previous stages based on one or more `Ordering` criteria. + /// + /// This stage allows you to order the results of your pipeline. You can specify multiple + /// `Ordering` instances to sort by multiple fields in ascending or descending order. + /// If documents have the same value for a field used for sorting, the next specified ordering + /// will be used. If all orderings result in equal comparison, the documents are considered + /// equal and the order is unspecified. + /// + /// - Parameter orderings: One or more `Ordering` instances specifying the sorting criteria. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func sort(_ orderings: [Ordering]) -> Pipeline { + // Implementation + return self + } + + /// Fully overwrites all fields in a document with those coming from a nested map. + /// + /// This stage allows you to emit a map value as a document. Each key of the map becomes a + /// field on the document that contains the corresponding value. + /// + /// - Parameter field: The `Selectable` field containing the nested map. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func replace(_ field: SelectableOrFieldName) -> Pipeline { + // Implementation + return self + } + + /// Performs a pseudo-random sampling of the input documents. + /// + /// This stage will filter documents pseudo-randomly. The parameter specifies how number of + /// documents to be returned. + /// + /// - Parameter documents: The number of documents to sample. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func sample(documents: Int64) -> Pipeline { + // Implementation + return self + } + + /// Performs a pseudo-random sampling of the input documents. + /// + /// This stage will filter documents pseudo-randomly. The `options` parameter specifies how + /// sampling will be performed. See `SampleOptions` for more information. + /// + /// - Parameter options: The `SampleOptions` specifies how sampling is performed. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func sample(options: SampleOptions) -> Pipeline { + // Implementation + return self + } + + /// Performs union of all documents from two pipelines, including duplicates. + /// + /// This stage will pass through documents from previous stage, and also pass through documents + /// from previous stage of the `other` Pipeline given in parameter. The order of documents + /// emitted from this stage is undefined. + /// + /// - Parameter other: The other `Pipeline` that is part of union. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func union(_ other: Pipeline) -> Pipeline { + // Implementation + return self + } + + /// Takes an array field from the input documents and outputs a document for each element + /// with the array field mapped to the alias provided. + /// + /// For each previous stage document, this stage will emit zero or more augmented documents. + /// The input array found in the previous stage document field specified by the `fieldName` + /// parameter, will for each input array element produce an augmented document. The input array + /// element will augment the previous stage document by replacing the field specified by + /// `fieldName` parameter with the element value. + /// + /// In other words, the field containing the input array will be removed from the augmented + /// document and replaced by the corresponding array element. + /// + /// - Parameter field: The name of the field containing the array. + /// - Parameter indexField: Optional. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func unnest(field: Selectable, indexField: String? = nil) -> Pipeline { + // Implementation + return self + } + + /// Adds a stage to the pipeline by specifying the stage name as an argument. This does + /// not offer any type safety on the stage params and requires the caller to know the + /// order (and optionally names) of parameters accepted by the stage. + /// + /// This method provides a way to call stages that are supported by the Firestore backend + /// but that are not implemented in the SDK version being used. + /// + /// - Parameter name: The unique name of the stage to add. + /// - Parameter params: A list of ordered parameters to configure the stage's behavior. + /// - Parameter options: A list of optional, named parameters to configure the stage's behavior. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + func genericStage(name: String, params: [Any], options: [String: Any]? = nil) -> Pipeline { + // Implementation + return self + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift new file mode 100644 index 00000000000..08573aa196b --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -0,0 +1,82 @@ +// 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. + +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineSnapshot { + /// The Pipeline on which `execute()` was called to obtain this `PipelineSnapshot`. + public let pipeline: Pipeline + + /// An array of all the results in the `PipelineSnapshot`. + public let results: [PipelineResult] + + /// The time at which the pipeline producing this result was executed. + public let executionTime: Timestamp + + init(pipeline: Pipeline, results: [PipelineResult], executionTime: Timestamp) { + self.pipeline = pipeline + self.results = results + self.executionTime = executionTime + } +} + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineResult { + let cppObj: firebase.firestore.api.PipelineResult + + init(_ cppSource: firebase.firestore.api.PipelineResult) { + cppObj = cppSource + } + + /// The reference of the document, if the query returns the `__name__` field. + public let ref: DocumentReference? = nil + + /// The ID of the document for which this `PipelineResult` contains data, if available. + public let id: String? = nil + + /// The time the document was created, if available. + public let createTime: Timestamp? = nil + + /// The time the document was last updated when the snapshot was generated. + public let updateTime: Timestamp? = nil + + /// Retrieves all fields in the result as a dictionary. + public let data: [String: Any] = [:] + + /// Retrieves the field specified by `fieldPath`. + /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). + /// - Returns: The data at the specified field location or `nil` if no such field exists. + public func get(_ fieldPath: Any) -> Any? { + return "PLACEHOLDER" + } + + static func convertToArrayFromCppVector(_ vector: CppPipelineResult) + -> [PipelineResult] { + // Create a Swift array and populate it by iterating over the C++ vector + var swiftArray: [PipelineResult] = [] + +// for index in vector.indices { +// let cppResult = vector[index] +// swiftArray.append(PipelineResult(cppResult)) +// } + + return swiftArray + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift new file mode 100644 index 00000000000..b81ff923fb3 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift @@ -0,0 +1,15 @@ +// 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. + +public protocol PipelineType: Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift new file mode 100644 index 00000000000..d3a6614c251 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift @@ -0,0 +1,16 @@ +// 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. + +public struct RealtimePipeline: PipelineType { +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift new file mode 100644 index 00000000000..92f48244b25 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.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. + +public struct SampleOptions { + let percentage: Double? + let documents: Int? + + private init(percentage: Double?, documents: Int?) { + self.percentage = percentage + self.documents = documents + } + + static func percentage(_ value: Double) -> SampleOptions { + return SampleOptions(percentage: value, documents: nil) + } + + static func documents(_ count: Int) -> SampleOptions { + return SampleOptions(percentage: nil, documents: count) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift new file mode 100644 index 00000000000..f4f66ab797f --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift @@ -0,0 +1,41 @@ +// 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. + +public struct SelectableOrFieldName: Equatable { + enum Kind: Equatable { + case selectable(Selectable) + case field(String) + + static func == (lhs: Kind, rhs: Kind) -> Bool { + switch (lhs, rhs) { + case let (.selectable(a), .selectable(b)): + return a.alias == b.alias + case let (.field(a), .field(b)): + return a == b + default: + return false + } + } + } + + let kind: Kind + + public static func selectable(_ value: Selectable) -> SelectableOrFieldName { + return SelectableOrFieldName(kind: .selectable(value)) + } + + public static func field(_ name: String) -> SelectableOrFieldName { + return SelectableOrFieldName(kind: .field(name)) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift new file mode 100644 index 00000000000..6bbdab0b20c --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift @@ -0,0 +1,15 @@ +// 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. + +public struct Sum {} From 8519a5ecf8544262569c228f6137d253e4f83755 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Sun, 2 Mar 2025 20:54:41 -0500 Subject: [PATCH 03/43] Add stage API --- .../AsyncAwait/Firestore+AsyncAwait.swift | 4 ++ .../Source/SwiftAPI/Pipeline/Pipeline.swift | 28 ++++------ .../SwiftAPI/Pipeline/PipelineResult.swift | 55 +++++-------------- .../SwiftAPI/Pipeline/PipelineSnapshot.swift | 31 +++++++++++ .../SwiftAPI/Pipeline/PipelineSource.swift | 50 +++++++++++++++++ .../SwiftAPI/Pipeline/RealtimePipeline.swift | 3 +- .../{PipelineType.swift => Selectable.swift} | 4 +- .../Tests/Integration/PipelineApiTests.swift | 39 +++++++++++++ 8 files changed, 153 insertions(+), 61 deletions(-) create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift rename Firestore/Swift/Source/SwiftAPI/Pipeline/{PipelineType.swift => Selectable.swift} (92%) create mode 100644 Firestore/Swift/Tests/Integration/PipelineApiTests.swift diff --git a/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift b/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift index e85ca9a9791..f58e9e1d706 100644 --- a/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift +++ b/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift @@ -115,4 +115,8 @@ public extension Firestore { } } } + + func pipeline() -> PipelineSource { + return PipelineSource(self) + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 677421777aa..3be5aae64fd 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -20,27 +20,19 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct Pipeline: PipelineType { - var cppObj: firebase.firestore.api.Pipeline? = nil +public struct Pipeline { + let db: Firestore - init(_ cppSource: firebase.firestore.api.Pipeline) { - cppObj = cppSource + init(_ db: Firestore) { + self.db = db } - init() {} - - public func execute() async throws -> PipelineResult { - return try await withCheckedThrowingContinuation { continuation in - let listener = CallbackWrapper.wrapPipelineCallback(firestore: cppObj!.GetFirestore()) { - result, error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: PipelineResult(result!.pointee)) - } - } - cppObj!.Execute(listener) - } + public func execute() async throws -> PipelineSnapshot { + return PipelineSnapshot( + pipeline: self, + results: [], + executionTime: Timestamp(seconds: 0, nanoseconds: 0) + ) } /// Adds new fields to outputs from previous stages. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift index 08573aa196b..a4556815f71 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -19,46 +19,34 @@ #endif // SWIFT_PACKAGE import Foundation -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct PipelineSnapshot { - /// The Pipeline on which `execute()` was called to obtain this `PipelineSnapshot`. - public let pipeline: Pipeline - - /// An array of all the results in the `PipelineSnapshot`. - public let results: [PipelineResult] - - /// The time at which the pipeline producing this result was executed. - public let executionTime: Timestamp - - init(pipeline: Pipeline, results: [PipelineResult], executionTime: Timestamp) { - self.pipeline = pipeline - self.results = results - self.executionTime = executionTime - } -} - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct PipelineResult { - let cppObj: firebase.firestore.api.PipelineResult - - init(_ cppSource: firebase.firestore.api.PipelineResult) { - cppObj = cppSource + init(ref: DocumentReference? = nil, + id: String? = nil, + createTime: Timestamp? = nil, + updateTime: Timestamp? = nil, + data: [String: Any] = [:]) { + self.ref = ref + self.id = id + self.createTime = createTime + self.updateTime = updateTime + self.data = data } /// The reference of the document, if the query returns the `__name__` field. - public let ref: DocumentReference? = nil + public let ref: DocumentReference? /// The ID of the document for which this `PipelineResult` contains data, if available. - public let id: String? = nil + public let id: String? /// The time the document was created, if available. - public let createTime: Timestamp? = nil + public let createTime: Timestamp? /// The time the document was last updated when the snapshot was generated. - public let updateTime: Timestamp? = nil + public let updateTime: Timestamp? /// Retrieves all fields in the result as a dictionary. - public let data: [String: Any] = [:] + public let data: [String: Any] /// Retrieves the field specified by `fieldPath`. /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). @@ -66,17 +54,4 @@ public struct PipelineResult { public func get(_ fieldPath: Any) -> Any? { return "PLACEHOLDER" } - - static func convertToArrayFromCppVector(_ vector: CppPipelineResult) - -> [PipelineResult] { - // Create a Swift array and populate it by iterating over the C++ vector - var swiftArray: [PipelineResult] = [] - -// for index in vector.indices { -// let cppResult = vector[index] -// swiftArray.append(PipelineResult(cppResult)) -// } - - return swiftArray - } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift new file mode 100644 index 00000000000..69f695d3568 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.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. + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineSnapshot { + /// The Pipeline on which `execute()` was called to obtain this `PipelineSnapshot`. + public let pipeline: Pipeline + + /// An array of all the results in the `PipelineSnapshot`. + public let results: [PipelineResult] + + /// The time at which the pipeline producing this result was executed. + public let executionTime: Timestamp + + init(pipeline: Pipeline, results: [PipelineResult], executionTime: Timestamp) { + self.pipeline = pipeline + self.results = results + self.executionTime = executionTime + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift new file mode 100644 index 00000000000..75b56fa4424 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -0,0 +1,50 @@ +// 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. + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +public struct PipelineSource { + let db: Firestore + + init(_ db: Firestore) { + self.db = db + } + + public func collection(_ path: String) -> Pipeline { + return Pipeline(db) + } + + public func collectionGroup(_ collectionId: String) -> Pipeline { + return Pipeline(db) + } + + public func database() -> Pipeline { + return Pipeline(db) + } + + public func documents(_ docs: [DocumentReference]) -> Pipeline { + return Pipeline(db) + } + + public func documents(_ paths: [String]) -> Pipeline { + return Pipeline(db) + } + + public func createFrom(_ query: Query) -> Pipeline { + return Pipeline(db) + } + + public func createFrom(_ aggregateQuery: AggregateQuery) -> Pipeline { + return Pipeline(db) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift index d3a6614c251..672f8586caf 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift @@ -12,5 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct RealtimePipeline: PipelineType { -} +public struct RealtimePipeline {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift similarity index 92% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift index b81ff923fb3..3063c5c3d8c 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineType.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift @@ -12,4 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -public protocol PipelineType: Sendable {} +public struct Selectable { + let alias: String +} diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift new file mode 100644 index 00000000000..542f9a82451 --- /dev/null +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -0,0 +1,39 @@ +// 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 +import XCTest + +import FirebaseFirestore + +final class PipelineTests: FSTIntegrationTestCase { + func testCreatePipeline() async throws { + let pipelineSource: PipelineSource = db.pipeline() + + let pipeline: Pipeline = pipelineSource.documents( + [db.collection("foo").document("bar"), db.document("foo/baz")] + ) + let _: Pipeline = pipelineSource.collection("foo") + let _: Pipeline = pipelineSource.collectionGroup("foo") + let _: Pipeline = pipelineSource.database() + + let query: Query = db.collection("foo").limit(to: 2) + let _: Pipeline = pipelineSource.createFrom(query) + + let aggregateQuery = db.collection("foo").count + let _: Pipeline = pipelineSource.createFrom(aggregateQuery) + + let _: PipelineResult = try await pipeline.execute() + } +} From 454893b4457d15044c7a6f97f272b9cf00dff409 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Fri, 7 Mar 2025 11:22:46 -0500 Subject: [PATCH 04/43] Add stage API --- .../Firestore.xcodeproj/project.pbxproj | 8 + .../Eq.swift => Helper/PipelineHelper.swift} | 18 +- .../SwiftAPI/Pipeline/AccumulatorExpr.swift | 15 - .../{Field.swift => AggregateFunction.swift} | 11 +- .../SwiftAPI/Pipeline/AggregateOption.swift | 4 +- .../{Add.swift => AggregateWithAlias.swift} | 2 +- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 781 +++++++++++++++++- .../Pipeline/{ => Expr}/Constant.swift | 7 +- .../FunctionExpr.swift} | 10 +- .../{ => Expr/FunctionExpr}/BooleanExpr.swift | 6 +- .../SwiftAPI/Pipeline/ExprWithAlias.swift | 11 +- .../SwiftAPI/Pipeline/FunctionExpr.swift | 15 - .../Source/SwiftAPI/Pipeline/LogicalMin.swift | 15 - .../Source/SwiftAPI/Pipeline/Ordering.swift | 1 + .../Source/SwiftAPI/Pipeline/Pipeline.swift | 32 +- .../Source/SwiftAPI/Pipeline/Selectable.swift | 5 +- .../Source/SwiftAPI/Pipeline/TimeUnit.swift | 45 + .../Tests/Integration/PipelineApiTests.swift | 45 +- 18 files changed, 944 insertions(+), 87 deletions(-) rename Firestore/Swift/Source/{SwiftAPI/Pipeline/Eq.swift => Helper/PipelineHelper.swift} (59%) delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift rename Firestore/Swift/Source/SwiftAPI/Pipeline/{Field.swift => AggregateFunction.swift} (74%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{Add.swift => AggregateWithAlias.swift} (94%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{ => Expr}/Constant.swift (82%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{AccumulatorWithAlias.swift => Expr/FunctionExpr.swift} (72%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{ => Expr/FunctionExpr}/BooleanExpr.swift (78%) delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 8deefcabab8..92dbd969902 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 0535C1B65DADAE1CE47FA3CA /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; }; 053C11420E49AE1A77E21C20 /* memory_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */; }; 056542AD1D0F78E29E22EFA9 /* grpc_connection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */; }; + 0572A3237093932AA84B96F4 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0764E9C6775D82C8E1B2D5 /* PipelineApiTests.swift */; }; 0575F3004B896D94456A74CE /* status_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA33F964042646FDDAF9F9 /* status_testing.cc */; }; 0595B5EBEB8F09952B72C883 /* logic_utils_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */; }; 05D99904EA713414928DD920 /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; @@ -502,6 +503,7 @@ 4DF18D15AC926FB7A4888313 /* lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */; }; 4E0777435A9A26B8B2C08A1E /* remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7EB299CF85034F09CFD6F3FD /* remote_document_cache_test.cc */; }; 4E2E0314F9FDD7BCED60254A /* counting_query_engine.cc in Sources */ = {isa = PBXBuildFile; fileRef = 99434327614FEFF7F7DC88EC /* counting_query_engine.cc */; }; + 4E710512DE64FA56F85A3753 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0764E9C6775D82C8E1B2D5 /* PipelineApiTests.swift */; }; 4E7981690432CDFA2058E3EC /* FSTTestingHooks.mm in Sources */ = {isa = PBXBuildFile; fileRef = D85AC18C55650ED230A71B82 /* FSTTestingHooks.mm */; }; 4EC642DFC4AE98DBFFB37B17 /* fields_array_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA4CBA48204C9E25B56993BC /* fields_array_test.cc */; }; 4EE1ABA574FBFDC95165624C /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; }; @@ -997,6 +999,7 @@ 8C602DAD4E8296AB5EFB962A /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; 8C82D4D3F9AB63E79CC52DC8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */; }; 8D0EF43F1B7B156550E65C20 /* FSTGoogleTestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */; }; + 8D412B38191A5F9FBC5DB688 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0764E9C6775D82C8E1B2D5 /* PipelineApiTests.swift */; }; 8D67BAAD6D2F1913BACA6AC1 /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; 8DBA8DC55722ED9D3A1BB2C9 /* Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 1A7D48A017ECB54FD381D126 /* Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json */; }; 8E103A426D6E650DC338F281 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8FB22BCB9F454DA44BA80C8 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json */; }; @@ -2083,6 +2086,7 @@ CCC9BD953F121B9E29F9AA42 /* user_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = user_test.cc; path = credentials/user_test.cc; sourceTree = ""; }; CD422AF3E4515FB8E9BE67A0 /* equals_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = equals_tester.h; sourceTree = ""; }; CE37875365497FFA8687B745 /* message_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = message_test.cc; path = nanopb/message_test.cc; sourceTree = ""; }; + CF0764E9C6775D82C8E1B2D5 /* PipelineApiTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PipelineApiTests.swift; sourceTree = ""; }; CF39535F2C41AB0006FA6C0E /* create_noop_connectivity_monitor.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = create_noop_connectivity_monitor.cc; sourceTree = ""; }; CF39ECA1293D21A0A2AB2626 /* FIRTransactionOptionsTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRTransactionOptionsTests.mm; sourceTree = ""; }; D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = delayed_constructor_test.cc; sourceTree = ""; }; @@ -2272,6 +2276,7 @@ 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */, 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */, 62E54B832A9E910A003347C8 /* IndexingTests.swift */, + CF0764E9C6775D82C8E1B2D5 /* PipelineApiTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */, @@ -4649,6 +4654,7 @@ 432056C4D1259F76C80FC2A8 /* FSTUserDataReaderTests.mm in Sources */, 3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */, 62E54B862A9E910B003347C8 /* IndexingTests.swift in Sources */, + 8D412B38191A5F9FBC5DB688 /* PipelineApiTests.swift in Sources */, 621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAC2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -4896,6 +4902,7 @@ 75A176239B37354588769206 /* FSTUserDataReaderTests.mm in Sources */, 5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */, 62E54B852A9E910B003347C8 /* IndexingTests.swift in Sources */, + 4E710512DE64FA56F85A3753 /* PipelineApiTests.swift in Sources */, 621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAB2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, @@ -5395,6 +5402,7 @@ F5BDECEB3B43BD1591EEADBD /* FSTUserDataReaderTests.mm in Sources */, 6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */, 62E54B842A9E910B003347C8 /* IndexingTests.swift in Sources */, + 0572A3237093932AA84B96F4 /* PipelineApiTests.swift in Sources */, 621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */, EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */, diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift similarity index 59% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift rename to Firestore/Swift/Source/Helper/PipelineHelper.swift index 12fa745aea1..8feada23082 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Eq.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -12,4 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct Eq {} +enum Helper { + static func exprConvertFrom(_ value: Any) -> any Expr { + return Field("PLACEHOLDER") + } + + static func valueToDefaultExpr(_ value: Any) -> any Expr { + return Field("PLACEHOLDER") + } + + static func vectorToExpr(_ value: VectorValue) -> any Expr { + return Field("PLACEHOLDER") + } + + static func timeUnitToExpr(_ value: TimeUnit) -> any Expr { + return Field("PLACEHOLDER") + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift deleted file mode 100644 index 7d29433fe61..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorExpr.swift +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -public protocol AccumulatorExpr: Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateFunction.swift similarity index 74% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateFunction.swift index 22236b67005..386650738dd 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateFunction.swift @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct Field: Expr, Sendable { - public var alias: String - public let fieldName: String +public struct AggregateFunction { + let functionName: String + let agrs: [Expr] + + public init(_ functionName: String, _ agrs: [Expr]) { + self.functionName = functionName + self.agrs = agrs + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift index d6743852692..847f5d79db7 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift @@ -13,10 +13,10 @@ // limitations under the License. public struct AggregateOption { - public let accumulators: [AccumulatorWithAlias] + public let accumulators: [AggregateWithAlias] public let groups: [SelectableOrFieldName]? - public init(accumulators: [AccumulatorWithAlias], groups: [SelectableOrFieldName]? = nil) { + public init(accumulators: [AggregateWithAlias], groups: [SelectableOrFieldName]? = nil) { self.accumulators = accumulators self.groups = groups } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateWithAlias.swift similarity index 94% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateWithAlias.swift index 69a39ee901c..f333b748971 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Add.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateWithAlias.swift @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct Add {} +public struct AggregateWithAlias {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index b4718f6fdce..05f0029f348 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -12,24 +12,785 @@ // See the License for the specific language governing permissions and // limitations under the License. -public protocol Expr: Sendable, Equatable { - func alias(_ alias: String) -> ExprWithAlias +public protocol Expr: Sendable { + func alias(_ name: String) -> ExprWithAlias - func eq(_ expr: BooleanExpr) + // MARK: Comparison Operators - func add(_ expr: some FunctionExpr) + func eq(_ other: Expr) -> BooleanExpr + func eq(_ other: Any) -> BooleanExpr - func add(_ expr: Constant) + func neq(_ other: Expr) -> BooleanExpr + func neq(_ other: Any) -> BooleanExpr + + func lt(_ other: Expr) -> BooleanExpr + func lt(_ other: Any) -> BooleanExpr + + func lte(_ other: Expr) -> BooleanExpr + func lte(_ other: Any) -> BooleanExpr + + func gt(_ other: Expr) -> BooleanExpr + func gt(_ other: Any) -> BooleanExpr + + func gte(_ other: Expr) -> BooleanExpr + func gte(_ other: Any) -> BooleanExpr + + // MARK: Arithmetic Operators + + func add(_ second: Expr, _ others: Expr...) -> FunctionExpr + func add(_ second: Any, _ others: Any...) -> FunctionExpr + + func subtract(_ other: Expr) -> FunctionExpr + func subtract(_ other: Any) -> FunctionExpr + + func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr + func multiply(_ second: Any, _ others: Any...) -> FunctionExpr + + func divide(_ other: Expr) -> FunctionExpr + func divide(_ other: Any) -> FunctionExpr + + func mod(_ other: Expr) -> FunctionExpr + func mod(_ other: Any) -> FunctionExpr + + // MARK: Array Operations + + func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr + func arrayConcat(_ secondArray: [Any], _ otherArrays: [Any]...) -> FunctionExpr + + func arrayContains(_ element: Expr) -> BooleanExpr + func arrayContains(_ element: Any) -> BooleanExpr + + func arrayContainsAll(_ values: Expr...) -> BooleanExpr + func arrayContainsAll(_ values: Any...) -> BooleanExpr + + func arrayContainsAny(_ values: Expr...) -> BooleanExpr + func arrayContainsAny(_ values: Any...) -> BooleanExpr + + func arrayLength() -> FunctionExpr + + func arrayOffset(_ offset: Int) -> FunctionExpr + func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr + + // MARK: Equality with Any + + func eqAny(_ others: Expr...) -> BooleanExpr + func eqAny(_ others: Any...) -> BooleanExpr + + func notEqAny(_ others: Expr...) -> BooleanExpr + func notEqAny(_ others: Any...) -> BooleanExpr + + // MARK: Checks + + func isNan() -> BooleanExpr + func isNull() -> BooleanExpr + func exists() -> BooleanExpr + func isError() -> BooleanExpr + func isAbsent() -> BooleanExpr + func isNotNull() -> BooleanExpr + func isNotNan() -> BooleanExpr + + // MARK: String Operations + + func charLength() -> FunctionExpr + func like(_ pattern: String) -> FunctionExpr + func like(_ pattern: Expr) -> FunctionExpr + + func regexContains(_ pattern: String) -> BooleanExpr + func regexContains(_ pattern: Expr) -> BooleanExpr + + func regexMatch(_ pattern: String) -> BooleanExpr + func regexMatch(_ pattern: Expr) -> BooleanExpr + + func strContains(_ substring: String) -> BooleanExpr + func strContains(_ expr: Expr) -> BooleanExpr + + func startsWith(_ prefix: String) -> BooleanExpr + func startsWith(_ prefix: Expr) -> BooleanExpr + + func endsWith(_ suffix: String) -> BooleanExpr + func endsWith(_ suffix: Expr) -> BooleanExpr + + func toLower() -> FunctionExpr + func toUpper() -> FunctionExpr + func trim() -> FunctionExpr + + func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr + func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr + + func reverse() -> FunctionExpr + func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr + func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr + func replaceAll(_ find: String, _ replace: String) -> FunctionExpr + func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr + + func byteLength() -> FunctionExpr + + func substr(_ position: Int, _ length: Int?) -> FunctionExpr + func substr(_ position: Expr, _ length: Expr?) -> FunctionExpr + + // MARK: Map Operations + + func mapGet(_ subfield: String) -> FunctionExpr + func mapRemove(_ key: String) -> FunctionExpr + func mapRemove(_ keyExpr: Expr) -> FunctionExpr + func mapMerge(_ secondMap: [String: Any], _ otherMaps: [String: Any]...) -> FunctionExpr + func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr + + // MARK: Aggregations + + func count() -> AggregateFunction + func sum() -> AggregateFunction + func avg() -> AggregateFunction + func minimum() -> AggregateFunction + func maximum() -> AggregateFunction + + // MARK: Logical min/max + + func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr + func logicalMaximum(_ second: Any, _ others: Any...) -> FunctionExpr + + func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr + func logicalMinimum(_ second: Any, _ others: Any...) -> FunctionExpr + + // MARK: Vector Operations + + func vectorLength() -> FunctionExpr + func cosineDistance(_ other: Expr) -> FunctionExpr + func cosineDistance(_ other: VectorValue) -> FunctionExpr + func cosineDistance(_ other: [Double]) -> FunctionExpr + + func dotProduct(_ other: Expr) -> FunctionExpr + func dotProduct(_ other: VectorValue) -> FunctionExpr + func dotProduct(_ other: [Double]) -> FunctionExpr + + func euclideanDistance(_ other: Expr) -> FunctionExpr + func euclideanDistance(_ other: VectorValue) -> FunctionExpr + func euclideanDistance(_ other: [Double]) -> FunctionExpr + + func manhattanDistance(_ other: Expr) -> FunctionExpr + func manhattanDistance(_ other: VectorValue) -> FunctionExpr + func manhattanDistance(_ other: [Double]) -> FunctionExpr + + // MARK: Timestamp operations + + func unixMicrosToTimestamp() -> FunctionExpr + func timestampToUnixMicros() -> FunctionExpr + func unixMillisToTimestamp() -> FunctionExpr + func timestampToUnixMillis() -> FunctionExpr + func unixSecondsToTimestamp() -> FunctionExpr + func timestampToUnixSeconds() -> FunctionExpr + + func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr + func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr + func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr + func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr + + // MARK: - Bitwise operations + + func bitAnd(_ otherBits: Int) -> FunctionExpr + func bitAnd(_ otherBits: UInt8) -> FunctionExpr + func bitAnd(_ bitsExpression: Expr) -> FunctionExpr + + func bitOr(_ otherBits: Int) -> FunctionExpr + func bitOr(_ otherBits: UInt8) -> FunctionExpr + func bitOr(_ bitsExpression: Expr) -> FunctionExpr + + func bitXor(_ otherBits: Int) -> FunctionExpr + func bitXor(_ otherBits: UInt8) -> FunctionExpr + func bitXor(_ bitsExpression: Expr) -> FunctionExpr + + func bitNot() -> FunctionExpr + + func bitLeftShift(_ y: Int) -> FunctionExpr + func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr + + func bitRightShift(_ y: Int) -> FunctionExpr + func bitRightShift(_ numberExpr: Expr) -> FunctionExpr + + // MARK: - String operations. + + func documentId() -> FunctionExpr + + func ifError(_ catchExpr: Expr) -> FunctionExpr + func ifError(_ catchValue: Any) -> FunctionExpr + + // MARK: Sorting + + func ascending() -> Ordering + func descending() -> Ordering } public extension Expr { - func alias(_ alias: String) -> ExprWithAlias { - return ExprWithAlias(self, alias) + func alias(_ name: String) -> ExprWithAlias { + return ExprWithAlias(self, name) + } + + // MARK: Comparison Operators + + func eq(_ other: Expr) -> BooleanExpr { + return BooleanExpr("eq", [self, other]) + } + + func eq(_ other: Any) -> BooleanExpr { + return BooleanExpr("eq", [self, Helper.valueToDefaultExpr(other)]) + } + + func neq(_ other: Expr) -> BooleanExpr { + return BooleanExpr("neq", [self, other]) + } + + func neq(_ other: Any) -> BooleanExpr { + return BooleanExpr("neq", [self, Helper.valueToDefaultExpr(other)]) + } + + func lt(_ other: Expr) -> BooleanExpr { + return BooleanExpr("lt", [self, other]) + } + + func lt(_ other: Any) -> BooleanExpr { + return BooleanExpr("lt", [self, Helper.valueToDefaultExpr(other)]) + } + + func lte(_ other: Expr) -> BooleanExpr { + return BooleanExpr("lte", [self, other]) + } + + func lte(_ other: Any) -> BooleanExpr { + return BooleanExpr("lte", [self, Helper.valueToDefaultExpr(other)]) + } + + func gt(_ other: Expr) -> BooleanExpr { + return BooleanExpr("gt", [self, other]) + } + + func gt(_ other: Any) -> BooleanExpr { + return BooleanExpr("gt", [self, Helper.valueToDefaultExpr(other)]) + } + + func gte(_ other: Expr) -> BooleanExpr { + return BooleanExpr("gte", [self, other]) } - func eq(_ expr: some Expr) {} + func gte(_ other: Any) -> BooleanExpr { + return BooleanExpr("gte", [self, Helper.valueToDefaultExpr(other)]) + } + + // MARK: Arithmetic Operators + + func add(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("add", [self, second] + others) + } + + func add(_ second: Any, _ others: Any...) -> FunctionExpr { + let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others + .map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("add", exprs) + } + + func subtract(_ other: Expr) -> FunctionExpr { + return FunctionExpr("subtract", [self, other]) + } + + func subtract(_ other: Any) -> FunctionExpr { + return FunctionExpr("subtract", [self, Helper.valueToDefaultExpr(other)]) + } + + func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("multiply", [self, second] + others) + } + + func multiply(_ second: Any, _ others: Any...) -> FunctionExpr { + let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others + .map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("multiply", exprs) + } + + func divide(_ other: Expr) -> FunctionExpr { + return FunctionExpr("divide", [self, other]) + } + + func divide(_ other: Any) -> FunctionExpr { + return FunctionExpr("divide", [self, Helper.valueToDefaultExpr(other)]) + } - func add(_ expr: some Expr) {} + func mod(_ other: Expr) -> FunctionExpr { + return FunctionExpr("mod", [self, other]) + } + + func mod(_ other: Any) -> FunctionExpr { + return FunctionExpr("mod", [self, Helper.valueToDefaultExpr(other)]) + } + + // MARK: Array Operations + + func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr { + return FunctionExpr("array_concat", [self, secondArray] + otherArrays) + } + + func arrayConcat(_ secondArray: [Any], _ otherArrays: [Any]...) -> FunctionExpr { + let exprs = [self] + [Helper.valueToDefaultExpr(secondArray)] + otherArrays + .map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("array_concat", exprs) + } + + func arrayContains(_ element: Expr) -> BooleanExpr { + return BooleanExpr("array_contains", [self, element]) + } + + func arrayContains(_ element: Any) -> BooleanExpr { + return BooleanExpr("array_contains", [self, Helper.valueToDefaultExpr(element)]) + } + + func arrayContainsAll(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_all", [self] + values) + } + + func arrayContainsAll(_ values: Any...) -> BooleanExpr { + let exprValues = values.map { Helper.valueToDefaultExpr($0) } + return BooleanExpr("array_contains_all", [self] + exprValues) + } + + func arrayContainsAny(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_any", [self] + values) + } + + func arrayContainsAny(_ values: Any...) -> BooleanExpr { + let exprValues = values.map { Helper.valueToDefaultExpr($0) } + return BooleanExpr("array_contains_any", [self] + exprValues) + } - func add(_ expr: Constant) {} + func arrayLength() -> FunctionExpr { + return FunctionExpr("array_length", [self]) + } + + func arrayOffset(_ offset: Int) -> FunctionExpr { + return FunctionExpr("array_offset", [self, Helper.valueToDefaultExpr(offset)]) + } + + func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr { + return FunctionExpr("array_offset", [self, offsetExpr]) + } + + // MARK: Equality with Any + + func eqAny(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("eq_any", [self] + others) + } + + func eqAny(_ others: Any...) -> BooleanExpr { + let exprOthers = others.map { Helper.valueToDefaultExpr($0) } + return BooleanExpr("eq_any", [self] + exprOthers) + } + + func notEqAny(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("not_eq_any", [self] + others) + } + + func notEqAny(_ others: Any...) -> BooleanExpr { + let exprOthers = others.map { Helper.valueToDefaultExpr($0) } + return BooleanExpr("not_eq_any", [self] + exprOthers) + } + + // MARK: Checks + + func isNan() -> BooleanExpr { + return BooleanExpr("is_nan", [self]) + } + + func isNull() -> BooleanExpr { + return BooleanExpr("is_null", [self]) + } + + func exists() -> BooleanExpr { + return BooleanExpr("exists", [self]) + } + + func isError() -> BooleanExpr { + return BooleanExpr("is_error", [self]) + } + + func isAbsent() -> BooleanExpr { + return BooleanExpr("is_absent", [self]) + } + + func isNotNull() -> BooleanExpr { + return BooleanExpr("is_not_null", [self]) + } + + func isNotNan() -> BooleanExpr { + return BooleanExpr("is_not_nan", [self]) + } + + // MARK: String Operations + + func charLength() -> FunctionExpr { + return FunctionExpr("char_length", [self]) + } + + func like(_ pattern: String) -> FunctionExpr { + return FunctionExpr("like", [self, Helper.valueToDefaultExpr(pattern)]) + } + + func like(_ pattern: Expr) -> FunctionExpr { + return FunctionExpr("like", [self, pattern]) + } + + func regexContains(_ pattern: String) -> BooleanExpr { + return BooleanExpr("regex_contains", [self, Helper.valueToDefaultExpr(pattern)]) + } + + func regexContains(_ pattern: Expr) -> BooleanExpr { + return BooleanExpr("regex_contains", [self, pattern]) + } + + func regexMatch(_ pattern: String) -> BooleanExpr { + return BooleanExpr("regex_match", [self, Helper.valueToDefaultExpr(pattern)]) + } + + func regexMatch(_ pattern: Expr) -> BooleanExpr { + return BooleanExpr("regex_match", [self, pattern]) + } + + func strContains(_ substring: String) -> BooleanExpr { + return BooleanExpr("str_contains", [self, Helper.valueToDefaultExpr(substring)]) + } + + func strContains(_ expr: Expr) -> BooleanExpr { + return BooleanExpr("str_contains", [self, expr]) + } + + func startsWith(_ prefix: String) -> BooleanExpr { + return BooleanExpr("starts_with", [self, Helper.valueToDefaultExpr(prefix)]) + } + + func startsWith(_ prefix: Expr) -> BooleanExpr { + return BooleanExpr("starts_with", [self, prefix]) + } + + func endsWith(_ suffix: String) -> BooleanExpr { + return BooleanExpr("ends_with", [self, Helper.valueToDefaultExpr(suffix)]) + } + + func endsWith(_ suffix: Expr) -> BooleanExpr { + return BooleanExpr("ends_with", [self, suffix]) + } + + func toLower() -> FunctionExpr { + return FunctionExpr("to_lower", [self]) + } + + func toUpper() -> FunctionExpr { + return FunctionExpr("to_upper", [self]) + } + + func trim() -> FunctionExpr { + return FunctionExpr("trim", [self]) + } + + func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr { + return FunctionExpr("str_concat", [self, secondString] + otherStrings) + } + + func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr { + let exprs = [self] + [Helper.valueToDefaultExpr(secondString)] + otherStrings + .map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("str_concat", exprs) + } + + func reverse() -> FunctionExpr { + return FunctionExpr("reverse", [self]) + } + + func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr { + return FunctionExpr( + "replace_first", + [self, Helper.valueToDefaultExpr(find), Helper.valueToDefaultExpr(replace)] + ) + } + + func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr { + return FunctionExpr("replace_first", [self, find, replace]) + } + + func replaceAll(_ find: String, _ replace: String) -> FunctionExpr { + return FunctionExpr( + "replace_all", + [self, Helper.valueToDefaultExpr(find), Helper.valueToDefaultExpr(replace)] + ) + } + + func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr { + return FunctionExpr("replace_all", [self, find, replace]) + } + + func byteLength() -> FunctionExpr { + return FunctionExpr("byte_length", [self]) + } + + func substr(_ position: Int, _ length: Int? = nil) -> FunctionExpr { + let positionExpr = Helper.valueToDefaultExpr(position) + if let length = length { + return FunctionExpr("substr", [self, positionExpr, Helper.valueToDefaultExpr(length)]) + } else { + return FunctionExpr("substr", [self, positionExpr]) + } + } + + func substr(_ position: Expr, _ length: Expr? = nil) -> FunctionExpr { + if let length = length { + return FunctionExpr("substr", [self, position, length]) + } else { + return FunctionExpr("substr", [self, position]) + } + } + + // MARK: Map Operations + + func mapGet(_ subfield: String) -> FunctionExpr { + return FunctionExpr("map_get", [self, Constant(subfield)]) + } + + func mapRemove(_ key: String) -> FunctionExpr { + return FunctionExpr("map_remove", [self, Helper.valueToDefaultExpr(key)]) + } + + func mapRemove(_ keyExpr: Expr) -> FunctionExpr { + return FunctionExpr("map_remove", [self, keyExpr]) + } + + func mapMerge(_ secondMap: [String: Any], _ otherMaps: [String: Any]...) -> FunctionExpr { + let secondMapExpr = Helper.valueToDefaultExpr(secondMap) + let otherMapExprs = otherMaps.map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("map_merge", [self, secondMapExpr] + otherMapExprs) + } + + func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr { + return FunctionExpr("map_merge", [self, secondMap] + otherMaps) + } + + // MARK: Aggregations + + func count() -> AggregateFunction { + return AggregateFunction("count", [self]) + } + + func sum() -> AggregateFunction { + return AggregateFunction("sum", [self]) + } + + func avg() -> AggregateFunction { + return AggregateFunction("avg", [self]) + } + + func minimum() -> AggregateFunction { + return AggregateFunction("minimum", [self]) + } + + func maximum() -> AggregateFunction { + return AggregateFunction("maximum", [self]) + } + + // MARK: Logical min/max + + func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("logical_maximum", [self, second] + others) + } + + func logicalMaximum(_ second: Any, _ others: Any...) -> FunctionExpr { + let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others + .map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("logical_maximum", exprs) + } + + func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("logical_min", [self, second] + others) + } + + func logicalMinimum(_ second: Any, _ others: Any...) -> FunctionExpr { + let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others + .map { Helper.valueToDefaultExpr($0) } + return FunctionExpr("logical_min", exprs) + } + + // MARK: Vector Operations + + func vectorLength() -> FunctionExpr { + return FunctionExpr("vector_length", [self]) + } + + func cosineDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, other]) + } + + func cosineDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, Helper.vectorToExpr(other)]) + } + + func cosineDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, Helper.valueToDefaultExpr(other)]) + } + + func dotProduct(_ other: Expr) -> FunctionExpr { + return FunctionExpr("dot_product", [self, other]) + } + + func dotProduct(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("dot_product", [self, Helper.vectorToExpr(other)]) + } + + func dotProduct(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("dot_product", [self, Helper.valueToDefaultExpr(other)]) + } + + func euclideanDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, other]) + } + + func euclideanDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, Helper.vectorToExpr(other)]) + } + + func euclideanDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, Helper.valueToDefaultExpr(other)]) + } + + func manhattanDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, other]) + } + + func manhattanDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, Helper.vectorToExpr(other)]) + } + + func manhattanDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, Helper.valueToDefaultExpr(other)]) + } + + // MARK: Timestamp operations + + func unixMicrosToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_micros_to_timestamp", [self]) + } + + func timestampToUnixMicros() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_micros", [self]) + } + + func unixMillisToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_millis_to_timestamp", [self]) + } + + func timestampToUnixMillis() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_millis", [self]) + } + + func unixSecondsToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_seconds_to_timestamp", [self]) + } + + func timestampToUnixSeconds() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_seconds", [self]) + } + + func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr { + return FunctionExpr("timestamp_add", [self, unit, amount]) + } + + func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { + return FunctionExpr( + "timestamp_add", + [self, Helper.timeUnitToExpr(unit), Helper.valueToDefaultExpr(amount)] + ) + } + + func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr { + return FunctionExpr("timestamp_sub", [self, unit, amount]) + } + + func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { + return FunctionExpr( + "timestamp_sub", + [self, Helper.timeUnitToExpr(unit), Helper.valueToDefaultExpr(amount)] + ) + } + + // MARK: - Bitwise operations + + func bitAnd(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_and", [self, Helper.valueToDefaultExpr(otherBits)]) + } + + func bitAnd(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_and", [self, Helper.valueToDefaultExpr(otherBits)]) + } + + func bitAnd(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_and", [self, bitsExpression]) + } + + func bitOr(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_or", [self, Helper.valueToDefaultExpr(otherBits)]) + } + + func bitOr(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_or", [self, Helper.valueToDefaultExpr(otherBits)]) + } + + func bitOr(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_or", [self, bitsExpression]) + } + + func bitXor(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, Helper.valueToDefaultExpr(otherBits)]) + } + + func bitXor(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, Helper.valueToDefaultExpr(otherBits)]) + } + + func bitXor(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, bitsExpression]) + } + + func bitNot() -> FunctionExpr { + return FunctionExpr("bit_not", [self]) + } + + func bitLeftShift(_ y: Int) -> FunctionExpr { + return FunctionExpr("bit_left_shift", [self, Helper.valueToDefaultExpr(y)]) + } + + func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr { + return FunctionExpr("bit_left_shift", [self, numberExpr]) + } + + func bitRightShift(_ y: Int) -> FunctionExpr { + return FunctionExpr("bit_right_shift", [self, Helper.valueToDefaultExpr(y)]) + } + + func bitRightShift(_ numberExpr: Expr) -> FunctionExpr { + return FunctionExpr("bit_right_shift", [self, numberExpr]) + } + + func documentId() -> FunctionExpr { + return FunctionExpr("document_id", [self]) + } + + func ifError(_ catchExpr: Expr) -> FunctionExpr { + return FunctionExpr("if_error", [self, catchExpr]) + } + + func ifError(_ catchValue: Any) -> FunctionExpr { + return FunctionExpr("if_error", [self, Helper.valueToDefaultExpr(catchValue)]) + } + + // MARK: Sorting + + func ascending() -> Ordering { + return Ordering(expr: self, direction: .ascending) + } + + func descending() -> Ordering { + return Ordering(expr: self, direction: .descending) + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift similarity index 82% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index ea567bae58c..99f50cd3b39 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -12,4 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct Constant: Expr, Sendable {} +public struct Constant: Expr, @unchecked Sendable { + let value: Any + public init(_ value: Any) { + self.value = value + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift similarity index 72% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift index c227de4d1c4..c78a482cd18 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AccumulatorWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -12,6 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct AccumulatorWithAlias { - init(_ expr: some AccumulatorExpr, _ alias: String) {} +public class FunctionExpr: Expr, @unchecked Sendable { + let functionName: String + let agrs: [Expr] + + public init(_ functionName: String, _ agrs: [Expr]) { + self.functionName = functionName + self.agrs = agrs + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift similarity index 78% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift index 587c0093c7d..20a37d53392 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/BooleanExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift @@ -12,4 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct BooleanExpr: FunctionExpr, Sendable {} +public class BooleanExpr: FunctionExpr, @unchecked Sendable { + public static func and(_ value: Expr ...) -> BooleanExpr { + return BooleanExpr("and", value) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift index 0f32d09a345..7a9ba5b8620 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct ExprWithAlias { - init(_ expr: some Expr, _ alias: String) {} +public struct ExprWithAlias: Selectable { + public var alias: String + + public var expr: any Expr + + init(_ expr: some Expr, _ alias: String) { + self.alias = alias + self.expr = expr + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift deleted file mode 100644 index 7f78feebcd5..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/FunctionExpr.swift +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -public protocol FunctionExpr: Expr, Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift deleted file mode 100644 index c565ea79bb2..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/LogicalMin.swift +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -public struct LogicalMin: FunctionExpr, Sendable, Equatable {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index 27e9ee048af..e7d603d7d62 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -15,6 +15,7 @@ */ public struct Ordering { + let expr: Expr let direction: Direction } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 3be5aae64fd..ccf27a4adde 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -48,7 +48,7 @@ public struct Pipeline { /// /// - Parameter fields: The fields to add to the documents, specified as `Selectable`s. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func addFields(_ fields: Selectable...) -> Pipeline { + public func addFields(_ fields: Selectable...) -> Pipeline { return self } @@ -66,7 +66,7 @@ public struct Pipeline { /// - Parameter selections: The fields to include in the output documents, specified as /// `Selectable` expressions or `String` values representing field names. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func select(_ selections: SelectableOrFieldName...) -> Pipeline { + public func select(_ selections: SelectableOrFieldName...) -> Pipeline { // Implementation return self } @@ -87,7 +87,7 @@ public struct Pipeline { /// /// - Parameter condition: The `BooleanExpr` to apply. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func `where`(condition: BooleanExpr) -> Pipeline { + public func `where`(_ condition: BooleanExpr) -> Pipeline { return self } @@ -99,7 +99,7 @@ public struct Pipeline { /// /// - Parameter offset: The number of documents to skip. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func offset(_ offset: Int32) -> Pipeline { + public func offset(_ offset: Int32) -> Pipeline { return self } @@ -114,7 +114,7 @@ public struct Pipeline { /// /// - Parameter limit: The maximum number of documents to return. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func limit(_ limit: Int32) -> Pipeline { + public func limit(_ limit: Int32) -> Pipeline { return self } @@ -132,7 +132,7 @@ public struct Pipeline { /// /// - Parameter selections: The fields to include in the output documents, specified as /// `Selectable` expressions or `String` values representing field names. - func distinct(_ groups: SelectableOrFieldName...) -> Pipeline { + public func distinct(_ groups: SelectableOrFieldName...) -> Pipeline { return self } @@ -145,7 +145,7 @@ public struct Pipeline { /// /// - Parameter accumulators: The `AccumulatorWithAlias` expressions, each wrapping an /// `Accumulator` and assigning a name to the accumulated results. - func aggregate(_ aggregates: AccumulatorWithAlias...) -> Pipeline { + public func aggregate(_ aggregates: AggregateWithAlias...) -> Pipeline { return self } @@ -168,13 +168,13 @@ public struct Pipeline { /// calculations. /// - groups: An optional list of grouping fields or expressions. /// - Returns: A new `Pipeline` object with this stage appended. - func aggregate(option: AggregateOption) -> Pipeline { + public func aggregate(option: AggregateOption) -> Pipeline { return self } /// Performs a vector similarity search, ordering the result set by most similar to least /// similar, and returning the first N documents in the result set. - func findNearest(options: FindNearestOptions) -> Pipeline { + public func findNearest(options: FindNearestOptions) -> Pipeline { return self } @@ -188,7 +188,7 @@ public struct Pipeline { /// /// - Parameter orderings: One or more `Ordering` instances specifying the sorting criteria. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func sort(_ orderings: [Ordering]) -> Pipeline { + public func sort(_ orderings: [Ordering]) -> Pipeline { // Implementation return self } @@ -200,7 +200,7 @@ public struct Pipeline { /// /// - Parameter field: The `Selectable` field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func replace(_ field: SelectableOrFieldName) -> Pipeline { + public func replace(_ field: SelectableOrFieldName) -> Pipeline { // Implementation return self } @@ -212,7 +212,7 @@ public struct Pipeline { /// /// - Parameter documents: The number of documents to sample. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func sample(documents: Int64) -> Pipeline { + public func sample(documents: Int64) -> Pipeline { // Implementation return self } @@ -224,7 +224,7 @@ public struct Pipeline { /// /// - Parameter options: The `SampleOptions` specifies how sampling is performed. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func sample(options: SampleOptions) -> Pipeline { + public func sample(options: SampleOptions) -> Pipeline { // Implementation return self } @@ -237,7 +237,7 @@ public struct Pipeline { /// /// - Parameter other: The other `Pipeline` that is part of union. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func union(_ other: Pipeline) -> Pipeline { + public func union(_ other: Pipeline) -> Pipeline { // Implementation return self } @@ -257,7 +257,7 @@ public struct Pipeline { /// - Parameter field: The name of the field containing the array. /// - Parameter indexField: Optional. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func unnest(field: Selectable, indexField: String? = nil) -> Pipeline { + public func unnest(field: Selectable, indexField: String? = nil) -> Pipeline { // Implementation return self } @@ -273,7 +273,7 @@ public struct Pipeline { /// - Parameter params: A list of ordered parameters to configure the stage's behavior. /// - Parameter options: A list of optional, named parameters to configure the stage's behavior. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - func genericStage(name: String, params: [Any], options: [String: Any]? = nil) -> Pipeline { + public func genericStage(name: String, params: [Any], options: [String: Any]? = nil) -> Pipeline { // Implementation return self } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift index 3063c5c3d8c..d321c821aba 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct Selectable { - let alias: String +public protocol Selectable: Sendable { + var alias: String { get } + var expr: Expr { get } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift new file mode 100644 index 00000000000..e5030cc7a83 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.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. + +public struct TimeUnit: Sendable, Equatable, Hashable { + enum Kind: String { + case microsecond + case millisecond + case second + case minute + case hour + case day + } + + public static let microsecond = TimeUnit(kind: .microsecond) + public static let millisecond = TimeUnit(kind: .millisecond) + public static let second = TimeUnit(kind: .second) + public static let minute = TimeUnit(kind: .minute) + public static let hour = TimeUnit(kind: .hour) + public static let day = TimeUnit(kind: .day) + + public let rawValue: String + + init(kind: Kind) { + rawValue = kind.rawValue + } + + public init(rawValue: String) { + if let kind = Kind(rawValue: rawValue) { + self.rawValue = kind.rawValue + } else { + fatalError("Invalid TimeUnit: \(rawValue)") + } + } +} diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index 542f9a82451..fadce54a7da 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -34,6 +34,49 @@ final class PipelineTests: FSTIntegrationTestCase { let aggregateQuery = db.collection("foo").count let _: Pipeline = pipelineSource.createFrom(aggregateQuery) - let _: PipelineResult = try await pipeline.execute() + let _: PipelineSnapshot = try await pipeline.execute() + } + + func testWhereStage() async throws { + _ = db.pipeline().collection("books") + .where( + BooleanExpr.and( + Field("rating").gt(4.0), // Filter for ratings greater than 4.0 + Field("genre").eq("Science Fiction") + ) + ) + } + + func testAddFieldStage() async throws { + // Input + // { title: 'title1', price: 10, discount: 0.8 }, + // { title: 'title2', price: 12, discount: 1.0 }, + // { title: 'title3', price: 5, discount: 0.66 } + + // An expression that will compute price from the value of msrp field and discount field + let priceExpr: FunctionExpr = Field("msrp").multiply(Field("discount")) + + // An expression becomes a Selectable when given an alias. In this case + // the alias is 'salePrice' + let priceSelectableExpr: Selectable = priceExpr.alias("salePrice") + + _ = db.pipeline().collection("books") + .addFields( + priceSelectableExpr // Add field `salePrice` based computed from msrp and discount + ) + + // We don't expect customers to separate the Expression definition from the + // Pipeline definition. This was shown above so readers of this doc can see + // the different types involved. The cleaner way to write the code above + // is to inline the Expr definition + _ = db.pipeline().collection("books") + .addFields( + Field("msrp").multiply(Field("discount")).alias("salePrice") + ) + + // Output + // { title: 'title1', price: 10, discount: 0.8, salePrice: 8.0}, + // { title: 'title2', price: 12, discount: 1.0, salePrice: 12.0 }, + // { title: 'title3', price: 5, discount: 0.66, salePrice: 3.30 } } } From ed1a19bf60afa46da4e6b09637069ae4dedd75c2 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 12 Mar 2025 00:53:58 -0400 Subject: [PATCH 05/43] API changes --- .../Swift/Source/Helper/PipelineHelper.swift | 4 -- .../SwiftAPI/Pipeline/AggregateOption.swift | 4 +- .../SwiftAPI/Pipeline/ArrayContains.swift | 19 ++++++ .../Source/SwiftAPI/Pipeline/Ascending.swift | 19 ++++++ .../Source/SwiftAPI/Pipeline/Descending.swift | 19 ++++++ .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 23 ++++++- .../Expr/FunctionExpr/BooleanExpr.swift | 19 ++++++ .../Source/SwiftAPI/Pipeline/Ordering.swift | 7 ++- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 62 ++++++++++++++++--- .../SwiftAPI/Pipeline/PipelineSource.swift | 4 +- .../Pipeline/SelectableOrFieldName.swift | 41 ------------ .../Tests/Integration/PipelineApiTests.swift | 48 +++++++++++--- 12 files changed, 200 insertions(+), 69 deletions(-) create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift index 8feada23082..8746c676507 100644 --- a/Firestore/Swift/Source/Helper/PipelineHelper.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -13,10 +13,6 @@ // limitations under the License. enum Helper { - static func exprConvertFrom(_ value: Any) -> any Expr { - return Field("PLACEHOLDER") - } - static func valueToDefaultExpr(_ value: Any) -> any Expr { return Field("PLACEHOLDER") } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift index 847f5d79db7..2885abc0abe 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift @@ -14,9 +14,9 @@ public struct AggregateOption { public let accumulators: [AggregateWithAlias] - public let groups: [SelectableOrFieldName]? + public let groups: [Selectable]? - public init(accumulators: [AggregateWithAlias], groups: [SelectableOrFieldName]? = nil) { + public init(accumulators: [AggregateWithAlias], groups: [Selectable]? = nil) { self.accumulators = accumulators self.groups = groups } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift new file mode 100644 index 00000000000..a390cf97749 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift @@ -0,0 +1,19 @@ +// 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. + +public class ArrayContains: BooleanExpr, @unchecked Sendable { + public init(_ values: Any...) { + super.init("array_concat", values as! [any Expr]) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift new file mode 100644 index 00000000000..e872b6e7f8a --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ascending.swift @@ -0,0 +1,19 @@ +// 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. + +public class Ascending: Ordering, @unchecked Sendable { + public init(_ fieldName: String) { + super.init(expr: Field(fieldName), direction: .ascending) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift new file mode 100644 index 00000000000..584d7b7ada3 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Descending.swift @@ -0,0 +1,19 @@ +// 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. + +public class Descending: Ordering, @unchecked Sendable { + public init(_ fieldName: String) { + super.init(expr: Field(fieldName), direction: .descending) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 05f0029f348..e3e573fc0c7 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -13,7 +13,7 @@ // limitations under the License. public protocol Expr: Sendable { - func alias(_ name: String) -> ExprWithAlias + func `as`(_ name: String) -> ExprWithAlias // MARK: Comparison Operators @@ -221,7 +221,7 @@ public protocol Expr: Sendable { } public extension Expr { - func alias(_ name: String) -> ExprWithAlias { + func `as`(_ name: String) -> ExprWithAlias { return ExprWithAlias(self, name) } @@ -794,3 +794,22 @@ public extension Expr { return Ordering(expr: self, direction: .descending) } } + +// protocal cannot overwrite operator, since every inheritated class will have this function +// it will lead to error: Generic parameter 'Self' could not be inferred + +public func > (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { + try BooleanExpr("gt", [lhs, Helper.valueToDefaultExpr(rhs())]) +} + +public func < (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { + try BooleanExpr("lt", [lhs, Helper.valueToDefaultExpr(rhs())]) +} + +public func <= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { + try BooleanExpr("lte", [lhs, Helper.valueToDefaultExpr(rhs())]) +} + +public func == (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { + try BooleanExpr("eq", [lhs, Helper.valueToDefaultExpr(rhs())]) +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift index 20a37d53392..38ffee75cdf 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift @@ -16,4 +16,23 @@ public class BooleanExpr: FunctionExpr, @unchecked Sendable { public static func and(_ value: Expr ...) -> BooleanExpr { return BooleanExpr("and", value) } + + override init(_ functionName: String, _ agrs: [any Expr]) { + super.init(functionName, agrs) + } + + public static func && (lhs: BooleanExpr, + rhs: @autoclosure () throws -> BooleanExpr) rethrows -> BooleanExpr { + try BooleanExpr("and", [lhs, rhs()]) + } + + public static func || (lhs: BooleanExpr, + rhs: @autoclosure () throws -> BooleanExpr) rethrows -> BooleanExpr { + try BooleanExpr("or", [lhs, rhs()]) + } + + // not + public static prefix func ! (lhs: BooleanExpr) -> BooleanExpr { + return BooleanExpr("not", [lhs]) + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index e7d603d7d62..373839c20cd 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -14,9 +14,14 @@ * limitations under the License. */ -public struct Ordering { +public class Ordering: @unchecked Sendable { let expr: Expr let direction: Direction + + init(expr: Expr, direction: Direction) { + self.expr = expr + self.direction = direction + } } public struct Direction: Sendable, Equatable, Hashable { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index ccf27a4adde..4cf62dfe678 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -64,9 +64,27 @@ public struct Pipeline { /// only additions are desired. /// /// - Parameter selections: The fields to include in the output documents, specified as - /// `Selectable` expressions or `String` values representing field names. + /// `Selectable` expressions. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func select(_ selections: SelectableOrFieldName...) -> Pipeline { + public func select(_ selections: Selectable...) -> Pipeline { + // Implementation + return self + } + + /// Selects or creates a set of fields from the outputs of previous stages. + /// + /// The selected fields are defined using `Selectable` expressions, which can be: + /// + /// - `String`: Name of an existing field. + /// - `Field`: References an existing field. + /// - `Function`: Represents the result of a function with an assigned alias name using `Expr#as`. + /// + /// If no selections are provided, the output of this stage is empty. Use `addFields` instead if + /// only additions are desired. + /// + /// - Parameter selections: `String` values representing field names. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + public func select(_ selections: String...) -> Pipeline { // Implementation return self } @@ -87,7 +105,7 @@ public struct Pipeline { /// /// - Parameter condition: The `BooleanExpr` to apply. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func `where`(_ condition: BooleanExpr) -> Pipeline { + public func `where`(_ condition: () -> BooleanExpr) -> Pipeline { return self } @@ -131,8 +149,26 @@ public struct Pipeline { /// `Expr.alias(_:)`. /// /// - Parameter selections: The fields to include in the output documents, specified as - /// `Selectable` expressions or `String` values representing field names. - public func distinct(_ groups: SelectableOrFieldName...) -> Pipeline { + /// `String` values representing field names. + public func distinct(_ groups: String...) -> Pipeline { + return self + } + + /// Returns a set of distinct `Expr` values from the inputs to this stage. + /// + /// This stage processes the results from previous stages, ensuring that only unique + /// combinations of `Expr` values (such as `Field` and `Function`) are included. + /// + /// The parameters to this stage are defined using `Selectable` expressions or field names: + /// + /// - `String`: The name of an existing field. + /// - `Field`: A reference to an existing document field. + /// - `Function`: Represents the result of a function with an assigned alias using + /// `Expr.alias(_:)`. + /// + /// - Parameter selections: The fields to include in the output documents, specified as + /// `Selectable` expressions. + public func distinct(_ groups: Selectable...) -> Pipeline { return self } @@ -188,7 +224,7 @@ public struct Pipeline { /// /// - Parameter orderings: One or more `Ordering` instances specifying the sorting criteria. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func sort(_ orderings: [Ordering]) -> Pipeline { + public func sort(_ orderings: Ordering...) -> Pipeline { // Implementation return self } @@ -200,7 +236,19 @@ public struct Pipeline { /// /// - Parameter field: The `Selectable` field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func replace(_ field: SelectableOrFieldName) -> Pipeline { + public func replace(_ field: Selectable) -> Pipeline { + // Implementation + return self + } + + /// Fully overwrites all fields in a document with those coming from a nested map. + /// + /// This stage allows you to emit a map value as a document. Each key of the map becomes a + /// field on the document that contains the corresponding value. + /// + /// - Parameter fieldName: The field containing the nested map. + /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + public func replace(_ fieldName: String) -> Pipeline { // Implementation return self } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 75b56fa4424..9d9e74caf48 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -40,11 +40,11 @@ public struct PipelineSource { return Pipeline(db) } - public func createFrom(_ query: Query) -> Pipeline { + public func create(from query: Query) -> Pipeline { return Pipeline(db) } - public func createFrom(_ aggregateQuery: AggregateQuery) -> Pipeline { + public func create(from aggregateQuery: AggregateQuery) -> Pipeline { return Pipeline(db) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift deleted file mode 100644 index f4f66ab797f..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/SelectableOrFieldName.swift +++ /dev/null @@ -1,41 +0,0 @@ -// 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. - -public struct SelectableOrFieldName: Equatable { - enum Kind: Equatable { - case selectable(Selectable) - case field(String) - - static func == (lhs: Kind, rhs: Kind) -> Bool { - switch (lhs, rhs) { - case let (.selectable(a), .selectable(b)): - return a.alias == b.alias - case let (.field(a), .field(b)): - return a == b - default: - return false - } - } - } - - let kind: Kind - - public static func selectable(_ value: Selectable) -> SelectableOrFieldName { - return SelectableOrFieldName(kind: .selectable(value)) - } - - public static func field(_ name: String) -> SelectableOrFieldName { - return SelectableOrFieldName(kind: .field(name)) - } -} diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index fadce54a7da..6d31da9fca8 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -29,22 +29,19 @@ final class PipelineTests: FSTIntegrationTestCase { let _: Pipeline = pipelineSource.database() let query: Query = db.collection("foo").limit(to: 2) - let _: Pipeline = pipelineSource.createFrom(query) + let _: Pipeline = pipelineSource.create(from: query) let aggregateQuery = db.collection("foo").count - let _: Pipeline = pipelineSource.createFrom(aggregateQuery) + let _: Pipeline = pipelineSource.create(from: aggregateQuery) let _: PipelineSnapshot = try await pipeline.execute() } func testWhereStage() async throws { _ = db.pipeline().collection("books") - .where( - BooleanExpr.and( - Field("rating").gt(4.0), // Filter for ratings greater than 4.0 - Field("genre").eq("Science Fiction") - ) - ) + .where { + Field("rating") > 4.0 && Field("genre") == "Science Fiction" || ArrayContains("rating") + } } func testAddFieldStage() async throws { @@ -58,7 +55,7 @@ final class PipelineTests: FSTIntegrationTestCase { // An expression becomes a Selectable when given an alias. In this case // the alias is 'salePrice' - let priceSelectableExpr: Selectable = priceExpr.alias("salePrice") + let priceSelectableExpr: Selectable = priceExpr.as("salePrice") _ = db.pipeline().collection("books") .addFields( @@ -71,7 +68,7 @@ final class PipelineTests: FSTIntegrationTestCase { // is to inline the Expr definition _ = db.pipeline().collection("books") .addFields( - Field("msrp").multiply(Field("discount")).alias("salePrice") + Field("msrp").multiply(Field("discount")).as("salePrice") ) // Output @@ -79,4 +76,35 @@ final class PipelineTests: FSTIntegrationTestCase { // { title: 'title2', price: 12, discount: 1.0, salePrice: 12.0 }, // { title: 'title3', price: 5, discount: 0.66, salePrice: 3.30 } } + + func testSelectStage() async throws { + // Input + // { title: 'title1', price: 10, discount: 0.8 }, + // { title: 'title2', price: 12, discount: 1.0 }, + // { title: 'title3', price: 5, discount: 0.66 } + + // Overload for string and Selectable + _ = db.pipeline().collection("books") + .select( + Field("title"), // Field class inheritates Selectable + Field("msrp").multiply(Field("discount")).as("salePrice") + ) + + _ = db.pipeline().collection("books").select("title", "author") + + // Output + // { title: 'title1', salePrice: 8.0}, + // { title: 'title2', salePrice: 12.0 }, + // { title: 'title3', salePrice: 3.30 } + } + + func testSortStage() async throws { + // Sort books by rating in descending order, and then by title in ascending order for books + // with the same rating + _ = db.pipeline().collection("books") + .sort( + Field("rating").descending(), + Ascending("title") // alternative API offered + ) + } } From 819853ca7d8e16ac5dc056def0d0d39504631779 Mon Sep 17 00:00:00 2001 From: wu-hui Date: Tue, 11 Mar 2025 09:23:05 -0400 Subject: [PATCH 06/43] Add cpp stages and expressions --- .../core/src/api/aggregate_expressions.cc | 43 +++ .../core/src/api/aggregate_expressions.h | 49 ++++ Firestore/core/src/api/expressions.h | 11 +- Firestore/core/src/api/ordering.cc | 47 ++++ Firestore/core/src/api/ordering.h | 50 ++++ Firestore/core/src/api/stages.cc | 249 ++++++++++++++++++ Firestore/core/src/api/stages.h | 187 ++++++++++++- 7 files changed, 633 insertions(+), 3 deletions(-) create mode 100644 Firestore/core/src/api/aggregate_expressions.cc create mode 100644 Firestore/core/src/api/aggregate_expressions.h create mode 100644 Firestore/core/src/api/ordering.cc create mode 100644 Firestore/core/src/api/ordering.h diff --git a/Firestore/core/src/api/aggregate_expressions.cc b/Firestore/core/src/api/aggregate_expressions.cc new file mode 100644 index 00000000000..87fc69c368a --- /dev/null +++ b/Firestore/core/src/api/aggregate_expressions.cc @@ -0,0 +1,43 @@ +/* + * 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/aggregate_expressions.h" + +#include "Firestore/core/src/nanopb/nanopb_util.h" + +namespace firebase { +namespace firestore { +namespace api { + +google_firestore_v1_Value AggregateExpr::to_proto() const { + google_firestore_v1_Value result; + result.which_value_type = google_firestore_v1_Value_function_value_tag; + + result.function_value.name = nanopb::MakeBytesArray(name_); + result.function_value.args_count = static_cast(params_.size()); + result.function_value.args = nanopb::MakeArray( + result.function_value.args_count); + + for (size_t i = 0; i < params_.size(); ++i) { + result.function_value.args[i] = params_[i]->to_proto(); + } + + return result; +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/aggregate_expressions.h b/Firestore/core/src/api/aggregate_expressions.h new file mode 100644 index 00000000000..22cdecb4b86 --- /dev/null +++ b/Firestore/core/src/api/aggregate_expressions.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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_AGGREGATE_EXPRESSIONS_H_ +#define FIRESTORE_CORE_SRC_API_AGGREGATE_EXPRESSIONS_H_ + +#include +#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 AggregateExpr { + public: + AggregateExpr(std::string name, std::vector> params) + : name_(std::move(name)), params_(std::move(params)) { + } + ~AggregateExpr() = default; + + google_firestore_v1_Value to_proto() const; + + private: + std::string name_; + std::vector> params_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_AGGREGATE_EXPRESSIONS_H_ diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index 2ab134249cf..f59b621075c 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -35,11 +35,20 @@ class Expr { virtual google_firestore_v1_Value to_proto() const = 0; }; -class Field : public Expr { +class Selectable : public Expr { + public: + virtual ~Selectable() = default; + virtual const std::string& alias() const = 0; +}; + +class Field : public Selectable { public: explicit Field(std::string name) : name_(std::move(name)) { } google_firestore_v1_Value to_proto() const override; + const std::string& alias() const override { + return name_; + } private: std::string name_; diff --git a/Firestore/core/src/api/ordering.cc b/Firestore/core/src/api/ordering.cc new file mode 100644 index 00000000000..6520cea5b6f --- /dev/null +++ b/Firestore/core/src/api/ordering.cc @@ -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. + */ + +#include "Firestore/core/src/api/ordering.h" + +#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 Ordering::to_proto() const { + google_firestore_v1_Value result; + result.which_value_type = google_firestore_v1_Value_map_value_tag; + + result.map_value.fields_count = 2; + result.map_value.fields = + nanopb::MakeArray(2); + result.map_value.fields[0].key = nanopb::MakeBytesArray("expression"); + result.map_value.fields[0].value = field_.to_proto(); + result.map_value.fields[1].key = nanopb::MakeBytesArray("direction"); + google_firestore_v1_Value direction; + direction.which_value_type = google_firestore_v1_Value_string_value_tag; + direction.string_value = nanopb::MakeBytesArray( + this->direction_ == ASCENDING ? "ascending" : "descending"); + result.map_value.fields[1].value = direction; + + return result; +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/api/ordering.h b/Firestore/core/src/api/ordering.h new file mode 100644 index 00000000000..a7281a76566 --- /dev/null +++ b/Firestore/core/src/api/ordering.h @@ -0,0 +1,50 @@ +/* + * 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_ORDERING_H_ +#define FIRESTORE_CORE_SRC_API_ORDERING_H_ + +#include "Firestore/core/src/api/expressions.h" + +namespace firebase { +namespace firestore { +namespace api { + +class UserDataReader; // forward declaration + +class Ordering { + public: + enum Direction { + ASCENDING, + DESCENDING, + }; + + Ordering(Field field, Direction direction) + : field_(std::move(field)), direction_(direction) { + } + + google_firestore_v1_Value to_proto() const; + + private: + Field field_; + Direction direction_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_ORDERING_H_ diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 6843a1b4ce5..0e62bc08c4c 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -16,6 +16,10 @@ #include "Firestore/core/src/api/stages.h" +#include + +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/nanopb/message.h" #include "Firestore/core/src/nanopb/nanopb_util.h" namespace firebase { @@ -39,6 +43,112 @@ google_firestore_v1_Pipeline_Stage CollectionSource::to_proto() const { return result; } +google_firestore_v1_Pipeline_Stage DatabaseSource::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + + result.name = nanopb::MakeBytesArray("database"); + result.args_count = 0; + result.args = nullptr; + result.options_count = 0; + result.options = nullptr; + + return result; +} + +google_firestore_v1_Pipeline_Stage CollectionGroupSource::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + + result.name = nanopb::MakeBytesArray("collection_group"); + + result.args_count = 2; + result.args = nanopb::MakeArray(2); + // First argument is an empty reference value. + result.args[0].which_value_type = + google_firestore_v1_Value_reference_value_tag; + result.args[0].reference_value = nanopb::MakeBytesArray(""); + + // Second argument is the collection ID (encoded as a string value). + result.args[1].which_value_type = google_firestore_v1_Value_string_value_tag; + result.args[1].string_value = nanopb::MakeBytesArray(collection_id_); + + result.options_count = 0; + result.options = nullptr; + + return result; +} + +google_firestore_v1_Pipeline_Stage DocumentsSource::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + + result.name = nanopb::MakeBytesArray("documents"); + + result.args_count = documents_.size(); + result.args = nanopb::MakeArray(result.args_count); + + for (size_t i = 0; i < documents_.size(); ++i) { + result.args[i].which_value_type = + google_firestore_v1_Value_string_value_tag; + result.args[i].string_value = nanopb::MakeBytesArray(documents_[i]); + } + + result.options_count = 0; + result.options = nullptr; + + return result; +} + +google_firestore_v1_Pipeline_Stage AddFields::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("add_fields"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + + result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, + fields_, [](const std::shared_ptr& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + }); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +google_firestore_v1_Pipeline_Stage AggregateStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("aggregate"); + + result.args_count = 2; + result.args = nanopb::MakeArray(2); + + // Encode accumulators map. + result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, + this->accumulators_, + [](const std::pair>& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; + }); + + // Encode groups map. + result.args[1].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[1].map_value.fields, &result.args[1].map_value.fields_count, + this->groups_, + [](const std::pair>& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; + }); + + result.options_count = 0; + result.options = nullptr; + return result; +} + google_firestore_v1_Pipeline_Stage Where::to_proto() const { google_firestore_v1_Pipeline_Stage result; @@ -54,6 +164,145 @@ google_firestore_v1_Pipeline_Stage Where::to_proto() const { return result; } +google_firestore_v1_Value FindNearestStage::DistanceMeasure::proto() const { + google_firestore_v1_Value result; + result.which_value_type = google_firestore_v1_Value_string_value_tag; + switch (measure_) { + case EUCLIDEAN: + result.string_value = nanopb::MakeBytesArray("euclidean"); + break; + case COSINE: + result.string_value = nanopb::MakeBytesArray("cosine"); + break; + case DOT_PRODUCT: + result.string_value = nanopb::MakeBytesArray("dot_product"); + break; + } + return result; +} + +google_firestore_v1_Pipeline_Stage FindNearestStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("find_nearest"); + + result.args_count = 3; + result.args = nanopb::MakeArray(3); + result.args[0] = property_->to_proto(); + result.args[1] = *vector_; + result.args[2] = distance_measure_.proto(); + + nanopb::SetRepeatedField( + &result.options, &result.options_count, options_, + [](const std::pair>& + entry) { + return _google_firestore_v1_Pipeline_Stage_OptionsEntry{ + nanopb::MakeBytesArray(entry.first), *entry.second}; + }); + + return result; +} + +google_firestore_v1_Pipeline_Stage LimitStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("limit"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0].which_value_type = google_firestore_v1_Value_integer_value_tag; + result.args[0].integer_value = limit_; + + result.options_count = 0; + result.options = nullptr; + return result; +} + +google_firestore_v1_Pipeline_Stage OffsetStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("offset"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0].which_value_type = google_firestore_v1_Value_integer_value_tag; + result.args[0].integer_value = offset_; + + result.options_count = 0; + result.options = nullptr; + return result; +} + +google_firestore_v1_Pipeline_Stage SelectStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("select"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + + result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, + fields_, [](const std::shared_ptr& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + }); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +google_firestore_v1_Pipeline_Stage SortStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("sort"); + + result.args_count = static_cast(orders_.size()); + result.args = nanopb::MakeArray(result.args_count); + + for (size_t i = 0; i < orders_.size(); ++i) { + result.args[i] = orders_[i].to_proto(); + } + + result.options_count = 0; + result.options = nullptr; + return result; +} + +google_firestore_v1_Pipeline_Stage DistinctStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("distinct"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + + result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; + nanopb::SetRepeatedField( + &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, + groups_, [](const std::shared_ptr& entry) { + return _google_firestore_v1_MapValue_FieldsEntry{ + nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + }); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +google_firestore_v1_Pipeline_Stage RemoveFieldsStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("remove_fields"); + + result.args_count = static_cast(fields_.size()); + result.args = nanopb::MakeArray(result.args_count); + + for (size_t i = 0; i < fields_.size(); ++i) { + result.args[i] = fields_[i].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 index f037a70408e..649d4732d53 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -17,11 +17,17 @@ #ifndef FIRESTORE_CORE_SRC_API_STAGES_H_ #define FIRESTORE_CORE_SRC_API_STAGES_H_ +#include #include #include +#include +#include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/api/aggregate_expressions.h" #include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/api/ordering.h" +#include "Firestore/core/src/nanopb/message.h" namespace firebase { namespace firestore { @@ -47,10 +53,71 @@ class CollectionSource : public Stage { std::string path_; }; -class Where : public Stage { +class DatabaseSource : public Stage { + public: + DatabaseSource() = default; + ~DatabaseSource() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; +}; + +class CollectionGroupSource : public Stage { + public: + explicit CollectionGroupSource(std::string collection_id) + : collection_id_(std::move(collection_id)) { + } + ~CollectionGroupSource() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::string collection_id_; +}; + +class DocumentsSource : public Stage { + public: + explicit DocumentsSource(std::vector documents) + : documents_(std::move(documents)) { + } + ~DocumentsSource() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::vector documents_; +}; + +class AddFields : public Stage { + public: + explicit AddFields(std::vector> fields) + : fields_(std::move(fields)) { + } + ~AddFields() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::vector> fields_; +}; + +class AggregateStage : public Stage { public: - explicit Where(std::shared_ptr expr) : expr_(expr) { + AggregateStage(std::unordered_map> + accumulators, + std::unordered_map> groups) + : accumulators_(std::move(accumulators)), groups_(std::move(groups)) { } + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::unordered_map> accumulators_; + std::unordered_map> groups_; +}; + +class Where : public Stage { + public: + explicit Where(std::shared_ptr expr) : expr_(std::move(expr)) {}; ~Where() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; @@ -59,6 +126,122 @@ class Where : public Stage { std::shared_ptr expr_; }; +class FindNearestStage : public Stage { + public: + class DistanceMeasure { + public: + enum Measure { EUCLIDEAN, COSINE, DOT_PRODUCT }; + + explicit DistanceMeasure(Measure measure) : measure_(measure) { + } + google_firestore_v1_Value proto() const; + + private: + Measure measure_; + }; + + FindNearestStage( + std::shared_ptr property, + nanopb::SharedMessage vector, + DistanceMeasure distance_measure, + std::unordered_map> + options) + : property_(std::move(property)), + vector_(std::move(vector)), + distance_measure_(distance_measure), + options_(options) { + } + + ~FindNearestStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr property_; + nanopb::SharedMessage vector_; + DistanceMeasure distance_measure_; + std::unordered_map> + options_; +}; + +class LimitStage : public Stage { + public: + explicit LimitStage(long limit) : limit_(limit) { + } + ~LimitStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + long limit_; +}; + +class OffsetStage : public Stage { + public: + explicit OffsetStage(long offset) : offset_(offset) { + } + ~OffsetStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + long offset_; +}; + +class SelectStage : public Stage { + public: + explicit SelectStage(std::vector> fields) + : fields_(std::move(fields)) { + } + ~SelectStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::vector> fields_; +}; + +class SortStage : public Stage { + public: + explicit SortStage(std::vector orders) + : orders_(std::move(orders)) { + } + ~SortStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::vector orders_; +}; + +class DistinctStage : public Stage { + public: + explicit DistinctStage(std::vector> groups) + : groups_(std::move(groups)) { + } + ~DistinctStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::vector> groups_; +}; + +class RemoveFieldsStage : public Stage { + public: + explicit RemoveFieldsStage(std::vector fields) + : fields_(std::move(fields)) { + } + ~RemoveFieldsStage() override = default; + + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::vector fields_; +}; + } // namespace api } // namespace firestore } // namespace firebase From 1be141b9a9dfcb44ea11e5fcad69c6ce6e7faf0d Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 18 Mar 2025 00:13:22 -0400 Subject: [PATCH 07/43] Add more APIs --- .../{ => Aggregation}/AggregateFunction.swift | 6 +- .../AggregateWithAlias.swift | 5 +- .../CountAll.swift} | 10 +- .../SwiftAPI/Pipeline/ArrayContains.swift | 4 +- ...estOptions.swift => DistanceMeasure.swift} | 22 +- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 40 +-- .../SwiftAPI/Pipeline/Expr/Constant.swift | 51 ++- .../SwiftAPI/Pipeline/Expr/DocumentId.swift | 19 ++ .../Expr/FunctionExpr/BooleanExpr.swift | 6 +- .../Source/SwiftAPI/Pipeline/Ordering.swift | 13 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 76 ++++- .../SwiftAPI/Pipeline/SampleOptions.swift | 16 +- .../Tests/Integration/PipelineApiTests.swift | 300 +++++++++++++++++- 13 files changed, 466 insertions(+), 102 deletions(-) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{ => Aggregation}/AggregateFunction.swift (80%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{ => Aggregation}/AggregateWithAlias.swift (84%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{AggregateOption.swift => Aggregation/CountAll.swift} (69%) rename Firestore/Swift/Source/SwiftAPI/Pipeline/{FindNearestOptions.swift => DistanceMeasure.swift} (73%) create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift similarity index 80% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateFunction.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift index 386650738dd..9a36df9fd04 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateFunction.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct AggregateFunction { +public class AggregateFunction: @unchecked Sendable { let functionName: String let agrs: [Expr] @@ -20,4 +20,8 @@ public struct AggregateFunction { self.functionName = functionName self.agrs = agrs } + + public func `as`(_ name: String) -> AggregateWithAlias { + return AggregateWithAlias(aggregate: self, alias: name) + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateWithAlias.swift similarity index 84% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateWithAlias.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateWithAlias.swift index f333b748971..8a1871907c6 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateWithAlias.swift @@ -12,4 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct AggregateWithAlias {} +public struct AggregateWithAlias { + public let aggregate: AggregateFunction + public let alias: String +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/CountAll.swift similarity index 69% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/CountAll.swift index 2885abc0abe..064eb6d99bc 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/AggregateOption.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/CountAll.swift @@ -12,12 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct AggregateOption { - public let accumulators: [AggregateWithAlias] - public let groups: [Selectable]? - - public init(accumulators: [AggregateWithAlias], groups: [Selectable]? = nil) { - self.accumulators = accumulators - self.groups = groups +public class CountAll: AggregateFunction, @unchecked Sendable { + public init() { + super.init("count", []) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift index a390cf97749..df426d36f79 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift @@ -13,7 +13,7 @@ // limitations under the License. public class ArrayContains: BooleanExpr, @unchecked Sendable { - public init(_ values: Any...) { - super.init("array_concat", values as! [any Expr]) + public init(fieldName: String, values: Any...) { + super.init("array_concat", values.map { Helper.valueToDefaultExpr($0) }) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift similarity index 73% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift rename to Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift index 50383debf7e..6bd54e9e71b 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/FindNearestOptions.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift @@ -20,15 +20,9 @@ import Foundation -public struct FindNearestOptions { - let field: Field - let vectorValue: [VectorValue] - let distanceMeasure: DistanceMeasure - let limit: Int? - let distanceField: String? -} - public struct DistanceMeasure: Sendable, Equatable, Hashable { + let kind: Kind + enum Kind: String { case euclidean case cosine @@ -47,17 +41,7 @@ public struct DistanceMeasure: Sendable, Equatable, Hashable { return self.init(kind: .dotProduct) } - /// Returns the raw string representation of the `DistanceMeasure` value. - public let rawValue: String - init(kind: Kind) { - rawValue = kind.rawValue - } - - public init(rawValue: String) { - if Kind(rawValue: rawValue) == nil { - // impl - } - self.rawValue = rawValue + self.kind = kind } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index e3e573fc0c7..5cecc515f67 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -15,26 +15,6 @@ public protocol Expr: Sendable { func `as`(_ name: String) -> ExprWithAlias - // MARK: Comparison Operators - - func eq(_ other: Expr) -> BooleanExpr - func eq(_ other: Any) -> BooleanExpr - - func neq(_ other: Expr) -> BooleanExpr - func neq(_ other: Any) -> BooleanExpr - - func lt(_ other: Expr) -> BooleanExpr - func lt(_ other: Any) -> BooleanExpr - - func lte(_ other: Expr) -> BooleanExpr - func lte(_ other: Any) -> BooleanExpr - - func gt(_ other: Expr) -> BooleanExpr - func gt(_ other: Any) -> BooleanExpr - - func gte(_ other: Expr) -> BooleanExpr - func gte(_ other: Any) -> BooleanExpr - // MARK: Arithmetic Operators func add(_ second: Expr, _ others: Expr...) -> FunctionExpr @@ -110,8 +90,8 @@ public protocol Expr: Sendable { func endsWith(_ suffix: String) -> BooleanExpr func endsWith(_ suffix: Expr) -> BooleanExpr - func toLower() -> FunctionExpr - func toUpper() -> FunctionExpr + func lowercased() -> FunctionExpr + func uppercased() -> FunctionExpr func trim() -> FunctionExpr func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr @@ -207,10 +187,6 @@ public protocol Expr: Sendable { func bitRightShift(_ y: Int) -> FunctionExpr func bitRightShift(_ numberExpr: Expr) -> FunctionExpr - // MARK: - String operations. - - func documentId() -> FunctionExpr - func ifError(_ catchExpr: Expr) -> FunctionExpr func ifError(_ catchValue: Any) -> FunctionExpr @@ -475,11 +451,11 @@ public extension Expr { return BooleanExpr("ends_with", [self, suffix]) } - func toLower() -> FunctionExpr { + func lowercased() -> FunctionExpr { return FunctionExpr("to_lower", [self]) } - func toUpper() -> FunctionExpr { + func uppercased() -> FunctionExpr { return FunctionExpr("to_upper", [self]) } @@ -802,6 +778,10 @@ public func > (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> Boolea try BooleanExpr("gt", [lhs, Helper.valueToDefaultExpr(rhs())]) } +public func >= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { + try BooleanExpr("gte", [lhs, Helper.valueToDefaultExpr(rhs())]) +} + public func < (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { try BooleanExpr("lt", [lhs, Helper.valueToDefaultExpr(rhs())]) } @@ -813,3 +793,7 @@ public func <= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> Boole public func == (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { try BooleanExpr("eq", [lhs, Helper.valueToDefaultExpr(rhs())]) } + +public func != (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { + try BooleanExpr("neq", [lhs, Helper.valueToDefaultExpr(rhs())]) +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 99f50cd3b39..93ad9f47e3d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -13,8 +13,55 @@ // limitations under the License. public struct Constant: Expr, @unchecked Sendable { - let value: Any - public init(_ value: Any) { + let value: Any? + + // Initializer for numbers + public init(_ value: Double) { + self.value = value + } + + // Initializer for strings + public init(_ value: String) { + self.value = value + } + + // Initializer for boolean values + public init(_ value: Bool) { + self.value = value + } + + // Initializer for GeoPoint values + public init(_ value: GeoPoint) { + self.value = value + } + + // Initializer for Timestamp values + public init(_ value: Timestamp) { + self.value = value + } + + // Initializer for Date values + public init(_ value: Date) { + self.value = value + } + + // Initializer for DocumentReference + public init(_ value: DocumentReference) { + self.value = value + } + + // Initializer for vector values + public init(_ value: VectorValue) { + self.value = value + } + + // Initializer for vector values + public init(_ vectorValue: [Double]) { + value = vectorValue + } + + // Initializer for optional values (including nil) + public init(_ value: Any?) { self.value = value } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift new file mode 100644 index 00000000000..70c621d8cbd --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/DocumentId.swift @@ -0,0 +1,19 @@ +// 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. + +public class DocumentId: Field, @unchecked Sendable { + public init() { + super.init("__name__") + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift index 38ffee75cdf..e4964095e14 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift @@ -13,11 +13,7 @@ // limitations under the License. public class BooleanExpr: FunctionExpr, @unchecked Sendable { - public static func and(_ value: Expr ...) -> BooleanExpr { - return BooleanExpr("and", value) - } - - override init(_ functionName: String, _ agrs: [any Expr]) { + override public init(_ functionName: String, _ agrs: [any Expr]) { super.init(functionName, agrs) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index 373839c20cd..1761be8ebca 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -25,6 +25,8 @@ public class Ordering: @unchecked Sendable { } public struct Direction: Sendable, Equatable, Hashable { + let kind: Kind + enum Kind: String { case ascending case descending @@ -38,16 +40,7 @@ public struct Direction: Sendable, Equatable, Hashable { return self.init(kind: .descending) } - public let rawValue: String - init(kind: Kind) { - rawValue = kind.rawValue - } - - public init(rawValue: String) { - if Kind(rawValue: rawValue) == nil { - // impl - } - self.rawValue = rawValue + self.kind = kind } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 4cf62dfe678..fc55cf046ad 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -48,7 +48,21 @@ public struct Pipeline { /// /// - Parameter fields: The fields to add to the documents, specified as `Selectable`s. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func addFields(_ fields: Selectable...) -> Pipeline { + public func addFields(_ field: Selectable, _ additionalFields: Selectable...) -> Pipeline { + return self + } + + /// Remove fields from outputs of previous stages. + /// - Parameter fields: The fields to remove. + /// - Returns: A new Pipeline object with this stage appended to the stage list. + public func removeFields(_ field: Field, _ additionalFields: Field...) -> Pipeline { + return self + } + + /// Remove fields from outputs of previous stages. + /// - Parameter fields: The fields to remove. + /// - Returns: A new Pipeline object with this stage appended to the stage list. + public func removeFields(_ field: String, _ additionalFields: String...) -> Pipeline { return self } @@ -66,7 +80,7 @@ public struct Pipeline { /// - Parameter selections: The fields to include in the output documents, specified as /// `Selectable` expressions. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func select(_ selections: Selectable...) -> Pipeline { + public func select(_ selection: Selectable, _ additionalSelections: Selectable...) -> Pipeline { // Implementation return self } @@ -84,7 +98,7 @@ public struct Pipeline { /// /// - Parameter selections: `String` values representing field names. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func select(_ selections: String...) -> Pipeline { + public func select(_ selection: String, _ additionalSelections: String...) -> Pipeline { // Implementation return self } @@ -105,11 +119,12 @@ public struct Pipeline { /// /// - Parameter condition: The `BooleanExpr` to apply. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func `where`(_ condition: () -> BooleanExpr) -> Pipeline { + public func `where`(_ condition: BooleanExpr) -> Pipeline { return self } /// Skips the first `offset` number of documents from the results of previous stages. + /// The negative input number will count back from the result set. /// /// This stage is useful for implementing pagination in your pipelines, allowing you to /// retrieve results in chunks. It is typically used in conjunction with `limit` to control the @@ -122,6 +137,7 @@ public struct Pipeline { } /// Limits the maximum number of documents returned by previous stages to `limit`. + /// The negative input number will count back from the result set. /// /// This stage is particularly useful when you want to retrieve a controlled /// subset of data from a potentially large result set. It's often used for: @@ -150,7 +166,7 @@ public struct Pipeline { /// /// - Parameter selections: The fields to include in the output documents, specified as /// `String` values representing field names. - public func distinct(_ groups: String...) -> Pipeline { + public func distinct(_ group: String, _ additionalGroups: String...) -> Pipeline { return self } @@ -168,7 +184,7 @@ public struct Pipeline { /// /// - Parameter selections: The fields to include in the output documents, specified as /// `Selectable` expressions. - public func distinct(_ groups: Selectable...) -> Pipeline { + public func distinct(_ group: Selectable, _ additionalGroups: Selectable...) -> Pipeline { return self } @@ -181,7 +197,8 @@ public struct Pipeline { /// /// - Parameter accumulators: The `AccumulatorWithAlias` expressions, each wrapping an /// `Accumulator` and assigning a name to the accumulated results. - public func aggregate(_ aggregates: AggregateWithAlias...) -> Pipeline { + public func aggregate(_ accumulator: AggregateWithAlias, + _ additionalAccumulators: AggregateWithAlias...) -> Pipeline { return self } @@ -204,13 +221,42 @@ public struct Pipeline { /// calculations. /// - groups: An optional list of grouping fields or expressions. /// - Returns: A new `Pipeline` object with this stage appended. - public func aggregate(option: AggregateOption) -> Pipeline { + public func aggregate(_ accumulator: [AggregateWithAlias], + groups: [Selectable]? = nil) -> Pipeline { + return self + } + + /// Performs optionally grouped aggregation operations on the documents from previous stages. + /// + /// This stage calculates aggregate values over a set of documents, optionally grouped by + /// one or more fields or computed expressions. + /// + /// - **Grouping Fields or Expressions:** Defines how documents are grouped. For each + /// unique combination of values in the specified fields or expressions, a separate group + /// is created. If no grouping fields are provided, all documents are placed into a single + /// group. + /// - **Accumulators:** Defines the accumulation operations to perform within each group. + /// These are provided as `AccumulatorWithAlias` expressions, typically created by + /// calling `alias(_:)` on `Accumulator` instances. Each aggregation computes a + /// value (e.g., sum, average, count) based on the documents in its group. + /// + /// - Parameters: + /// - accumulators: A list of `AccumulatorWithAlias` expressions defining the aggregation + /// calculations. + /// - groups: An optional list of grouping field names. + /// - Returns: A new `Pipeline` object with this stage appended. + public func aggregate(_ accumulator: [AggregateWithAlias], + groups: [String]? = nil) -> Pipeline { return self } /// Performs a vector similarity search, ordering the result set by most similar to least /// similar, and returning the first N documents in the result set. - public func findNearest(options: FindNearestOptions) -> Pipeline { + public func findNearest(field: Field, + vectorValue: [Double], + distanceMeasure: DistanceMeasure, + limit: Int? = nil, + distanceField: String? = nil) -> Pipeline { return self } @@ -224,7 +270,7 @@ public struct Pipeline { /// /// - Parameter orderings: One or more `Ordering` instances specifying the sorting criteria. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func sort(_ orderings: Ordering...) -> Pipeline { + public func sort(_ ordering: Ordering, _ additionalOrdering: Ordering...) -> Pipeline { // Implementation return self } @@ -236,7 +282,7 @@ public struct Pipeline { /// /// - Parameter field: The `Selectable` field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func replace(_ field: Selectable) -> Pipeline { + public func replace(with field: Selectable) -> Pipeline { // Implementation return self } @@ -248,7 +294,7 @@ public struct Pipeline { /// /// - Parameter fieldName: The field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func replace(_ fieldName: String) -> Pipeline { + public func replace(with fieldName: String) -> Pipeline { // Implementation return self } @@ -260,7 +306,7 @@ public struct Pipeline { /// /// - Parameter documents: The number of documents to sample. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func sample(documents: Int64) -> Pipeline { + public func sample(_ count: Int64) -> Pipeline { // Implementation return self } @@ -272,7 +318,7 @@ public struct Pipeline { /// /// - Parameter options: The `SampleOptions` specifies how sampling is performed. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func sample(options: SampleOptions) -> Pipeline { + public func sample(with option: SampleOption) -> Pipeline { // Implementation return self } @@ -305,7 +351,7 @@ public struct Pipeline { /// - Parameter field: The name of the field containing the array. /// - Parameter indexField: Optional. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func unnest(field: Selectable, indexField: String? = nil) -> Pipeline { + public func unnest(_ field: Selectable, indexField: String? = nil) -> Pipeline { // Implementation return self } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift index 92f48244b25..c5965482899 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift @@ -12,20 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct SampleOptions { +public struct SampleOption { let percentage: Double? - let documents: Int? + let count: Int64? - private init(percentage: Double?, documents: Int?) { + private init(percentage: Double?, count: Int64?) { self.percentage = percentage - self.documents = documents + self.count = count } - static func percentage(_ value: Double) -> SampleOptions { - return SampleOptions(percentage: value, documents: nil) + public init(percentage: Double) { + self.init(percentage: percentage, count: nil) } - static func documents(_ count: Int) -> SampleOptions { - return SampleOptions(percentage: nil, documents: count) + public init(count: Int64) { + self.init(percentage: nil, count: count) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index 6d31da9fca8..a8d795b7dbd 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -39,9 +39,12 @@ final class PipelineTests: FSTIntegrationTestCase { func testWhereStage() async throws { _ = db.pipeline().collection("books") - .where { - Field("rating") > 4.0 && Field("genre") == "Science Fiction" || ArrayContains("rating") - } + .where( + Field("rating") > 4.0 && Field("genre") == "Science Fiction" || ArrayContains( + fieldName: "fieldName", + values: "rating" + ) + ) } func testAddFieldStage() async throws { @@ -68,7 +71,8 @@ final class PipelineTests: FSTIntegrationTestCase { // is to inline the Expr definition _ = db.pipeline().collection("books") .addFields( - Field("msrp").multiply(Field("discount")).as("salePrice") + Field("msrp").multiply(Field("discount")).as("salePrice"), + Field("author") ) // Output @@ -77,6 +81,14 @@ final class PipelineTests: FSTIntegrationTestCase { // { title: 'title3', price: 5, discount: 0.66, salePrice: 3.30 } } + func testRemoveFieldsStage() async throws { + // removes field 'rating' and 'cost' from the previous stage outputs. + _ = db.pipeline().collection("books").removeFields("rating", "cost") + + // removes field 'rating'. + _ = db.pipeline().collection("books").removeFields(Field("rating")) + } + func testSelectStage() async throws { // Input // { title: 'title1', price: 10, discount: 0.8 }, @@ -107,4 +119,284 @@ final class PipelineTests: FSTIntegrationTestCase { Ascending("title") // alternative API offered ) } + + func testLimitStage() async throws { + // Limit the results to the top 10 highest-rated books + _ = db.pipeline().collection("books") + .sort(Field("rating").descending()) + .limit(10) + } + + func testOffsetStage() async throws { + // Retrieve the second page of 20 results + _ = db.pipeline().collection("books") + .sort(Field("published").descending()) + .offset(20) // Skip the first 20 results. Note that this must come + // before .limit(...) unlike in Query where the order did not matter. + .limit(20) // Take the next 20 results + } + + func testDistinctStage() async throws { + // Input + // { author: 'authorA', genre: 'genreA', title: 'title1' }, + // { author: 'authorb', genre: 'genreB', title: 'title2' }, + // { author: 'authorB', genre: 'genreB', title: 'title3' } + + // Get a list of unique author names in uppercase and genre combinations. + _ = db.pipeline().collection("books") + .distinct( + Field("author").uppercased().as("authorName"), + Field("genre") + ) + + // Output + // { authorName: 'AUTHORA', genre: 'genreA' }, + // { authorName: 'AUTHORB', genre: 'genreB' } + } + + func testAggregateStage() async throws { + // Input + // { genre: 'genreA', title: 'title1', rating: 5.0 }, + // { genre: 'genreB', title: 'title2', rating: 1.5 }, + // { genre: 'genreB', title: 'title3', rating: 2.5 } + + // Calculate the average rating and the total number of books + _ = db.pipeline().collection("books") + .aggregate( + Field("rating").avg().as("averageRating"), + CountAll().as("totalBooks") + ) + + // Output + // { totalBooks: 3, averageRating: 3.0 } + + // Input + // { genre: 'genreA', title: 'title1', rating: 5.0 }, + // { genre: 'genreB', title: 'title2', rating: 1.5 }, + // { genre: 'genreB', title: 'title3', rating: 2.5 } + + // Calculate the average rating and the total number of books and group by field 'genre' + _ = db.pipeline().collection("books") + .aggregate([ + Field("rating").avg().as("averageRating"), + CountAll().as("totalBooks"), + ], + groups: ["genre"]) + + // Output + // { genre: 'genreA', totalBooks: 1, averageRating: 5.0 } + // { genre: 'genreB', totalBooks: 2, averageRating: 2.0 } + } + + func testFindNearestStage() async throws { + _ = db.pipeline().collection("books").findNearest( + field: Field("embedding"), + vectorValue: [5.0], + distanceMeasure: .cosine, + limit: 3) + } + + func testReplaceStage() async throws { + // Input. + // { +// "name": "John Doe Jr.", +// "parents": { +// "father": "John Doe Sr.", +// "mother": "Jane Doe" +// } + // } + + // Emit field parents as the document. + _ = db.pipeline().collection("people") + .replace(with: Field("parents")) + + // Output + // { +// "father": "John Doe Sr.", +// "mother": "Jane Doe" + // } + } + + func testSampleStage() async throws { + // Sample 25 books, if the collection contains at least 25 documents + _ = db.pipeline().collection("books").sample(25) + + // Sample 25 books, if the collection contains at least 25 documents + _ = db.pipeline().collection("books").sample(with: SampleOption(count: 10)) + + // Sample 10 percent of the collection of books + _ = db.pipeline().collection("books").sample(with: SampleOption(percentage: 10)) + } + + func testUnionStage() async throws { + // Emit documents from books collection and magazines collection. + _ = db.pipeline().collection("books") + .union(db.pipeline().collection("magazines")) + } + + func testUnnestStage() async throws { + // Input: + // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" + // ], ... } + + // Emit a book document for each tag of the book. + _ = db.pipeline().collection("books") + .unnest(Field("tags").as("tag")) + + // Output: + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", tags: [...], ... } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", tags: [...], ... } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", tags: [...], ... } + + // Emit a book document for each tag of the book mapped to its' index in the array. + _ = db.pipeline().collection("books") + .unnest(Field("tags").as("tag"), indexField: "index") + + // Output: + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", index: 0, tags: [...], + // ... } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", index: 1, tags: [...], ... + // } + // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", index: 2, tags: [...], + // ... } + } + + func testGenericStage() async throws { + // Assume we don't have a built-in "where" stage, the customer could still + // add this stage by calling genericStage, passing the name of the stage "where", + // and providing positional argument values. + _ = db.pipeline().collection("books") + .genericStage(name: "where", + params: [Field("published").lt(1900)]) + .select("title", "author") + + // In cases where the stage also supports named argument values, then these can be + // provided with a third argument that maps the argument name to value. + // Note that these named arguments are always optional in the stage definition. + _ = db.pipeline().collection("books") + .genericStage(name: "where", + params: [Field("published").lt(1900)], + options: ["someOptionalParamName": "the argument value for this param"]) + .select("title", "author") + } + + func testField() async throws { + // An expression that will return the value of the field `name` in the document + let nameField = Field("name") + + // An expression that will return the value of the field `description` in the document + // Field is a sub-type of Expr, so we can also declare our var of type Expr + let descriptionField: Expr = Field("description") + + // USAGE: anywhere an Expr type is accepted + // Use a field in a pipeline + _ = db.pipeline().collection("books") + .addFields( + Field("rating").as("bookRating") // Duplicate field 'rating' as 'bookRating' + ) + + // One special Field value is conveniently exposed as static function to help the user reference + // reserved field values of __name__. + _ = db.pipeline().collection("books") + .addFields( + DocumentId() + ) + } + + func testConstant() async throws { + // A constant for a number + let three = Constant(3) + + // A constant for a string + let name = Constant("Expressions API") + + // Const is a sub-type of Expr, so we can also declare our var of type Expr + let nothing: Expr = Constant(nil) + + // USAGE: Anywhere an Expr type is accepted + // Add field `fromTheLibraryOf: 'Rafi'` to every document in the collection. + _ = db.pipeline().collection("books") + .addFields(Constant("Rafi").as("fromTheLibraryOf")) + } + + func testFunctionExpr() async throws { + let secondsField = Field("seconds") + + // Create a FunctionExpr using the multiply function to compute milliseconds + let milliseconds: FunctionExpr = secondsField.multiply(1000) + + // A firestore function is also a sub-type of Expr + let myExpr: Expr = milliseconds + } + + func testBooleanExpr() async throws { + let isApple: BooleanExpr = Field("type") == "apple" + + // USAGE: stage where requires an expression of type BooleanExpr + let allAppleOptions: Pipeline = db.pipeline().collection("fruitOptions").where(isApple) + } + + func testSelectableExpr() async throws { + let secondsField = Field("seconds") + + // Create a selectable from our milliseconds expression. + let millisecondsSelectable: Selectable = secondsField.multiply(1000).as("milliseconds") + + // USAGE: stages addFields and select accept expressions of type Selectable + // Add (or overwrite) the 'milliseconds` field to each of our documents using the + // `.addFields(...)` stage. + _ = db.pipeline().collection("lapTimes") + .addFields(secondsField.multiply(1000).as("milliseconds")) + + // NOTE: Field implements Selectable, the alias is the same as the name + let secondsSelectable: Selectable = secondsField + } + + func testAggregateExpr() async throws { + let lapTimeSum: AggregateFunction = Field("seconds").sum() + + let lapTimeSumTarget: AggregateWithAlias = lapTimeSum.as("totalTrackTime") + + // USAGE: stage aggregate accepts expressions of type AggregateWithAlias + // A pipeline that will return one document with one field `totalTrackTime` that + // is the sum of all laps ever taken on the track. + _ = db.pipeline().collection("lapTimes") + .aggregate(lapTimeSum.as("totalTrackTime")) + } + + func testOrdering() async throws { + let fastestToSlowest: Ordering = Field("seconds").ascending() + + // USAGE: stage sort accepts objects of type Ordering + // Use this ordering to sort our lap times collection from fastest to slowest + _ = db.pipeline().collection("lapTimes").sort(fastestToSlowest) + } + + func testExpr() async throws { + // An expression that computes the area of a circle + // by chaining together two calls to the multiply function + let radiusField: Expr = Field("radius") + let radiusSq: Expr = radiusField.multiply(Field("radius")) + let areaExpr: Expr = radiusSq.multiply(3.14) + + // Or define this expression in one clean, fluent statement + let areaOfCircle: Selectable = Field("radius") + .multiply(Field("radius")) + .multiply(3.14) + .as("area") + + // And pass the expression to a Pipeline for evaluation + _ = db.pipeline().collection("circles").addFields(areaOfCircle) + } + + func testGeneric() async throws { + // This is the same of the logicalMin('price', 0)', if it did not exist + let myLm = FunctionExpr("logicalMin", [Field("price"), Constant(0)]) + + // Create a generic BooleanExpr for use where BooleanExpr is required + let myEq = BooleanExpr("eq", [Field("price"), Constant(10)]) + + // Create a generic AggregateFunction for use where AggregateFunction is required + let mySum = AggregateFunction("sum", [Field("price")]) + } } From 8fd5f8aec1cf7e1b5eeda843ed851a572a27d6d4 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 18 Mar 2025 16:12:50 -0400 Subject: [PATCH 08/43] add Field --- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift new file mode 100644 index 00000000000..45810caa182 --- /dev/null +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.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. + +public class Field: Expr, Selectable, @unchecked Sendable { + public var alias: String + + public let fieldName: String + + public init(_ fieldName: String) { + self.fieldName = fieldName + alias = fieldName + } + + public var expr: any Expr { + return self + } +} From 5127bd08bf236dab7cc73f679ce53b5e9f109b44 Mon Sep 17 00:00:00 2001 From: wu-hui Date: Mon, 17 Mar 2025 09:54:00 -0400 Subject: [PATCH 09/43] Add UserDataReader support for constant and expr in pipelines --- .../Source/API/FIRPipelineBridge+Internal.h | 4 +- Firestore/Source/API/FIRPipelineBridge.mm | 76 ++++++++++++------- .../FirebaseFirestore/FIRPipelineBridge.h | 2 +- .../Swift/Source/SwiftAPI/Expressions.swift | 30 +++++--- Firestore/Swift/Source/SwiftAPI/Stages.swift | 2 +- .../Tests/Integration/PipelineTests.swift | 5 +- .../core/src/api/aggregate_expressions.h | 1 + Firestore/core/src/api/expressions.cc | 7 +- Firestore/core/src/api/expressions.h | 6 +- Firestore/core/src/api/ordering.h | 2 + Firestore/core/src/api/stages.cc | 1 + Firestore/core/src/api/stages.h | 15 ++-- 12 files changed, 92 insertions(+), 59 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge+Internal.h b/Firestore/Source/API/FIRPipelineBridge+Internal.h index bfe7befe923..626c3d12339 100644 --- a/Firestore/Source/API/FIRPipelineBridge+Internal.h +++ b/Firestore/Source/API/FIRPipelineBridge+Internal.h @@ -30,13 +30,13 @@ NS_ASSUME_NONNULL_BEGIN @interface FIRExprBridge (Internal) -- (std::shared_ptr)cpp_expr; +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader; @end @interface FIRStageBridge (Internal) -- (std::shared_ptr)cpp_stage; +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader; @end diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 013cedcccab..bcd59e69530 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -20,6 +20,7 @@ #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRPipelineBridge+Internal.h" +#import "Firestore/Source/API/FSTUserDataReader.h" #include "Firestore/core/src/api/expressions.h" #include "Firestore/core/src/api/pipeline.h" @@ -57,7 +58,7 @@ - (id)init:(NSString *)name { return self; } -- (std::shared_ptr)cpp_expr { +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { return field; } @@ -65,16 +66,22 @@ - (id)init:(NSString *)name { @implementation FIRConstantBridge { std::shared_ptr constant; + id _input; + Boolean isUserDataRead; } -- (id)init:(NSNumber *)value { +- (id)init:(id)input { self = [super init]; - if (self) { - constant = std::make_shared(value.doubleValue); - } + _input = input; + isUserDataRead = NO; return self; } -- (std::shared_ptr)cpp_expr { +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + constant = std::make_shared([reader parsedQueryValue:_input]); + } + + isUserDataRead = YES; return constant; } @@ -82,22 +89,29 @@ - (id)init:(NSNumber *)value { @implementation FIRFunctionExprBridge { std::shared_ptr eq; + NSString *_name; + NSArray *_args; + Boolean isUserDataRead; } - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray *)args { self = [super init]; - if (self) { + _name = name; + _args = args; + isUserDataRead = NO; + return self; +} + +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { std::vector> cpp_args; - for (FIRExprBridge *arg in args) { - cpp_args.push_back(arg.cpp_expr); + for (FIRExprBridge *arg in _args) { + cpp_args.push_back([arg cppExprWithReader:reader]); } - - eq = std::make_shared(MakeString(name), std::move(cpp_args)); + eq = std::make_shared(MakeString(_name), std::move(cpp_args)); } - return self; -} -- (std::shared_ptr)cpp_expr { + isUserDataRead = YES; return eq; } @@ -118,25 +132,33 @@ - (id)initWithPath:(NSString *)path { return self; } -- (std::shared_ptr)cpp_stage { +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { return collection_source; } @end @implementation FIRWhereStageBridge { + FIRExprBridge *_exprBridge; + Boolean isUserDataRead; std::shared_ptr where; } - (id)initWithExpr:(FIRExprBridge *)expr { self = [super init]; if (self) { - where = std::make_shared(expr.cpp_expr); + _exprBridge = expr; + isUserDataRead = NO; } return self; } -- (std::shared_ptr)cpp_stage { +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + where = std::make_shared([_exprBridge cppExprWithReader:reader]); + } + + isUserDataRead = YES; return where; } @@ -158,23 +180,25 @@ - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { @end @implementation FIRPipelineBridge { + NSArray *_stages; + FIRFirestore *firestore; 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; + _stages = stages; + firestore = db; + return [super init]; } - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, NSError *_Nullable error))completion { + std::vector> cpp_stages; + for (FIRStageBridge *stage in _stages) { + cpp_stages.push_back([stage cppStageWithReader:firestore.dataReader]); + } + pipeline = std::make_shared(cpp_stages, firestore.wrapped); + pipeline->execute([completion](StatusOr maybe_value) { if (maybe_value.ok()) { __FIRPipelineSnapshotBridge *bridge = [[__FIRPipelineSnapshotBridge alloc] diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index fa7472e3292..6c8a4769a6b 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -31,7 +31,7 @@ NS_SWIFT_NAME(FieldBridge) NS_SWIFT_NAME(ConstantBridge) @interface FIRConstantBridge : FIRExprBridge -- (id)init:(NSNumber *)value; +- (id)init:(id)input; @end NS_SWIFT_NAME(FunctionExprBridge) diff --git a/Firestore/Swift/Source/SwiftAPI/Expressions.swift b/Firestore/Swift/Source/SwiftAPI/Expressions.swift index 729b5c9fb67..22af7ae6471 100644 --- a/Firestore/Swift/Source/SwiftAPI/Expressions.swift +++ b/Firestore/Swift/Source/SwiftAPI/Expressions.swift @@ -16,26 +16,28 @@ import Foundation -public protocol Expr { +public protocol Expr {} + +protocol BridgeWrapper { var bridge: ExprBridge { get } } -public struct Constant: Expr { - public var bridge: ExprBridge +public struct Constant: Expr, BridgeWrapper { + var bridge: ExprBridge - var value: any Numeric - init(value: any Numeric) { + var value: Any + init(value: Any) { self.value = value - bridge = ConstantBridge(value as! NSNumber) + bridge = ConstantBridge(value) } } -public func constant(_ number: any Numeric) -> Constant { +public func constant(_ number: Any) -> Constant { return Constant(value: number) } -public struct Field: Expr { - public var bridge: ExprBridge +public struct Field: Expr, BridgeWrapper { + var bridge: ExprBridge var name: String init(name: String) { @@ -52,8 +54,8 @@ protocol Function: Expr { var name: String { get } } -public struct FunctionExpr: Function { - public var bridge: ExprBridge +public struct FunctionExpr: Function, BridgeWrapper { + var bridge: ExprBridge var name: String private var args: [Expr] @@ -61,7 +63,11 @@ public struct FunctionExpr: Function { init(name: String, args: [Expr]) { self.name = name self.args = args - bridge = FunctionExprBridge(name: name, args: args.map { $0.bridge }) + bridge = FunctionExprBridge( + name: name, + args: args.map { ($0 as! (Expr & BridgeWrapper)).bridge + } + ) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index df3c163e803..c5de0c00e52 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -42,6 +42,6 @@ class Where: Stage { init(condition: Expr) { self.condition = condition - bridge = WhereStageBridge(expr: condition.bridge) + bridge = WhereStageBridge(expr: (condition as! (Expr & BridgeWrapper)).bridge) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 79185762b91..2a1b78c48bf 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -20,10 +20,11 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class PipelineIntegrationTests: FSTIntegrationTestCase { func testCount() async throws { + try await firestore().collection("foo").addDocument(data: ["foo": "bar", "x": 42]) let snapshot = try await firestore() .pipeline() - .collection(path: "foo") - .where(eq(field("foo"), constant(42))) + .collection(path: "/foo") + .where(eq(field("foo"), constant("bar"))) .execute() print(snapshot) diff --git a/Firestore/core/src/api/aggregate_expressions.h b/Firestore/core/src/api/aggregate_expressions.h index 22cdecb4b86..119198b2abd 100644 --- a/Firestore/core/src/api/aggregate_expressions.h +++ b/Firestore/core/src/api/aggregate_expressions.h @@ -19,6 +19,7 @@ #include #include +#include #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" diff --git a/Firestore/core/src/api/expressions.cc b/Firestore/core/src/api/expressions.cc index 07e99b1e848..a707e78ca70 100644 --- a/Firestore/core/src/api/expressions.cc +++ b/Firestore/core/src/api/expressions.cc @@ -35,12 +35,7 @@ google_firestore_v1_Value Field::to_proto() const { } 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; + return *value_; } google_firestore_v1_Value FunctionExpr::to_proto() const { diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index f59b621075c..5b08a277e3b 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -23,6 +23,7 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/nanopb/message.h" namespace firebase { namespace firestore { @@ -56,12 +57,13 @@ class Field : public Selectable { class Constant : public Expr { public: - explicit Constant(double value) : value_(value) { + explicit Constant(nanopb::SharedMessage value) + : value_(std::move(value)) { } google_firestore_v1_Value to_proto() const override; private: - double value_; + nanopb::SharedMessage value_; }; class FunctionExpr : public Expr { diff --git a/Firestore/core/src/api/ordering.h b/Firestore/core/src/api/ordering.h index a7281a76566..130dda12b19 100644 --- a/Firestore/core/src/api/ordering.h +++ b/Firestore/core/src/api/ordering.h @@ -17,6 +17,8 @@ #ifndef FIRESTORE_CORE_SRC_API_ORDERING_H_ #define FIRESTORE_CORE_SRC_API_ORDERING_H_ +#include + #include "Firestore/core/src/api/expressions.h" namespace firebase { diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 0e62bc08c4c..eaa19cb03bd 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/api/stages.h" #include +#include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/core/src/nanopb/message.h" diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index 649d4732d53..11534278002 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -17,10 +17,10 @@ #ifndef FIRESTORE_CORE_SRC_API_STAGES_H_ #define FIRESTORE_CORE_SRC_API_STAGES_H_ -#include #include #include #include +#include #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" @@ -43,7 +43,7 @@ class Stage { class CollectionSource : public Stage { public: - explicit CollectionSource(std::string path) : path_(path) { + explicit CollectionSource(std::string path) : path_(std::move(path)) { } ~CollectionSource() override = default; @@ -117,7 +117,8 @@ class AggregateStage : public Stage { class Where : public Stage { public: - explicit Where(std::shared_ptr expr) : expr_(std::move(expr)) {}; + explicit Where(std::shared_ptr expr) : expr_(std::move(expr)) { + } ~Where() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; @@ -168,26 +169,26 @@ class FindNearestStage : public Stage { class LimitStage : public Stage { public: - explicit LimitStage(long limit) : limit_(limit) { + explicit LimitStage(int64_t limit) : limit_(limit) { } ~LimitStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: - long limit_; + int64_t limit_; }; class OffsetStage : public Stage { public: - explicit OffsetStage(long offset) : offset_(offset) { + explicit OffsetStage(int64_t offset) : offset_(offset) { } ~OffsetStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: - long offset_; + int64_t offset_; }; class SelectStage : public Stage { From 14d2e5cf08c64f99c786c5fa82d8384fe2ed11e0 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 20 Mar 2025 13:00:38 -0400 Subject: [PATCH 10/43] Add bridge --- Firestore/Swift/Source/BridgeWrapper.swift | 18 ++++++++++++++++++ .../Swift/Source/SwiftAPI/Expressions.swift | 14 -------------- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 7 +++++++ .../SwiftAPI/Pipeline/Expr/Constant.swift | 16 +++++++++++++++- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 2 +- Firestore/core/src/api/expressions.cc | 11 +++++------ Firestore/core/src/api/expressions.h | 10 ++++++++-- 7 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 Firestore/Swift/Source/BridgeWrapper.swift diff --git a/Firestore/Swift/Source/BridgeWrapper.swift b/Firestore/Swift/Source/BridgeWrapper.swift new file mode 100644 index 00000000000..b37f3718331 --- /dev/null +++ b/Firestore/Swift/Source/BridgeWrapper.swift @@ -0,0 +1,18 @@ +// 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. + + +internal protocol BridgeWrapper { + var bridge: ExprBridge { get } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Expressions.swift b/Firestore/Swift/Source/SwiftAPI/Expressions.swift index 729b5c9fb67..65b43c3365d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Expressions.swift +++ b/Firestore/Swift/Source/SwiftAPI/Expressions.swift @@ -16,20 +16,6 @@ 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) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 5cecc515f67..6fe62ac5810 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + public protocol Expr: Sendable { func `as`(_ name: String) -> ExprWithAlias diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 93ad9f47e3d..4b648aebf8e 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -12,8 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct Constant: Expr, @unchecked Sendable { +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + +public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { + var bridge: ExprBridge + let value: Any? + + internal init(_ value: Any?) { + self.value = value + // TODO + self.bridge = ConstantBridge(value as! NSNumber) + } // Initializer for numbers public init(_ value: Double) { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift index 45810caa182..ff2de41f643 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public class Field: Expr, Selectable, @unchecked Sendable { +public class Field: ExprBridge, Expr, Selectable, @unchecked Sendable { public var alias: String public let fieldName: String diff --git a/Firestore/core/src/api/expressions.cc b/Firestore/core/src/api/expressions.cc index 07e99b1e848..ad61cc7fa05 100644 --- a/Firestore/core/src/api/expressions.cc +++ b/Firestore/core/src/api/expressions.cc @@ -19,8 +19,12 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/model/value_util.h" #include "Firestore/core/src/nanopb/nanopb_util.h" +using firebase::firestore::model::ObjectValue; +using firebase::firestore::model::DeepClone; + namespace firebase { namespace firestore { namespace api { @@ -35,12 +39,7 @@ google_firestore_v1_Value Field::to_proto() const { } 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; + return *DeepClone(value_.Get()).release(); } google_firestore_v1_Value FunctionExpr::to_proto() const { diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index 2ab134249cf..c6ecc1ed097 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -23,9 +23,15 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/model/object_value.h" namespace firebase { namespace firestore { + +namespace model { + class ObjectValue; +} + namespace api { class Expr { @@ -47,12 +53,12 @@ class Field : public Expr { class Constant : public Expr { public: - explicit Constant(double value) : value_(value) { + explicit Constant(model::ObjectValue value) : value_(value) { } google_firestore_v1_Value to_proto() const override; private: - double value_; + const model::ObjectValue value_; }; class FunctionExpr : public Expr { From 5d701f8ed4b8b81d96989fe9cca31a68af97f47a Mon Sep 17 00:00:00 2001 From: wu-hui Date: Mon, 17 Mar 2025 09:54:00 -0400 Subject: [PATCH 11/43] Add UserDataReader support for constant and expr in pipelines --- .../Source/API/FIRPipelineBridge+Internal.h | 11 +- Firestore/Source/API/FIRPipelineBridge.mm | 154 +++++++++++++++--- .../FirebaseFirestore/FIRPipelineBridge.h | 6 +- .../Swift/Source/SwiftAPI/Expressions.swift | 30 ++-- Firestore/Swift/Source/SwiftAPI/Stages.swift | 2 +- .../Tests/Integration/PipelineTests.swift | 5 +- .../core/src/api/aggregate_expressions.h | 1 + Firestore/core/src/api/expressions.cc | 9 +- Firestore/core/src/api/expressions.h | 6 +- Firestore/core/src/api/ordering.h | 2 + Firestore/core/src/api/pipeline_result.h | 4 + Firestore/core/src/api/pipeline_snapshot.h | 9 + Firestore/core/src/api/stages.cc | 1 + Firestore/core/src/api/stages.h | 15 +- Firestore/core/src/remote/datastore.cc | 26 +-- .../core/src/remote/remote_objc_bridge.cc | 14 +- .../core/src/remote/remote_objc_bridge.h | 3 +- 17 files changed, 223 insertions(+), 75 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge+Internal.h b/Firestore/Source/API/FIRPipelineBridge+Internal.h index bfe7befe923..30bee14aa02 100644 --- a/Firestore/Source/API/FIRPipelineBridge+Internal.h +++ b/Firestore/Source/API/FIRPipelineBridge+Internal.h @@ -19,6 +19,7 @@ #include #include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/api/firestore.h" #include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/api/stages.h" @@ -30,13 +31,13 @@ NS_ASSUME_NONNULL_BEGIN @interface FIRExprBridge (Internal) -- (std::shared_ptr)cpp_expr; +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader; @end @interface FIRStageBridge (Internal) -- (std::shared_ptr)cpp_stage; +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader; @end @@ -46,4 +47,10 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface __FIRPipelineResultBridge (Internal) + +- (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr)db; + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 013cedcccab..c10a05f4d88 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -18,9 +18,15 @@ #include +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRPipelineBridge+Internal.h" +#import "Firestore/Source/API/FSTUserDataReader.h" +#import "Firestore/Source/API/FSTUserDataWriter.h" +#include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" + +#include "Firestore/core/src/api/document_reference.h" #include "Firestore/core/src/api/expressions.h" #include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/api/pipeline_result.h" @@ -32,12 +38,14 @@ using firebase::firestore::api::CollectionSource; using firebase::firestore::api::Constant; +using firebase::firestore::api::DocumentReference; 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::MakeNSString; using firebase::firestore::util::MakeString; NS_ASSUME_NONNULL_BEGIN @@ -57,7 +65,7 @@ - (id)init:(NSString *)name { return self; } -- (std::shared_ptr)cpp_expr { +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { return field; } @@ -65,16 +73,22 @@ - (id)init:(NSString *)name { @implementation FIRConstantBridge { std::shared_ptr constant; + id _input; + Boolean isUserDataRead; } -- (id)init:(NSNumber *)value { +- (id)init:(id)input { self = [super init]; - if (self) { - constant = std::make_shared(value.doubleValue); - } + _input = input; + isUserDataRead = NO; return self; } -- (std::shared_ptr)cpp_expr { +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + constant = std::make_shared([reader parsedQueryValue:_input]); + } + + isUserDataRead = YES; return constant; } @@ -82,22 +96,29 @@ - (id)init:(NSNumber *)value { @implementation FIRFunctionExprBridge { std::shared_ptr eq; + NSString *_name; + NSArray *_args; + Boolean isUserDataRead; } - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray *)args { self = [super init]; - if (self) { + _name = name; + _args = args; + isUserDataRead = NO; + return self; +} + +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { std::vector> cpp_args; - for (FIRExprBridge *arg in args) { - cpp_args.push_back(arg.cpp_expr); + for (FIRExprBridge *arg in _args) { + cpp_args.push_back([arg cppExprWithReader:reader]); } - - eq = std::make_shared(MakeString(name), std::move(cpp_args)); + eq = std::make_shared(MakeString(_name), std::move(cpp_args)); } - return self; -} -- (std::shared_ptr)cpp_expr { + isUserDataRead = YES; return eq; } @@ -118,63 +139,142 @@ - (id)initWithPath:(NSString *)path { return self; } -- (std::shared_ptr)cpp_stage { +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { return collection_source; } @end @implementation FIRWhereStageBridge { + FIRExprBridge *_exprBridge; + Boolean isUserDataRead; std::shared_ptr where; } - (id)initWithExpr:(FIRExprBridge *)expr { self = [super init]; if (self) { - where = std::make_shared(expr.cpp_expr); + _exprBridge = expr; + isUserDataRead = NO; } return self; } -- (std::shared_ptr)cpp_stage { +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + where = std::make_shared([_exprBridge cppExprWithReader:reader]); + } + + isUserDataRead = YES; return where; } @end +@interface __FIRPipelineSnapshotBridge () + +@property(nonatomic, strong, readwrite) NSArray<__FIRPipelineSnapshotBridge *> *results; + +@end + @implementation __FIRPipelineSnapshotBridge { - absl::optional pipeline; + absl::optional snapshot_; + NSMutableArray<__FIRPipelineResultBridge *> *results_; } - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { self = [super init]; if (self) { - pipeline = std::move(snapshot); + snapshot_ = std::move(snapshot); + if (!snapshot_.has_value()) { + results_ = nil; + } else { + NSMutableArray<__FIRPipelineResultBridge *> *results = [NSMutableArray array]; + for (auto &result : snapshot_.value().results()) { + [results addObject:[[__FIRPipelineResultBridge alloc] + initWithCppResult:result + db:snapshot_.value().firestore()]]; + } + results_ = results; + } } return self; } +- (NSArray<__FIRPipelineResultBridge *> *)results { + return results_; +} + @end -@implementation FIRPipelineBridge { - std::shared_ptr pipeline; +@implementation __FIRPipelineResultBridge { + api::PipelineResult _result; + std::shared_ptr _db; } -- (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { +- (FIRDocumentReference *)reference { + if (!_result.internal_key().has_value()) return nil; + + return [[FIRDocumentReference alloc] initWithKey:_result.internal_key().value() firestore:_db]; +} + +- (NSString *)documentID { + if (!_result.document_id().has_value()) { + return nil; + } + + return MakeNSString(_result.document_id().value()); +} + +- (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr)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); + _result = std::move(result); + _db = std::move(db); } + return self; } +- (nullable NSDictionary *)data { + return [self dataWithServerTimestampBehavior:FIRServerTimestampBehaviorNone]; +} + +- (nullable NSDictionary *)dataWithServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior { + absl::optional data = + _result.internal_value()->Get(); + if (!data) return nil; + + FSTUserDataWriter *dataWriter = + [[FSTUserDataWriter alloc] initWithFirestore:_db + serverTimestampBehavior:serverTimestampBehavior]; + return [dataWriter convertedValue:*data]; +} + +@end + +@implementation FIRPipelineBridge { + NSArray *_stages; + FIRFirestore *firestore; + std::shared_ptr pipeline; +} + +- (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { + _stages = stages; + firestore = db; + return [super init]; +} + - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable result, NSError *_Nullable error))completion { + std::vector> cpp_stages; + for (FIRStageBridge *stage in _stages) { + cpp_stages.push_back([stage cppStageWithReader:firestore.dataReader]); + } + pipeline = std::make_shared(cpp_stages, firestore.wrapped); + pipeline->execute([completion](StatusOr maybe_value) { if (maybe_value.ok()) { __FIRPipelineSnapshotBridge *bridge = [[__FIRPipelineSnapshotBridge alloc] diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index fa7472e3292..a27b2b7aa18 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -18,6 +18,8 @@ #import +#import "FIRDocumentSnapshot.h" + NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(ExprBridge) @@ -31,7 +33,7 @@ NS_SWIFT_NAME(FieldBridge) NS_SWIFT_NAME(ConstantBridge) @interface FIRConstantBridge : FIRExprBridge -- (id)init:(NSNumber *)value; +- (id)init:(id)input; @end NS_SWIFT_NAME(FunctionExprBridge) @@ -72,6 +74,8 @@ NS_SWIFT_NAME(__PipelineResultBridge) @property(nonatomic, copy, readonly) NSString *documentID; - (nullable NSDictionary *)data; +- (nullable NSDictionary *)dataWithServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior; @end diff --git a/Firestore/Swift/Source/SwiftAPI/Expressions.swift b/Firestore/Swift/Source/SwiftAPI/Expressions.swift index 729b5c9fb67..22af7ae6471 100644 --- a/Firestore/Swift/Source/SwiftAPI/Expressions.swift +++ b/Firestore/Swift/Source/SwiftAPI/Expressions.swift @@ -16,26 +16,28 @@ import Foundation -public protocol Expr { +public protocol Expr {} + +protocol BridgeWrapper { var bridge: ExprBridge { get } } -public struct Constant: Expr { - public var bridge: ExprBridge +public struct Constant: Expr, BridgeWrapper { + var bridge: ExprBridge - var value: any Numeric - init(value: any Numeric) { + var value: Any + init(value: Any) { self.value = value - bridge = ConstantBridge(value as! NSNumber) + bridge = ConstantBridge(value) } } -public func constant(_ number: any Numeric) -> Constant { +public func constant(_ number: Any) -> Constant { return Constant(value: number) } -public struct Field: Expr { - public var bridge: ExprBridge +public struct Field: Expr, BridgeWrapper { + var bridge: ExprBridge var name: String init(name: String) { @@ -52,8 +54,8 @@ protocol Function: Expr { var name: String { get } } -public struct FunctionExpr: Function { - public var bridge: ExprBridge +public struct FunctionExpr: Function, BridgeWrapper { + var bridge: ExprBridge var name: String private var args: [Expr] @@ -61,7 +63,11 @@ public struct FunctionExpr: Function { init(name: String, args: [Expr]) { self.name = name self.args = args - bridge = FunctionExprBridge(name: name, args: args.map { $0.bridge }) + bridge = FunctionExprBridge( + name: name, + args: args.map { ($0 as! (Expr & BridgeWrapper)).bridge + } + ) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index df3c163e803..c5de0c00e52 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -42,6 +42,6 @@ class Where: Stage { init(condition: Expr) { self.condition = condition - bridge = WhereStageBridge(expr: condition.bridge) + bridge = WhereStageBridge(expr: (condition as! (Expr & BridgeWrapper)).bridge) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 79185762b91..a2252488312 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -20,10 +20,11 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class PipelineIntegrationTests: FSTIntegrationTestCase { func testCount() async throws { + try await firestore().collection("foo").document("bar").setData(["foo": "bar", "x": 42]) let snapshot = try await firestore() .pipeline() - .collection(path: "foo") - .where(eq(field("foo"), constant(42))) + .collection(path: "/foo") + .where(eq(field("foo"), constant("bar"))) .execute() print(snapshot) diff --git a/Firestore/core/src/api/aggregate_expressions.h b/Firestore/core/src/api/aggregate_expressions.h index 22cdecb4b86..119198b2abd 100644 --- a/Firestore/core/src/api/aggregate_expressions.h +++ b/Firestore/core/src/api/aggregate_expressions.h @@ -19,6 +19,7 @@ #include #include +#include #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" diff --git a/Firestore/core/src/api/expressions.cc b/Firestore/core/src/api/expressions.cc index 07e99b1e848..7ec517f2aab 100644 --- a/Firestore/core/src/api/expressions.cc +++ b/Firestore/core/src/api/expressions.cc @@ -19,6 +19,7 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/model/value_util.h" #include "Firestore/core/src/nanopb/nanopb_util.h" namespace firebase { @@ -35,12 +36,8 @@ google_firestore_v1_Value Field::to_proto() const { } 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; + // Return a copy of the value proto to avoid double delete. + return *model::DeepClone(*value_).release(); } google_firestore_v1_Value FunctionExpr::to_proto() const { diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index f59b621075c..5b08a277e3b 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -23,6 +23,7 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/nanopb/message.h" namespace firebase { namespace firestore { @@ -56,12 +57,13 @@ class Field : public Selectable { class Constant : public Expr { public: - explicit Constant(double value) : value_(value) { + explicit Constant(nanopb::SharedMessage value) + : value_(std::move(value)) { } google_firestore_v1_Value to_proto() const override; private: - double value_; + nanopb::SharedMessage value_; }; class FunctionExpr : public Expr { diff --git a/Firestore/core/src/api/ordering.h b/Firestore/core/src/api/ordering.h index a7281a76566..130dda12b19 100644 --- a/Firestore/core/src/api/ordering.h +++ b/Firestore/core/src/api/ordering.h @@ -17,6 +17,8 @@ #ifndef FIRESTORE_CORE_SRC_API_ORDERING_H_ #define FIRESTORE_CORE_SRC_API_ORDERING_H_ +#include + #include "Firestore/core/src/api/expressions.h" namespace firebase { diff --git a/Firestore/core/src/api/pipeline_result.h b/Firestore/core/src/api/pipeline_result.h index 4680d058c7b..53761752cdc 100644 --- a/Firestore/core/src/api/pipeline_result.h +++ b/Firestore/core/src/api/pipeline_result.h @@ -53,6 +53,10 @@ class PipelineResult { std::shared_ptr internal_value() const; absl::optional document_id() const; + const absl::optional& internal_key() const { + return internal_key_; + } + private: absl::optional internal_key_; // Using a shared pointer to ObjectValue makes PipelineResult copy-assignable diff --git a/Firestore/core/src/api/pipeline_snapshot.h b/Firestore/core/src/api/pipeline_snapshot.h index a19e76138a7..079f2d57375 100644 --- a/Firestore/core/src/api/pipeline_snapshot.h +++ b/Firestore/core/src/api/pipeline_snapshot.h @@ -41,9 +41,18 @@ class PipelineSnapshot { return results_; } + const std::shared_ptr firestore() const { + return firestore_; + } + + void SetFirestore(std::shared_ptr db) { + firestore_ = std::move(db); + } + private: std::vector results_; model::SnapshotVersion execution_time_; + std::shared_ptr firestore_; }; } // namespace api diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 0e62bc08c4c..eaa19cb03bd 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/api/stages.h" #include +#include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/core/src/nanopb/message.h" diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index 649d4732d53..11534278002 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -17,10 +17,10 @@ #ifndef FIRESTORE_CORE_SRC_API_STAGES_H_ #define FIRESTORE_CORE_SRC_API_STAGES_H_ -#include #include #include #include +#include #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" @@ -43,7 +43,7 @@ class Stage { class CollectionSource : public Stage { public: - explicit CollectionSource(std::string path) : path_(path) { + explicit CollectionSource(std::string path) : path_(std::move(path)) { } ~CollectionSource() override = default; @@ -117,7 +117,8 @@ class AggregateStage : public Stage { class Where : public Stage { public: - explicit Where(std::shared_ptr expr) : expr_(std::move(expr)) {}; + explicit Where(std::shared_ptr expr) : expr_(std::move(expr)) { + } ~Where() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; @@ -168,26 +169,26 @@ class FindNearestStage : public Stage { class LimitStage : public Stage { public: - explicit LimitStage(long limit) : limit_(limit) { + explicit LimitStage(int64_t limit) : limit_(limit) { } ~LimitStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: - long limit_; + int64_t limit_; }; class OffsetStage : public Stage { public: - explicit OffsetStage(long offset) : offset_(offset) { + explicit OffsetStage(int64_t offset) : offset_(offset) { } ~OffsetStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: - long offset_; + int64_t offset_; }; class SelectStage : public Stage { diff --git a/Firestore/core/src/remote/datastore.cc b/Firestore/core/src/remote/datastore.cc index d5950ca09c6..504beadbd99 100644 --- a/Firestore/core/src/remote/datastore.cc +++ b/Firestore/core/src/remote/datastore.cc @@ -340,20 +340,22 @@ void Datastore::RunPipelineWithCredentials( 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()); + call->Start( + [this, db = pipeline.firestore(), 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()); - } + if (result.ok()) { + auto response = datastore_serializer_.DecodeExecutePipelineResponse( + result.ValueOrDie(), std::move(db)); + callback(response); + } else { + callback(result.status()); + } - RemoveGrpcCall(call); - }); + RemoveGrpcCall(call); + }); } void Datastore::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 466ed1229cc..6cc675d4f7f 100644 --- a/Firestore/core/src/remote/remote_objc_bridge.cc +++ b/Firestore/core/src/remote/remote_objc_bridge.cc @@ -33,6 +33,7 @@ #include "Firestore/core/src/remote/grpc_util.h" #include "Firestore/core/src/remote/watch_change.h" #include "Firestore/core/src/util/hard_assert.h" +#include "Firestore/core/src/util/log.h" #include "Firestore/core/src/util/status.h" #include "Firestore/core/src/util/statusor.h" #include "grpcpp/support/status.h" @@ -398,7 +399,8 @@ DatastoreSerializer::EncodeExecutePipelineRequest( util::StatusOr DatastoreSerializer::DecodeExecutePipelineResponse( - const grpc::ByteBuffer& response) const { + const grpc::ByteBuffer& response, + std::shared_ptr db) const { ByteBufferReader reader{response}; auto message = Message::TryParse(&reader); @@ -406,7 +408,15 @@ DatastoreSerializer::DecodeExecutePipelineResponse( return reader.status(); } - return serializer_.DecodePipelineResponse(reader.context(), message); + LOG_DEBUG("Pipeline Response: %s", message.ToString()); + + auto snapshot = serializer_.DecodePipelineResponse(reader.context(), message); + if (!reader.ok()) { + return reader.status(); + } + + snapshot.SetFirestore(std::move(db)); + return snapshot; } } // namespace remote diff --git a/Firestore/core/src/remote/remote_objc_bridge.h b/Firestore/core/src/remote/remote_objc_bridge.h index f6615003eed..96329c1ae25 100644 --- a/Firestore/core/src/remote/remote_objc_bridge.h +++ b/Firestore/core/src/remote/remote_objc_bridge.h @@ -156,7 +156,8 @@ class DatastoreSerializer { const firebase::firestore::api::Pipeline& pipeline) const; util::StatusOr DecodeExecutePipelineResponse( - const grpc::ByteBuffer& response) const; + const grpc::ByteBuffer& response, + std::shared_ptr db) const; private: Serializer serializer_; From b2af1c61d83d74d8223235d09311568512d8b17e Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 20 Mar 2025 14:36:16 -0400 Subject: [PATCH 12/43] solve merge conflicts --- .../Swift/Source/SwiftAPI/Expressions.swift | 76 ------------------- .../SwiftAPI/Pipeline/Expr/Constant.swift | 2 +- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 5 +- .../SwiftAPI/Pipeline/Expr/FunctionExpr.swift | 9 ++- 4 files changed, 13 insertions(+), 79 deletions(-) delete mode 100644 Firestore/Swift/Source/SwiftAPI/Expressions.swift diff --git a/Firestore/Swift/Source/SwiftAPI/Expressions.swift b/Firestore/Swift/Source/SwiftAPI/Expressions.swift deleted file mode 100644 index 22af7ae6471..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Expressions.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 {} - -protocol BridgeWrapper { - var bridge: ExprBridge { get } -} - -public struct Constant: Expr, BridgeWrapper { - var bridge: ExprBridge - - var value: Any - init(value: Any) { - self.value = value - bridge = ConstantBridge(value) - } -} - -public func constant(_ number: Any) -> Constant { - return Constant(value: number) -} - -public struct Field: Expr, BridgeWrapper { - 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, BridgeWrapper { - 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 as! (Expr & BridgeWrapper)).bridge - } - ) - } -} - -public func eq(_ left: Expr, _ right: Expr) -> FunctionExpr { - return FunctionExpr(name: "eq", args: [left, right]) -} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 4b648aebf8e..67ade111d4a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -26,7 +26,7 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { internal init(_ value: Any?) { self.value = value // TODO - self.bridge = ConstantBridge(value as! NSNumber) + self.bridge = ConstantBridge(value) } // Initializer for numbers diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift index ff2de41f643..ba04fd3c406 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -public class Field: ExprBridge, Expr, Selectable, @unchecked Sendable { +public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, @unchecked Sendable { + var bridge: ExprBridge + public var alias: String public let fieldName: String @@ -20,6 +22,7 @@ public class Field: ExprBridge, Expr, Selectable, @unchecked Sendable { public init(_ fieldName: String) { self.fieldName = fieldName alias = fieldName + self.bridge = FieldBridge(alias) } public var expr: any Expr { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift index c78a482cd18..742455374db 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -12,12 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -public class FunctionExpr: Expr, @unchecked Sendable { +public class FunctionExpr: Expr, BridgeWrapper, @unchecked Sendable { + var bridge: ExprBridge + let functionName: String let agrs: [Expr] public init(_ functionName: String, _ agrs: [Expr]) { self.functionName = functionName self.agrs = agrs + bridge = FunctionExprBridge( + name: functionName, + args: self.agrs.map { ($0 as! (Expr & BridgeWrapper)).bridge + } + ) } } From 30b92512399c6c42c5f6790143677d599777d9bc Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 20 Mar 2025 15:56:15 -0400 Subject: [PATCH 13/43] solve conflicts --- .../AsyncAwait/Firestore+AsyncAwait.swift | 4 -- .../Source/SwiftAPI/Firestore+Pipeline.swift | 5 +++ .../Swift/Source/SwiftAPI/Pipeline.swift | 45 ------------------- .../SwiftAPI/Pipeline/Expr/Constant.swift | 29 +++++------- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 22 ++++++--- .../SwiftAPI/Pipeline/PipelineSource.swift | 4 +- .../Source/SwiftAPI/PipelineSource.swift | 28 ------------ .../Tests/Integration/PipelineTests.swift | 2 +- 8 files changed, 33 insertions(+), 106 deletions(-) delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline.swift delete mode 100644 Firestore/Swift/Source/SwiftAPI/PipelineSource.swift diff --git a/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift b/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift index f58e9e1d706..e85ca9a9791 100644 --- a/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift +++ b/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift @@ -115,8 +115,4 @@ public extension Firestore { } } } - - func pipeline() -> PipelineSource { - return PipelineSource(self) - } } diff --git a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift index 0179ece4e04..9d6a192176f 100644 --- a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift @@ -14,6 +14,11 @@ * limitations under the License. */ +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE import Foundation @objc public extension Firestore { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline.swift deleted file mode 100644 index 8c8a4364d30..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 67ade111d4a..1a3851e47ee 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -23,7 +23,8 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { let value: Any? - internal init(_ value: Any?) { + // Initializer for optional values (including nil) + public init(_ value: Any?) { self.value = value // TODO self.bridge = ConstantBridge(value) @@ -31,51 +32,41 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { // Initializer for numbers public init(_ value: Double) { - self.value = value + self.init(value as Any) } // Initializer for strings public init(_ value: String) { - self.value = value + self.init(value as Any) } // Initializer for boolean values public init(_ value: Bool) { - self.value = value + self.init(value as Any) } // Initializer for GeoPoint values public init(_ value: GeoPoint) { - self.value = value + self.init(value as Any) } // Initializer for Timestamp values public init(_ value: Timestamp) { - self.value = value + self.init(value as Any) } // Initializer for Date values public init(_ value: Date) { - self.value = value + self.init(value as Any) } // Initializer for DocumentReference public init(_ value: DocumentReference) { - self.value = value + self.init(value as Any) } // Initializer for vector values public init(_ value: VectorValue) { - self.value = value - } - - // Initializer for vector values - public init(_ vectorValue: [Double]) { - value = vectorValue - } - - // Initializer for optional values (including nil) - public init(_ value: Any?) { - self.value = value + self.init(value as Any) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index fc55cf046ad..bf6d8bbd9b4 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -21,18 +21,26 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct Pipeline { + private var stages: [Stage] + private var bridge: PipelineBridge let db: Firestore - init(_ db: Firestore) { + init(stages: [Stage], db: Firestore) { + self.stages = stages self.db = db + bridge = PipelineBridge(stages: stages.map { $0.bridge }, db: db) } public func execute() async throws -> PipelineSnapshot { - return PipelineSnapshot( - pipeline: self, - results: [], - executionTime: Timestamp(seconds: 0, nanoseconds: 0) - ) + 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!)) + } + } + } } /// Adds new fields to outputs from previous stages. @@ -120,7 +128,7 @@ public struct Pipeline { /// - Parameter condition: The `BooleanExpr` to apply. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func `where`(_ condition: BooleanExpr) -> Pipeline { - return self + return Pipeline(stages: stages + [Where(condition: condition)], db: db) } /// Skips the first `offset` number of documents from the results of previous stages. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 9d9e74caf48..40d812cba5a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -16,12 +16,12 @@ public struct PipelineSource { let db: Firestore - init(_ db: Firestore) { + init(db: Firestore) { self.db = db } public func collection(_ path: String) -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: path)], db: db) } public func collectionGroup(_ collectionId: String) -> Pipeline { diff --git a/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift deleted file mode 100644 index ce84c0356ac..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/PipelineSource.swift +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index a2252488312..869514e008b 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -24,7 +24,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let snapshot = try await firestore() .pipeline() .collection(path: "/foo") - .where(eq(field("foo"), constant("bar"))) + .where(Field("foo") == Constant("bar")) .execute() print(snapshot) From 1ad093a6e51cb13ab5bb22abbd3b598eaa443d5c Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 20 Mar 2025 18:51:09 -0400 Subject: [PATCH 14/43] format and fix conflicts --- Firestore/Swift/Source/BridgeWrapper.swift | 3 +-- .../Swift/Source/SwiftAPI/Firestore+Pipeline.swift | 3 ++- .../Source/SwiftAPI/Pipeline/Expr/Constant.swift | 6 +++--- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 4 ++-- .../SwiftAPI/Pipeline/Expr/FunctionExpr.swift | 2 +- .../Source/SwiftAPI/Pipeline/PipelineSource.swift | 14 +++++++------- .../Swift/Tests/Integration/PipelineTests.swift | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Firestore/Swift/Source/BridgeWrapper.swift b/Firestore/Swift/Source/BridgeWrapper.swift index b37f3718331..8b4d13fc9a4 100644 --- a/Firestore/Swift/Source/BridgeWrapper.swift +++ b/Firestore/Swift/Source/BridgeWrapper.swift @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - -internal protocol BridgeWrapper { +protocol BridgeWrapper { var bridge: ExprBridge { get } } diff --git a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift index 9d6a192176f..e35a9bceac5 100644 --- a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift @@ -22,7 +22,8 @@ import Foundation @objc public extension Firestore { + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @nonobjc func pipeline() -> PipelineSource { - return PipelineSource(db: self) + return PipelineSource(self) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 1a3851e47ee..0a5805aa20c 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -22,12 +22,12 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { var bridge: ExprBridge let value: Any? - + // Initializer for optional values (including nil) public init(_ value: Any?) { self.value = value - // TODO - self.bridge = ConstantBridge(value) + // TODO: + bridge = ConstantBridge(value) } // Initializer for numbers diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift index ba04fd3c406..4cb63076fb3 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -14,7 +14,7 @@ public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, @unchecked Sendable { var bridge: ExprBridge - + public var alias: String public let fieldName: String @@ -22,7 +22,7 @@ public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, @unchecked Send public init(_ fieldName: String) { self.fieldName = fieldName alias = fieldName - self.bridge = FieldBridge(alias) + bridge = FieldBridge(alias) } public var expr: any Expr { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift index 742455374db..2726caba284 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -14,7 +14,7 @@ public class FunctionExpr: Expr, BridgeWrapper, @unchecked Sendable { var bridge: ExprBridge - + let functionName: String let agrs: [Expr] diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 40d812cba5a..49e757d2c61 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -16,7 +16,7 @@ public struct PipelineSource { let db: Firestore - init(db: Firestore) { + init(_ db: Firestore) { self.db = db } @@ -25,26 +25,26 @@ public struct PipelineSource { } public func collectionGroup(_ collectionId: String) -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) } public func database() -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) } public func documents(_ docs: [DocumentReference]) -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) } public func documents(_ paths: [String]) -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) } public func create(from query: Query) -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) } public func create(from aggregateQuery: AggregateQuery) -> Pipeline { - return Pipeline(db) + return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 869514e008b..1ee1bd688ce 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -23,7 +23,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { try await firestore().collection("foo").document("bar").setData(["foo": "bar", "x": 42]) let snapshot = try await firestore() .pipeline() - .collection(path: "/foo") + .collection("/foo") .where(Field("foo") == Constant("bar")) .execute() From b59a4fd639fe838d5dbf7c05e51673d1eaf7f08d Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Fri, 21 Mar 2025 16:07:35 -0400 Subject: [PATCH 15/43] Remove placeholder for the test case --- Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift | 2 +- .../SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 6fe62ac5810..0d133a369d6 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -798,7 +798,7 @@ public func <= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> Boole } public func == (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { - try BooleanExpr("eq", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("eq", [lhs, rhs() as! Expr]) } public func != (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift index e4964095e14..1f6b6af67a3 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift @@ -13,7 +13,7 @@ // limitations under the License. public class BooleanExpr: FunctionExpr, @unchecked Sendable { - override public init(_ functionName: String, _ agrs: [any Expr]) { + override public init(_ functionName: String, _ agrs: [Expr]) { super.init(functionName, agrs) } From 7d73155c9cd69ce12fc92612c4886897678d9ac3 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 25 Mar 2025 01:11:17 -0400 Subject: [PATCH 16/43] Support tests --- Firestore/Source/API/FIRPipelineBridge.mm | 40 ++++++++++++++++--- .../FirebaseFirestore/FIRPipelineBridge.h | 29 +++++++++----- .../Swift/Source/Helper/PipelineHelper.swift | 3 ++ ...Options.swift => SelectableInternal.swift} | 19 ++------- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 2 +- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 13 +++--- .../SwiftAPI/Pipeline/ExprWithAlias.swift | 4 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 14 +++---- .../SwiftAPI/Pipeline/PipelineResult.swift | 19 +++++---- .../SwiftAPI/Pipeline/PipelineSnapshot.swift | 9 +++-- .../Source/SwiftAPI/Pipeline/Selectable.swift | 5 +-- .../Source/SwiftAPI/PipelineSnapshot.swift | 25 ------------ .../Tests/Integration/PipelineApiTests.swift | 7 +--- Firestore/core/src/api/pipeline_result.h | 8 ++++ Firestore/core/src/api/pipeline_snapshot.h | 4 ++ 15 files changed, 106 insertions(+), 95 deletions(-) rename Firestore/Swift/Source/{SwiftAPI/Pipeline/SampleOptions.swift => SelectableInternal.swift} (62%) delete mode 100644 Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index c10a05f4d88..355a31a123e 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -16,6 +16,8 @@ #import "FIRPipelineBridge.h" +#import + #include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" @@ -23,6 +25,7 @@ #import "Firestore/Source/API/FIRPipelineBridge+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" #import "Firestore/Source/API/FSTUserDataWriter.h" +#import "Firestore/Source/API/converters.h" #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" @@ -42,6 +45,7 @@ using firebase::firestore::api::Expr; using firebase::firestore::api::Field; using firebase::firestore::api::FunctionExpr; +using firebase::firestore::api::MakeFIRTimestamp; using firebase::firestore::api::Pipeline; using firebase::firestore::api::Where; using firebase::firestore::util::MakeCallback; @@ -173,7 +177,7 @@ - (id)initWithExpr:(FIRExprBridge *)expr { @interface __FIRPipelineSnapshotBridge () -@property(nonatomic, strong, readwrite) NSArray<__FIRPipelineSnapshotBridge *> *results; +@property(nonatomic, strong, readwrite) NSArray<__FIRPipelineResultBridge *> *results; @end @@ -206,6 +210,14 @@ - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { return results_; } +- (FIRTimestamp *)execution_time { + if (!snapshot_.has_value()) { + return nil; + } else { + return MakeFIRTimestamp(snapshot_.value().execution_time().timestamp()); + } +} + @end @implementation __FIRPipelineResultBridge { @@ -213,13 +225,13 @@ @implementation __FIRPipelineResultBridge { std::shared_ptr _db; } -- (FIRDocumentReference *)reference { +- (nullable FIRDocumentReference *)reference { if (!_result.internal_key().has_value()) return nil; return [[FIRDocumentReference alloc] initWithKey:_result.internal_key().value() firestore:_db]; } -- (NSString *)documentID { +- (nullable NSString *)documentID { if (!_result.document_id().has_value()) { return nil; } @@ -227,6 +239,22 @@ - (NSString *)documentID { return MakeNSString(_result.document_id().value()); } +- (nullable FIRTimestamp *)create_time { + if (!_result.create_time().has_value()) { + return nil; + } + + return MakeFIRTimestamp(_result.create_time().value().timestamp()); +} + +- (nullable FIRTimestamp *)update_time { + if (!_result.update_time().has_value()) { + return nil; + } + + return MakeFIRTimestamp(_result.update_time().value().timestamp()); +} + - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr)db { self = [super init]; if (self) { @@ -237,15 +265,15 @@ - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr *)data { +- (NSDictionary *)data { return [self dataWithServerTimestampBehavior:FIRServerTimestampBehaviorNone]; } -- (nullable NSDictionary *)dataWithServerTimestampBehavior: +- (NSDictionary *)dataWithServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior { absl::optional data = _result.internal_value()->Get(); - if (!data) return nil; + if (!data) return [NSDictionary dictionary]; FSTUserDataWriter *dataWriter = [[FSTUserDataWriter alloc] initWithFirestore:_db diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index a27b2b7aa18..a096da52531 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -20,6 +20,8 @@ #import "FIRDocumentSnapshot.h" +@class FIRTimestamp; + NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(ExprBridge) @@ -59,26 +61,33 @@ NS_SWIFT_NAME(WhereStageBridge) @end -NS_SWIFT_NAME(__PipelineSnapshotBridge) -@interface __FIRPipelineSnapshotBridge : NSObject +NS_SWIFT_NAME(__PipelineResultBridge) +@interface __FIRPipelineResultBridge : NSObject -@property(nonatomic, strong, readonly) NSArray<__FIRPipelineSnapshotBridge *> *results; +@property(nonatomic, strong, readonly, nullable) FIRDocumentReference *reference; -@end +@property(nonatomic, copy, readonly, nullable) NSString *documentID; -NS_SWIFT_NAME(__PipelineResultBridge) -@interface __FIRPipelineResultBridge : NSObject +@property(nonatomic, strong, readonly, nullable) FIRTimestamp *create_time; -@property(nonatomic, strong, readonly) FIRDocumentReference *reference; +@property(nonatomic, strong, readonly, nullable) FIRTimestamp *update_time; -@property(nonatomic, copy, readonly) NSString *documentID; +- (NSDictionary *)data; -- (nullable NSDictionary *)data; -- (nullable NSDictionary *)dataWithServerTimestampBehavior: +- (NSDictionary *)dataWithServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior; @end +NS_SWIFT_NAME(__PipelineSnapshotBridge) +@interface __FIRPipelineSnapshotBridge : NSObject + +@property(nonatomic, strong, readonly) NSArray<__FIRPipelineResultBridge *> *results; + +@property(nonatomic, strong, readonly) FIRTimestamp *execution_time; + +@end + NS_SWIFT_NAME(PipelineBridge) @interface FIRPipelineBridge : NSObject diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift index 8746c676507..8e1518ec6e4 100644 --- a/Firestore/Swift/Source/Helper/PipelineHelper.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -14,6 +14,9 @@ enum Helper { static func valueToDefaultExpr(_ value: Any) -> any Expr { + if value is Constant { + return value as! Expr + } return Field("PLACEHOLDER") } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift b/Firestore/Swift/Source/SelectableInternal.swift similarity index 62% rename from Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift rename to Firestore/Swift/Source/SelectableInternal.swift index c5965482899..8f35e738dec 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/SampleOptions.swift +++ b/Firestore/Swift/Source/SelectableInternal.swift @@ -12,20 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct SampleOption { - let percentage: Double? - let count: Int64? - - private init(percentage: Double?, count: Int64?) { - self.percentage = percentage - self.count = count - } - - public init(percentage: Double) { - self.init(percentage: percentage, count: nil) - } - - public init(count: Int64) { - self.init(percentage: nil, count: count) - } +protocol SelectableInternal: Sendable { + var alias: String { get } + var expr: Expr { get } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 0d133a369d6..6fe62ac5810 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -798,7 +798,7 @@ public func <= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> Boole } public func == (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { - try BooleanExpr("eq", [lhs, rhs() as! Expr]) + try BooleanExpr("eq", [lhs, Helper.valueToDefaultExpr(rhs())]) } public func != (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift index 4cb63076fb3..d8c9dcd4961 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -12,10 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, @unchecked Sendable { +public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, SelectableInternal, + @unchecked Sendable { var bridge: ExprBridge - public var alias: String + var alias: String + + var expr: any Expr { + return self + } public let fieldName: String @@ -24,8 +29,4 @@ public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, @unchecked Send alias = fieldName bridge = FieldBridge(alias) } - - public var expr: any Expr { - return self - } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift index 7a9ba5b8620..24b3cb956d3 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -15,9 +15,9 @@ public struct ExprWithAlias: Selectable { public var alias: String - public var expr: any Expr + public var expr: Expr - init(_ expr: some Expr, _ alias: String) { + init(_ expr: Expr, _ alias: String) { self.alias = alias self.expr = expr } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index bf6d8bbd9b4..6f0f7eaef20 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -37,7 +37,7 @@ public struct Pipeline { if let error { continuation.resume(throwing: error) } else { - continuation.resume(returning: PipelineSnapshot(result!)) + continuation.resume(returning: PipelineSnapshot(result!, pipeline: self)) } } } @@ -288,9 +288,9 @@ public struct Pipeline { /// This stage allows you to emit a map value as a document. Each key of the map becomes a /// field on the document that contains the corresponding value. /// - /// - Parameter field: The `Selectable` field containing the nested map. + /// - Parameter field: The `Expr` field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func replace(with field: Selectable) -> Pipeline { + public func replace(with expr: Expr) -> Pipeline { // Implementation return self } @@ -312,9 +312,9 @@ public struct Pipeline { /// This stage will filter documents pseudo-randomly. The parameter specifies how number of /// documents to be returned. /// - /// - Parameter documents: The number of documents to sample. + /// - Parameter count: The number of documents to sample. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func sample(_ count: Int64) -> Pipeline { + public func sample(count: Int64) -> Pipeline { // Implementation return self } @@ -324,9 +324,9 @@ public struct Pipeline { /// This stage will filter documents pseudo-randomly. The `options` parameter specifies how /// sampling will be performed. See `SampleOptions` for more information. /// - /// - Parameter options: The `SampleOptions` specifies how sampling is performed. + /// - Parameter percentage: The percentage of documents to sample. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func sample(with option: SampleOption) -> Pipeline { + public func sample(percentage: Double) -> Pipeline { // Implementation return self } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift index a4556815f71..2afa66e0d8d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -21,16 +21,15 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct PipelineResult { - init(ref: DocumentReference? = nil, - id: String? = nil, - createTime: Timestamp? = nil, - updateTime: Timestamp? = nil, - data: [String: Any] = [:]) { - self.ref = ref - self.id = id - self.createTime = createTime - self.updateTime = updateTime - self.data = data + let bridge: __PipelineResultBridge + + init(_ bridge: __PipelineResultBridge) { + self.bridge = bridge + ref = self.bridge.reference + id = self.bridge.documentID + data = self.bridge.data() + createTime = self.bridge.create_time + updateTime = self.bridge.update_time } /// The reference of the document, if the query returns the `__name__` field. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift index 69f695d3568..5395ecee926 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift @@ -23,9 +23,12 @@ public struct PipelineSnapshot { /// The time at which the pipeline producing this result was executed. public let executionTime: Timestamp - init(pipeline: Pipeline, results: [PipelineResult], executionTime: Timestamp) { + let bridge: __PipelineSnapshotBridge + + init(_ bridge: __PipelineSnapshotBridge, pipeline: Pipeline) { + self.bridge = bridge self.pipeline = pipeline - self.results = results - self.executionTime = executionTime + executionTime = self.bridge.execution_time + results = self.bridge.results.map { PipelineResult($0) } } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift index d321c821aba..a9c655f4e6a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift @@ -12,7 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -public protocol Selectable: Sendable { - var alias: String { get } - var expr: Expr { get } -} +public protocol Selectable: Sendable {} diff --git a/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift deleted file mode 100644 index 00386d0c6dc..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/PipelineSnapshot.swift +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index a8d795b7dbd..e6278016cb9 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -219,13 +219,10 @@ final class PipelineTests: FSTIntegrationTestCase { func testSampleStage() async throws { // Sample 25 books, if the collection contains at least 25 documents - _ = db.pipeline().collection("books").sample(25) - - // Sample 25 books, if the collection contains at least 25 documents - _ = db.pipeline().collection("books").sample(with: SampleOption(count: 10)) + _ = db.pipeline().collection("books").sample(count: 10) // Sample 10 percent of the collection of books - _ = db.pipeline().collection("books").sample(with: SampleOption(percentage: 10)) + _ = db.pipeline().collection("books").sample(percentage: 10) } func testUnionStage() async throws { diff --git a/Firestore/core/src/api/pipeline_result.h b/Firestore/core/src/api/pipeline_result.h index 53761752cdc..662ea721c6b 100644 --- a/Firestore/core/src/api/pipeline_result.h +++ b/Firestore/core/src/api/pipeline_result.h @@ -53,6 +53,14 @@ class PipelineResult { std::shared_ptr internal_value() const; absl::optional document_id() const; + absl::optional create_time() const { + return create_time_; + } + + absl::optional update_time() const { + return update_time_; + } + const absl::optional& internal_key() const { return internal_key_; } diff --git a/Firestore/core/src/api/pipeline_snapshot.h b/Firestore/core/src/api/pipeline_snapshot.h index 079f2d57375..2bb0a1e94d2 100644 --- a/Firestore/core/src/api/pipeline_snapshot.h +++ b/Firestore/core/src/api/pipeline_snapshot.h @@ -41,6 +41,10 @@ class PipelineSnapshot { return results_; } + model::SnapshotVersion execution_time() const { + return execution_time_; + } + const std::shared_ptr firestore() const { return firestore_; } From 828bb12e2011eb61bc2e502680f5af799f8f57b7 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 27 Mar 2025 11:45:50 -0400 Subject: [PATCH 17/43] Fix bug in decoding --- Firestore/Source/API/FIRPipelineBridge.mm | 1 + Firestore/core/src/remote/serializer.cc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 355a31a123e..6ee88617ef5 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -194,6 +194,7 @@ - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { results_ = nil; } else { NSMutableArray<__FIRPipelineResultBridge *> *results = [NSMutableArray array]; + auto &cpp_result = snapshot_.value().results(); for (auto &result : snapshot_.value().results()) { [results addObject:[[__FIRPipelineResultBridge alloc] initWithCppResult:result diff --git a/Firestore/core/src/remote/serializer.cc b/Firestore/core/src/remote/serializer.cc index 2985f86d623..5dffd848c09 100644 --- a/Firestore/core/src/remote/serializer.cc +++ b/Firestore/core/src/remote/serializer.cc @@ -1514,7 +1514,8 @@ api::PipelineSnapshot Serializer::DecodePipelineResponse( const { auto execution_time = DecodeVersion(context, message->execution_time); - std::vector results(message->results_count); + std::vector results; + results.reserve(message->results_count); for (pb_size_t i = 0; i < message->results_count; ++i) { absl::optional key; From d1d0aa449837e24fc446cc9111191854e82fabec Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 1 Apr 2025 01:08:24 -0400 Subject: [PATCH 18/43] Add some convert functions --- .../FirebaseFirestore/FIRPipelineBridge.h | 10 ++ .../Swift/Source/Helper/PipelineHelper.swift | 25 +++- .../SwiftAPI/Pipeline/Expr/Constant.swift | 4 +- .../SwiftAPI/Pipeline/ExprWithAlias.swift | 2 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 2 +- .../SwiftAPI/Pipeline/PipelineResult.swift | 2 +- .../SwiftAPI/Pipeline/PipelineSnapshot.swift | 2 +- .../SwiftAPI/Pipeline/PipelineSource.swift | 2 +- .../SwiftAPI/Pipeline/RealtimePipeline.swift | 2 +- .../Tests/Integration/PipelineApiTests.swift | 2 +- .../Tests/Integration/PipelineTests.swift | 109 ++++++++++++++++++ 11 files changed, 152 insertions(+), 10 deletions(-) diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index a096da52531..ba730d7b4cb 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -24,29 +24,35 @@ NS_ASSUME_NONNULL_BEGIN +NS_SWIFT_SENDABLE NS_SWIFT_NAME(ExprBridge) @interface FIRExprBridge : NSObject @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(FieldBridge) @interface FIRFieldBridge : FIRExprBridge - (id)init:(NSString *)name; @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(ConstantBridge) @interface FIRConstantBridge : FIRExprBridge - (id)init:(id)input; @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(FunctionExprBridge) @interface FIRFunctionExprBridge : FIRExprBridge - (id)initWithName:(NSString *)name Args:(NSArray *)args; @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(StageBridge) @interface FIRStageBridge : NSObject @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(CollectionSourceStageBridge) @interface FIRCollectionSourceStageBridge : FIRStageBridge @@ -54,6 +60,7 @@ NS_SWIFT_NAME(CollectionSourceStageBridge) @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(WhereStageBridge) @interface FIRWhereStageBridge : FIRStageBridge @@ -61,6 +68,7 @@ NS_SWIFT_NAME(WhereStageBridge) @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(__PipelineResultBridge) @interface __FIRPipelineResultBridge : NSObject @@ -79,6 +87,7 @@ NS_SWIFT_NAME(__PipelineResultBridge) @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(__PipelineSnapshotBridge) @interface __FIRPipelineSnapshotBridge : NSObject @@ -88,6 +97,7 @@ NS_SWIFT_NAME(__PipelineSnapshotBridge) @end +NS_SWIFT_SENDABLE NS_SWIFT_NAME(PipelineBridge) @interface FIRPipelineBridge : NSObject diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift index 8e1518ec6e4..d2eed20cd83 100644 --- a/Firestore/Swift/Source/Helper/PipelineHelper.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -14,10 +14,15 @@ enum Helper { static func valueToDefaultExpr(_ value: Any) -> any Expr { - if value is Constant { + if value is Expr { return value as! Expr + } else if value is [String: Any] { + return map(value as! [String: Any]) + } else if value is [Any] { + return array(value as! [Any]) + } else { + return Constant(value) } - return Field("PLACEHOLDER") } static func vectorToExpr(_ value: VectorValue) -> any Expr { @@ -27,4 +32,20 @@ enum Helper { static func timeUnitToExpr(_ value: TimeUnit) -> any Expr { return Field("PLACEHOLDER") } + + static func map(_ elements: [String: Any]) -> FunctionExpr { + var result: [Expr] = [] + for (key, value) in elements { + result.append(Constant(key)) + result.append(valueToDefaultExpr(value)) + } + return FunctionExpr("map", result) + } + + static func array(_ elements: [Any]) -> FunctionExpr { + let transformedElements = elements.map { element in + valueToDefaultExpr(element) + } + return FunctionExpr("array", transformedElements) + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 0a5805aa20c..5d3109a5fcb 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -24,7 +24,7 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { let value: Any? // Initializer for optional values (including nil) - public init(_ value: Any?) { + init(_ value: Any?) { self.value = value // TODO: bridge = ConstantBridge(value) @@ -69,4 +69,6 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { public init(_ value: VectorValue) { self.init(value as Any) } + + public static let `nil` = Constant(nil) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift index 24b3cb956d3..af5447541ab 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct ExprWithAlias: Selectable { +public struct ExprWithAlias: Selectable, Sendable { public var alias: String public var expr: Expr diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 6f0f7eaef20..5480c3304cc 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -20,7 +20,7 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct Pipeline { +public struct Pipeline: @unchecked Sendable { private var stages: [Stage] private var bridge: PipelineBridge let db: Firestore diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift index 2afa66e0d8d..96ea35c5567 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -20,7 +20,7 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct PipelineResult { +public struct PipelineResult: @unchecked Sendable { let bridge: __PipelineResultBridge init(_ bridge: __PipelineResultBridge) { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift index 5395ecee926..097b3bc5eea 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift @@ -13,7 +13,7 @@ // limitations under the License. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct PipelineSnapshot { +public struct PipelineSnapshot: Sendable { /// The Pipeline on which `execute()` was called to obtain this `PipelineSnapshot`. public let pipeline: Pipeline diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 49e757d2c61..a9353c2594b 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -13,7 +13,7 @@ // limitations under the License. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct PipelineSource { +public struct PipelineSource: @unchecked Sendable { let db: Firestore init(_ db: Firestore) { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift index 672f8586caf..de1a709d44d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/RealtimePipeline.swift @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct RealtimePipeline {} +public struct RealtimePipeline: @unchecked Sendable {} diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index e6278016cb9..2e4b71ff0c5 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -308,7 +308,7 @@ final class PipelineTests: FSTIntegrationTestCase { let name = Constant("Expressions API") // Const is a sub-type of Expr, so we can also declare our var of type Expr - let nothing: Expr = Constant(nil) + let nothing: Expr = Constant.nil // USAGE: Anywhere an Expr type is accepted // Add field `fromTheLibraryOf: 'Rafi'` to every document in the collection. diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 1ee1bd688ce..4b7a7ceeb2a 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -17,6 +17,100 @@ import FirebaseFirestore import Foundation +private let bookDocs: [String: [String: Any]] = [ + "book1": [ + "title": "The Hitchhiker's Guide to the Galaxy", + "author": "Douglas Adams", + "genre": "Science Fiction", + "published": 1979, + "rating": 4.2, + "tags": ["comedy", "space", "adventure"], // Array literal + "awards": ["hugo": true, "nebula": false], // Dictionary literal + "nestedField": ["level.1": ["level.2": true]], // Nested dictionary literal + ], + "book2": [ + "title": "Pride and Prejudice", + "author": "Jane Austen", + "genre": "Romance", + "published": 1813, + "rating": 4.5, + "tags": ["classic", "social commentary", "love"], + "awards": ["none": true], + ], + "book3": [ + "title": "One Hundred Years of Solitude", + "author": "Gabriel García Márquez", + "genre": "Magical Realism", + "published": 1967, + "rating": 4.3, + "tags": ["family", "history", "fantasy"], + "awards": ["nobel": true, "nebula": false], + ], + "book4": [ + "title": "The Lord of the Rings", + "author": "J.R.R. Tolkien", + "genre": "Fantasy", + "published": 1954, + "rating": 4.7, + "tags": ["adventure", "magic", "epic"], + "awards": ["hugo": false, "nebula": false], + ], + "book5": [ + "title": "The Handmaid's Tale", + "author": "Margaret Atwood", + "genre": "Dystopian", + "published": 1985, + "rating": 4.1, + "tags": ["feminism", "totalitarianism", "resistance"], + "awards": ["arthur c. clarke": true, "booker prize": false], + ], + "book6": [ + "title": "Crime and Punishment", + "author": "Fyodor Dostoevsky", + "genre": "Psychological Thriller", + "published": 1866, + "rating": 4.3, + "tags": ["philosophy", "crime", "redemption"], + "awards": ["none": true], + ], + "book7": [ + "title": "To Kill a Mockingbird", + "author": "Harper Lee", + "genre": "Southern Gothic", + "published": 1960, + "rating": 4.2, + "tags": ["racism", "injustice", "coming-of-age"], + "awards": ["pulitzer": true], + ], + "book8": [ + "title": "1984", + "author": "George Orwell", + "genre": "Dystopian", + "published": 1949, + "rating": 4.2, + "tags": ["surveillance", "totalitarianism", "propaganda"], + "awards": ["prometheus": true], + ], + "book9": [ + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "genre": "Modernist", + "published": 1925, + "rating": 4.0, + "tags": ["wealth", "american dream", "love"], + "awards": ["none": true], + ], + "book10": [ + "title": "Dune", + "author": "Frank Herbert", + "genre": "Science Fiction", + "published": 1965, + "rating": 4.6, + "tags": ["politics", "desert", "ecology"], + "awards": ["hugo": true, "nebula": true], + ], +] + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class PipelineIntegrationTests: FSTIntegrationTestCase { func testCount() async throws { @@ -29,4 +123,19 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { print(snapshot) } + + func testEmptyResults() async throws { + let collRef = collectionRef( + withDocuments: bookDocs + ) + let db = collRef.firestore + + let snapshot = try await db + .pipeline() + .collection(collRef.collectionID) + .limit(0) + .execute() + + XCTAssertTrue(snapshot.results.isEmpty) + } } From 113da450e7f25dd8950e2460a6871156fe19f2ff Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 2 Apr 2025 15:07:38 -0400 Subject: [PATCH 19/43] add tests --- .gitignore | 1 + .../Tests/Util/FSTIntegrationTestCase.mm | 2 +- Firestore/Source/API/FIRPipelineBridge.mm | 115 ++++++++++++++++++ .../FirebaseFirestore/FIRPipelineBridge.h | 40 ++++++ .../Source/SwiftAPI/Pipeline/Pipeline.swift | 4 +- .../SwiftAPI/Pipeline/PipelineSource.swift | 5 +- Firestore/Swift/Source/SwiftAPI/Stages.swift | 42 ++++++- .../Tests/Integration/PipelineTests.swift | 2 +- Firestore/core/src/api/stages.h | 4 +- 9 files changed, 205 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index dc909fe4338..74607c5993b 100644 --- a/.gitignore +++ b/.gitignore @@ -155,6 +155,7 @@ FirebaseAppCheck/Apps/AppCheckCustomProvideApp/AppCheckCustomProvideApp/GoogleSe /Example/FirestoreSample/ui-debug.log /Example/FirestoreSample/firestore-debug.log /Example/FirestoreSample/firebase-debug.log +Firestore/Example/GoogleService-Info.plist # generated Terraform docs .terraform/* diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 4b7c7b9f034..b11a37b394c 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -85,7 +85,7 @@ static const double kPrimingTimeout = 45.0; static NSString *defaultProjectId; -static NSString *defaultDatabaseId = @"(default)"; +static NSString *defaultDatabaseId = @"enterprise"; static FIRFirestoreSettings *defaultSettings; static bool runningAgainstEmulator = false; diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 6ee88617ef5..713456ba3a1 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -39,13 +39,18 @@ #include "Firestore/core/src/util/status.h" #include "Firestore/core/src/util/string_apple.h" +using firebase::firestore::api::CollectionGroupSource; using firebase::firestore::api::CollectionSource; using firebase::firestore::api::Constant; +using firebase::firestore::api::DatabaseSource; using firebase::firestore::api::DocumentReference; +using firebase::firestore::api::DocumentsSource; using firebase::firestore::api::Expr; using firebase::firestore::api::Field; using firebase::firestore::api::FunctionExpr; +using firebase::firestore::api::LimitStage; using firebase::firestore::api::MakeFIRTimestamp; +using firebase::firestore::api::OffsetStage; using firebase::firestore::api::Pipeline; using firebase::firestore::api::Where; using firebase::firestore::util::MakeCallback; @@ -149,6 +154,64 @@ - (id)initWithPath:(NSString *)path { @end +@implementation FIRDatabaseSourceStageBridge { + std::shared_ptr database_source; +} + +- (id)init { + self = [super init]; + if (self) { + database_source = std::make_shared(); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + return database_source; +} + +@end + +@implementation FIRCollectionGroupSourceStageBridge { + std::shared_ptr collection_group_source; +} + +- (id)initWithCollectionId:(NSString *)id { + self = [super init]; + if (self) { + collection_group_source = std::make_shared(MakeString(id)); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + return collection_group_source; +} + +@end + +@implementation FIRDocumentsSourceStageBridge { + std::shared_ptr document_source; +} + +- (id)initWithDocuments:(NSArray *)documents { + self = [super init]; + if (self) { + std::vector cpp_documents; + for (NSString *doc in documents) { + cpp_documents.push_back(MakeString(doc)); + } + document_source = std::make_shared(std::move(cpp_documents)); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + return document_source; +} + +@end + @implementation FIRWhereStageBridge { FIRExprBridge *_exprBridge; Boolean isUserDataRead; @@ -175,6 +238,58 @@ - (id)initWithExpr:(FIRExprBridge *)expr { @end +@implementation FIRLimitStageBridge { + Boolean isUserDataRead; + std::shared_ptr limit_stage; + int32_t limit; +} + +- (id)initWithLimit:(NSInteger)value { + self = [super init]; + if (self) { + isUserDataRead = NO; + limit = static_cast(value); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + limit_stage = std::make_shared(limit); + } + + isUserDataRead = YES; + return limit_stage; +} + +@end + +@implementation FIROffsetStageBridge { + Boolean isUserDataRead; + std::shared_ptr offset_stage; + int32_t offset; +} + +- (id)initWithOffset:(NSInteger)value { + self = [super init]; + if (self) { + isUserDataRead = NO; + offset = static_cast(value); + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + offset_stage = std::make_shared(offset); + } + + isUserDataRead = YES; + return offset_stage; +} + +@end + @interface __FIRPipelineSnapshotBridge () @property(nonatomic, strong, readwrite) NSArray<__FIRPipelineResultBridge *> *results; diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index ba730d7b4cb..527e97a062a 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -60,6 +60,30 @@ NS_SWIFT_NAME(CollectionSourceStageBridge) @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DatabaseSourceStageBridge) +@interface FIRDatabaseSourceStageBridge : FIRStageBridge + +- (id)init; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(CollectionGroupSourceStageBridge) +@interface FIRCollectionGroupSourceStageBridge : FIRStageBridge + +- (id)initWithCollectionId:(NSString *)id; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DocumentsSourceStageBridge) +@interface FIRDocumentsSourceStageBridge : FIRStageBridge + +- (id)initWithDocuments:(NSArray *)documents; + +@end + NS_SWIFT_SENDABLE NS_SWIFT_NAME(WhereStageBridge) @interface FIRWhereStageBridge : FIRStageBridge @@ -68,6 +92,22 @@ NS_SWIFT_NAME(WhereStageBridge) @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(LimitStageBridge) +@interface FIRLimitStageBridge : FIRStageBridge + +- (id)initWithLimit:(NSInteger)value; + +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(OffsetStageBridge) +@interface FIROffsetStageBridge : FIRStageBridge + +- (id)initWithOffset:(NSInteger)value; + +@end + NS_SWIFT_SENDABLE NS_SWIFT_NAME(__PipelineResultBridge) @interface __FIRPipelineResultBridge : NSObject diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 5480c3304cc..5abf45afcfc 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -141,7 +141,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter offset: The number of documents to skip. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func offset(_ offset: Int32) -> Pipeline { - return self + return Pipeline(stages: stages + [Offset(offset)], db: db) } /// Limits the maximum number of documents returned by previous stages to `limit`. @@ -157,7 +157,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter limit: The maximum number of documents to return. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func limit(_ limit: Int32) -> Pipeline { - return self + return Pipeline(stages: stages + [Limit(limit)], db: db) } /// Returns a set of distinct `Expr` values from the inputs to this stage. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index a9353c2594b..68ee48c6f6d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -25,7 +25,10 @@ public struct PipelineSource: @unchecked Sendable { } public func collectionGroup(_ collectionId: String) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) + return Pipeline( + stages: [CollectionGroupSource(collectionId: collectionId)], + db: db + ) } public func database() -> Pipeline { diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index c5de0c00e52..1ed077409e5 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -34,14 +34,50 @@ class CollectionSource: Stage { } } +class CollectionGroupSource: Stage { + var name: String = "collectionId" + + var bridge: StageBridge + private var collectionId: String + + init(collectionId: String) { + self.collectionId = collectionId + bridge = CollectionGroupSourceStageBridge(collectionId: collectionId) + } +} + class Where: Stage { var name: String = "where" var bridge: StageBridge - private var condition: Expr // TODO: should be FilterCondition + private var condition: BooleanExpr - init(condition: Expr) { + init(condition: BooleanExpr) { self.condition = condition - bridge = WhereStageBridge(expr: (condition as! (Expr & BridgeWrapper)).bridge) + bridge = WhereStageBridge(expr: condition.bridge) + } +} + +class Limit: Stage { + var name: String = "limit" + + var bridge: StageBridge + private var limit: Int32 + + init(_ limit: Int32) { + self.limit = limit + bridge = LimitStageBridge(limit: NSInteger(limit)) + } +} + +class Offset: Stage { + var name: String = "offset" + + var bridge: StageBridge + private var offset: Int32 + + init(_ offset: Int32) { + self.offset = offset + bridge = OffsetStageBridge(offset: NSInteger(offset)) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 4b7a7ceeb2a..b0a96207017 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -132,7 +132,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let snapshot = try await db .pipeline() - .collection(collRef.collectionID) + .collection("/" + collRef.path) .limit(0) .execute() diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index 11534278002..f2c20f8bd9e 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -169,14 +169,14 @@ class FindNearestStage : public Stage { class LimitStage : public Stage { public: - explicit LimitStage(int64_t limit) : limit_(limit) { + explicit LimitStage(int32_t limit) : limit_(limit) { } ~LimitStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: - int64_t limit_; + int32_t limit_; }; class OffsetStage : public Stage { From 39803e59531db3d3b6601359106d4e12167230b3 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 2 Apr 2025 17:03:29 -0400 Subject: [PATCH 20/43] replace Any with sendable --- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 115 +++++++++--------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 6fe62ac5810..8cb92042303 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -8,7 +8,7 @@ // // 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. +// WITHOUT WARRANTIES OR CONDITIONS OF Sendable KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. @@ -25,46 +25,46 @@ public protocol Expr: Sendable { // MARK: Arithmetic Operators func add(_ second: Expr, _ others: Expr...) -> FunctionExpr - func add(_ second: Any, _ others: Any...) -> FunctionExpr + func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr func subtract(_ other: Expr) -> FunctionExpr - func subtract(_ other: Any) -> FunctionExpr + func subtract(_ other: Sendable) -> FunctionExpr func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr - func multiply(_ second: Any, _ others: Any...) -> FunctionExpr + func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr func divide(_ other: Expr) -> FunctionExpr - func divide(_ other: Any) -> FunctionExpr + func divide(_ other: Sendable) -> FunctionExpr func mod(_ other: Expr) -> FunctionExpr - func mod(_ other: Any) -> FunctionExpr + func mod(_ other: Sendable) -> FunctionExpr // MARK: Array Operations func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr - func arrayConcat(_ secondArray: [Any], _ otherArrays: [Any]...) -> FunctionExpr + func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr func arrayContains(_ element: Expr) -> BooleanExpr - func arrayContains(_ element: Any) -> BooleanExpr + func arrayContains(_ element: Sendable) -> BooleanExpr func arrayContainsAll(_ values: Expr...) -> BooleanExpr - func arrayContainsAll(_ values: Any...) -> BooleanExpr + func arrayContainsAll(_ values: Sendable...) -> BooleanExpr - func arrayContainsAny(_ values: Expr...) -> BooleanExpr - func arrayContainsAny(_ values: Any...) -> BooleanExpr + func arrayContainsSendable(_ values: Expr...) -> BooleanExpr + func arrayContainsSendable(_ values: Sendable...) -> BooleanExpr func arrayLength() -> FunctionExpr func arrayOffset(_ offset: Int) -> FunctionExpr func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr - // MARK: Equality with Any + // MARK: Equality with Sendable - func eqAny(_ others: Expr...) -> BooleanExpr - func eqAny(_ others: Any...) -> BooleanExpr + func eqSendable(_ others: Expr...) -> BooleanExpr + func eqSendable(_ others: Sendable...) -> BooleanExpr - func notEqAny(_ others: Expr...) -> BooleanExpr - func notEqAny(_ others: Any...) -> BooleanExpr + func notEqSendable(_ others: Expr...) -> BooleanExpr + func notEqSendable(_ others: Sendable...) -> BooleanExpr // MARK: Checks @@ -120,7 +120,7 @@ public protocol Expr: Sendable { func mapGet(_ subfield: String) -> FunctionExpr func mapRemove(_ key: String) -> FunctionExpr func mapRemove(_ keyExpr: Expr) -> FunctionExpr - func mapMerge(_ secondMap: [String: Any], _ otherMaps: [String: Any]...) -> FunctionExpr + func mapMerge(_ secondMap: [String: Sendable], _ otherMaps: [String: Sendable]...) -> FunctionExpr func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr // MARK: Aggregations @@ -134,10 +134,10 @@ public protocol Expr: Sendable { // MARK: Logical min/max func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr - func logicalMaximum(_ second: Any, _ others: Any...) -> FunctionExpr + func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr - func logicalMinimum(_ second: Any, _ others: Any...) -> FunctionExpr + func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr // MARK: Vector Operations @@ -195,7 +195,7 @@ public protocol Expr: Sendable { func bitRightShift(_ numberExpr: Expr) -> FunctionExpr func ifError(_ catchExpr: Expr) -> FunctionExpr - func ifError(_ catchValue: Any) -> FunctionExpr + func ifError(_ catchValue: Sendable) -> FunctionExpr // MARK: Sorting @@ -214,7 +214,7 @@ public extension Expr { return BooleanExpr("eq", [self, other]) } - func eq(_ other: Any) -> BooleanExpr { + func eq(_ other: Sendable) -> BooleanExpr { return BooleanExpr("eq", [self, Helper.valueToDefaultExpr(other)]) } @@ -222,7 +222,7 @@ public extension Expr { return BooleanExpr("neq", [self, other]) } - func neq(_ other: Any) -> BooleanExpr { + func neq(_ other: Sendable) -> BooleanExpr { return BooleanExpr("neq", [self, Helper.valueToDefaultExpr(other)]) } @@ -230,7 +230,7 @@ public extension Expr { return BooleanExpr("lt", [self, other]) } - func lt(_ other: Any) -> BooleanExpr { + func lt(_ other: Sendable) -> BooleanExpr { return BooleanExpr("lt", [self, Helper.valueToDefaultExpr(other)]) } @@ -238,7 +238,7 @@ public extension Expr { return BooleanExpr("lte", [self, other]) } - func lte(_ other: Any) -> BooleanExpr { + func lte(_ other: Sendable) -> BooleanExpr { return BooleanExpr("lte", [self, Helper.valueToDefaultExpr(other)]) } @@ -246,7 +246,7 @@ public extension Expr { return BooleanExpr("gt", [self, other]) } - func gt(_ other: Any) -> BooleanExpr { + func gt(_ other: Sendable) -> BooleanExpr { return BooleanExpr("gt", [self, Helper.valueToDefaultExpr(other)]) } @@ -254,7 +254,7 @@ public extension Expr { return BooleanExpr("gte", [self, other]) } - func gte(_ other: Any) -> BooleanExpr { + func gte(_ other: Sendable) -> BooleanExpr { return BooleanExpr("gte", [self, Helper.valueToDefaultExpr(other)]) } @@ -264,7 +264,7 @@ public extension Expr { return FunctionExpr("add", [self, second] + others) } - func add(_ second: Any, _ others: Any...) -> FunctionExpr { + func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others .map { Helper.valueToDefaultExpr($0) } return FunctionExpr("add", exprs) @@ -274,7 +274,7 @@ public extension Expr { return FunctionExpr("subtract", [self, other]) } - func subtract(_ other: Any) -> FunctionExpr { + func subtract(_ other: Sendable) -> FunctionExpr { return FunctionExpr("subtract", [self, Helper.valueToDefaultExpr(other)]) } @@ -282,7 +282,7 @@ public extension Expr { return FunctionExpr("multiply", [self, second] + others) } - func multiply(_ second: Any, _ others: Any...) -> FunctionExpr { + func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others .map { Helper.valueToDefaultExpr($0) } return FunctionExpr("multiply", exprs) @@ -292,7 +292,7 @@ public extension Expr { return FunctionExpr("divide", [self, other]) } - func divide(_ other: Any) -> FunctionExpr { + func divide(_ other: Sendable) -> FunctionExpr { return FunctionExpr("divide", [self, Helper.valueToDefaultExpr(other)]) } @@ -300,7 +300,7 @@ public extension Expr { return FunctionExpr("mod", [self, other]) } - func mod(_ other: Any) -> FunctionExpr { + func mod(_ other: Sendable) -> FunctionExpr { return FunctionExpr("mod", [self, Helper.valueToDefaultExpr(other)]) } @@ -310,7 +310,7 @@ public extension Expr { return FunctionExpr("array_concat", [self, secondArray] + otherArrays) } - func arrayConcat(_ secondArray: [Any], _ otherArrays: [Any]...) -> FunctionExpr { + func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr { let exprs = [self] + [Helper.valueToDefaultExpr(secondArray)] + otherArrays .map { Helper.valueToDefaultExpr($0) } return FunctionExpr("array_concat", exprs) @@ -320,7 +320,7 @@ public extension Expr { return BooleanExpr("array_contains", [self, element]) } - func arrayContains(_ element: Any) -> BooleanExpr { + func arrayContains(_ element: Sendable) -> BooleanExpr { return BooleanExpr("array_contains", [self, Helper.valueToDefaultExpr(element)]) } @@ -328,18 +328,18 @@ public extension Expr { return BooleanExpr("array_contains_all", [self] + values) } - func arrayContainsAll(_ values: Any...) -> BooleanExpr { + func arrayContainsAll(_ values: Sendable...) -> BooleanExpr { let exprValues = values.map { Helper.valueToDefaultExpr($0) } return BooleanExpr("array_contains_all", [self] + exprValues) } - func arrayContainsAny(_ values: Expr...) -> BooleanExpr { - return BooleanExpr("array_contains_any", [self] + values) + func arrayContainsSendable(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_Sendable", [self] + values) } - func arrayContainsAny(_ values: Any...) -> BooleanExpr { + func arrayContainsSendable(_ values: Sendable...) -> BooleanExpr { let exprValues = values.map { Helper.valueToDefaultExpr($0) } - return BooleanExpr("array_contains_any", [self] + exprValues) + return BooleanExpr("array_contains_Sendable", [self] + exprValues) } func arrayLength() -> FunctionExpr { @@ -354,24 +354,24 @@ public extension Expr { return FunctionExpr("array_offset", [self, offsetExpr]) } - // MARK: Equality with Any + // MARK: Equality with Sendable - func eqAny(_ others: Expr...) -> BooleanExpr { - return BooleanExpr("eq_any", [self] + others) + func eqSendable(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("eq_Sendable", [self] + others) } - func eqAny(_ others: Any...) -> BooleanExpr { + func eqSendable(_ others: Sendable...) -> BooleanExpr { let exprOthers = others.map { Helper.valueToDefaultExpr($0) } - return BooleanExpr("eq_any", [self] + exprOthers) + return BooleanExpr("eq_Sendable", [self] + exprOthers) } - func notEqAny(_ others: Expr...) -> BooleanExpr { - return BooleanExpr("not_eq_any", [self] + others) + func notEqSendable(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("not_eq_Sendable", [self] + others) } - func notEqAny(_ others: Any...) -> BooleanExpr { + func notEqSendable(_ others: Sendable...) -> BooleanExpr { let exprOthers = others.map { Helper.valueToDefaultExpr($0) } - return BooleanExpr("not_eq_any", [self] + exprOthers) + return BooleanExpr("not_eq_Sendable", [self] + exprOthers) } // MARK: Checks @@ -541,7 +541,8 @@ public extension Expr { return FunctionExpr("map_remove", [self, keyExpr]) } - func mapMerge(_ secondMap: [String: Any], _ otherMaps: [String: Any]...) -> FunctionExpr { + func mapMerge(_ secondMap: [String: Sendable], + _ otherMaps: [String: Sendable]...) -> FunctionExpr { let secondMapExpr = Helper.valueToDefaultExpr(secondMap) let otherMapExprs = otherMaps.map { Helper.valueToDefaultExpr($0) } return FunctionExpr("map_merge", [self, secondMapExpr] + otherMapExprs) @@ -579,7 +580,7 @@ public extension Expr { return FunctionExpr("logical_maximum", [self, second] + others) } - func logicalMaximum(_ second: Any, _ others: Any...) -> FunctionExpr { + func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others .map { Helper.valueToDefaultExpr($0) } return FunctionExpr("logical_maximum", exprs) @@ -589,7 +590,7 @@ public extension Expr { return FunctionExpr("logical_min", [self, second] + others) } - func logicalMinimum(_ second: Any, _ others: Any...) -> FunctionExpr { + func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others .map { Helper.valueToDefaultExpr($0) } return FunctionExpr("logical_min", exprs) @@ -763,7 +764,7 @@ public extension Expr { return FunctionExpr("if_error", [self, catchExpr]) } - func ifError(_ catchValue: Any) -> FunctionExpr { + func ifError(_ catchValue: Sendable) -> FunctionExpr { return FunctionExpr("if_error", [self, Helper.valueToDefaultExpr(catchValue)]) } @@ -781,26 +782,26 @@ public extension Expr { // protocal cannot overwrite operator, since every inheritated class will have this function // it will lead to error: Generic parameter 'Self' could not be inferred -public func > (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { +public func > (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { try BooleanExpr("gt", [lhs, Helper.valueToDefaultExpr(rhs())]) } -public func >= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { +public func >= (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { try BooleanExpr("gte", [lhs, Helper.valueToDefaultExpr(rhs())]) } -public func < (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { +public func < (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { try BooleanExpr("lt", [lhs, Helper.valueToDefaultExpr(rhs())]) } -public func <= (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { +public func <= (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { try BooleanExpr("lte", [lhs, Helper.valueToDefaultExpr(rhs())]) } -public func == (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { +public func == (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { try BooleanExpr("eq", [lhs, Helper.valueToDefaultExpr(rhs())]) } -public func != (lhs: Expr, rhs: @autoclosure () throws -> Any) rethrows -> BooleanExpr { +public func != (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { try BooleanExpr("neq", [lhs, Helper.valueToDefaultExpr(rhs())]) } From 6c78523890a504aeaed00377ae623f347383644a Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 3 Apr 2025 15:37:53 -0400 Subject: [PATCH 21/43] Update PipelineResult --- .../SwiftAPI/Pipeline/PipelineResult.swift | 8 +++---- .../SwiftAPI/Pipeline/PipelineSnapshot.swift | 24 +++++++++++++++++-- .../Tests/Integration/PipelineTests.swift | 6 ++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift index 96ea35c5567..9b4a9e3c575 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -20,14 +20,14 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct PipelineResult: @unchecked Sendable { +public struct PipelineResult: @unchecked Sendable { let bridge: __PipelineResultBridge init(_ bridge: __PipelineResultBridge) { self.bridge = bridge ref = self.bridge.reference id = self.bridge.documentID - data = self.bridge.data() + data = self.bridge.data() as! T createTime = self.bridge.create_time updateTime = self.bridge.update_time } @@ -45,12 +45,12 @@ public struct PipelineResult: @unchecked Sendable { public let updateTime: Timestamp? /// Retrieves all fields in the result as a dictionary. - public let data: [String: Any] + public let data: T /// Retrieves the field specified by `fieldPath`. /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). /// - Returns: The data at the specified field location or `nil` if no such field exists. - public func get(_ fieldPath: Any) -> Any? { + public func get(_ fieldPath: Any) -> Sendable? { return "PLACEHOLDER" } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift index 097b3bc5eea..bc9468923ef 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift @@ -12,13 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE +import Foundation + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct PipelineSnapshot: Sendable { /// The Pipeline on which `execute()` was called to obtain this `PipelineSnapshot`. public let pipeline: Pipeline /// An array of all the results in the `PipelineSnapshot`. - public let results: [PipelineResult] + let results_cache: [PipelineResult<[String: Sendable]>] /// The time at which the pipeline producing this result was executed. public let executionTime: Timestamp @@ -29,6 +36,19 @@ public struct PipelineSnapshot: Sendable { self.bridge = bridge self.pipeline = pipeline executionTime = self.bridge.execution_time - results = self.bridge.results.map { PipelineResult($0) } + results_cache = self.bridge.results.map { PipelineResult($0) } + } + + public func results() -> [PipelineResult<[String: Sendable]>] { + return results_cache + } + + public func results(decodeAsType: T.Type = T.self, + decoder: Firestore + .Decoder = .init()) async throws -> [PipelineResult< + T + >] { + return try decoder + .decode(T.self, from: results, in: nil as DocumentReference?) as! [PipelineResult] } } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index b0a96207017..9dbdcb5b7fc 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -136,6 +136,10 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .limit(0) .execute() - XCTAssertTrue(snapshot.results.isEmpty) + XCTAssertTrue(snapshot.results().isEmpty) + + struct MyStruct: Decodable {} + let pplResult: [PipelineResult] = try await snapshot + .results(decodeAsType: MyStruct.self) } } From 95987645fa3e35cb101e8e364ceb6c18102e1570 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Mon, 14 Apr 2025 16:42:41 -0400 Subject: [PATCH 22/43] add expression and stages --- .../Source/API/FIRPipelineBridge+Internal.h | 12 + Firestore/Source/API/FIRPipelineBridge.mm | 515 ++++++++++++++++++ .../FirebaseFirestore/FIRPipelineBridge.h | 95 ++++ Firestore/Swift/Source/BridgeWrapper.swift | 4 + .../Swift/Source/Helper/PipelineHelper.swift | 36 +- .../Aggregation/AggregateFunction.swift | 9 +- .../SwiftAPI/Pipeline/ArrayContains.swift | 4 +- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 128 ++--- .../SwiftAPI/Pipeline/Expr/FunctionExpr.swift | 2 +- .../SwiftAPI/Pipeline/ExprWithAlias.swift | 2 +- .../Source/SwiftAPI/Pipeline/Ordering.swift | 10 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 91 ++-- Firestore/Swift/Source/SwiftAPI/Stages.swift | 205 ++++++- .../Tests/Integration/PipelineTests.swift | 4 - .../core/src/api/aggregate_expressions.cc | 2 +- .../core/src/api/aggregate_expressions.h | 6 +- Firestore/core/src/api/expressions.h | 10 +- Firestore/core/src/api/ordering.cc | 2 +- Firestore/core/src/api/ordering.h | 13 +- Firestore/core/src/api/pipeline.cc | 14 + Firestore/core/src/api/pipeline.h | 2 + Firestore/core/src/api/stages.cc | 167 +++++- Firestore/core/src/api/stages.h | 122 ++++- 23 files changed, 1271 insertions(+), 184 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge+Internal.h b/Firestore/Source/API/FIRPipelineBridge+Internal.h index 30bee14aa02..603bc7b88ac 100644 --- a/Firestore/Source/API/FIRPipelineBridge+Internal.h +++ b/Firestore/Source/API/FIRPipelineBridge+Internal.h @@ -35,6 +35,12 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface FIROrderingBridge (Internal) + +- (std::shared_ptr)cppOrderingWithReader:(FSTUserDataReader *)reader; + +@end + @interface FIRStageBridge (Internal) - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader; @@ -53,4 +59,10 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface FIRPipelineBridge (Internal) + +- (std::shared_ptr)cppPipelineWithReader:(FSTUserDataReader *)reader; + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 713456ba3a1..48c82caf2cb 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -26,11 +26,14 @@ #import "Firestore/Source/API/FSTUserDataReader.h" #import "Firestore/Source/API/FSTUserDataWriter.h" #import "Firestore/Source/API/converters.h" +#import "Firestore/Source/Public/FirebaseFirestore/FIRVectorValue.h" #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/api/aggregate_expressions.h" #include "Firestore/core/src/api/document_reference.h" #include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/api/ordering.h" #include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/api/pipeline_result.h" #include "Firestore/core/src/api/pipeline_snapshot.h" @@ -39,20 +42,35 @@ #include "Firestore/core/src/util/status.h" #include "Firestore/core/src/util/string_apple.h" +using firebase::firestore::api::AddFields; +using firebase::firestore::api::AggregateFunction; +using firebase::firestore::api::AggregateStage; using firebase::firestore::api::CollectionGroupSource; using firebase::firestore::api::CollectionSource; using firebase::firestore::api::Constant; using firebase::firestore::api::DatabaseSource; +using firebase::firestore::api::DistinctStage; using firebase::firestore::api::DocumentReference; using firebase::firestore::api::DocumentsSource; using firebase::firestore::api::Expr; using firebase::firestore::api::Field; +using firebase::firestore::api::FindNearestStage; using firebase::firestore::api::FunctionExpr; +using firebase::firestore::api::GenericStage; using firebase::firestore::api::LimitStage; using firebase::firestore::api::MakeFIRTimestamp; using firebase::firestore::api::OffsetStage; +using firebase::firestore::api::Ordering; using firebase::firestore::api::Pipeline; +using firebase::firestore::api::RemoveFieldsStage; +using firebase::firestore::api::ReplaceWith; +using firebase::firestore::api::Sample; +using firebase::firestore::api::SelectStage; +using firebase::firestore::api::SortStage; +using firebase::firestore::api::Union; +using firebase::firestore::api::Unnest; using firebase::firestore::api::Where; +using firebase::firestore::nanopb::SharedMessage; using firebase::firestore::util::MakeCallback; using firebase::firestore::util::MakeNSString; using firebase::firestore::util::MakeString; @@ -133,6 +151,61 @@ - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray cpp_bridge; + NSString *_name; + NSArray *_args; + Boolean isUserDataRead; +} + +- (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray *)args { + _name = name; + _args = args; + isUserDataRead = NO; + return self; +} + +- (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector> cpp_args; + for (FIRExprBridge *arg in _args) { + cpp_args.push_back([arg cppExprWithReader:reader]); + } + cpp_bridge = std::make_shared(MakeString(_name), std::move(cpp_args)); + } + + isUserDataRead = YES; + return cpp_bridge; +} + +@end + +@implementation FIROrderingBridge { + std::shared_ptr cpp_bridge; + NSString *_direction; + FIRExprBridge *_expr; + Boolean isUserDataRead; +} + +- (nonnull id)initWithExpr:(FIRExprBridge *)expr Direction:(NSString *)direction { + _expr = expr; + _direction = direction; + isUserDataRead = NO; + return self; +} + +- (std::shared_ptr)cppOrderingWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + cpp_bridge = std::make_shared([_expr cppExprWithReader:reader], + Ordering::DirectionFromString(MakeString(_direction))); + } + + isUserDataRead = YES; + return cpp_bridge; +} + +@end + @implementation FIRStageBridge @end @@ -290,6 +363,444 @@ - (id)initWithOffset:(NSInteger)value { @end +// TBD + +@implementation FIRAddFieldsStageBridge { + NSDictionary *_fields; + Boolean isUserDataRead; + std::shared_ptr add_fields; +} + +- (id)initWithFields:(NSDictionary *)fields { + self = [super init]; + if (self) { + _fields = fields; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_fields; + for (NSString *key in _fields) { + cpp_fields[MakeString(key)] = [_fields[key] cppExprWithReader:reader]; + } + add_fields = std::make_shared(std::move(cpp_fields)); + } + + isUserDataRead = YES; + return add_fields; +} + +@end + +@implementation FIRRemoveFieldsStageBridge { + NSArray *_fields; + Boolean isUserDataRead; + std::shared_ptr remove_fields; +} + +- (id)initWithFields:(NSArray *)fields { + self = [super init]; + if (self) { + _fields = fields; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector cpp_fields; + for (id field in _fields) { + cpp_fields.push_back(Field(MakeString(field))); + } + remove_fields = std::make_shared(std::move(cpp_fields)); + } + + isUserDataRead = YES; + return remove_fields; +} + +@end + +@implementation FIRSelectStageBridge { + NSDictionary *_selections; + Boolean isUserDataRead; + std::shared_ptr select; +} + +- (id)initWithSelections:(NSDictionary *)selections { + self = [super init]; + if (self) { + _selections = selections; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_selections; + for (NSString *key in _selections) { + cpp_selections[MakeString(key)] = [_selections[key] cppExprWithReader:reader]; + } + select = std::make_shared(std::move(cpp_selections)); + } + + isUserDataRead = YES; + return select; +} + +@end + +@implementation FIRDistinctStageBridge { + NSDictionary *_groups; + Boolean isUserDataRead; + std::shared_ptr distinct; +} + +- (id)initWithGroups:(NSDictionary *)groups { + self = [super init]; + if (self) { + _groups = groups; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_groups; + for (NSString *key in _groups) { + cpp_groups[MakeString(key)] = [_groups[key] cppExprWithReader:reader]; + } + distinct = std::make_shared(std::move(cpp_groups)); + } + + isUserDataRead = YES; + return distinct; +} + +@end + +@implementation FIRAggregateStageBridge { + NSDictionary *_accumulators; + NSDictionary *_groups; + Boolean isUserDataRead; + std::shared_ptr aggregate; +} + +- (id)initWithAccumulators:(NSDictionary *)accumulators + groups:(NSDictionary *)groups { + self = [super init]; + if (self) { + _accumulators = accumulators; + _groups = groups; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> cpp_accumulators; + for (NSString *key in _accumulators) { + cpp_accumulators[MakeString(key)] = [_accumulators[key] cppExprWithReader:reader]; + } + + std::unordered_map> cpp_groups; + for (NSString *key in _groups) { + cpp_groups[MakeString(key)] = [_groups[key] cppExprWithReader:reader]; + } + aggregate = + std::make_shared(std::move(cpp_accumulators), std::move(cpp_groups)); + } + + isUserDataRead = YES; + return aggregate; +} + +@end + +@implementation FIRFindNearestStageBridge { + FIRFieldBridge *_field; + FIRVectorValue *_vectorValue; + NSString *_distanceMeasure; + NSNumber *_limit; + NSString *_Nullable _distanceField; + Boolean isUserDataRead; + std::shared_ptr find_nearest; +} + +- (id)initWithField:(FIRFieldBridge *)field + vectorValue:(FIRVectorValue *)vectorValue + distanceMeasure:(NSString *)distanceMeasure + limit:(NSNumber *_Nullable)limit + distanceField:(NSString *_Nullable)distanceField { + self = [super init]; + if (self) { + _field = field; + _vectorValue = vectorValue; + _distanceMeasure = distanceMeasure; + _limit = limit; + _distanceField = distanceField; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::unordered_map> + optional_value; + if (_limit) { + optional_value.emplace( + std::make_pair(std::string("limit"), + nanopb::SharedMessage( + [reader parsedQueryValue:_limit]))); + } + + if (_distanceField) { + optional_value.emplace( + std::make_pair(std::string("distance_field"), + nanopb::SharedMessage( + [reader parsedQueryValue:_distanceField]))); + } + + find_nearest = std::make_shared([_field cppExprWithReader:reader], + [reader parsedQueryValue:_vectorValue], + MakeString(_distanceMeasure), optional_value); + } + + isUserDataRead = YES; + return find_nearest; +} + +@end + +@implementation FIRSorStageBridge { + NSArray *_orderings; + Boolean isUserDataRead; + std::shared_ptr sort; +} + +- (id)initWithOrderings:(NSArray *)orderings { + self = [super init]; + if (self) { + _orderings = orderings; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector> cpp_orderings; + for (FIROrderingBridge *ordering in _orderings) { + cpp_orderings.push_back([ordering cppOrderingWithReader:reader]); + } + sort = std::make_shared(std::move(cpp_orderings)); + } + + isUserDataRead = YES; + return sort; +} + +@end + +@implementation FIRReplaceWithStageBridge { + FIRExprBridge *_expr; + NSString *_fieldName; + Boolean isUserDataRead; + std::shared_ptr replace_with; +} + +- (id)initWithExpr:(FIRExprBridge *)expr { + self = [super init]; + if (self) { + _expr = expr; + _fieldName = nil; + isUserDataRead = NO; + } + return self; +} + +- (id)initWithFieldName:(NSString *)fieldName { + self = [super init]; + if (self) { + _fieldName = fieldName; + _expr = nil; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + if (_expr) { + replace_with = std::make_shared([_expr cppExprWithReader:reader]); + } else { + replace_with = std::make_shared(MakeString(_fieldName)); + } + } + + isUserDataRead = YES; + return replace_with; +} + +@end + +@implementation FIRSampleStageBridge { + int64_t _count; + double _percentage; + Boolean isUserDataRead; + NSString *type; + std::shared_ptr sample; +} + +- (id)initWithCount:(int64_t)count { + self = [super init]; + if (self) { + _count = count; + _percentage = 0; + type = @"count"; + isUserDataRead = NO; + } + return self; +} + +- (id)initWithPercentage:(double)percentage { + self = [super init]; + if (self) { + _percentage = percentage; + _count = 0; + type = @"percentage"; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + if ([type isEqualToString:@"count"]) { + sample = std::make_shared("count", _count, 0); + } else { + sample = std::make_shared("percentage", 0, _percentage); + } + } + + isUserDataRead = YES; + return sample; +} + +@end + +@implementation FIRUnionStageBridge { + FIRPipelineBridge *_other; + Boolean isUserDataRead; + std::shared_ptr union_stage; +} + +- (id)initWithOther:(FIRPipelineBridge *)other { + self = [super init]; + if (self) { + _other = other; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + union_stage = std::make_shared([_other cppPipelineWithReader:reader]); + } + + isUserDataRead = YES; + return union_stage; +} + +@end + +@implementation FIRUnnestStageBridge { + FIRExprBridge *_field; + NSString *_Nullable _indexField; + Boolean isUserDataRead; + std::shared_ptr unnest; +} + +- (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)indexField { + self = [super init]; + if (self) { + _field = field; + _indexField = indexField; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + absl::optional cpp_index_field; + if (_indexField != nil) { + cpp_index_field = MakeString(_indexField); + } else { + cpp_index_field = absl::nullopt; + } + unnest = std::make_shared([_field cppExprWithReader:reader], cpp_index_field); + } + + isUserDataRead = YES; + return unnest; +} + +@end + +@implementation FIRGenericStageBridge { + NSString *_name; + NSArray *_params; + NSDictionary *_Nullable _options; + Boolean isUserDataRead; + std::shared_ptr generic_stage; +} + +- (id)initWithName:(NSString *)name + params:(NSArray *)params + options:(NSDictionary *_Nullable)options { + self = [super init]; + if (self) { + _name = name; + _params = params; + _options = options; + isUserDataRead = NO; + } + return self; +} + +- (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { + if (!isUserDataRead) { + std::vector> cpp_params; + for (FIRExprBridge *param in _params) { + cpp_params.push_back([param cppExprWithReader:reader]); + } + std::unordered_map> cpp_options; + if (_options) { + for (NSString *key in _options) { + cpp_options[MakeString(key)] = [_options[key] cppExprWithReader:reader]; + } + } + generic_stage = std::make_shared(MakeString(_name), std::move(cpp_params), + std::move(cpp_options)); + } + + isUserDataRead = YES; + return generic_stage; +} + +@end + @interface __FIRPipelineSnapshotBridge () @property(nonatomic, strong, readwrite) NSArray<__FIRPipelineResultBridge *> *results; @@ -430,6 +941,10 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r }); } +- (std::shared_ptr)cppPipelineWithReader:(FSTUserDataReader *)reader { + return pipeline; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index 527e97a062a..1ff773bb46c 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -21,6 +21,8 @@ #import "FIRDocumentSnapshot.h" @class FIRTimestamp; +@class FIRVectorValue; +@class FIRPipelineBridge; NS_ASSUME_NONNULL_BEGIN @@ -47,6 +49,18 @@ NS_SWIFT_NAME(FunctionExprBridge) - (id)initWithName:(NSString *)name Args:(NSArray *)args; @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(AggregateFunctionBridge) +@interface FIRAggregateFunctionBridge : NSObject +- (id)initWithName:(NSString *)name Args:(NSArray *)args; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(OrderingBridge) +@interface FIROrderingBridge : NSObject +- (id)initWithExpr:(FIRExprBridge *)expr Direction:(NSString *)direction; +@end + NS_SWIFT_SENDABLE NS_SWIFT_NAME(StageBridge) @interface FIRStageBridge : NSObject @@ -108,6 +122,87 @@ NS_SWIFT_NAME(OffsetStageBridge) @end +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(AddFieldsStageBridge) +@interface FIRAddFieldsStageBridge : FIRStageBridge +- (id)initWithFields:(NSDictionary *)fields; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(RemoveFieldsStageBridge) +@interface FIRRemoveFieldsStageBridge : FIRStageBridge +- (id)initWithFields:(NSArray *)fields; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(SelectStageBridge) +@interface FIRSelectStageBridge : FIRStageBridge +- (id)initWithSelections:(NSDictionary *)selections; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(DistinctStageBridge) +@interface FIRDistinctStageBridge : FIRStageBridge +- (id)initWithGroups:(NSDictionary *)groups; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(AggregateStageBridge) +@interface FIRAggregateStageBridge : FIRStageBridge +- (id)initWithAccumulators:(NSDictionary *)accumulators + groups:(NSDictionary *)groups; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(FindNearestStageBridge) +@interface FIRFindNearestStageBridge : FIRStageBridge +- (id)initWithField:(FIRFieldBridge *)field + vectorValue:(FIRVectorValue *)vectorValue + distanceMeasure:(NSString *)distanceMeasure + limit:(NSNumber *_Nullable)limit + distanceField:(NSString *_Nullable)distanceField; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(SortStageBridge) +@interface FIRSorStageBridge : FIRStageBridge +- (id)initWithOrderings:(NSArray *)orderings; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(ReplaceWithStageBridge) +@interface FIRReplaceWithStageBridge : FIRStageBridge +- (id)initWithExpr:(FIRExprBridge *)expr; +- (id)initWithFieldName:(NSString *)fieldName; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(SampleStageBridge) +@interface FIRSampleStageBridge : FIRStageBridge +- (id)initWithCount:(int64_t)count; +- (id)initWithPercentage:(double)percentage; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(UnionStageBridge) +@interface FIRUnionStageBridge : FIRStageBridge +- (id)initWithOther:(FIRPipelineBridge *)other; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(UnnestStageBridge) +@interface FIRUnnestStageBridge : FIRStageBridge +- (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)indexField; +@end + +NS_SWIFT_SENDABLE +NS_SWIFT_NAME(GenericStageBridge) +@interface FIRGenericStageBridge : FIRStageBridge +- (id)initWithName:(NSString *)name + params:(NSArray *)params + options:(NSDictionary *_Nullable)options; +@end + NS_SWIFT_SENDABLE NS_SWIFT_NAME(__PipelineResultBridge) @interface __FIRPipelineResultBridge : NSObject diff --git a/Firestore/Swift/Source/BridgeWrapper.swift b/Firestore/Swift/Source/BridgeWrapper.swift index 8b4d13fc9a4..a3f60de9d54 100644 --- a/Firestore/Swift/Source/BridgeWrapper.swift +++ b/Firestore/Swift/Source/BridgeWrapper.swift @@ -15,3 +15,7 @@ protocol BridgeWrapper { var bridge: ExprBridge { get } } + +protocol AggregateBridgeWrapper { + var bridge: AggregateFunctionBridge { get } +} diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift index d2eed20cd83..05e832e7525 100644 --- a/Firestore/Swift/Source/Helper/PipelineHelper.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -13,38 +13,52 @@ // limitations under the License. enum Helper { - static func valueToDefaultExpr(_ value: Any) -> any Expr { + static func sendableToExpr(_ value: Sendable) -> Expr { if value is Expr { return value as! Expr - } else if value is [String: Any] { - return map(value as! [String: Any]) - } else if value is [Any] { - return array(value as! [Any]) + } else if value is [String: Sendable] { + return map(value as! [String: Sendable]) + } else if value is [Sendable] { + return array(value as! [Sendable]) } else { return Constant(value) } } - static func vectorToExpr(_ value: VectorValue) -> any Expr { + static func selectablesToMap(selectables: [Any]) -> [String: Expr] { + var result = [String: Expr]() + for selectable in selectables { + if let stringSelectable = selectable as? String { + result[stringSelectable] = Field(stringSelectable) + } else if let fieldSelectable = selectable as? Field { + result[fieldSelectable.alias] = fieldSelectable.expr + } else if let exprAliasSelectable = selectable as? ExprWithAlias { + result[exprAliasSelectable.alias] = exprAliasSelectable.expr + } + } + return result + } + + static func vectorToExpr(_ value: VectorValue) -> Expr { return Field("PLACEHOLDER") } - static func timeUnitToExpr(_ value: TimeUnit) -> any Expr { + static func timeUnitToExpr(_ value: TimeUnit) -> Expr { return Field("PLACEHOLDER") } - static func map(_ elements: [String: Any]) -> FunctionExpr { + static func map(_ elements: [String: Sendable]) -> FunctionExpr { var result: [Expr] = [] for (key, value) in elements { result.append(Constant(key)) - result.append(valueToDefaultExpr(value)) + result.append(sendableToExpr(value)) } return FunctionExpr("map", result) } - static func array(_ elements: [Any]) -> FunctionExpr { + static func array(_ elements: [Sendable]) -> FunctionExpr { let transformedElements = elements.map { element in - valueToDefaultExpr(element) + sendableToExpr(element) } return FunctionExpr("array", transformedElements) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift index 9a36df9fd04..d1b0538eb97 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift @@ -12,13 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -public class AggregateFunction: @unchecked Sendable { +public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { + var bridge: AggregateFunctionBridge + let functionName: String let agrs: [Expr] public init(_ functionName: String, _ agrs: [Expr]) { self.functionName = functionName self.agrs = agrs + bridge = AggregateFunctionBridge( + name: functionName, + args: self.agrs.map { ($0 as! BridgeWrapper).bridge + } + ) } public func `as`(_ name: String) -> AggregateWithAlias { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift index df426d36f79..7a70cfbc77b 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ArrayContains.swift @@ -13,7 +13,7 @@ // limitations under the License. public class ArrayContains: BooleanExpr, @unchecked Sendable { - public init(fieldName: String, values: Any...) { - super.init("array_concat", values.map { Helper.valueToDefaultExpr($0) }) + public init(fieldName: String, values: Sendable...) { + super.init("array_contains", values.map { Helper.sendableToExpr($0) }) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 8cb92042303..d54e8c43eb4 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -215,7 +215,7 @@ public extension Expr { } func eq(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("eq", [self, Helper.valueToDefaultExpr(other)]) + return BooleanExpr("eq", [self, Helper.sendableToExpr(other)]) } func neq(_ other: Expr) -> BooleanExpr { @@ -223,7 +223,7 @@ public extension Expr { } func neq(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("neq", [self, Helper.valueToDefaultExpr(other)]) + return BooleanExpr("neq", [self, Helper.sendableToExpr(other)]) } func lt(_ other: Expr) -> BooleanExpr { @@ -231,7 +231,7 @@ public extension Expr { } func lt(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("lt", [self, Helper.valueToDefaultExpr(other)]) + return BooleanExpr("lt", [self, Helper.sendableToExpr(other)]) } func lte(_ other: Expr) -> BooleanExpr { @@ -239,7 +239,7 @@ public extension Expr { } func lte(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("lte", [self, Helper.valueToDefaultExpr(other)]) + return BooleanExpr("lte", [self, Helper.sendableToExpr(other)]) } func gt(_ other: Expr) -> BooleanExpr { @@ -247,7 +247,7 @@ public extension Expr { } func gt(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("gt", [self, Helper.valueToDefaultExpr(other)]) + return BooleanExpr("gt", [self, Helper.sendableToExpr(other)]) } func gte(_ other: Expr) -> BooleanExpr { @@ -255,7 +255,7 @@ public extension Expr { } func gte(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("gte", [self, Helper.valueToDefaultExpr(other)]) + return BooleanExpr("gte", [self, Helper.sendableToExpr(other)]) } // MARK: Arithmetic Operators @@ -265,8 +265,8 @@ public extension Expr { } func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others - .map { Helper.valueToDefaultExpr($0) } + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } return FunctionExpr("add", exprs) } @@ -275,7 +275,7 @@ public extension Expr { } func subtract(_ other: Sendable) -> FunctionExpr { - return FunctionExpr("subtract", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("subtract", [self, Helper.sendableToExpr(other)]) } func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr { @@ -283,8 +283,8 @@ public extension Expr { } func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others - .map { Helper.valueToDefaultExpr($0) } + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } return FunctionExpr("multiply", exprs) } @@ -293,7 +293,7 @@ public extension Expr { } func divide(_ other: Sendable) -> FunctionExpr { - return FunctionExpr("divide", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("divide", [self, Helper.sendableToExpr(other)]) } func mod(_ other: Expr) -> FunctionExpr { @@ -301,7 +301,7 @@ public extension Expr { } func mod(_ other: Sendable) -> FunctionExpr { - return FunctionExpr("mod", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("mod", [self, Helper.sendableToExpr(other)]) } // MARK: Array Operations @@ -311,8 +311,8 @@ public extension Expr { } func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr { - let exprs = [self] + [Helper.valueToDefaultExpr(secondArray)] + otherArrays - .map { Helper.valueToDefaultExpr($0) } + let exprs = [self] + [Helper.sendableToExpr(secondArray)] + otherArrays + .map { Helper.sendableToExpr($0) } return FunctionExpr("array_concat", exprs) } @@ -321,7 +321,7 @@ public extension Expr { } func arrayContains(_ element: Sendable) -> BooleanExpr { - return BooleanExpr("array_contains", [self, Helper.valueToDefaultExpr(element)]) + return BooleanExpr("array_contains", [self, Helper.sendableToExpr(element)]) } func arrayContainsAll(_ values: Expr...) -> BooleanExpr { @@ -329,7 +329,7 @@ public extension Expr { } func arrayContainsAll(_ values: Sendable...) -> BooleanExpr { - let exprValues = values.map { Helper.valueToDefaultExpr($0) } + let exprValues = values.map { Helper.sendableToExpr($0) } return BooleanExpr("array_contains_all", [self] + exprValues) } @@ -338,7 +338,7 @@ public extension Expr { } func arrayContainsSendable(_ values: Sendable...) -> BooleanExpr { - let exprValues = values.map { Helper.valueToDefaultExpr($0) } + let exprValues = values.map { Helper.sendableToExpr($0) } return BooleanExpr("array_contains_Sendable", [self] + exprValues) } @@ -347,7 +347,7 @@ public extension Expr { } func arrayOffset(_ offset: Int) -> FunctionExpr { - return FunctionExpr("array_offset", [self, Helper.valueToDefaultExpr(offset)]) + return FunctionExpr("array_offset", [self, Helper.sendableToExpr(offset)]) } func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr { @@ -361,7 +361,7 @@ public extension Expr { } func eqSendable(_ others: Sendable...) -> BooleanExpr { - let exprOthers = others.map { Helper.valueToDefaultExpr($0) } + let exprOthers = others.map { Helper.sendableToExpr($0) } return BooleanExpr("eq_Sendable", [self] + exprOthers) } @@ -370,7 +370,7 @@ public extension Expr { } func notEqSendable(_ others: Sendable...) -> BooleanExpr { - let exprOthers = others.map { Helper.valueToDefaultExpr($0) } + let exprOthers = others.map { Helper.sendableToExpr($0) } return BooleanExpr("not_eq_Sendable", [self] + exprOthers) } @@ -411,7 +411,7 @@ public extension Expr { } func like(_ pattern: String) -> FunctionExpr { - return FunctionExpr("like", [self, Helper.valueToDefaultExpr(pattern)]) + return FunctionExpr("like", [self, Helper.sendableToExpr(pattern)]) } func like(_ pattern: Expr) -> FunctionExpr { @@ -419,7 +419,7 @@ public extension Expr { } func regexContains(_ pattern: String) -> BooleanExpr { - return BooleanExpr("regex_contains", [self, Helper.valueToDefaultExpr(pattern)]) + return BooleanExpr("regex_contains", [self, Helper.sendableToExpr(pattern)]) } func regexContains(_ pattern: Expr) -> BooleanExpr { @@ -427,7 +427,7 @@ public extension Expr { } func regexMatch(_ pattern: String) -> BooleanExpr { - return BooleanExpr("regex_match", [self, Helper.valueToDefaultExpr(pattern)]) + return BooleanExpr("regex_match", [self, Helper.sendableToExpr(pattern)]) } func regexMatch(_ pattern: Expr) -> BooleanExpr { @@ -435,7 +435,7 @@ public extension Expr { } func strContains(_ substring: String) -> BooleanExpr { - return BooleanExpr("str_contains", [self, Helper.valueToDefaultExpr(substring)]) + return BooleanExpr("str_contains", [self, Helper.sendableToExpr(substring)]) } func strContains(_ expr: Expr) -> BooleanExpr { @@ -443,7 +443,7 @@ public extension Expr { } func startsWith(_ prefix: String) -> BooleanExpr { - return BooleanExpr("starts_with", [self, Helper.valueToDefaultExpr(prefix)]) + return BooleanExpr("starts_with", [self, Helper.sendableToExpr(prefix)]) } func startsWith(_ prefix: Expr) -> BooleanExpr { @@ -451,7 +451,7 @@ public extension Expr { } func endsWith(_ suffix: String) -> BooleanExpr { - return BooleanExpr("ends_with", [self, Helper.valueToDefaultExpr(suffix)]) + return BooleanExpr("ends_with", [self, Helper.sendableToExpr(suffix)]) } func endsWith(_ suffix: Expr) -> BooleanExpr { @@ -475,8 +475,8 @@ public extension Expr { } func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr { - let exprs = [self] + [Helper.valueToDefaultExpr(secondString)] + otherStrings - .map { Helper.valueToDefaultExpr($0) } + let exprs = [self] + [Helper.sendableToExpr(secondString)] + otherStrings + .map { Helper.sendableToExpr($0) } return FunctionExpr("str_concat", exprs) } @@ -487,7 +487,7 @@ public extension Expr { func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr { return FunctionExpr( "replace_first", - [self, Helper.valueToDefaultExpr(find), Helper.valueToDefaultExpr(replace)] + [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] ) } @@ -498,7 +498,7 @@ public extension Expr { func replaceAll(_ find: String, _ replace: String) -> FunctionExpr { return FunctionExpr( "replace_all", - [self, Helper.valueToDefaultExpr(find), Helper.valueToDefaultExpr(replace)] + [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] ) } @@ -511,9 +511,9 @@ public extension Expr { } func substr(_ position: Int, _ length: Int? = nil) -> FunctionExpr { - let positionExpr = Helper.valueToDefaultExpr(position) + let positionExpr = Helper.sendableToExpr(position) if let length = length { - return FunctionExpr("substr", [self, positionExpr, Helper.valueToDefaultExpr(length)]) + return FunctionExpr("substr", [self, positionExpr, Helper.sendableToExpr(length)]) } else { return FunctionExpr("substr", [self, positionExpr]) } @@ -534,7 +534,7 @@ public extension Expr { } func mapRemove(_ key: String) -> FunctionExpr { - return FunctionExpr("map_remove", [self, Helper.valueToDefaultExpr(key)]) + return FunctionExpr("map_remove", [self, Helper.sendableToExpr(key)]) } func mapRemove(_ keyExpr: Expr) -> FunctionExpr { @@ -543,8 +543,8 @@ public extension Expr { func mapMerge(_ secondMap: [String: Sendable], _ otherMaps: [String: Sendable]...) -> FunctionExpr { - let secondMapExpr = Helper.valueToDefaultExpr(secondMap) - let otherMapExprs = otherMaps.map { Helper.valueToDefaultExpr($0) } + let secondMapExpr = Helper.sendableToExpr(secondMap) + let otherMapExprs = otherMaps.map { Helper.sendableToExpr($0) } return FunctionExpr("map_merge", [self, secondMapExpr] + otherMapExprs) } @@ -581,8 +581,8 @@ public extension Expr { } func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others - .map { Helper.valueToDefaultExpr($0) } + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } return FunctionExpr("logical_maximum", exprs) } @@ -591,8 +591,8 @@ public extension Expr { } func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.valueToDefaultExpr(second)] + others - .map { Helper.valueToDefaultExpr($0) } + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } return FunctionExpr("logical_min", exprs) } @@ -611,7 +611,7 @@ public extension Expr { } func cosineDistance(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("cosine_distance", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) } func dotProduct(_ other: Expr) -> FunctionExpr { @@ -623,7 +623,7 @@ public extension Expr { } func dotProduct(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("dot_product", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) } func euclideanDistance(_ other: Expr) -> FunctionExpr { @@ -635,7 +635,7 @@ public extension Expr { } func euclideanDistance(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("euclidean_distance", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) } func manhattanDistance(_ other: Expr) -> FunctionExpr { @@ -647,7 +647,7 @@ public extension Expr { } func manhattanDistance(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("manhattan_distance", [self, Helper.valueToDefaultExpr(other)]) + return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) } // MARK: Timestamp operations @@ -683,7 +683,7 @@ public extension Expr { func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { return FunctionExpr( "timestamp_add", - [self, Helper.timeUnitToExpr(unit), Helper.valueToDefaultExpr(amount)] + [self, Helper.timeUnitToExpr(unit), Helper.sendableToExpr(amount)] ) } @@ -694,18 +694,18 @@ public extension Expr { func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { return FunctionExpr( "timestamp_sub", - [self, Helper.timeUnitToExpr(unit), Helper.valueToDefaultExpr(amount)] + [self, Helper.timeUnitToExpr(unit), Helper.sendableToExpr(amount)] ) } // MARK: - Bitwise operations func bitAnd(_ otherBits: Int) -> FunctionExpr { - return FunctionExpr("bit_and", [self, Helper.valueToDefaultExpr(otherBits)]) + return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) } func bitAnd(_ otherBits: UInt8) -> FunctionExpr { - return FunctionExpr("bit_and", [self, Helper.valueToDefaultExpr(otherBits)]) + return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) } func bitAnd(_ bitsExpression: Expr) -> FunctionExpr { @@ -713,11 +713,11 @@ public extension Expr { } func bitOr(_ otherBits: Int) -> FunctionExpr { - return FunctionExpr("bit_or", [self, Helper.valueToDefaultExpr(otherBits)]) + return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) } func bitOr(_ otherBits: UInt8) -> FunctionExpr { - return FunctionExpr("bit_or", [self, Helper.valueToDefaultExpr(otherBits)]) + return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) } func bitOr(_ bitsExpression: Expr) -> FunctionExpr { @@ -725,11 +725,11 @@ public extension Expr { } func bitXor(_ otherBits: Int) -> FunctionExpr { - return FunctionExpr("bit_xor", [self, Helper.valueToDefaultExpr(otherBits)]) + return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) } func bitXor(_ otherBits: UInt8) -> FunctionExpr { - return FunctionExpr("bit_xor", [self, Helper.valueToDefaultExpr(otherBits)]) + return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) } func bitXor(_ bitsExpression: Expr) -> FunctionExpr { @@ -741,7 +741,7 @@ public extension Expr { } func bitLeftShift(_ y: Int) -> FunctionExpr { - return FunctionExpr("bit_left_shift", [self, Helper.valueToDefaultExpr(y)]) + return FunctionExpr("bit_left_shift", [self, Helper.sendableToExpr(y)]) } func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr { @@ -749,7 +749,7 @@ public extension Expr { } func bitRightShift(_ y: Int) -> FunctionExpr { - return FunctionExpr("bit_right_shift", [self, Helper.valueToDefaultExpr(y)]) + return FunctionExpr("bit_right_shift", [self, Helper.sendableToExpr(y)]) } func bitRightShift(_ numberExpr: Expr) -> FunctionExpr { @@ -765,7 +765,7 @@ public extension Expr { } func ifError(_ catchValue: Sendable) -> FunctionExpr { - return FunctionExpr("if_error", [self, Helper.valueToDefaultExpr(catchValue)]) + return FunctionExpr("if_error", [self, Helper.sendableToExpr(catchValue)]) } // MARK: Sorting @@ -779,29 +779,35 @@ public extension Expr { } } +extension Expr { + func exprToExprBridge() -> ExprBridge { + return (self as! BridgeWrapper).bridge + } +} + // protocal cannot overwrite operator, since every inheritated class will have this function // it will lead to error: Generic parameter 'Self' could not be inferred public func > (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("gt", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("gt", [lhs, Helper.sendableToExpr(rhs())]) } public func >= (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("gte", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("gte", [lhs, Helper.sendableToExpr(rhs())]) } public func < (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("lt", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("lt", [lhs, Helper.sendableToExpr(rhs())]) } public func <= (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("lte", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("lte", [lhs, Helper.sendableToExpr(rhs())]) } public func == (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("eq", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("eq", [lhs, Helper.sendableToExpr(rhs())]) } public func != (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("neq", [lhs, Helper.valueToDefaultExpr(rhs())]) + try BooleanExpr("neq", [lhs, Helper.sendableToExpr(rhs())]) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift index 2726caba284..a80f078d853 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -23,7 +23,7 @@ public class FunctionExpr: Expr, BridgeWrapper, @unchecked Sendable { self.agrs = agrs bridge = FunctionExprBridge( name: functionName, - args: self.agrs.map { ($0 as! (Expr & BridgeWrapper)).bridge + args: self.agrs.map { ($0 as! BridgeWrapper).bridge } ) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift index af5447541ab..c49e756d02c 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct ExprWithAlias: Selectable, Sendable { +public struct ExprWithAlias: Selectable, SelectableInternal, Sendable { public var alias: String public var expr: Expr diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index 1761be8ebca..6ca47be47fd 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -17,15 +17,18 @@ public class Ordering: @unchecked Sendable { let expr: Expr let direction: Direction + var bridge: OrderingBridge init(expr: Expr, direction: Direction) { self.expr = expr self.direction = direction + bridge = OrderingBridge(expr: expr.exprToExprBridge(), direction: direction.rawValue) } } public struct Direction: Sendable, Equatable, Hashable { let kind: Kind + let rawValue: String enum Kind: String { case ascending @@ -33,14 +36,15 @@ public struct Direction: Sendable, Equatable, Hashable { } public static var ascending: Direction { - return self.init(kind: .ascending) + return self.init(kind: .ascending, rawValue: "ascending") } public static var descending: Direction { - return self.init(kind: .descending) + return self.init(kind: .descending, rawValue: "descending") } - init(kind: Kind) { + init(kind: Kind, rawValue: String) { self.kind = kind + self.rawValue = rawValue } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 5abf45afcfc..bc90b83fe6a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -22,7 +22,7 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct Pipeline: @unchecked Sendable { private var stages: [Stage] - private var bridge: PipelineBridge + var bridge: PipelineBridge let db: Firestore init(stages: [Stage], db: Firestore) { @@ -57,21 +57,28 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter fields: The fields to add to the documents, specified as `Selectable`s. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func addFields(_ field: Selectable, _ additionalFields: Selectable...) -> Pipeline { - return self + let fields = [field] + additionalFields + return Pipeline(stages: stages + [AddFields(fields: fields)], db: db) } /// Remove fields from outputs of previous stages. /// - Parameter fields: The fields to remove. /// - Returns: A new Pipeline object with this stage appended to the stage list. public func removeFields(_ field: Field, _ additionalFields: Field...) -> Pipeline { - return self + return Pipeline( + stages: stages + [RemoveFieldsStage(fields: [field] + additionalFields)], + db: db + ) } /// Remove fields from outputs of previous stages. /// - Parameter fields: The fields to remove. /// - Returns: A new Pipeline object with this stage appended to the stage list. public func removeFields(_ field: String, _ additionalFields: String...) -> Pipeline { - return self + return Pipeline( + stages: stages + [RemoveFieldsStage(fields: [field] + additionalFields)], + db: db + ) } /// Selects or creates a set of fields from the outputs of previous stages. @@ -89,8 +96,11 @@ public struct Pipeline: @unchecked Sendable { /// `Selectable` expressions. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func select(_ selection: Selectable, _ additionalSelections: Selectable...) -> Pipeline { - // Implementation - return self + let selections = [selection] + additionalSelections + return Pipeline( + stages: stages + [Select(selections: selections + additionalSelections)], + db: db + ) } /// Selects or creates a set of fields from the outputs of previous stages. @@ -107,8 +117,11 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter selections: `String` values representing field names. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func select(_ selection: String, _ additionalSelections: String...) -> Pipeline { - // Implementation - return self + let selections = [selection] + additionalSelections + return Pipeline( + stages: stages + [Select(selections: selections + additionalSelections)], + db: db + ) } /// Filters the documents from previous stages to only include those matching the specified @@ -175,7 +188,8 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter selections: The fields to include in the output documents, specified as /// `String` values representing field names. public func distinct(_ group: String, _ additionalGroups: String...) -> Pipeline { - return self + let groups = [group] + additionalGroups + return Pipeline(stages: stages + [Distinct(groups: groups + additionalGroups)], db: db) } /// Returns a set of distinct `Expr` values from the inputs to this stage. @@ -193,7 +207,8 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter selections: The fields to include in the output documents, specified as /// `Selectable` expressions. public func distinct(_ group: Selectable, _ additionalGroups: Selectable...) -> Pipeline { - return self + let groups = [group] + additionalGroups + return Pipeline(stages: stages + [Distinct(groups: groups + additionalGroups)], db: db) } /// Performs aggregation operations on the documents from previous stages. @@ -207,7 +222,13 @@ public struct Pipeline: @unchecked Sendable { /// `Accumulator` and assigning a name to the accumulated results. public func aggregate(_ accumulator: AggregateWithAlias, _ additionalAccumulators: AggregateWithAlias...) -> Pipeline { - return self + return Pipeline( + stages: stages + [Aggregate( + accumulators: [accumulator] + additionalAccumulators, + groups: nil + )], + db: db + ) } /// Performs optionally grouped aggregation operations on the documents from previous stages. @@ -231,7 +252,7 @@ public struct Pipeline: @unchecked Sendable { /// - Returns: A new `Pipeline` object with this stage appended. public func aggregate(_ accumulator: [AggregateWithAlias], groups: [Selectable]? = nil) -> Pipeline { - return self + return Pipeline(stages: stages + [Aggregate(accumulators: accumulator, groups: groups)], db: db) } /// Performs optionally grouped aggregation operations on the documents from previous stages. @@ -255,7 +276,7 @@ public struct Pipeline: @unchecked Sendable { /// - Returns: A new `Pipeline` object with this stage appended. public func aggregate(_ accumulator: [AggregateWithAlias], groups: [String]? = nil) -> Pipeline { - return self + return Pipeline(stages: stages + [Aggregate(accumulators: accumulator, groups: groups)], db: db) } /// Performs a vector similarity search, ordering the result set by most similar to least @@ -265,7 +286,18 @@ public struct Pipeline: @unchecked Sendable { distanceMeasure: DistanceMeasure, limit: Int? = nil, distanceField: String? = nil) -> Pipeline { - return self + return Pipeline( + stages: stages + [ + FindNearest( + field: field, + vectorValue: vectorValue, + distanceMeasure: distanceMeasure, + limit: limit, + distanceField: distanceField + ), + ], + db: db + ) } /// Sorts the documents from previous stages based on one or more `Ordering` criteria. @@ -279,8 +311,8 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter orderings: One or more `Ordering` instances specifying the sorting criteria. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func sort(_ ordering: Ordering, _ additionalOrdering: Ordering...) -> Pipeline { - // Implementation - return self + let orderings = [ordering] + additionalOrdering + return Pipeline(stages: stages + [Sort(orderings: orderings)], db: db) } /// Fully overwrites all fields in a document with those coming from a nested map. @@ -291,8 +323,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter field: The `Expr` field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func replace(with expr: Expr) -> Pipeline { - // Implementation - return self + return Pipeline(stages: stages + [ReplaceWith(expr: expr)], db: db) } /// Fully overwrites all fields in a document with those coming from a nested map. @@ -303,8 +334,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter fieldName: The field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func replace(with fieldName: String) -> Pipeline { - // Implementation - return self + return Pipeline(stages: stages + [ReplaceWith(fieldName: fieldName)], db: db) } /// Performs a pseudo-random sampling of the input documents. @@ -315,8 +345,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter count: The number of documents to sample. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func sample(count: Int64) -> Pipeline { - // Implementation - return self + return Pipeline(stages: stages + [Sample(count: count)], db: db) } /// Performs a pseudo-random sampling of the input documents. @@ -327,8 +356,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter percentage: The percentage of documents to sample. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func sample(percentage: Double) -> Pipeline { - // Implementation - return self + return Pipeline(stages: stages + [Sample(percentage: percentage)], db: db) } /// Performs union of all documents from two pipelines, including duplicates. @@ -340,8 +368,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter other: The other `Pipeline` that is part of union. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func union(_ other: Pipeline) -> Pipeline { - // Implementation - return self + return Pipeline(stages: stages + [Union(other: other)], db: db) } /// Takes an array field from the input documents and outputs a document for each element @@ -360,8 +387,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter indexField: Optional. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func unnest(_ field: Selectable, indexField: String? = nil) -> Pipeline { - // Implementation - return self + return Pipeline(stages: stages + [Unnest(field: field, indexField: indexField)], db: db) } /// Adds a stage to the pipeline by specifying the stage name as an argument. This does @@ -375,8 +401,11 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter params: A list of ordered parameters to configure the stage's behavior. /// - Parameter options: A list of optional, named parameters to configure the stage's behavior. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. - public func genericStage(name: String, params: [Any], options: [String: Any]? = nil) -> Pipeline { - // Implementation - return self + public func genericStage(name: String, params: [Sendable], + options: [String: Sendable]? = nil) -> Pipeline { + return Pipeline( + stages: stages + [GenericStage(name: name, params: params, options: options)], + db: db + ) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index 1ed077409e5..c028376f2f9 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -54,7 +54,7 @@ class Where: Stage { init(condition: BooleanExpr) { self.condition = condition - bridge = WhereStageBridge(expr: condition.bridge) + bridge = WhereStageBridge(expr: condition.exprToExprBridge()) } } @@ -81,3 +81,206 @@ class Offset: Stage { bridge = OffsetStageBridge(offset: NSInteger(offset)) } } + +class AddFields: Stage { + var name: String = "addFields" + var bridge: StageBridge + private var fields: [Selectable] + + init(fields: [Selectable]) { + self.fields = fields + let objc_accumulators = fields.reduce(into: [String: ExprBridge]()) { + result, + field + in + let seletable = field as! SelectableInternal + result[seletable.alias] = seletable.expr.exprToExprBridge() + } + bridge = AddFieldsStageBridge(fields: objc_accumulators) + } +} + +class RemoveFieldsStage: Stage { + var name: String = "removeFields" + var bridge: StageBridge + private var fields: [String] + + init(fields: [String]) { + self.fields = fields + bridge = RemoveFieldsStageBridge(fields: fields) + } + + init(fields: [Field]) { + self.fields = fields.map { $0.fieldName } + bridge = RemoveFieldsStageBridge(fields: self.fields) + } +} + +class Select: Stage { + var name: String = "select" + var bridge: StageBridge + private var selections: [Any] + + init(selections: [Any]) { + self.selections = selections + let objc_selections = Helper.selectablesToMap(selectables: selections) + bridge = SelectStageBridge(selections: objc_selections + .mapValues { Helper.sendableToExpr($0).exprToExprBridge() }) + } +} + +class Distinct: Stage { + var name: String = "distinct" + var bridge: StageBridge + private var groups: [Any] + + init(groups: [Any]) { + self.groups = groups + let objc_groups = Helper.selectablesToMap(selectables: groups) + bridge = DistinctStageBridge(groups: objc_groups + .mapValues { Helper.sendableToExpr($0).exprToExprBridge() }) + } +} + +class Aggregate: Stage { + var name: String = "aggregate" + var bridge: StageBridge + private var accumulators: [AggregateWithAlias] + private var groups: [String: Expr] = [:] + + init(accumulators: [AggregateWithAlias], groups: [Any]?) { + self.accumulators = accumulators + if groups != nil { + self.groups = Helper.selectablesToMap(selectables: groups!) + } + let objc_accumulators = accumulators + .reduce(into: [String: AggregateFunctionBridge]()) { result, accumulator in + result[accumulator.alias] = accumulator.aggregate.bridge + } + bridge = AggregateStageBridge( + accumulators: objc_accumulators, + groups: self.groups.mapValues { Helper.sendableToExpr($0).exprToExprBridge() } + ) + } +} + +class FindNearest: Stage { + var name: String = "findNearest" + var bridge: StageBridge + private var field: Field + private var vectorValue: [Double] + private var distanceMeasure: DistanceMeasure + private var limit: Int? + private var distanceField: String? + + init(field: Field, + vectorValue: [Double], + distanceMeasure: DistanceMeasure, + limit: Int? = nil, + distanceField: String? = nil) { + self.field = field + self.vectorValue = vectorValue + self.distanceMeasure = distanceMeasure + self.limit = limit + self.distanceField = distanceField + bridge = FindNearestStageBridge( + field: field.bridge as! FieldBridge, + vectorValue: VectorValue(vectorValue), + distanceMeasure: distanceMeasure.kind.rawValue, + limit: limit as NSNumber?, + distanceField: distanceField + ) + } +} + +class Sort: Stage { + var name: String = "sort" + var bridge: StageBridge + private var orderings: [Ordering] + + init(orderings: [Ordering]) { + self.orderings = orderings + bridge = SortStageBridge(orderings: orderings.map { $0.bridge }) + } +} + +class ReplaceWith: Stage { + var name: String = "replaceWith" + var bridge: StageBridge + private var expr: Expr? + private var fieldName: String? + + init(expr: Expr) { + self.expr = expr + fieldName = nil + bridge = ReplaceWithStageBridge(expr: expr.exprToExprBridge()) + } + + init(fieldName: String) { + self.fieldName = fieldName + expr = nil + bridge = ReplaceWithStageBridge(fieldName: fieldName) + } +} + +class Sample: Stage { + var name: String = "sample" + var bridge: StageBridge + private var count: Int64? + private var percentage: Double? + + init(count: Int64) { + self.count = count + percentage = nil + bridge = SampleStageBridge(count: count) + } + + init(percentage: Double) { + self.percentage = percentage + count = nil + bridge = SampleStageBridge(percentage: percentage) + } +} + +class Union: Stage { + var name: String = "union" + var bridge: StageBridge + private var other: Pipeline + + init(other: Pipeline) { + self.other = other + bridge = UnionStageBridge(other: other.bridge) + } +} + +class Unnest: Stage { + var name: String = "unnest" + var bridge: StageBridge + private var field: Selectable + private var indexField: String? + + init(field: Selectable, indexField: String? = nil) { + self.field = field + self.indexField = indexField + bridge = UnnestStageBridge( + field: Helper.sendableToExpr(field).exprToExprBridge(), + indexField: indexField + ) + } +} + +class GenericStage: Stage { + var name: String + var bridge: StageBridge + private var params: [Sendable] + private var options: [String: Sendable]? + + init(name: String, params: [Sendable], options: [String: Sendable]? = nil) { + self.name = name + self.params = params + self.options = options + let bridgeParams = params.map { Helper.sendableToExpr($0).exprToExprBridge() } + let bridgeOptions = options?.mapValues { Helper.sendableToExpr($0).exprToExprBridge() } + bridge = GenericStageBridge(name: name, params: bridgeParams, options: bridgeOptions) + } +} diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 9dbdcb5b7fc..bf310c10323 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -137,9 +137,5 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .execute() XCTAssertTrue(snapshot.results().isEmpty) - - struct MyStruct: Decodable {} - let pplResult: [PipelineResult] = try await snapshot - .results(decodeAsType: MyStruct.self) } } diff --git a/Firestore/core/src/api/aggregate_expressions.cc b/Firestore/core/src/api/aggregate_expressions.cc index 87fc69c368a..fb58918833c 100644 --- a/Firestore/core/src/api/aggregate_expressions.cc +++ b/Firestore/core/src/api/aggregate_expressions.cc @@ -22,7 +22,7 @@ namespace firebase { namespace firestore { namespace api { -google_firestore_v1_Value AggregateExpr::to_proto() const { +google_firestore_v1_Value AggregateFunction::to_proto() const { google_firestore_v1_Value result; result.which_value_type = google_firestore_v1_Value_function_value_tag; diff --git a/Firestore/core/src/api/aggregate_expressions.h b/Firestore/core/src/api/aggregate_expressions.h index 119198b2abd..fc19eacb0a5 100644 --- a/Firestore/core/src/api/aggregate_expressions.h +++ b/Firestore/core/src/api/aggregate_expressions.h @@ -29,12 +29,12 @@ namespace firebase { namespace firestore { namespace api { -class AggregateExpr { +class AggregateFunction { public: - AggregateExpr(std::string name, std::vector> params) + AggregateFunction(std::string name, std::vector> params) : name_(std::move(name)), params_(std::move(params)) { } - ~AggregateExpr() = default; + ~AggregateFunction() = default; google_firestore_v1_Value to_proto() const; diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index 5b08a277e3b..fe6f4fde9c8 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -36,18 +36,12 @@ class Expr { virtual google_firestore_v1_Value to_proto() const = 0; }; -class Selectable : public Expr { - public: - virtual ~Selectable() = default; - virtual const std::string& alias() const = 0; -}; - -class Field : public Selectable { +class Field : public Expr { public: explicit Field(std::string name) : name_(std::move(name)) { } google_firestore_v1_Value to_proto() const override; - const std::string& alias() const override { + const std::string& alias() const { return name_; } diff --git a/Firestore/core/src/api/ordering.cc b/Firestore/core/src/api/ordering.cc index 6520cea5b6f..388280b532a 100644 --- a/Firestore/core/src/api/ordering.cc +++ b/Firestore/core/src/api/ordering.cc @@ -31,7 +31,7 @@ google_firestore_v1_Value Ordering::to_proto() const { result.map_value.fields = nanopb::MakeArray(2); result.map_value.fields[0].key = nanopb::MakeBytesArray("expression"); - result.map_value.fields[0].value = field_.to_proto(); + result.map_value.fields[0].value = expr_->to_proto(); result.map_value.fields[1].key = nanopb::MakeBytesArray("direction"); google_firestore_v1_Value direction; direction.which_value_type = google_firestore_v1_Value_string_value_tag; diff --git a/Firestore/core/src/api/ordering.h b/Firestore/core/src/api/ordering.h index 130dda12b19..1350b2a0552 100644 --- a/Firestore/core/src/api/ordering.h +++ b/Firestore/core/src/api/ordering.h @@ -20,6 +20,7 @@ #include #include "Firestore/core/src/api/expressions.h" +#include "Firestore/core/src/util/exception.h" namespace firebase { namespace firestore { @@ -34,14 +35,20 @@ class Ordering { DESCENDING, }; - Ordering(Field field, Direction direction) - : field_(std::move(field)), direction_(direction) { + static Direction DirectionFromString(const std::string& str) { + if (str == "ascending") return ASCENDING; + if (str == "descending") return DESCENDING; + util::ThrowInvalidArgument("Unknown direction: '%s' ", str); + } + + Ordering(std::shared_ptr expr, Direction direction) + : expr_(expr), direction_(direction) { } google_firestore_v1_Value to_proto() const; private: - Field field_; + std::shared_ptr expr_; Direction direction_; }; diff --git a/Firestore/core/src/api/pipeline.cc b/Firestore/core/src/api/pipeline.cc index 24c5109bd95..53d332f3259 100644 --- a/Firestore/core/src/api/pipeline.cc +++ b/Firestore/core/src/api/pipeline.cc @@ -40,6 +40,20 @@ void Pipeline::execute(util::StatusOrCallback callback) { this->firestore_->RunPipeline(*this, std::move(callback)); } +google_firestore_v1_Value Pipeline::to_proto() const { + google_firestore_v1_Value result; + + result.which_value_type = google_firestore_v1_Value_pipeline_value_tag; + result.pipeline_value = google_firestore_v1_Pipeline{}; + result.pipeline_value.stages_count = this->stages_.size(); + nanopb::SetRepeatedField( + &result.pipeline_value.stages, &result.pipeline_value.stages_count, + stages_, + [](const std::shared_ptr& arg) { return arg->to_proto(); }); + + return result; +} + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/api/pipeline.h b/Firestore/core/src/api/pipeline.h index 6103f366eda..edea35dce6d 100644 --- a/Firestore/core/src/api/pipeline.h +++ b/Firestore/core/src/api/pipeline.h @@ -47,6 +47,8 @@ class Pipeline { void execute(util::StatusOrCallback callback); + google_firestore_v1_Value to_proto() const; + private: std::vector> stages_; std::shared_ptr firestore_; diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index eaa19cb03bd..f33f7e74107 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -20,6 +20,7 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/api/pipeline.h" #include "Firestore/core/src/nanopb/message.h" #include "Firestore/core/src/nanopb/nanopb_util.h" @@ -108,9 +109,9 @@ google_firestore_v1_Pipeline_Stage AddFields::to_proto() const { result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; nanopb::SetRepeatedField( &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, - fields_, [](const std::shared_ptr& entry) { + fields_, [](const std::pair>& entry) { return _google_firestore_v1_MapValue_FieldsEntry{ - nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); result.options_count = 0; @@ -130,7 +131,8 @@ google_firestore_v1_Pipeline_Stage AggregateStage::to_proto() const { nanopb::SetRepeatedField( &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, this->accumulators_, - [](const std::pair>& entry) { + [](const std::pair>& + entry) { return _google_firestore_v1_MapValue_FieldsEntry{ nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); @@ -165,23 +167,6 @@ google_firestore_v1_Pipeline_Stage Where::to_proto() const { return result; } -google_firestore_v1_Value FindNearestStage::DistanceMeasure::proto() const { - google_firestore_v1_Value result; - result.which_value_type = google_firestore_v1_Value_string_value_tag; - switch (measure_) { - case EUCLIDEAN: - result.string_value = nanopb::MakeBytesArray("euclidean"); - break; - case COSINE: - result.string_value = nanopb::MakeBytesArray("cosine"); - break; - case DOT_PRODUCT: - result.string_value = nanopb::MakeBytesArray("dot_product"); - break; - } - return result; -} - google_firestore_v1_Pipeline_Stage FindNearestStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; result.name = nanopb::MakeBytesArray("find_nearest"); @@ -190,7 +175,8 @@ google_firestore_v1_Pipeline_Stage FindNearestStage::to_proto() const { result.args = nanopb::MakeArray(3); result.args[0] = property_->to_proto(); result.args[1] = *vector_; - result.args[2] = distance_measure_.proto(); + result.args[2].which_value_type = google_firestore_v1_Value_string_value_tag; + result.args[2].string_value = nanopb::MakeBytesArray(distance_measure_); nanopb::SetRepeatedField( &result.options, &result.options_count, options_, @@ -242,9 +228,9 @@ google_firestore_v1_Pipeline_Stage SelectStage::to_proto() const { result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; nanopb::SetRepeatedField( &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, - fields_, [](const std::shared_ptr& entry) { + fields_, [](const std::pair>& entry) { return _google_firestore_v1_MapValue_FieldsEntry{ - nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); result.options_count = 0; @@ -260,7 +246,7 @@ google_firestore_v1_Pipeline_Stage SortStage::to_proto() const { result.args = nanopb::MakeArray(result.args_count); for (size_t i = 0; i < orders_.size(); ++i) { - result.args[i] = orders_[i].to_proto(); + result.args[i] = orders_[i]->to_proto(); } result.options_count = 0; @@ -278,9 +264,9 @@ google_firestore_v1_Pipeline_Stage DistinctStage::to_proto() const { result.args[0].which_value_type = google_firestore_v1_Value_map_value_tag; nanopb::SetRepeatedField( &result.args[0].map_value.fields, &result.args[0].map_value.fields_count, - groups_, [](const std::shared_ptr& entry) { + groups_, [](const std::pair>& entry) { return _google_firestore_v1_MapValue_FieldsEntry{ - nanopb::MakeBytesArray(entry->alias()), entry->to_proto()}; + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; }); result.options_count = 0; @@ -304,6 +290,135 @@ google_firestore_v1_Pipeline_Stage RemoveFieldsStage::to_proto() const { return result; } +// TBD + +google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("replace_with"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + if (expr_) { + result.args[0] = expr_->to_proto(); + } else { + result.args[0].which_value_type = + google_firestore_v1_Value_string_value_tag; + result.args[0].string_value = nanopb::MakeBytesArray(field_name_.value()); + } + + result.options_count = 0; + result.options = nullptr; + return result; +} + +ReplaceWith::ReplaceWith(std::shared_ptr expr) + : expr_(std::move(expr)), field_name_(absl::nullopt) { +} +ReplaceWith::ReplaceWith(std::string field_name) + : expr_(nullptr), field_name_(std::move(field_name)) { +} + +Sample::Sample(std::string type, int64_t count, double percentage_) + : type_(type), count_(count), percentage_(percentage_) { +} + +google_firestore_v1_Pipeline_Stage Sample::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("sample"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + if (type_ == "count") { + result.args[0].which_value_type = + google_firestore_v1_Value_integer_value_tag; + result.args[0].integer_value = count_; + } else { + result.args[0].which_value_type = + google_firestore_v1_Value_double_value_tag; + result.args[0].double_value = percentage_; + } + + result.options_count = 0; + result.options = nullptr; + return result; +} + +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.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0] = other_->to_proto(); + + result.options_count = 0; + result.options = nullptr; + return result; +} + +Unnest::Unnest(std::shared_ptr field, + absl::optional index_field) + : field_(std::move(field)), index_field_(std::move(index_field)) { +} + +google_firestore_v1_Pipeline_Stage Unnest::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray("unnest"); + + result.args_count = 1; + result.args = nanopb::MakeArray(1); + result.args[0] = field_->to_proto(); + + if (index_field_.has_value()) { + result.options_count = 1; + result.options = + nanopb::MakeArray(1); + result.options[0].key = nanopb::MakeBytesArray("index_field"); + result.options[0].value.which_value_type = + google_firestore_v1_Value_string_value_tag; + result.options[0].value.string_value = + nanopb::MakeBytesArray(index_field_.value()); + } else { + result.options_count = 0; + result.options = nullptr; + } + + return result; +} + +GenericStage::GenericStage( + std::string name, + std::vector> params, + std::unordered_map> options) + : name_(std::move(name)), + params_(std::move(params)), + options_(std::move(options)) { +} + +google_firestore_v1_Pipeline_Stage GenericStage::to_proto() const { + google_firestore_v1_Pipeline_Stage result; + result.name = nanopb::MakeBytesArray(name_); + + result.args_count = static_cast(params_.size()); + result.args = nanopb::MakeArray(result.args_count); + + for (size_t i = 0; i < result.args_count; i++) { + result.args[i] = params_[i]->to_proto(); + } + + nanopb::SetRepeatedField( + &result.options, &result.options_count, options_, + [](const std::pair>& entry) { + return _google_firestore_v1_Pipeline_Stage_OptionsEntry{ + nanopb::MakeBytesArray(entry.first), entry.second->to_proto()}; + }); + + return result; +} + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index f2c20f8bd9e..669556a1ee7 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -25,9 +25,11 @@ #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/core/src/api/aggregate_expressions.h" +#include "Firestore/core/src/api/api_fwd.h" #include "Firestore/core/src/api/expressions.h" #include "Firestore/core/src/api/ordering.h" #include "Firestore/core/src/nanopb/message.h" +#include "absl/types/optional.h" namespace firebase { namespace firestore { @@ -89,7 +91,8 @@ class DocumentsSource : public Stage { class AddFields : public Stage { public: - explicit AddFields(std::vector> fields) + explicit AddFields( + std::unordered_map> fields) : fields_(std::move(fields)) { } ~AddFields() override = default; @@ -97,21 +100,23 @@ class AddFields : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector> fields_; + std::unordered_map> fields_; }; class AggregateStage : public Stage { public: - AggregateStage(std::unordered_map> - accumulators, - std::unordered_map> groups) + AggregateStage( + std::unordered_map> + accumulators, + std::unordered_map> groups) : accumulators_(std::move(accumulators)), groups_(std::move(groups)) { } google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::unordered_map> accumulators_; + std::unordered_map> + accumulators_; std::unordered_map> groups_; }; @@ -129,22 +134,10 @@ class Where : public Stage { class FindNearestStage : public Stage { public: - class DistanceMeasure { - public: - enum Measure { EUCLIDEAN, COSINE, DOT_PRODUCT }; - - explicit DistanceMeasure(Measure measure) : measure_(measure) { - } - google_firestore_v1_Value proto() const; - - private: - Measure measure_; - }; - FindNearestStage( std::shared_ptr property, nanopb::SharedMessage vector, - DistanceMeasure distance_measure, + std::string distance_measure, std::unordered_map> options) @@ -161,7 +154,7 @@ class FindNearestStage : public Stage { private: std::shared_ptr property_; nanopb::SharedMessage vector_; - DistanceMeasure distance_measure_; + std::string distance_measure_; std::unordered_map> options_; @@ -193,7 +186,8 @@ class OffsetStage : public Stage { class SelectStage : public Stage { public: - explicit SelectStage(std::vector> fields) + explicit SelectStage( + std::unordered_map> fields) : fields_(std::move(fields)) { } ~SelectStage() override = default; @@ -201,12 +195,12 @@ class SelectStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector> fields_; + std::unordered_map> fields_; }; class SortStage : public Stage { public: - explicit SortStage(std::vector orders) + explicit SortStage(std::vector> orders) : orders_(std::move(orders)) { } ~SortStage() override = default; @@ -214,12 +208,13 @@ class SortStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector orders_; + std::vector> orders_; }; class DistinctStage : public Stage { public: - explicit DistinctStage(std::vector> groups) + explicit DistinctStage( + std::unordered_map> groups) : groups_(std::move(groups)) { } ~DistinctStage() override = default; @@ -227,7 +222,7 @@ class DistinctStage : public Stage { google_firestore_v1_Pipeline_Stage to_proto() const override; private: - std::vector> groups_; + std::unordered_map> groups_; }; class RemoveFieldsStage : public Stage { @@ -243,6 +238,81 @@ class RemoveFieldsStage : public Stage { std::vector fields_; }; +// TBD +/** + * A stage that replaces the document with a new value. + */ +class ReplaceWith : public Stage { + public: + explicit ReplaceWith(std::shared_ptr expr); + explicit ReplaceWith(std::string field_name); + ~ReplaceWith() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr expr_; + absl::optional field_name_; +}; + +/** + * A stage that samples documents. + */ +class Sample : public Stage { + public: + Sample(std::string type, int64_t count, double percentage_); + ~Sample() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::string type_; + int64_t count_; + double percentage_; +}; + +/** + * A stage that unions documents from another pipeline. + */ +class Union : public Stage { + public: + explicit Union(std::shared_ptr other); + ~Union() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr other_; +}; + +/** + * A stage that unnests an array. + */ +class Unnest : public Stage { + public: + Unnest(std::shared_ptr field, absl::optional index_field); + ~Unnest() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::shared_ptr field_; + absl::optional index_field_; +}; + +/** + * A stage that is a generic stage. + */ +class GenericStage : public Stage { + public: + GenericStage(std::string name, + std::vector> params, + std::unordered_map> options); + ~GenericStage() override = default; + google_firestore_v1_Pipeline_Stage to_proto() const override; + + private: + std::string name_; + std::vector> params_; + std::unordered_map> options_; +}; + } // namespace api } // namespace firestore } // namespace firebase From a5f558f6945f461d4ae5e7eda2a8f69e393654b8 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 17 Apr 2025 16:08:25 -0400 Subject: [PATCH 23/43] add tests --- Firestore/Source/API/FIRPipelineBridge.mm | 15 +++++++++--- .../SwiftAPI/Pipeline/PipelineSource.swift | 3 ++- .../Tests/Integration/PipelineTests.swift | 24 ++++++++++++++++++- Firestore/core/src/api/expressions.h | 1 + 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 48c82caf2cb..8a2b7f9842c 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -570,9 +570,18 @@ - (id)initWithField:(FIRFieldBridge *)field [reader parsedQueryValue:_distanceField]))); } - find_nearest = std::make_shared([_field cppExprWithReader:reader], - [reader parsedQueryValue:_vectorValue], - MakeString(_distanceMeasure), optional_value); + FindNearestStage::DistanceMeasure::Measure measure_enum; + if ([_distanceMeasure isEqualToString:@"cosine"]) { + measure_enum = FindNearestStage::DistanceMeasure::COSINE; + } else if ([_distanceMeasure isEqualToString:@"dot_product"]) { + measure_enum = FindNearestStage::DistanceMeasure::DOT_PRODUCT; + } else { + measure_enum = FindNearestStage::DistanceMeasure::EUCLIDEAN; + } + + find_nearest = std::make_shared( + [_field cppExprWithReader:reader], [reader parsedQueryValue:_vectorValue], + FindNearestStage::DistanceMeasure(measure_enum), optional_value); } isUserDataRead = YES; diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 68ee48c6f6d..c6d41bfe466 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -21,7 +21,8 @@ public struct PipelineSource: @unchecked Sendable { } public func collection(_ path: String) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: path)], db: db) + let normalizedPath = path.hasPrefix("/") ? path : "/" + path + return Pipeline(stages: [CollectionSource(collection: normalizedPath)], db: db) } public func collectionGroup(_ collectionId: String) -> Pipeline { diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index bf310c10323..51b831ad065 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -132,10 +132,32 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let snapshot = try await db .pipeline() - .collection("/" + collRef.path) + .collection(collRef.path) .limit(0) .execute() XCTAssertTrue(snapshot.results().isEmpty) } + + func testFullResults() async throws { + let collRef = collectionRef( + withDocuments: bookDocs + ) + let db = collRef.firestore + + let snapshot = try await db + .pipeline() + .collection(collRef.path) + .execute() + + let results = snapshot.results() + XCTAssertEqual(results.count, 10) + + let actualIDs = Set(results.map { $0.id }) + let expectedIDs = Set([ + "book1", "book2", "book3", "book4", "book5", + "book6", "book7", "book8", "book9", "book10", + ]) + XCTAssertEqual(actualIDs, expectedIDs) + } } diff --git a/Firestore/core/src/api/expressions.h b/Firestore/core/src/api/expressions.h index 95f1391370b..fe6f4fde9c8 100644 --- a/Firestore/core/src/api/expressions.h +++ b/Firestore/core/src/api/expressions.h @@ -23,6 +23,7 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" +#include "Firestore/core/src/nanopb/message.h" namespace firebase { namespace firestore { From be5b0b264dc42a5698b5d1eef7dcfa5d32d86b8a Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 24 Apr 2025 15:05:55 -0400 Subject: [PATCH 24/43] change implementation details --- Firestore/Source/API/FIRPipelineBridge.mm | 148 +++++++++--------- .../Swift/Source/Helper/PipelineHelper.swift | 24 +-- ...dgeWrapper.swift => PipelineWrapper.swift} | 5 + .../Swift/Source/SelectableInternal.swift | 18 --- .../Aggregation/AggregateFunction.swift | 2 +- .../Source/SwiftAPI/Pipeline/Count.swift | 13 -- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 15 +- .../SwiftAPI/Pipeline/Expr/Constant.swift | 7 +- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 4 +- .../SwiftAPI/Pipeline/Expr/FunctionExpr.swift | 2 +- .../Expr/FunctionExpr/BooleanExpr.swift | 1 - .../SwiftAPI/Pipeline/ExprWithAlias.swift | 2 +- .../Swift/Source/SwiftAPI/Pipeline/Min.swift | 13 -- .../Source/SwiftAPI/Pipeline/Ordering.swift | 2 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 14 +- .../SwiftAPI/Pipeline/PipelineResult.swift | 22 ++- .../SwiftAPI/Pipeline/PipelineSnapshot.swift | 13 +- .../SwiftAPI/Pipeline/PipelineSource.swift | 11 +- .../Swift/Source/SwiftAPI/Pipeline/Sum.swift | 15 -- .../Source/SwiftAPI/Pipeline/TimeUnit.swift | 8 - Firestore/Swift/Source/SwiftAPI/Stages.swift | 91 ++++++++--- Firestore/core/src/api/stages.cc | 2 - 22 files changed, 208 insertions(+), 224 deletions(-) rename Firestore/Swift/Source/{BridgeWrapper.swift => PipelineWrapper.swift} (88%) delete mode 100644 Firestore/Swift/Source/SelectableInternal.swift delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift delete mode 100644 Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 8a2b7f9842c..ea45db7ebdf 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -99,7 +99,7 @@ - (id)init:(NSString *)name { @end @implementation FIRConstantBridge { - std::shared_ptr constant; + std::shared_ptr cpp_constant; id _input; Boolean isUserDataRead; } @@ -112,17 +112,17 @@ - (id)init:(id)input { - (std::shared_ptr)cppExprWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - constant = std::make_shared([reader parsedQueryValue:_input]); + cpp_constant = std::make_shared([reader parsedQueryValue:_input]); } isUserDataRead = YES; - return constant; + return cpp_constant; } @end @implementation FIRFunctionExprBridge { - std::shared_ptr eq; + std::shared_ptr cpp_function; NSString *_name; NSArray *_args; Boolean isUserDataRead; @@ -142,17 +142,17 @@ - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray(MakeString(_name), std::move(cpp_args)); + cpp_function = std::make_shared(MakeString(_name), std::move(cpp_args)); } isUserDataRead = YES; - return eq; + return cpp_function; } @end @implementation FIRAggregateFunctionBridge { - std::shared_ptr cpp_bridge; + std::shared_ptr cpp_function; NSString *_name; NSArray *_args; Boolean isUserDataRead; @@ -171,17 +171,17 @@ - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray(MakeString(_name), std::move(cpp_args)); + cpp_function = std::make_shared(MakeString(_name), std::move(cpp_args)); } isUserDataRead = YES; - return cpp_bridge; + return cpp_function; } @end @implementation FIROrderingBridge { - std::shared_ptr cpp_bridge; + std::shared_ptr cpp_ordering; NSString *_direction; FIRExprBridge *_expr; Boolean isUserDataRead; @@ -196,12 +196,12 @@ - (nonnull id)initWithExpr:(FIRExprBridge *)expr Direction:(NSString *)direction - (std::shared_ptr)cppOrderingWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - cpp_bridge = std::make_shared([_expr cppExprWithReader:reader], - Ordering::DirectionFromString(MakeString(_direction))); + cpp_ordering = std::make_shared( + [_expr cppExprWithReader:reader], Ordering::DirectionFromString(MakeString(_direction))); } isUserDataRead = YES; - return cpp_bridge; + return cpp_ordering; } @end @@ -228,43 +228,43 @@ - (id)initWithPath:(NSString *)path { @end @implementation FIRDatabaseSourceStageBridge { - std::shared_ptr database_source; + std::shared_ptr cpp_database_source; } - (id)init { self = [super init]; if (self) { - database_source = std::make_shared(); + cpp_database_source = std::make_shared(); } return self; } - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { - return database_source; + return cpp_database_source; } @end @implementation FIRCollectionGroupSourceStageBridge { - std::shared_ptr collection_group_source; + std::shared_ptr cpp_collection_group_source; } - (id)initWithCollectionId:(NSString *)id { self = [super init]; if (self) { - collection_group_source = std::make_shared(MakeString(id)); + cpp_collection_group_source = std::make_shared(MakeString(id)); } return self; } - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { - return collection_group_source; + return cpp_collection_group_source; } @end @implementation FIRDocumentsSourceStageBridge { - std::shared_ptr document_source; + std::shared_ptr cpp_document_source; } - (id)initWithDocuments:(NSArray *)documents { @@ -274,13 +274,13 @@ - (id)initWithDocuments:(NSArray *)documents { for (NSString *doc in documents) { cpp_documents.push_back(MakeString(doc)); } - document_source = std::make_shared(std::move(cpp_documents)); + cpp_document_source = std::make_shared(std::move(cpp_documents)); } return self; } - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { - return document_source; + return cpp_document_source; } @end @@ -288,7 +288,7 @@ - (id)initWithDocuments:(NSArray *)documents { @implementation FIRWhereStageBridge { FIRExprBridge *_exprBridge; Boolean isUserDataRead; - std::shared_ptr where; + std::shared_ptr cpp_where; } - (id)initWithExpr:(FIRExprBridge *)expr { @@ -302,18 +302,18 @@ - (id)initWithExpr:(FIRExprBridge *)expr { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - where = std::make_shared([_exprBridge cppExprWithReader:reader]); + cpp_where = std::make_shared([_exprBridge cppExprWithReader:reader]); } isUserDataRead = YES; - return where; + return cpp_where; } @end @implementation FIRLimitStageBridge { Boolean isUserDataRead; - std::shared_ptr limit_stage; + std::shared_ptr cpp_limit_stage; int32_t limit; } @@ -328,18 +328,18 @@ - (id)initWithLimit:(NSInteger)value { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - limit_stage = std::make_shared(limit); + cpp_limit_stage = std::make_shared(limit); } isUserDataRead = YES; - return limit_stage; + return cpp_limit_stage; } @end @implementation FIROffsetStageBridge { Boolean isUserDataRead; - std::shared_ptr offset_stage; + std::shared_ptr cpp_offset_stage; int32_t offset; } @@ -354,11 +354,11 @@ - (id)initWithOffset:(NSInteger)value { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - offset_stage = std::make_shared(offset); + cpp_offset_stage = std::make_shared(offset); } isUserDataRead = YES; - return offset_stage; + return cpp_offset_stage; } @end @@ -368,7 +368,7 @@ - (id)initWithOffset:(NSInteger)value { @implementation FIRAddFieldsStageBridge { NSDictionary *_fields; Boolean isUserDataRead; - std::shared_ptr add_fields; + std::shared_ptr cpp_add_fields; } - (id)initWithFields:(NSDictionary *)fields { @@ -386,11 +386,11 @@ - (id)initWithFields:(NSDictionary *)fields { for (NSString *key in _fields) { cpp_fields[MakeString(key)] = [_fields[key] cppExprWithReader:reader]; } - add_fields = std::make_shared(std::move(cpp_fields)); + cpp_add_fields = std::make_shared(std::move(cpp_fields)); } isUserDataRead = YES; - return add_fields; + return cpp_add_fields; } @end @@ -398,7 +398,7 @@ - (id)initWithFields:(NSDictionary *)fields { @implementation FIRRemoveFieldsStageBridge { NSArray *_fields; Boolean isUserDataRead; - std::shared_ptr remove_fields; + std::shared_ptr cpp_remove_fields; } - (id)initWithFields:(NSArray *)fields { @@ -416,11 +416,11 @@ - (id)initWithFields:(NSArray *)fields { for (id field in _fields) { cpp_fields.push_back(Field(MakeString(field))); } - remove_fields = std::make_shared(std::move(cpp_fields)); + cpp_remove_fields = std::make_shared(std::move(cpp_fields)); } isUserDataRead = YES; - return remove_fields; + return cpp_remove_fields; } @end @@ -428,7 +428,7 @@ - (id)initWithFields:(NSArray *)fields { @implementation FIRSelectStageBridge { NSDictionary *_selections; Boolean isUserDataRead; - std::shared_ptr select; + std::shared_ptr cpp_select; } - (id)initWithSelections:(NSDictionary *)selections { @@ -446,11 +446,11 @@ - (id)initWithSelections:(NSDictionary *)selections for (NSString *key in _selections) { cpp_selections[MakeString(key)] = [_selections[key] cppExprWithReader:reader]; } - select = std::make_shared(std::move(cpp_selections)); + cpp_select = std::make_shared(std::move(cpp_selections)); } isUserDataRead = YES; - return select; + return cpp_select; } @end @@ -458,7 +458,7 @@ - (id)initWithSelections:(NSDictionary *)selections @implementation FIRDistinctStageBridge { NSDictionary *_groups; Boolean isUserDataRead; - std::shared_ptr distinct; + std::shared_ptr cpp_distinct; } - (id)initWithGroups:(NSDictionary *)groups { @@ -476,11 +476,11 @@ - (id)initWithGroups:(NSDictionary *)groups { for (NSString *key in _groups) { cpp_groups[MakeString(key)] = [_groups[key] cppExprWithReader:reader]; } - distinct = std::make_shared(std::move(cpp_groups)); + cpp_distinct = std::make_shared(std::move(cpp_groups)); } isUserDataRead = YES; - return distinct; + return cpp_distinct; } @end @@ -489,7 +489,7 @@ @implementation FIRAggregateStageBridge { NSDictionary *_accumulators; NSDictionary *_groups; Boolean isUserDataRead; - std::shared_ptr aggregate; + std::shared_ptr cpp_aggregate; } - (id)initWithAccumulators:(NSDictionary *)accumulators @@ -514,12 +514,12 @@ - (id)initWithAccumulators:(NSDictionary(std::move(cpp_accumulators), std::move(cpp_groups)); } isUserDataRead = YES; - return aggregate; + return cpp_aggregate; } @end @@ -531,7 +531,7 @@ @implementation FIRFindNearestStageBridge { NSNumber *_limit; NSString *_Nullable _distanceField; Boolean isUserDataRead; - std::shared_ptr find_nearest; + std::shared_ptr cpp_find_nearest; } - (id)initWithField:(FIRFieldBridge *)field @@ -579,13 +579,13 @@ - (id)initWithField:(FIRFieldBridge *)field measure_enum = FindNearestStage::DistanceMeasure::EUCLIDEAN; } - find_nearest = std::make_shared( + cpp_find_nearest = std::make_shared( [_field cppExprWithReader:reader], [reader parsedQueryValue:_vectorValue], FindNearestStage::DistanceMeasure(measure_enum), optional_value); } isUserDataRead = YES; - return find_nearest; + return cpp_find_nearest; } @end @@ -593,7 +593,7 @@ - (id)initWithField:(FIRFieldBridge *)field @implementation FIRSorStageBridge { NSArray *_orderings; Boolean isUserDataRead; - std::shared_ptr sort; + std::shared_ptr cpp_sort; } - (id)initWithOrderings:(NSArray *)orderings { @@ -611,11 +611,11 @@ - (id)initWithOrderings:(NSArray *)orderings { for (FIROrderingBridge *ordering in _orderings) { cpp_orderings.push_back([ordering cppOrderingWithReader:reader]); } - sort = std::make_shared(std::move(cpp_orderings)); + cpp_sort = std::make_shared(std::move(cpp_orderings)); } isUserDataRead = YES; - return sort; + return cpp_sort; } @end @@ -624,7 +624,7 @@ @implementation FIRReplaceWithStageBridge { FIRExprBridge *_expr; NSString *_fieldName; Boolean isUserDataRead; - std::shared_ptr replace_with; + std::shared_ptr cpp_replace_with; } - (id)initWithExpr:(FIRExprBridge *)expr { @@ -650,14 +650,14 @@ - (id)initWithFieldName:(NSString *)fieldName { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { if (_expr) { - replace_with = std::make_shared([_expr cppExprWithReader:reader]); + cpp_replace_with = std::make_shared([_expr cppExprWithReader:reader]); } else { - replace_with = std::make_shared(MakeString(_fieldName)); + cpp_replace_with = std::make_shared(MakeString(_fieldName)); } } isUserDataRead = YES; - return replace_with; + return cpp_replace_with; } @end @@ -667,7 +667,7 @@ @implementation FIRSampleStageBridge { double _percentage; Boolean isUserDataRead; NSString *type; - std::shared_ptr sample; + std::shared_ptr cpp_sample; } - (id)initWithCount:(int64_t)count { @@ -695,14 +695,14 @@ - (id)initWithPercentage:(double)percentage { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { if ([type isEqualToString:@"count"]) { - sample = std::make_shared("count", _count, 0); + cpp_sample = std::make_shared("count", _count, 0); } else { - sample = std::make_shared("percentage", 0, _percentage); + cpp_sample = std::make_shared("percentage", 0, _percentage); } } isUserDataRead = YES; - return sample; + return cpp_sample; } @end @@ -710,7 +710,7 @@ - (id)initWithPercentage:(double)percentage { @implementation FIRUnionStageBridge { FIRPipelineBridge *_other; Boolean isUserDataRead; - std::shared_ptr union_stage; + std::shared_ptr cpp_union_stage; } - (id)initWithOther:(FIRPipelineBridge *)other { @@ -724,11 +724,11 @@ - (id)initWithOther:(FIRPipelineBridge *)other { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - union_stage = std::make_shared([_other cppPipelineWithReader:reader]); + cpp_union_stage = std::make_shared([_other cppPipelineWithReader:reader]); } isUserDataRead = YES; - return union_stage; + return cpp_union_stage; } @end @@ -737,7 +737,7 @@ @implementation FIRUnnestStageBridge { FIRExprBridge *_field; NSString *_Nullable _indexField; Boolean isUserDataRead; - std::shared_ptr unnest; + std::shared_ptr cpp_unnest; } - (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)indexField { @@ -758,11 +758,11 @@ - (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)index } else { cpp_index_field = absl::nullopt; } - unnest = std::make_shared([_field cppExprWithReader:reader], cpp_index_field); + cpp_unnest = std::make_shared([_field cppExprWithReader:reader], cpp_index_field); } isUserDataRead = YES; - return unnest; + return cpp_unnest; } @end @@ -772,7 +772,7 @@ @implementation FIRGenericStageBridge { NSArray *_params; NSDictionary *_Nullable _options; Boolean isUserDataRead; - std::shared_ptr generic_stage; + std::shared_ptr cpp_generic_stage; } - (id)initWithName:(NSString *)name @@ -800,12 +800,12 @@ - (id)initWithName:(NSString *)name cpp_options[MakeString(key)] = [_options[key] cppExprWithReader:reader]; } } - generic_stage = std::make_shared(MakeString(_name), std::move(cpp_params), - std::move(cpp_options)); + cpp_generic_stage = std::make_shared(MakeString(_name), std::move(cpp_params), + std::move(cpp_options)); } isUserDataRead = YES; - return generic_stage; + return cpp_generic_stage; } @end @@ -922,7 +922,7 @@ - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr *_stages; FIRFirestore *firestore; - std::shared_ptr pipeline; + std::shared_ptr cpp_pipeline; } - (id)initWithStages:(NSArray *)stages db:(FIRFirestore *)db { @@ -937,9 +937,9 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r for (FIRStageBridge *stage in _stages) { cpp_stages.push_back([stage cppStageWithReader:firestore.dataReader]); } - pipeline = std::make_shared(cpp_stages, firestore.wrapped); + cpp_pipeline = std::make_shared(cpp_stages, firestore.wrapped); - pipeline->execute([completion](StatusOr maybe_value) { + cpp_pipeline->execute([completion](StatusOr maybe_value) { if (maybe_value.ok()) { __FIRPipelineSnapshotBridge *bridge = [[__FIRPipelineSnapshotBridge alloc] initWithCppSnapshot:std::move(maybe_value).ValueOrDie()]; @@ -951,7 +951,7 @@ - (void)executeWithCompletion:(void (^)(__FIRPipelineSnapshotBridge *_Nullable r } - (std::shared_ptr)cppPipelineWithReader:(FSTUserDataReader *)reader { - return pipeline; + return cpp_pipeline; } @end diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift index 05e832e7525..582e90021b1 100644 --- a/Firestore/Swift/Source/Helper/PipelineHelper.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -25,26 +25,12 @@ enum Helper { } } - static func selectablesToMap(selectables: [Any]) -> [String: Expr] { - var result = [String: Expr]() - for selectable in selectables { - if let stringSelectable = selectable as? String { - result[stringSelectable] = Field(stringSelectable) - } else if let fieldSelectable = selectable as? Field { - result[fieldSelectable.alias] = fieldSelectable.expr - } else if let exprAliasSelectable = selectable as? ExprWithAlias { - result[exprAliasSelectable.alias] = exprAliasSelectable.expr - } + static func selectablesToMap(selectables: [Selectable]) -> [String: Expr] { + let exprMap = selectables.reduce(into: [String: Expr]()) { result, selectable in + let value = selectable as! SelectableWrapper + result[value.alias] = value.expr } - return result - } - - static func vectorToExpr(_ value: VectorValue) -> Expr { - return Field("PLACEHOLDER") - } - - static func timeUnitToExpr(_ value: TimeUnit) -> Expr { - return Field("PLACEHOLDER") + return exprMap } static func map(_ elements: [String: Sendable]) -> FunctionExpr { diff --git a/Firestore/Swift/Source/BridgeWrapper.swift b/Firestore/Swift/Source/PipelineWrapper.swift similarity index 88% rename from Firestore/Swift/Source/BridgeWrapper.swift rename to Firestore/Swift/Source/PipelineWrapper.swift index a3f60de9d54..a057c2e4ea2 100644 --- a/Firestore/Swift/Source/BridgeWrapper.swift +++ b/Firestore/Swift/Source/PipelineWrapper.swift @@ -19,3 +19,8 @@ protocol BridgeWrapper { protocol AggregateBridgeWrapper { var bridge: AggregateFunctionBridge { get } } + +protocol SelectableWrapper: Sendable { + var alias: String { get } + var expr: Expr { get } +} diff --git a/Firestore/Swift/Source/SelectableInternal.swift b/Firestore/Swift/Source/SelectableInternal.swift deleted file mode 100644 index 8f35e738dec..00000000000 --- a/Firestore/Swift/Source/SelectableInternal.swift +++ /dev/null @@ -1,18 +0,0 @@ -// 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. - -protocol SelectableInternal: Sendable { - var alias: String { get } - var expr: Expr { get } -} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift index d1b0538eb97..f44aa6e8e7d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift @@ -23,7 +23,7 @@ public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { self.agrs = agrs bridge = AggregateFunctionBridge( name: functionName, - args: self.agrs.map { ($0 as! BridgeWrapper).bridge + args: self.agrs.map { $0.toBridge() } ) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift deleted file mode 100644 index 470cd3d1d5a..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Count.swift +++ /dev/null @@ -1,13 +0,0 @@ -// 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. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index d54e8c43eb4..5c5972849a3 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -19,6 +19,7 @@ #endif // SWIFT_PACKAGE import Foundation +// TODO: the implementation of `Expr` is not complete public protocol Expr: Sendable { func `as`(_ name: String) -> ExprWithAlias @@ -607,7 +608,7 @@ public extension Expr { } func cosineDistance(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("cosine_distance", [self, Helper.vectorToExpr(other)]) + return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) } func cosineDistance(_ other: [Double]) -> FunctionExpr { @@ -619,7 +620,7 @@ public extension Expr { } func dotProduct(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("dot_product", [self, Helper.vectorToExpr(other)]) + return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) } func dotProduct(_ other: [Double]) -> FunctionExpr { @@ -631,7 +632,7 @@ public extension Expr { } func euclideanDistance(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("euclidean_distance", [self, Helper.vectorToExpr(other)]) + return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) } func euclideanDistance(_ other: [Double]) -> FunctionExpr { @@ -643,7 +644,7 @@ public extension Expr { } func manhattanDistance(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("manhattan_distance", [self, Helper.vectorToExpr(other)]) + return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) } func manhattanDistance(_ other: [Double]) -> FunctionExpr { @@ -683,7 +684,7 @@ public extension Expr { func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { return FunctionExpr( "timestamp_add", - [self, Helper.timeUnitToExpr(unit), Helper.sendableToExpr(amount)] + [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] ) } @@ -694,7 +695,7 @@ public extension Expr { func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { return FunctionExpr( "timestamp_sub", - [self, Helper.timeUnitToExpr(unit), Helper.sendableToExpr(amount)] + [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] ) } @@ -780,7 +781,7 @@ public extension Expr { } extension Expr { - func exprToExprBridge() -> ExprBridge { + func toBridge() -> ExprBridge { return (self as! BridgeWrapper).bridge } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index 5d3109a5fcb..cd3c3ffa50d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -26,8 +26,11 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { // Initializer for optional values (including nil) init(_ value: Any?) { self.value = value - // TODO: - bridge = ConstantBridge(value) + if value == nil { + bridge = ConstantBridge(NSNull()) + } else { + bridge = ConstantBridge(value!) + } } // Initializer for numbers diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift index d8c9dcd4961..980695b9e78 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, SelectableInternal, +public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, SelectableWrapper, @unchecked Sendable { var bridge: ExprBridge var alias: String - var expr: any Expr { + var expr: Expr { return self } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift index a80f078d853..1849b3aacbc 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -23,7 +23,7 @@ public class FunctionExpr: Expr, BridgeWrapper, @unchecked Sendable { self.agrs = agrs bridge = FunctionExprBridge( name: functionName, - args: self.agrs.map { ($0 as! BridgeWrapper).bridge + args: self.agrs.map { $0.toBridge() } ) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift index 1f6b6af67a3..9826d1698c0 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr/BooleanExpr.swift @@ -27,7 +27,6 @@ public class BooleanExpr: FunctionExpr, @unchecked Sendable { try BooleanExpr("or", [lhs, rhs()]) } - // not public static prefix func ! (lhs: BooleanExpr) -> BooleanExpr { return BooleanExpr("not", [lhs]) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift index c49e756d02c..247427f2fd8 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/ExprWithAlias.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct ExprWithAlias: Selectable, SelectableInternal, Sendable { +public struct ExprWithAlias: Selectable, SelectableWrapper, Sendable { public var alias: String public var expr: Expr diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift deleted file mode 100644 index 470cd3d1d5a..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Min.swift +++ /dev/null @@ -1,13 +0,0 @@ -// 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. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index 6ca47be47fd..70768337b53 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -22,7 +22,7 @@ public class Ordering: @unchecked Sendable { init(expr: Expr, direction: Direction) { self.expr = expr self.direction = direction - bridge = OrderingBridge(expr: expr.exprToExprBridge(), direction: direction.rawValue) + bridge = OrderingBridge(expr: expr.toBridge(), direction: direction.rawValue) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index bc90b83fe6a..dff0f63dd40 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -117,9 +117,9 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter selections: `String` values representing field names. /// - Returns: A new `Pipeline` object with this stage appended to the stage list. public func select(_ selection: String, _ additionalSelections: String...) -> Pipeline { - let selections = [selection] + additionalSelections + let selections = ([selection] + additionalSelections).map { Field($0) } return Pipeline( - stages: stages + [Select(selections: selections + additionalSelections)], + stages: stages + [Select(selections: selections)], db: db ) } @@ -188,8 +188,8 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter selections: The fields to include in the output documents, specified as /// `String` values representing field names. public func distinct(_ group: String, _ additionalGroups: String...) -> Pipeline { - let groups = [group] + additionalGroups - return Pipeline(stages: stages + [Distinct(groups: groups + additionalGroups)], db: db) + let selections = ([group] + additionalGroups).map { Field($0) } + return Pipeline(stages: stages + [Distinct(groups: selections)], db: db) } /// Returns a set of distinct `Expr` values from the inputs to this stage. @@ -276,7 +276,11 @@ public struct Pipeline: @unchecked Sendable { /// - Returns: A new `Pipeline` object with this stage appended. public func aggregate(_ accumulator: [AggregateWithAlias], groups: [String]? = nil) -> Pipeline { - return Pipeline(stages: stages + [Aggregate(accumulators: accumulator, groups: groups)], db: db) + let selectables = groups?.map { Field($0) } + return Pipeline( + stages: stages + [Aggregate(accumulators: accumulator, groups: selectables)], + db: db + ) } /// Performs a vector similarity search, ordering the result set by most similar to least diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift index 9b4a9e3c575..61a1bdc7803 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -20,14 +20,14 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -public struct PipelineResult: @unchecked Sendable { +public struct PipelineResult: @unchecked Sendable { let bridge: __PipelineResultBridge init(_ bridge: __PipelineResultBridge) { self.bridge = bridge ref = self.bridge.reference id = self.bridge.documentID - data = self.bridge.data() as! T + data = self.bridge.data() createTime = self.bridge.create_time updateTime = self.bridge.update_time } @@ -45,12 +45,26 @@ public struct PipelineResult: @unchecked Sendable { public let updateTime: Timestamp? /// Retrieves all fields in the result as a dictionary. - public let data: T + public let data: [String: Sendable] /// Retrieves the field specified by `fieldPath`. /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). /// - Returns: The data at the specified field location or `nil` if no such field exists. - public func get(_ fieldPath: Any) -> Sendable? { + public func get(_ fieldName: String) -> Sendable? { + return data[fieldName] + } + + /// Retrieves the field specified by `fieldPath`. + /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). + /// - Returns: The data at the specified field location or `nil` if no such field exists. + public func get(_ fieldPath: FieldPath) -> Sendable? { return "PLACEHOLDER" } + + /// Retrieves the field specified by `fieldPath`. + /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). + /// - Returns: The data at the specified field location or `nil` if no such field exists. + public func get(_ field: Field) -> Sendable? { + return data[field.fieldName] + } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift index bc9468923ef..e25191b8ad2 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift @@ -25,7 +25,7 @@ public struct PipelineSnapshot: Sendable { public let pipeline: Pipeline /// An array of all the results in the `PipelineSnapshot`. - let results_cache: [PipelineResult<[String: Sendable]>] + let results_cache: [PipelineResult] /// The time at which the pipeline producing this result was executed. public let executionTime: Timestamp @@ -39,16 +39,7 @@ public struct PipelineSnapshot: Sendable { results_cache = self.bridge.results.map { PipelineResult($0) } } - public func results() -> [PipelineResult<[String: Sendable]>] { + public func results() -> [PipelineResult] { return results_cache } - - public func results(decodeAsType: T.Type = T.self, - decoder: Firestore - .Decoder = .init()) async throws -> [PipelineResult< - T - >] { - return try decoder - .decode(T.self, from: results, in: nil as DocumentReference?) as! [PipelineResult] - } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index c6d41bfe466..d02f338ec37 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -33,22 +33,23 @@ public struct PipelineSource: @unchecked Sendable { } public func database() -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) + return Pipeline(stages: [DatabaseSource()], db: db) } public func documents(_ docs: [DocumentReference]) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) + let paths = docs.map { $0.path } + return Pipeline(stages: [DocumentsSource(paths: paths)], db: db) } public func documents(_ paths: [String]) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) + return Pipeline(stages: [DocumentsSource(paths: paths)], db: db) } public func create(from query: Query) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) + return Pipeline(stages: [QuerySource(query: query)], db: db) } public func create(from aggregateQuery: AggregateQuery) -> Pipeline { - return Pipeline(stages: [CollectionSource(collection: "placeholder")], db: db) + return Pipeline(stages: [AggregateQuerySource(aggregateQuery: aggregateQuery)], db: db) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift deleted file mode 100644 index 6bbdab0b20c..00000000000 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Sum.swift +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -public struct Sum {} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift index e5030cc7a83..0b8aa112db8 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/TimeUnit.swift @@ -34,12 +34,4 @@ public struct TimeUnit: Sendable, Equatable, Hashable { init(kind: Kind) { rawValue = kind.rawValue } - - public init(rawValue: String) { - if let kind = Kind(rawValue: rawValue) { - self.rawValue = kind.rawValue - } else { - fatalError("Invalid TimeUnit: \(rawValue)") - } - } } diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index c028376f2f9..868d164769b 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -46,6 +46,55 @@ class CollectionGroupSource: Stage { } } +// Represents the entire database as a source. +class DatabaseSource: Stage { + var name: String = "database" + var bridge: StageBridge + + init() { + bridge = DatabaseSourceStageBridge() + } +} + +// Represents a list of document references as a source. +class DocumentsSource: Stage { + var name: String = "documents" + var bridge: StageBridge + private var references: [String] + + // Initialize with an array of String paths + init(paths: [String]) { + references = paths + bridge = DocumentsSourceStageBridge(documents: paths) + } +} + +// Represents an existing Query as a source. +class QuerySource: Stage { + var name: String = "query" + var 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. +class AggregateQuerySource: Stage { + var name: String = "aggregateQuery" + var bridge: StageBridge + private var aggregateQuery: AggregateQuery + + init(aggregateQuery: AggregateQuery) { + self.aggregateQuery = aggregateQuery + bridge = DatabaseSourceStageBridge() + // TODO: bridge = AggregateQuerySourceStageBridge(aggregateQuery: aggregateQuery.query) + } +} + class Where: Stage { var name: String = "where" @@ -54,7 +103,7 @@ class Where: Stage { init(condition: BooleanExpr) { self.condition = condition - bridge = WhereStageBridge(expr: condition.exprToExprBridge()) + bridge = WhereStageBridge(expr: condition.toBridge()) } } @@ -93,8 +142,8 @@ class AddFields: Stage { result, field in - let seletable = field as! SelectableInternal - result[seletable.alias] = seletable.expr.exprToExprBridge() + let seletable = field as! SelectableWrapper + result[seletable.alias] = seletable.expr.toBridge() } bridge = AddFieldsStageBridge(fields: objc_accumulators) } @@ -119,26 +168,26 @@ class RemoveFieldsStage: Stage { class Select: Stage { var name: String = "select" var bridge: StageBridge - private var selections: [Any] + private var selections: [Selectable] - init(selections: [Any]) { + init(selections: [Selectable]) { self.selections = selections - let objc_selections = Helper.selectablesToMap(selectables: selections) - bridge = SelectStageBridge(selections: objc_selections - .mapValues { Helper.sendableToExpr($0).exprToExprBridge() }) + let map = Helper.selectablesToMap(selectables: selections) + bridge = SelectStageBridge(selections: map + .mapValues { Helper.sendableToExpr($0).toBridge() }) } } class Distinct: Stage { var name: String = "distinct" var bridge: StageBridge - private var groups: [Any] + private var groups: [Selectable] - init(groups: [Any]) { + init(groups: [Selectable]) { self.groups = groups - let objc_groups = Helper.selectablesToMap(selectables: groups) - bridge = DistinctStageBridge(groups: objc_groups - .mapValues { Helper.sendableToExpr($0).exprToExprBridge() }) + let map = Helper.selectablesToMap(selectables: groups) + bridge = DistinctStageBridge(groups: map + .mapValues { Helper.sendableToExpr($0).toBridge() }) } } @@ -148,18 +197,18 @@ class Aggregate: Stage { private var accumulators: [AggregateWithAlias] private var groups: [String: Expr] = [:] - init(accumulators: [AggregateWithAlias], groups: [Any]?) { + init(accumulators: [AggregateWithAlias], groups: [Selectable]?) { self.accumulators = accumulators if groups != nil { self.groups = Helper.selectablesToMap(selectables: groups!) } - let objc_accumulators = accumulators + let map = accumulators .reduce(into: [String: AggregateFunctionBridge]()) { result, accumulator in result[accumulator.alias] = accumulator.aggregate.bridge } bridge = AggregateStageBridge( - accumulators: objc_accumulators, - groups: self.groups.mapValues { Helper.sendableToExpr($0).exprToExprBridge() } + accumulators: map, + groups: self.groups.mapValues { Helper.sendableToExpr($0).toBridge() } ) } } @@ -213,7 +262,7 @@ class ReplaceWith: Stage { init(expr: Expr) { self.expr = expr fieldName = nil - bridge = ReplaceWithStageBridge(expr: expr.exprToExprBridge()) + bridge = ReplaceWithStageBridge(expr: expr.toBridge()) } init(fieldName: String) { @@ -263,7 +312,7 @@ class Unnest: Stage { self.field = field self.indexField = indexField bridge = UnnestStageBridge( - field: Helper.sendableToExpr(field).exprToExprBridge(), + field: Helper.sendableToExpr(field).toBridge(), indexField: indexField ) } @@ -279,8 +328,8 @@ class GenericStage: Stage { self.name = name self.params = params self.options = options - let bridgeParams = params.map { Helper.sendableToExpr($0).exprToExprBridge() } - let bridgeOptions = options?.mapValues { Helper.sendableToExpr($0).exprToExprBridge() } + let bridgeParams = params.map { Helper.sendableToExpr($0).toBridge() } + let bridgeOptions = options?.mapValues { Helper.sendableToExpr($0).toBridge() } bridge = GenericStageBridge(name: name, params: bridgeParams, options: bridgeOptions) } } diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 59eb32894b0..07672000374 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -306,8 +306,6 @@ google_firestore_v1_Pipeline_Stage RemoveFieldsStage::to_proto() const { return result; } -// TBD - google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { google_firestore_v1_Pipeline_Stage result; result.name = nanopb::MakeBytesArray("replace_with"); From e699c17f34d45cdbd235bacdd7038e0c25bee66f Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 24 Apr 2025 16:22:57 -0400 Subject: [PATCH 25/43] Add get for ppl result --- Firestore/Source/API/FIRPipelineBridge.mm | 26 +++++++++++++++++++ .../FirebaseFirestore/FIRPipelineBridge.h | 2 ++ .../SwiftAPI/Pipeline/PipelineResult.swift | 6 ++--- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index ea45db7ebdf..7d463a93a56 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -21,6 +21,7 @@ #include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRPipelineBridge+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" @@ -70,10 +71,12 @@ using firebase::firestore::api::Union; using firebase::firestore::api::Unnest; using firebase::firestore::api::Where; +using firebase::firestore::model::FieldPath; using firebase::firestore::nanopb::SharedMessage; using firebase::firestore::util::MakeCallback; using firebase::firestore::util::MakeNSString; using firebase::firestore::util::MakeString; +using firebase::firestore::util::ThrowInvalidArgument; NS_ASSUME_NONNULL_BEGIN @@ -917,6 +920,29 @@ - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr fieldValue = + _result.internal_value()->Get(fieldPath); + if (!fieldValue) return nil; + FSTUserDataWriter *dataWriter = + [[FSTUserDataWriter alloc] initWithFirestore:_db + serverTimestampBehavior:serverTimestampBehavior]; + return [dataWriter convertedValue:*fieldValue]; +} + @end @implementation FIRPipelineBridge { diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index 1ff773bb46c..c8ea8be4ff0 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -220,6 +220,8 @@ NS_SWIFT_NAME(__PipelineResultBridge) - (NSDictionary *)dataWithServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior; +- (nullable id)get:(id)field; + @end NS_SWIFT_SENDABLE diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift index 61a1bdc7803..6e1d892f3cb 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift @@ -51,20 +51,20 @@ public struct PipelineResult: @unchecked Sendable { /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). /// - Returns: The data at the specified field location or `nil` if no such field exists. public func get(_ fieldName: String) -> Sendable? { - return data[fieldName] + return bridge.get(fieldName) } /// Retrieves the field specified by `fieldPath`. /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). /// - Returns: The data at the specified field location or `nil` if no such field exists. public func get(_ fieldPath: FieldPath) -> Sendable? { - return "PLACEHOLDER" + return bridge.get(fieldPath) } /// Retrieves the field specified by `fieldPath`. /// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar"). /// - Returns: The data at the specified field location or `nil` if no such field exists. public func get(_ field: Field) -> Sendable? { - return data[field.fieldName] + return bridge.get(field.fieldName) } } From daf1b6844aa1eaf47673dc611cb559aa02012565 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 24 Apr 2025 16:27:48 -0400 Subject: [PATCH 26/43] revert test settings --- .../xcschemes/Firestore_Example_iOS.xcscheme | 12 ++++++++++++ .../Example/Tests/Util/FSTIntegrationTestCase.mm | 2 +- Firestore/core/src/api/ordering.h | 4 ++-- Firestore/core/src/api/stages.cc | 4 ++-- Firestore/core/src/api/stages.h | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme index 1df610c09a8..279780fe448 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme @@ -89,6 +89,18 @@ ReferencedContainer = "container:Firestore.xcodeproj"> + + + + + + +#include #include #include "Firestore/core/src/api/expressions.h" @@ -26,8 +28,6 @@ namespace firebase { namespace firestore { namespace api { -class UserDataReader; // forward declaration - class Ordering { public: enum Direction { diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 07672000374..c4cc0b0edee 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -332,8 +332,8 @@ ReplaceWith::ReplaceWith(std::string field_name) : expr_(nullptr), field_name_(std::move(field_name)) { } -Sample::Sample(std::string type, int64_t count, double percentage_) - : type_(type), count_(count), percentage_(percentage_) { +Sample::Sample(std::string type, int64_t count, double percentage) + : type_(type), count_(count), percentage_(percentage) { } google_firestore_v1_Pipeline_Stage Sample::to_proto() const { diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index 9543bf85853..1c512c9b5e7 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -264,7 +264,7 @@ class ReplaceWith : public Stage { class Sample : public Stage { public: - Sample(std::string type, int64_t count, double percentage_); + Sample(std::string type, int64_t count, double percentage); ~Sample() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; From 5b2bd880d5b59e8ab7c72288d1c3a6fad06b8ae5 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Fri, 25 Apr 2025 11:27:04 -0400 Subject: [PATCH 27/43] Change cmake version --- scripts/install_prereqs.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/install_prereqs.sh b/scripts/install_prereqs.sh index 079ff6076ac..e35507de6bc 100755 --- a/scripts/install_prereqs.sh +++ b/scripts/install_prereqs.sh @@ -104,7 +104,6 @@ case "$project-$platform-$method" in ;; Firestore-iOS-cmake | Firestore-tvOS-cmake | Firestore-macOS-cmake) - brew outdated cmake || brew upgrade cmake brew outdated go || brew upgrade go # Somehow the build for Abseil requires this. brew install ccache brew install ninja From 23305c0211f2ae3940c351849c4a9bf2895bc320 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Mon, 28 Apr 2025 12:07:45 -0400 Subject: [PATCH 28/43] remove unused variable --- Firestore/Source/API/FIRPipelineBridge.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 7d463a93a56..1ddc0314c5c 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -832,7 +832,6 @@ - (id)initWithCppSnapshot:(api::PipelineSnapshot)snapshot { results_ = nil; } else { NSMutableArray<__FIRPipelineResultBridge *> *results = [NSMutableArray array]; - auto &cpp_result = snapshot_.value().results(); for (auto &result : snapshot_.value().results()) { [results addObject:[[__FIRPipelineResultBridge alloc] initWithCppResult:result From 6dd3515b31bf86d304bbedb9a2c3f8c2bf478d96 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Mon, 28 Apr 2025 12:14:19 -0400 Subject: [PATCH 29/43] revert settinggs --- .../xcschemes/Firestore_Example_iOS.xcscheme | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme index 279780fe448..1df610c09a8 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme @@ -89,18 +89,6 @@ ReferencedContainer = "container:Firestore.xcodeproj"> - - - - - - Date: Tue, 6 May 2025 12:06:23 -0400 Subject: [PATCH 30/43] add expressions --- Firestore/Swift/Source/ExprImpl.swift | 607 +++++ .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 1986 +++++++++++------ 2 files changed, 1972 insertions(+), 621 deletions(-) create mode 100644 Firestore/Swift/Source/ExprImpl.swift diff --git a/Firestore/Swift/Source/ExprImpl.swift b/Firestore/Swift/Source/ExprImpl.swift new file mode 100644 index 00000000000..6d55a7b479b --- /dev/null +++ b/Firestore/Swift/Source/ExprImpl.swift @@ -0,0 +1,607 @@ +// 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. + +extension Expr { + func toBridge() -> ExprBridge { + return (self as! BridgeWrapper).bridge + } +} + +public extension Expr { + func `as`(_ name: String) -> ExprWithAlias { + return ExprWithAlias(self, name) + } + + // MARK: Arithmetic Operators + + func add(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("add", [self, second] + others) + } + + func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("add", exprs) + } + + func subtract(_ other: Expr) -> FunctionExpr { + return FunctionExpr("subtract", [self, other]) + } + + func subtract(_ other: Sendable) -> FunctionExpr { + return FunctionExpr("subtract", [self, Helper.sendableToExpr(other)]) + } + + func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("multiply", [self, second] + others) + } + + func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("multiply", exprs) + } + + func divide(_ other: Expr) -> FunctionExpr { + return FunctionExpr("divide", [self, other]) + } + + func divide(_ other: Sendable) -> FunctionExpr { + return FunctionExpr("divide", [self, Helper.sendableToExpr(other)]) + } + + func mod(_ other: Expr) -> FunctionExpr { + return FunctionExpr("mod", [self, other]) + } + + func mod(_ other: Sendable) -> FunctionExpr { + return FunctionExpr("mod", [self, Helper.sendableToExpr(other)]) + } + + // MARK: Array Operations + + func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr { + return FunctionExpr("array_concat", [self, secondArray] + otherArrays) + } + + func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(secondArray)] + otherArrays + .map { Helper.sendableToExpr($0) } + return FunctionExpr("array_concat", exprs) + } + + func arrayContains(_ element: Expr) -> BooleanExpr { + return BooleanExpr("array_contains", [self, element]) + } + + func arrayContains(_ element: Sendable) -> BooleanExpr { + return BooleanExpr("array_contains", [self, Helper.sendableToExpr(element)]) + } + + func arrayContainsAll(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_all", [self] + values) + } + + func arrayContainsAll(_ values: Sendable...) -> BooleanExpr { + let exprValues = values.map { Helper.sendableToExpr($0) } + return BooleanExpr("array_contains_all", [self] + exprValues) + } + + func arrayContainsAny(_ values: Expr...) -> BooleanExpr { + return BooleanExpr("array_contains_any", [self] + values) + } + + func arrayContainsAny(_ values: Sendable...) -> BooleanExpr { + let exprValues = values.map { Helper.sendableToExpr($0) } + return BooleanExpr("array_contains_any", [self] + exprValues) + } + + func arrayLength() -> FunctionExpr { + return FunctionExpr("array_length", [self]) + } + + func arrayOffset(_ offset: Int) -> FunctionExpr { + return FunctionExpr("array_offset", [self, Helper.sendableToExpr(offset)]) + } + + func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr { + return FunctionExpr("array_offset", [self, offsetExpr]) + } + + func gt(_ other: Expr) -> BooleanExpr { + return BooleanExpr("gt", [self, other]) + } + + func gt(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("gt", [self, exprOther]) + } + + // MARK: - Greater Than or Equal (gte) + + func gte(_ other: Expr) -> BooleanExpr { + return BooleanExpr("gte", [self, other]) + } + + func gte(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("gte", [self, exprOther]) + } + + // MARK: - Less Than (lt) + + func lt(_ other: Expr) -> BooleanExpr { + return BooleanExpr("lt", [self, other]) + } + + func lt(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("lt", [self, exprOther]) + } + + // MARK: - Less Than or Equal (lte) + + func lte(_ other: Expr) -> BooleanExpr { + return BooleanExpr("lte", [self, other]) + } + + func lte(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("lte", [self, exprOther]) + } + + // MARK: - Equal (eq) + + func eq(_ other: Expr) -> BooleanExpr { + return BooleanExpr("eq", [self, other]) + } + + func eq(_ other: Sendable) -> BooleanExpr { + let exprOther = Helper.sendableToExpr(other) + return BooleanExpr("eq", [self, exprOther]) + } + + func neq(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("neq", [self] + others) + } + + func neq(_ others: Sendable...) -> BooleanExpr { + let exprOthers = others.map { Helper.sendableToExpr($0) } + return BooleanExpr("neq", [self] + exprOthers) + } + + func eqAny(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("eq_any", [self] + others) + } + + func eqAny(_ others: Sendable...) -> BooleanExpr { + let exprOthers = others.map { Helper.sendableToExpr($0) } + return BooleanExpr("eq_any", [self] + exprOthers) + } + + func notEqAny(_ others: Expr...) -> BooleanExpr { + return BooleanExpr("not_eq_any", [self] + others) + } + + func notEqAny(_ others: Sendable...) -> BooleanExpr { + let exprOthers = others.map { Helper.sendableToExpr($0) } + return BooleanExpr("not_eq_any", [self] + exprOthers) + } + + // MARK: Checks + + // --- Added Type Check Operations --- + + func isNan() -> BooleanExpr { + return BooleanExpr("is_nan", [self]) + } + + func isNull() -> BooleanExpr { + return BooleanExpr("is_null", [self]) + } + + func exists() -> BooleanExpr { + return BooleanExpr("exists", [self]) + } + + func isError() -> BooleanExpr { + return BooleanExpr("is_error", [self]) + } + + func isAbsent() -> BooleanExpr { + return BooleanExpr("is_absent", [self]) + } + + func isNotNull() -> BooleanExpr { + return BooleanExpr("is_not_null", [self]) + } + + func isNotNan() -> BooleanExpr { + return BooleanExpr("is_not_nan", [self]) + } + + // --- Added String Operations --- + + func charLength() -> FunctionExpr { + return FunctionExpr("char_length", [self]) + } + + func like(_ pattern: String) -> FunctionExpr { + return FunctionExpr("like", [self, Helper.sendableToExpr(pattern)]) + } + + func like(_ pattern: Expr) -> FunctionExpr { + return FunctionExpr("like", [self, pattern]) + } + + func regexContains(_ pattern: String) -> BooleanExpr { + return BooleanExpr("regex_contains", [self, Helper.sendableToExpr(pattern)]) + } + + func regexContains(_ pattern: Expr) -> BooleanExpr { + return BooleanExpr("regex_contains", [self, pattern]) + } + + func regexMatch(_ pattern: String) -> BooleanExpr { + return BooleanExpr("regex_match", [self, Helper.sendableToExpr(pattern)]) + } + + func regexMatch(_ pattern: Expr) -> BooleanExpr { + return BooleanExpr("regex_match", [self, pattern]) + } + + func strContains(_ substring: String) -> BooleanExpr { + return BooleanExpr("str_contains", [self, Helper.sendableToExpr(substring)]) + } + + func strContains(_ expr: Expr) -> BooleanExpr { + return BooleanExpr("str_contains", [self, expr]) + } + + func startsWith(_ prefix: String) -> BooleanExpr { + return BooleanExpr("starts_with", [self, Helper.sendableToExpr(prefix)]) + } + + func startsWith(_ prefix: Expr) -> BooleanExpr { + return BooleanExpr("starts_with", [self, prefix]) + } + + func endsWith(_ suffix: String) -> BooleanExpr { + return BooleanExpr("ends_with", [self, Helper.sendableToExpr(suffix)]) + } + + func endsWith(_ suffix: Expr) -> BooleanExpr { + return BooleanExpr("ends_with", [self, suffix]) + } + + func lowercased() -> FunctionExpr { + return FunctionExpr("to_lower", [self]) + } + + func uppercased() -> FunctionExpr { + return FunctionExpr("to_upper", [self]) + } + + func trim() -> FunctionExpr { + return FunctionExpr("trim", [self]) + } + + func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr { + return FunctionExpr("str_concat", [self, secondString] + otherStrings) + } + + func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(secondString)] + otherStrings + .map { Helper.sendableToExpr($0) } + return FunctionExpr("str_concat", exprs) + } + + func reverse() -> FunctionExpr { + return FunctionExpr("reverse", [self]) + } + + func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr { + return FunctionExpr( + "replace_first", + [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] + ) + } + + func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr { + return FunctionExpr("replace_first", [self, find, replace]) + } + + func replaceAll(_ find: String, _ replace: String) -> FunctionExpr { + return FunctionExpr( + "replace_all", + [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] + ) + } + + func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr { + return FunctionExpr("replace_all", [self, find, replace]) + } + + func byteLength() -> FunctionExpr { + return FunctionExpr("byte_length", [self]) + } + + func substr(_ position: Int, _ length: Int? = nil) -> FunctionExpr { + let positionExpr = Helper.sendableToExpr(position) + if let length = length { + return FunctionExpr("substr", [self, positionExpr, Helper.sendableToExpr(length)]) + } else { + return FunctionExpr("substr", [self, positionExpr]) + } + } + + func substr(_ position: Expr, _ length: Expr? = nil) -> FunctionExpr { + if let length = length { + return FunctionExpr("substr", [self, position, length]) + } else { + return FunctionExpr("substr", [self, position]) + } + } + + // --- Added Map Operations --- + + func mapGet(_ subfield: String) -> FunctionExpr { + return FunctionExpr("map_get", [self, Constant(subfield)]) + } + + func mapRemove(_ key: String) -> FunctionExpr { + return FunctionExpr("map_remove", [self, Helper.sendableToExpr(key)]) + } + + func mapRemove(_ keyExpr: Expr) -> FunctionExpr { + return FunctionExpr("map_remove", [self, keyExpr]) + } + + func mapMerge(_ secondMap: [String: Sendable], + _ otherMaps: [String: Sendable]...) -> FunctionExpr { + let secondMapExpr = Helper.sendableToExpr(secondMap) + let otherMapExprs = otherMaps.map { Helper.sendableToExpr($0) } + return FunctionExpr("map_merge", [self, secondMapExpr] + otherMapExprs) + } + + func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr { + return FunctionExpr("map_merge", [self, secondMap] + otherMaps) + } + + // --- Added Aggregate Operations (on Expr) --- + + func count() -> AggregateFunction { + return AggregateFunction("count", [self]) + } + + func sum() -> AggregateFunction { + return AggregateFunction("sum", [self]) + } + + func avg() -> AggregateFunction { + return AggregateFunction("avg", [self]) + } + + func minimum() -> AggregateFunction { + return AggregateFunction("minimum", [self]) + } + + func maximum() -> AggregateFunction { + return AggregateFunction("maximum", [self]) + } + + // MARK: Logical min/max + + func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("logical_maximum", [self, second] + others) + } + + func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("logical_maximum", exprs) + } + + func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr { + return FunctionExpr("logical_min", [self, second] + others) + } + + func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { + let exprs = [self] + [Helper.sendableToExpr(second)] + others + .map { Helper.sendableToExpr($0) } + return FunctionExpr("logical_min", exprs) + } + + // MARK: Vector Operations + + func vectorLength() -> FunctionExpr { + return FunctionExpr("vector_length", [self]) + } + + func cosineDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, other]) + } + + func cosineDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) + } + + func cosineDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) + } + + func dotProduct(_ other: Expr) -> FunctionExpr { + return FunctionExpr("dot_product", [self, other]) + } + + func dotProduct(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) + } + + func dotProduct(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) + } + + func euclideanDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, other]) + } + + func euclideanDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) + } + + func euclideanDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) + } + + func manhattanDistance(_ other: Expr) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, other]) + } + + func manhattanDistance(_ other: VectorValue) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) + } + + func manhattanDistance(_ other: [Double]) -> FunctionExpr { + return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) + } + + // MARK: Timestamp operations + + func unixMicrosToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_micros_to_timestamp", [self]) + } + + func timestampToUnixMicros() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_micros", [self]) + } + + func unixMillisToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_millis_to_timestamp", [self]) + } + + func timestampToUnixMillis() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_millis", [self]) + } + + func unixSecondsToTimestamp() -> FunctionExpr { + return FunctionExpr("unix_seconds_to_timestamp", [self]) + } + + func timestampToUnixSeconds() -> FunctionExpr { + return FunctionExpr("timestamp_to_unix_seconds", [self]) + } + + func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr { + return FunctionExpr("timestamp_add", [self, unit, amount]) + } + + func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { + return FunctionExpr( + "timestamp_add", + [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + ) + } + + func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr { + return FunctionExpr("timestamp_sub", [self, unit, amount]) + } + + func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { + return FunctionExpr( + "timestamp_sub", + [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + ) + } + + // MARK: - Bitwise operations + + func bitAnd(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitAnd(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitAnd(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_and", [self, bitsExpression]) + } + + func bitOr(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitOr(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitOr(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_or", [self, bitsExpression]) + } + + func bitXor(_ otherBits: Int) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitXor(_ otherBits: UInt8) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) + } + + func bitXor(_ bitsExpression: Expr) -> FunctionExpr { + return FunctionExpr("bit_xor", [self, bitsExpression]) + } + + func bitNot() -> FunctionExpr { + return FunctionExpr("bit_not", [self]) + } + + func bitLeftShift(_ y: Int) -> FunctionExpr { + return FunctionExpr("bit_left_shift", [self, Helper.sendableToExpr(y)]) + } + + func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr { + return FunctionExpr("bit_left_shift", [self, numberExpr]) + } + + func bitRightShift(_ y: Int) -> FunctionExpr { + return FunctionExpr("bit_right_shift", [self, Helper.sendableToExpr(y)]) + } + + func bitRightShift(_ numberExpr: Expr) -> FunctionExpr { + return FunctionExpr("bit_right_shift", [self, numberExpr]) + } + + func documentId() -> FunctionExpr { + return FunctionExpr("document_id", [self]) + } + + func ifError(_ catchExpr: Expr) -> FunctionExpr { + return FunctionExpr("if_error", [self, catchExpr]) + } + + func ifError(_ catchValue: Sendable) -> FunctionExpr { + return FunctionExpr("if_error", [self, Helper.sendableToExpr(catchValue)]) + } + + // MARK: Sorting + + func ascending() -> Ordering { + return Ordering(expr: self, direction: .ascending) + } + + func descending() -> Ordering { + return Ordering(expr: self, direction: .descending) + } +} diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 5c5972849a3..79428487f7e 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -21,794 +21,1538 @@ import Foundation // TODO: the implementation of `Expr` is not complete public protocol Expr: Sendable { + /// Assigns an alias to this expression. + /// + /// Aliases are useful for renaming fields in the output of a stage or for giving meaningful + /// names to calculated values. + /// + /// ```swift + /// // Calculate total price and alias it "totalPrice" + /// Field("price").multiply(Field("quantity")).`as`("totalPrice") + /// ``` + /// + /// - Parameter name: The alias to assign to this expression. + /// - Returns: A new `ExprWithAlias` wrapping this expression with the alias. func `as`(_ name: String) -> ExprWithAlias - // MARK: Arithmetic Operators - + // --- Added Mathematical Operations --- + + /// Creates an expression that adds this expression to one or more other expressions. + /// Assumes `self` and all parameters evaluate to compatible types for addition (e.g., numbers, or + /// string/array concatenation if supported by the specific "add" implementation). + /// + /// ```swift + /// // Add the value of the 'quantity' field and the 'reserve' field. + /// Field("quantity").add(Field("reserve")) + /// + /// // Add multiple numeric fields + /// Field("subtotal").add(Field("tax"), Field("shipping")) + /// ``` + /// + /// - Parameter second: An `Expr` to add to this expression. + /// - Parameter others: Optional additional `Expr` values to add. + /// - Returns: A new `FunctionExpr` representing the addition operation. func add(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that adds this expression to one or more literal values. + /// Assumes `self` and all parameters evaluate to compatible types for addition. + /// + /// ```swift + /// // Add 5 to the 'count' field + /// Field("count").add(5) + /// + /// // Add multiple literal numbers + /// Field("score").add(10, 20, -5) + /// ``` + /// + /// - Parameter second: A `Sendable` literal value to add to this expression. + /// - Parameter others: Optional additional `Sendable` literal values to add. + /// - Returns: A new `FunctionExpr` representing the addition operation. func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + /// Creates an expression that subtracts another expression from this expression. + /// Assumes `self` and `other` evaluate to numeric types. + /// + /// ```swift + /// // Subtract the 'discount' field from the 'price' field + /// Field("price").subtract(Field("discount")) + /// ``` + /// + /// - Parameter other: The `Expr` (evaluating to a number) to subtract from this expression. + /// - Returns: A new `FunctionExpr` representing the subtraction operation. func subtract(_ other: Expr) -> FunctionExpr + + /// Creates an expression that subtracts a literal value from this expression. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Subtract 20 from the value of the 'total' field + /// Field("total").subtract(20) + /// ``` + /// + /// - Parameter other: The `Sendable` literal (numeric) value to subtract from this expression. + /// - Returns: A new `FunctionExpr` representing the subtraction operation. func subtract(_ other: Sendable) -> FunctionExpr + /// Creates an expression that multiplies this expression by one or more other expressions. + /// Assumes `self` and all parameters evaluate to numeric types. + /// + /// ```swift + /// // Multiply the 'quantity' field by the 'price' field + /// Field("quantity").multiply(Field("price")) + /// + /// // Multiply 'rate' by 'time' and 'conversionFactor' fields + /// Field("rate").multiply(Field("time"), Field("conversionFactor")) + /// ``` + /// + /// - Parameter second: An `Expr` to multiply by. + /// - Parameter others: Optional additional `Expr` values to multiply by. + /// - Returns: A new `FunctionExpr` representing the multiplication operation. func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that multiplies this expression by one or more literal values. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Multiply the 'score' by 1.1 + /// Field("score").multiply(1.1) + /// + /// // Multiply 'base' by 2 and then by 3.0 + /// Field("base").multiply(2, 3.0) + /// ``` + /// + /// - Parameter second: A `Sendable` literal value to multiply by. + /// - Parameter others: Optional additional `Sendable` literal values to multiply by. + /// - Returns: A new `FunctionExpr` representing the multiplication operation. func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + /// Creates an expression that divides this expression by another expression. + /// Assumes `self` and `other` evaluate to numeric types. + /// + /// ```swift + /// // Divide the 'total' field by the 'count' field + /// Field("total").divide(Field("count")) + /// ``` + /// + /// - Parameter other: The `Expr` (evaluating to a number) to divide by. + /// - Returns: A new `FunctionExpr` representing the division operation. func divide(_ other: Expr) -> FunctionExpr + + /// Creates an expression that divides this expression by a literal value. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Divide the 'value' field by 10 + /// Field("value").divide(10) + /// ``` + /// + /// - Parameter other: The `Sendable` literal (numeric) value to divide by. + /// - Returns: A new `FunctionExpr` representing the division operation. func divide(_ other: Sendable) -> FunctionExpr + /// Creates an expression that calculates the modulo (remainder) of dividing this expression by + /// another expression. + /// Assumes `self` and `other` evaluate to numeric types. + /// + /// ```swift + /// // Calculate the remainder of dividing the 'value' field by the 'divisor' field + /// Field("value").mod(Field("divisor")) + /// ``` + /// + /// - Parameter other: The `Expr` (evaluating to a number) to use as the divisor. + /// - Returns: A new `FunctionExpr` representing the modulo operation. func mod(_ other: Expr) -> FunctionExpr - func mod(_ other: Sendable) -> FunctionExpr - // MARK: Array Operations + /// Creates an expression that calculates the modulo (remainder) of dividing this expression by a + /// literal value. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Calculate the remainder of dividing the 'value' field by 10 + /// Field("value").mod(10) + /// ``` + /// + /// - Parameter other: The `Sendable` literal (numeric) value to use as the divisor. + /// - Returns: A new `FunctionExpr` representing the modulo operation. + func mod(_ other: Sendable) -> FunctionExpr + // --- Added Array Operations --- + + /// Creates an expression that concatenates an array expression (from `self`) with one or more + /// other array expressions. + /// Assumes `self` and all parameters evaluate to arrays. + /// + /// ```swift + /// // Combine the 'items' array with 'otherItems' and 'archiveItems' array fields. + /// Field("items").arrayConcat(Field("otherItems"), Field("archiveItems")) + /// ``` + /// - Parameter secondArray: An `Expr` (evaluating to an array) to concatenate. + /// - Parameter otherArrays: Optional additional `Expr` values (evaluating to arrays) to + /// concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated array. func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr + + /// Creates an expression that concatenates an array expression (from `self`) with one or more + /// array literals. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Combine 'tags' (an array field) with ["new", "featured"] and ["urgent"] + /// Field("tags").arrayConcat(["new", "featured"], ["urgent"]) + /// ``` + /// - Parameter secondArray: An array literal of `Sendable` values to concatenate. + /// - Parameter otherArrays: Optional additional array literals of `Sendable` values to + /// concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated array. func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr + /// Creates an expression that checks if an array (from `self`) contains a specific element + /// expression. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'sizes' contains the value from 'selectedSize' field + /// Field("sizes").arrayContains(Field("selectedSize")) + /// ``` + /// + /// - Parameter element: The `Expr` representing the element to search for in the array. + /// - Returns: A new `BooleanExpr` representing the 'array_contains' comparison. func arrayContains(_ element: Expr) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains a specific literal + /// element. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'colors' array contains "red" + /// Field("colors").arrayContains("red") + /// ``` + /// + /// - Parameter element: The `Sendable` literal element to search for in the array. + /// - Returns: A new `BooleanExpr` representing the 'array_contains' comparison. func arrayContains(_ element: Sendable) -> BooleanExpr + /// Creates an expression that checks if an array (from `self`) contains all the specified element + /// expressions. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'candidateSkills' contains all skills from 'requiredSkill1' and 'requiredSkill2' + /// fields + /// Field("candidateSkills").arrayContainsAll(Field("requiredSkill1"), Field("requiredSkill2")) + /// ``` + /// + /// - Parameter values: A variadic list of `Expr` elements to check for in the array represented + /// by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_all' comparison. func arrayContainsAll(_ values: Expr...) -> BooleanExpr - func arrayContainsAll(_ values: Sendable...) -> BooleanExpr - func arrayContainsSendable(_ values: Expr...) -> BooleanExpr - func arrayContainsSendable(_ values: Sendable...) -> BooleanExpr + /// Creates an expression that checks if an array (from `self`) contains all the specified literal + /// elements. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'tags' contains both "urgent" and "review" + /// Field("tags").arrayContainsAll("urgent", "review") + /// ``` + /// + /// - Parameter values: A variadic list of `Sendable` literal elements to check for in the array + /// represented by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_all' comparison. + func arrayContainsAll(_ values: Sendable...) -> BooleanExpr + /// Creates an expression that checks if an array (from `self`) contains any of the specified + /// element expressions. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'userGroups' contains any group from 'allowedGroup1' or 'allowedGroup2' fields + /// Field("userGroups").arrayContainsAny(Field("allowedGroup1"), Field("allowedGroup2")) + /// ``` + /// + /// - Parameter values: A variadic list of `Expr` elements to check for in the array represented + /// by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_any' comparison. + func arrayContainsAny(_ values: Expr...) -> BooleanExpr + + /// Creates an expression that checks if an array (from `self`) contains any of the specified + /// literal elements. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Check if 'categories' contains either "electronics" or "books" + /// Field("categories").arrayContainsAny("electronics", "books") + /// ``` + /// + /// - Parameter values: A variadic list of `Sendable` literal elements to check for in the array + /// represented by `self`. + /// - Returns: A new `BooleanExpr` representing the 'array_contains_any' comparison. + func arrayContainsAny(_ values: Sendable...) -> BooleanExpr + + /// Creates an expression that calculates the length of an array. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Get the number of items in the 'cart' array + /// Field("cart").arrayLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length of the array. func arrayLength() -> FunctionExpr + /// Creates an expression that accesses an element in an array (from `self`) at the specified + /// integer offset. + /// A negative offset starts from the end. If the offset is out of bounds, an error may be + /// returned during evaluation. + /// Assumes `self` evaluates to an array. + /// + /// ```swift + /// // Return the value in the 'tags' field array at index 1. + /// Field("tags").arrayOffset(1) + /// // Return the last element in the 'tags' field array. + /// Field("tags").arrayOffset(-1) + /// ``` + /// + /// - Parameter offset: The literal `Int` offset of the element to return. + /// - Returns: A new `FunctionExpr` representing the 'arrayOffset' operation. func arrayOffset(_ offset: Int) -> FunctionExpr + + /// Creates an expression that accesses an element in an array (from `self`) at the offset + /// specified by an expression. + /// A negative offset starts from the end. If the offset is out of bounds, an error may be + /// returned during evaluation. + /// Assumes `self` evaluates to an array and `offsetExpr` evaluates to an integer. + /// + /// ```swift + /// // Return the value in the tags field array at index specified by field 'favoriteTagIndex'. + /// Field("tags").arrayOffset(Field("favoriteTagIndex")) + /// ``` + /// + /// - Parameter offsetExpr: An `Expr` (evaluating to an Int) representing the offset of the + /// element to return. + /// - Returns: A new `FunctionExpr` representing the 'arrayOffset' operation. func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr // MARK: Equality with Sendable - func eqSendable(_ others: Expr...) -> BooleanExpr - func eqSendable(_ others: Sendable...) -> BooleanExpr - - func notEqSendable(_ others: Expr...) -> BooleanExpr - func notEqSendable(_ others: Sendable...) -> BooleanExpr + /// Creates an expression that checks if this expression is equal to any of the provided + /// expression values. + /// This is similar to an "IN" operator in SQL. + /// + /// ```swift + /// // Check if 'categoryID' field is equal to 'featuredCategory' or 'popularCategory' fields + /// Field("categoryID").eqAny(Field("featuredCategory"), Field("popularCategory")) + /// ``` + /// + /// - Parameter others: A variadic list of `Expr` values to check against. + /// - Returns: A new `BooleanExpr` representing the 'IN' comparison (eq_any). + func eqAny(_ others: Expr...) -> BooleanExpr + + /// Creates an expression that checks if this expression is equal to any of the provided literal + /// values. + /// This is similar to an "IN" operator in SQL. + /// + /// ```swift + /// // Check if 'category' is "Electronics", "Books", or "Home Goods" + /// Field("category").eqAny("Electronics", "Books", "Home Goods") + /// ``` + /// + /// - Parameter others: A variadic list of `Sendable` literal values to check against. + /// - Returns: A new `BooleanExpr` representing the 'IN' comparison (eq_any). + func eqAny(_ others: Sendable...) -> BooleanExpr + + /// Creates an expression that checks if this expression is not equal to any of the provided + /// expression values. + /// This is similar to a "NOT IN" operator in SQL. + /// + /// ```swift + /// // Check if 'statusValue' is not equal to 'archivedStatus' or 'deletedStatus' fields + /// Field("statusValue").notEqAny(Field("archivedStatus"), Field("deletedStatus")) + /// ``` + /// + /// - Parameter others: A variadic list of `Expr` values to check against. + /// - Returns: A new `BooleanExpr` representing the 'NOT IN' comparison (not_eq_any). + func notEqAny(_ others: Expr...) -> BooleanExpr + + /// Creates an expression that checks if this expression is not equal to any of the provided + /// literal values. + /// This is similar to a "NOT IN" operator in SQL. + /// + /// ```swift + /// // Check if 'status' is neither "pending" nor "archived" + /// Field("status").notEqAny("pending", "archived") + /// ``` + /// + /// - Parameter others: A variadic list of `Sendable` literal values to check against. + /// - Returns: A new `BooleanExpr` representing the 'NOT IN' comparison (not_eq_any). + func notEqAny(_ others: Sendable...) -> BooleanExpr // MARK: Checks + /// Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Check if the result of a calculation is NaN + /// Field("value").divide(0).isNan() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNaN' check. func isNan() -> BooleanExpr + + /// Creates an expression that checks if this expression evaluates to 'Null'. + /// + /// ```swift + /// // Check if the 'optionalField' is null + /// Field("optionalField").isNull() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNull' check. func isNull() -> BooleanExpr + + /// Creates an expression that checks if a field exists in the document. + /// + /// - Note: This typically only makes sense when `self` is a `Field` expression. + /// + /// ```swift + /// // Check if the document has a field named "phoneNumber" + /// Field("phoneNumber").exists() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'exists' check. func exists() -> BooleanExpr + + /// Creates an expression that checks if this expression produces an error during evaluation. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Check if accessing a non-existent array index causes an error + /// Field("myArray").arrayOffset(100).isError() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isError' check. func isError() -> BooleanExpr + + /// Creates an expression that returns `true` if the result of this expression + /// is absent (e.g., a field does not exist in a map). Otherwise, returns `false`, even if the + /// value is `null`. + /// + /// - Note: This API is in beta. + /// - Note: This typically only makes sense when `self` is a `Field` expression. + /// + /// ```swift + /// // Check if the field `value` is absent. + /// Field("value").isAbsent() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isAbsent' check. func isAbsent() -> BooleanExpr + + /// Creates an expression that checks if the result of this expression is not null. + /// + /// ```swift + /// // Check if the value of the 'name' field is not null + /// Field("name").isNotNull() + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNotNull' check. func isNotNull() -> BooleanExpr + + /// Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a + /// Number). + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Check if the result of a calculation is NOT NaN + /// Field("value").divide(Field("count")).isNotNan() // Assuming count might be 0 + /// ``` + /// + /// - Returns: A new `BooleanExpr` representing the 'isNotNaN' check. func isNotNan() -> BooleanExpr // MARK: String Operations + /// Creates an expression that calculates the character length of a string in UTF-8. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Get the character length of the 'name' field in its UTF-8 form. + /// Field("name").charLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length of the string. func charLength() -> FunctionExpr + + /// Creates an expression that performs a case-sensitive string comparison using wildcards against + /// a literal pattern. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'title' field contains the word "guide" (case-sensitive) + /// Field("title").like("%guide%") + /// ``` + /// + /// - Parameter pattern: The literal string pattern to search for. Use "%" as a wildcard. + /// - Returns: A new `FunctionExpr` representing the 'like' comparison. func like(_ pattern: String) -> FunctionExpr + + /// Creates an expression that performs a case-sensitive string comparison using wildcards against + /// an expression pattern. + /// Assumes `self` evaluates to a string, and `pattern` evaluates to a string. + /// + /// ```swift + /// // Check if 'filename' matches a pattern stored in 'patternField' + /// Field("filename").like(Field("patternField")) + /// ``` + /// + /// - Parameter pattern: An `Expr` (evaluating to a string) representing the pattern to search + /// for. + /// - Returns: A new `FunctionExpr` representing the 'like' comparison. func like(_ pattern: Expr) -> FunctionExpr + /// Creates an expression that checks if a string (from `self`) contains a specified regular + /// expression literal as a substring. + /// Uses RE2 syntax. Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if 'description' contains "example" (case-insensitive) + /// Field("description").regexContains("(?i)example") + /// ``` + /// + /// - Parameter pattern: The literal string regular expression to use for the search. + /// - Returns: A new `BooleanExpr` representing the 'regex_contains' comparison. func regexContains(_ pattern: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) contains a specified regular + /// expression (from an expression) as a substring. + /// Uses RE2 syntax. Assumes `self` evaluates to a string, and `pattern` evaluates to a string. + /// + /// ```swift + /// // Check if 'logEntry' contains a pattern from 'errorPattern' field + /// Field("logEntry").regexContains(Field("errorPattern")) + /// ``` + /// + /// - Parameter pattern: An `Expr` (evaluating to a string) representing the regular expression to + /// use for the search. + /// - Returns: A new `BooleanExpr` representing the 'regex_contains' comparison. func regexContains(_ pattern: Expr) -> BooleanExpr + /// Creates an expression that checks if a string (from `self`) matches a specified regular + /// expression literal entirely. + /// Uses RE2 syntax. Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'email' field matches a valid email pattern + /// Field("email").regexMatch("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}") + /// ``` + /// + /// - Parameter pattern: The literal string regular expression to use for the match. + /// - Returns: A new `BooleanExpr` representing the regular expression match. func regexMatch(_ pattern: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) matches a specified regular + /// expression (from an expression) entirely. + /// Uses RE2 syntax. Assumes `self` evaluates to a string, and `pattern` evaluates to a string. + /// + /// ```swift + /// // Check if 'input' matches the regex stored in 'validationRegex' + /// Field("input").regexMatch(Field("validationRegex")) + /// ``` + /// + /// - Parameter pattern: An `Expr` (evaluating to a string) representing the regular expression to + /// use for the match. + /// - Returns: A new `BooleanExpr` representing the regular expression match. func regexMatch(_ pattern: Expr) -> BooleanExpr + /// Creates an expression that checks if a string (from `self`) contains a specified literal + /// substring (case-sensitive). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'description' field contains "example". + /// Field("description").strContains("example") + /// ``` + /// + /// - Parameter substring: The literal string substring to search for. + /// - Returns: A new `BooleanExpr` representing the 'str_contains' comparison. func strContains(_ substring: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) contains a specified substring + /// from an expression (case-sensitive). + /// Assumes `self` evaluates to a string, and `expr` evaluates to a string. + /// + /// ```swift + /// // Check if the 'message' field contains the value of the 'keyword' field. + /// Field("message").strContains(Field("keyword")) + /// ``` + /// + /// - Parameter expr: An `Expr` (evaluating to a string) representing the substring to search for. + /// - Returns: A new `BooleanExpr` representing the 'str_contains' comparison. func strContains(_ expr: Expr) -> BooleanExpr + /// Creates an expression that checks if a string (from `self`) starts with a given literal prefix + /// (case-sensitive). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'name' field starts with "Mr." + /// Field("name").startsWith("Mr.") + /// ``` + /// + /// - Parameter prefix: The literal string prefix to check for. + /// - Returns: A new `BooleanExpr` representing the 'starts_with' comparison. func startsWith(_ prefix: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) starts with a given prefix from an + /// expression (case-sensitive). + /// Assumes `self` evaluates to a string, and `prefix` evaluates to a string. + /// + /// ```swift + /// // Check if 'fullName' starts with the value of 'firstName' + /// Field("fullName").startsWith(Field("firstName")) + /// ``` + /// + /// - Parameter prefix: An `Expr` (evaluating to a string) representing the prefix to check for. + /// - Returns: A new `BooleanExpr` representing the 'starts_with' comparison. func startsWith(_ prefix: Expr) -> BooleanExpr + /// Creates an expression that checks if a string (from `self`) ends with a given literal suffix + /// (case-sensitive). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Check if the 'filename' field ends with ".txt" + /// Field("filename").endsWith(".txt") + /// ``` + /// + /// - Parameter suffix: The literal string suffix to check for. + /// - Returns: A new `BooleanExpr` representing the 'ends_with' comparison. func endsWith(_ suffix: String) -> BooleanExpr + + /// Creates an expression that checks if a string (from `self`) ends with a given suffix from an + /// expression (case-sensitive). + /// Assumes `self` evaluates to a string, and `suffix` evaluates to a string. + /// + /// ```swift + /// // Check if 'url' ends with the value of 'extension' field + /// Field("url").endsWith(Field("extension")) + /// ``` + /// + /// - Parameter suffix: An `Expr` (evaluating to a string) representing the suffix to check for. + /// - Returns: A new `BooleanExpr` representing the 'ends_with' comparison. func endsWith(_ suffix: Expr) -> BooleanExpr + /// Creates an expression that converts a string (from `self`) to lowercase. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Convert the 'name' field to lowercase + /// Field("name").lowercased() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the lowercase string. func lowercased() -> FunctionExpr + + /// Creates an expression that converts a string (from `self`) to uppercase. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Convert the 'title' field to uppercase + /// Field("title").uppercased() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the uppercase string. func uppercased() -> FunctionExpr + + /// Creates an expression that removes leading and trailing whitespace from a string (from + /// `self`). + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Trim whitespace from the 'userInput' field + /// Field("userInput").trim() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the trimmed string. func trim() -> FunctionExpr + /// Creates an expression that concatenates this string expression with other string expressions. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Combine 'part1', 'part2', and 'part3' fields + /// Field("part1").strConcat(Field("part2"), Field("part3")) + /// ``` + /// + /// - Parameter secondString: An `Expr` (evaluating to a string) to concatenate. + /// - Parameter otherStrings: Optional additional `Expr` (evaluating to strings) to concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated string. func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr + + /// Creates an expression that concatenates this string expression with other string literals. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Combine 'firstName', " ", and 'lastName' + /// Field("firstName").strConcat(" ", "lastName") + /// ``` + /// + /// - Parameter secondString: A string literal to concatenate. + /// - Parameter otherStrings: Optional additional string literals to concatenate. + /// - Returns: A new `FunctionExpr` representing the concatenated string. func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr + /// Creates an expression that reverses this string expression. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Reverse the value of the 'myString' field. + /// Field("myString").reverse() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the reversed string. func reverse() -> FunctionExpr + + /// Creates an expression that replaces the first occurrence of a literal substring within this + /// string expression with another literal substring. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Replace the first "hello" with "hi" in the 'message' field + /// Field("message").replaceFirst("hello", "hi") + /// ``` + /// + /// - Parameter find: The literal string substring to search for. + /// - Parameter replace: The literal string substring to replace the first occurrence with. + /// - Returns: A new `FunctionExpr` representing the string with the first occurrence replaced. func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr + + /// Creates an expression that replaces the first occurrence of a substring (from an expression) + /// within this string expression with another substring (from an expression). + /// Assumes `self` evaluates to a string, and `find`/`replace` evaluate to strings. + /// + /// ```swift + /// // Replace first occurrence of field 'findPattern' with field 'replacePattern' in 'text' + /// Field("text").replaceFirst(Field("findPattern"), Field("replacePattern")) + /// ``` + /// + /// - Parameter find: An `Expr` (evaluating to a string) for the substring to search for. + /// - Parameter replace: An `Expr` (evaluating to a string) for the substring to replace the first + /// occurrence with. + /// - Returns: A new `FunctionExpr` representing the string with the first occurrence replaced. func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr + + /// Creates an expression that replaces all occurrences of a literal substring within this string + /// expression with another literal substring. + /// Assumes `self` evaluates to a string. + /// + /// ```swift + /// // Replace all occurrences of " " with "_" in 'description' + /// Field("description").replaceAll(" ", "_") + /// ``` + /// + /// - Parameter find: The literal string substring to search for. + /// - Parameter replace: The literal string substring to replace all occurrences with. + /// - Returns: A new `FunctionExpr` representing the string with all occurrences replaced. func replaceAll(_ find: String, _ replace: String) -> FunctionExpr + + /// Creates an expression that replaces all occurrences of a substring (from an expression) within + /// this string expression with another substring (from an expression). + /// Assumes `self` evaluates to a string, and `find`/`replace` evaluate to strings. + /// + /// ```swift + /// // Replace all occurrences of field 'target' with field 'replacement' in 'content' + /// Field("content").replaceAll(Field("target"), Field("replacement")) + /// ``` + /// + /// - Parameter find: An `Expr` (evaluating to a string) for the substring to search for. + /// - Parameter replace: An `Expr` (evaluating to a string) for the substring to replace all + /// occurrences with. + /// - Returns: A new `FunctionExpr` representing the string with all occurrences replaced. func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr + /// Creates an expression that calculates the length of this string or bytes expression in bytes. + /// Assumes `self` evaluates to a string or bytes. + /// + /// ```swift + /// // Calculate the length of the 'myString' field in bytes. + /// Field("myString").byteLength() + /// + /// // Calculate the size of the 'avatar' (Data/Bytes) field. + /// Field("avatar").byteLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length in bytes. func byteLength() -> FunctionExpr + /// Creates an expression that returns a substring of this expression (String or Bytes) using + /// literal integers for position and optional length. + /// Indexing is 0-based. Assumes `self` evaluates to a string or bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Get substring from index 5 with length 10 + /// Field("myString").substr(5, 10) + /// + /// // Get substring from 'myString' starting at index 3 to the end + /// Field("myString").substr(3, nil) + /// ``` + /// + /// - Parameter position: Literal `Int` index of the first character/byte. + /// - Parameter length: Optional literal `Int` length of the substring. If `nil`, goes to the end. + /// - Returns: A new `FunctionExpr` representing the substring. func substr(_ position: Int, _ length: Int?) -> FunctionExpr + + /// Creates an expression that returns a substring of this expression (String or Bytes) using + /// expressions for position and optional length. + /// Indexing is 0-based. Assumes `self` evaluates to a string or bytes, and parameters evaluate to + /// integers. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Get substring from index calculated by Field("start") with length from Field("len") + /// Field("myString").substr(Field("start"), Field("len")) + /// + /// // Get substring from index calculated by Field("start") to the end + /// Field("myString").substr(Field("start"), nil) // Passing nil for optional Expr length + /// ``` + /// + /// - Parameter position: An `Expr` (evaluating to an Int) for the index of the first + /// character/byte. + /// - Parameter length: Optional `Expr` (evaluating to an Int) for the length of the substring. If + /// `nil`, goes to the end. + /// - Returns: A new `FunctionExpr` representing the substring. func substr(_ position: Expr, _ length: Expr?) -> FunctionExpr // MARK: Map Operations + /// Accesses a value from a map (object) field using the provided literal string key. + /// Assumes `self` evaluates to a Map. + /// + /// ```swift + /// // Get the 'city' value from the 'address' map field + /// Field("address").mapGet("city") + /// ``` + /// + /// - Parameter subfield: The literal string key to access in the map. + /// - Returns: A new `FunctionExpr` representing the value associated with the given key. func mapGet(_ subfield: String) -> FunctionExpr + + /// Creates an expression that removes a key (specified by a literal string) from the map produced + /// by evaluating this expression. + /// Assumes `self` evaluates to a Map. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Removes the key 'baz' from the map held in field 'myMap' + /// Field("myMap").mapRemove("baz") + /// ``` + /// + /// - Parameter key: The literal string key to remove from the map. + /// - Returns: A new `FunctionExpr` representing the 'map_remove' operation. func mapRemove(_ key: String) -> FunctionExpr + + /// Creates an expression that removes a key (specified by an expression) from the map produced by + /// evaluating this expression. + /// Assumes `self` evaluates to a Map, and `keyExpr` evaluates to a string. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Removes the key specified by field 'keyToRemove' from the map in 'settings' + /// Field("settings").mapRemove(Field("keyToRemove")) + /// ``` + /// + /// - Parameter keyExpr: An `Expr` (evaluating to a string) representing the key to remove from + /// the map. + /// - Returns: A new `FunctionExpr` representing the 'map_remove' operation. func mapRemove(_ keyExpr: Expr) -> FunctionExpr + + /// Creates an expression that merges this map with multiple other map literals. + /// Assumes `self` evaluates to a Map. Later maps overwrite keys from earlier maps. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Merge 'settings' field with { "enabled": true } and another map literal { "priority": 1 } + /// Field("settings").mapMerge(["enabled": true], ["priority": 1]) + /// ``` + /// + /// - Parameter secondMap: A required second map (dictionary literal with `Sendable` values) to + /// merge. + /// - Parameter otherMaps: Optional additional maps (dictionary literals with `Sendable` values) + /// to merge. + /// - Returns: A new `FunctionExpr` representing the 'map_merge' operation. func mapMerge(_ secondMap: [String: Sendable], _ otherMaps: [String: Sendable]...) -> FunctionExpr + + /// Creates an expression that merges this map with multiple other map expressions. + /// Assumes `self` and other arguments evaluate to Maps. Later maps overwrite keys from earlier + /// maps. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Merge 'baseSettings' field with 'userOverrides' field and 'adminConfig' field + /// Field("baseSettings").mapMerge(Field("userOverrides"), Field("adminConfig")) + /// ``` + /// + /// - Parameter secondMap: A required second `Expr` (evaluating to a Map) to merge. + /// - Parameter otherMaps: Optional additional `Expr` (evaluating to Maps) to merge. + /// - Returns: A new `FunctionExpr` representing the 'map_merge' operation. func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr // MARK: Aggregations + /// Creates an aggregation that counts the number of stage inputs where this expression evaluates + /// to a valid, non-null value. + /// + /// ```swift + /// // Count the total number of products with a 'productId' + /// Field("productId").count().alias("totalProducts") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'count' aggregation on this expression. func count() -> AggregateFunction + + /// Creates an aggregation that calculates the sum of this numeric expression across multiple + /// stage inputs. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Calculate the total revenue from a set of orders + /// Field("orderAmount").sum().alias("totalRevenue") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'sum' aggregation. func sum() -> AggregateFunction + + /// Creates an aggregation that calculates the average (mean) of this numeric expression across + /// multiple stage inputs. + /// Assumes `self` evaluates to a numeric type. + /// + /// ```swift + /// // Calculate the average age of users + /// Field("age").avg().alias("averageAge") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'avg' aggregation. func avg() -> AggregateFunction + + /// Creates an aggregation that finds the minimum value of this expression across multiple stage + /// inputs. + /// + /// ```swift + /// // Find the lowest price of all products + /// Field("price").minimum().alias("lowestPrice") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'min' aggregation. func minimum() -> AggregateFunction + + /// Creates an aggregation that finds the maximum value of this expression across multiple stage + /// inputs. + /// + /// ```swift + /// // Find the highest score in a leaderboard + /// Field("score").maximum().alias("highestScore") + /// ``` + /// + /// - Returns: A new `AggregateFunction` representing the 'max' aggregation. func maximum() -> AggregateFunction // MARK: Logical min/max + /// Creates an expression that returns the larger value between this expression and other + /// expressions, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the largest of 'val1', 'val2', and 'val3' fields + /// Field("val1").logicalMaximum(Field("val2"), Field("val3")) + /// ``` + /// + /// - Parameter second: The second `Expr` to compare with. + /// - Parameter others: Optional additional `Expr` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical max operation. func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that returns the larger value between this expression and other literal + /// values, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the largest of 'val1' (a field), 100, and 200.0 + /// Field("val1").logicalMaximum(100, 200.0) + /// ``` + /// + /// - Parameter second: The second literal `Sendable` value to compare with. + /// - Parameter others: Optional additional literal `Sendable` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical max operation. func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr + /// Creates an expression that returns the smaller value between this expression and other + /// expressions, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the smallest of 'val1', 'val2', and 'val3' fields + /// Field("val1").logicalMinimum(Field("val2"), Field("val3")) + /// ``` + /// + /// - Parameter second: The second `Expr` to compare with. + /// - Parameter others: Optional additional `Expr` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical min operation. func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr + + /// Creates an expression that returns the smaller value between this expression and other literal + /// values, based on Firestore's value type ordering. + /// + /// ```swift + /// // Returns the smallest of 'val1' (a field), 0, and -5.5 + /// Field("val1").logicalMinimum(0, -5.5) + /// ``` + /// + /// - Parameter second: The second literal `Sendable` value to compare with. + /// - Parameter others: Optional additional literal `Sendable` values to compare with. + /// - Returns: A new `FunctionExpr` representing the logical min operation. func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr // MARK: Vector Operations + /// Creates an expression that calculates the length (number of dimensions) of this Firestore + /// Vector expression. + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Get the vector length (dimension) of the field 'embedding'. + /// Field("embedding").vectorLength() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the length of the vector. func vectorLength() -> FunctionExpr + + /// Calculates the cosine distance between this vector expression and another vector expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// ```swift + /// // Cosine distance between 'userVector' field and 'itemVector' field + /// Field("userVector").cosineDistance(Field("itemVector")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to compare against. + /// - Returns: A new `FunctionExpr` representing the cosine distance. func cosineDistance(_ other: Expr) -> FunctionExpr + + /// Calculates the cosine distance between this vector expression and another vector literal + /// (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Cosine distance with a VectorValue + /// let targetVector = VectorValue(vector: [0.1, 0.2, 0.3]) + /// Field("docVector").cosineDistance(targetVector) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to compare against. + /// - Returns: A new `FunctionExpr` representing the cosine distance. func cosineDistance(_ other: VectorValue) -> FunctionExpr + + /// Calculates the cosine distance between this vector expression and another vector literal + /// (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Cosine distance between 'location' field and a target location + /// Field("location").cosineDistance([37.7749, -122.4194]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to compare against. + /// - Returns: A new `FunctionExpr` representing the cosine distance. func cosineDistance(_ other: [Double]) -> FunctionExpr + /// Calculates the dot product between this vector expression and another vector expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// ```swift + /// // Dot product between 'vectorA' and 'vectorB' fields + /// Field("vectorA").dotProduct(Field("vectorB")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to calculate with. + /// - Returns: A new `FunctionExpr` representing the dot product. func dotProduct(_ other: Expr) -> FunctionExpr + + /// Calculates the dot product between this vector expression and another vector literal + /// (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Dot product with a VectorValue + /// let weightVector = VectorValue(vector: [0.5, -0.5]) + /// Field("features").dotProduct(weightVector) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to calculate with. + /// - Returns: A new `FunctionExpr` representing the dot product. func dotProduct(_ other: VectorValue) -> FunctionExpr + + /// Calculates the dot product between this vector expression and another vector literal + /// (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Dot product between a feature vector and a target vector literal + /// Field("features").dotProduct([0.5, 0.8, 0.2]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to calculate with. + /// - Returns: A new `FunctionExpr` representing the dot product. func dotProduct(_ other: [Double]) -> FunctionExpr + /// Calculates the Euclidean distance between this vector expression and another vector + /// expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// ```swift + /// // Euclidean distance between 'pointA' and 'pointB' fields + /// Field("pointA").euclideanDistance(Field("pointB")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to compare against. + /// - Returns: A new `FunctionExpr` representing the Euclidean distance. func euclideanDistance(_ other: Expr) -> FunctionExpr + + /// Calculates the Euclidean distance between this vector expression and another vector literal + /// (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// let targetPoint = VectorValue(vector: [1.0, 2.0]) + /// Field("currentLocation").euclideanDistance(targetPoint) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to compare against. + /// - Returns: A new `FunctionExpr` representing the Euclidean distance. func euclideanDistance(_ other: VectorValue) -> FunctionExpr + + /// Calculates the Euclidean distance between this vector expression and another vector literal + /// (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// + /// ```swift + /// // Euclidean distance between 'location' field and a target location literal + /// Field("location").euclideanDistance([37.7749, -122.4194]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to compare against. + /// - Returns: A new `FunctionExpr` representing the Euclidean distance. func euclideanDistance(_ other: [Double]) -> FunctionExpr + /// Calculates the Manhattan (L1) distance between this vector expression and another vector + /// expression. + /// Assumes both `self` and `other` evaluate to Vectors. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Manhattan distance between 'vector1' field and 'vector2' field + /// Field("vector1").manhattanDistance(Field("vector2")) + /// ``` + /// + /// - Parameter other: The other vector as an `Expr` to compare against. + /// - Returns: A new `FunctionExpr` representing the Manhattan distance. func manhattanDistance(_ other: Expr) -> FunctionExpr + + /// Calculates the Manhattan (L1) distance between this vector expression and another vector + /// literal (`VectorValue`). + /// Assumes `self` evaluates to a Vector. + /// - Note: This API is in beta. + /// ```swift + /// let referencePoint = VectorValue(vector: [5.0, 10.0]) + /// Field("dataPoint").manhattanDistance(referencePoint) + /// ``` + /// - Parameter other: The other vector as a `VectorValue` to compare against. + /// - Returns: A new `FunctionExpr` representing the Manhattan distance. func manhattanDistance(_ other: VectorValue) -> FunctionExpr + + /// Calculates the Manhattan (L1) distance between this vector expression and another vector + /// literal (`[Double]`). + /// Assumes `self` evaluates to a Vector. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Manhattan distance between 'point' field and a target point + /// Field("point").manhattanDistance([10.0, 20.0]) + /// ``` + /// - Parameter other: The other vector as `[Double]` to compare against. + /// - Returns: A new `FunctionExpr` representing the Manhattan distance. func manhattanDistance(_ other: [Double]) -> FunctionExpr // MARK: Timestamp operations + /// Creates an expression that interprets this expression (evaluating to a number) as microseconds + /// since the Unix epoch and returns a timestamp. + /// Assumes `self` evaluates to a number. + /// + /// ```swift + /// // Interpret 'microseconds' field as microseconds since epoch. + /// Field("microseconds").unixMicrosToTimestamp() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the timestamp. func unixMicrosToTimestamp() -> FunctionExpr + + /// Creates an expression that converts this timestamp expression to the number of microseconds + /// since the Unix epoch. Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Convert 'timestamp' field to microseconds since epoch. + /// Field("timestamp").timestampToUnixMicros() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the number of microseconds. func timestampToUnixMicros() -> FunctionExpr + + /// Creates an expression that interprets this expression (evaluating to a number) as milliseconds + /// since the Unix epoch and returns a timestamp. + /// Assumes `self` evaluates to a number. + /// + /// ```swift + /// // Interpret 'milliseconds' field as milliseconds since epoch. + /// Field("milliseconds").unixMillisToTimestamp() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the timestamp. func unixMillisToTimestamp() -> FunctionExpr + + /// Creates an expression that converts this timestamp expression to the number of milliseconds + /// since the Unix epoch. Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Convert 'timestamp' field to milliseconds since epoch. + /// Field("timestamp").timestampToUnixMillis() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the number of milliseconds. func timestampToUnixMillis() -> FunctionExpr + + /// Creates an expression that interprets this expression (evaluating to a number) as seconds + /// since the Unix epoch and returns a timestamp. + /// Assumes `self` evaluates to a number. + /// + /// ```swift + /// // Interpret 'seconds' field as seconds since epoch. + /// Field("seconds").unixSecondsToTimestamp() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the timestamp. func unixSecondsToTimestamp() -> FunctionExpr + + /// Creates an expression that converts this timestamp expression to the number of seconds + /// since the Unix epoch. Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Convert 'timestamp' field to seconds since epoch. + /// Field("timestamp").timestampToUnixSeconds() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the number of seconds. func timestampToUnixSeconds() -> FunctionExpr + /// Creates an expression that adds a specified amount of time to this timestamp expression, + /// where unit and amount are provided as expressions. + /// Assumes `self` evaluates to a Timestamp, `unit` evaluates to a unit string, and `amount` + /// evaluates to an integer. + /// + /// ```swift + /// // Add duration from 'unitField'/'amountField' to 'timestamp' + /// Field("timestamp").timestampAdd(Field("unitField"), Field("amountField")) + /// ``` + /// + /// - Parameter unit: An `Expr` evaluating to the unit of time string (e.g., "day", "hour"). + /// Valid units are 'microsecond', 'millisecond', 'second', 'minute', 'hour', + /// 'day'. + /// - Parameter amount: An `Expr` evaluating to the amount (Int) of the unit to add. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr + + /// Creates an expression that adds a specified amount of time to this timestamp expression, + /// where unit and amount are provided as literals. + /// Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Add 1 day to the 'timestamp' field. + /// Field("timestamp").timestampAdd(.day, 1) + /// ``` + /// + /// - Parameter unit: The `TimeUnit` enum representing the unit of time. + /// - Parameter amount: The literal `Int` amount of the unit to add. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr + + /// Creates an expression that subtracts a specified amount of time from this timestamp + /// expression, + /// where unit and amount are provided as expressions. + /// Assumes `self` evaluates to a Timestamp, `unit` evaluates to a unit string, and `amount` + /// evaluates to an integer. + /// + /// ```swift + /// // Subtract duration from 'unitField'/'amountField' from 'timestamp' + /// Field("timestamp").timestampSub(Field("unitField"), Field("amountField")) + /// ``` + /// + /// - Parameter unit: An `Expr` evaluating to the unit of time string (e.g., "day", "hour"). + /// Valid units are 'microsecond', 'millisecond', 'second', 'minute', 'hour', + /// 'day'. + /// - Parameter amount: An `Expr` evaluating to the amount (Int) of the unit to subtract. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr + + /// Creates an expression that subtracts a specified amount of time from this timestamp + /// expression, + /// where unit and amount are provided as literals. + /// Assumes `self` evaluates to a Timestamp. + /// + /// ```swift + /// // Subtract 1 day from the 'timestamp' field. + /// Field("timestamp").timestampSub(.day, 1) + /// ``` + /// + /// - Parameter unit: The `TimeUnit` enum representing the unit of time. + /// - Parameter amount: The literal `Int` amount of the unit to subtract. + /// - Returns: A new `FunctionExpr` representing the resulting timestamp. func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr // MARK: - Bitwise operations + /// Creates an expression applying bitwise AND between this expression and an integer literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise AND of 'flags' field and 0xFF + /// Field("flags").bitAnd(0xFF) + /// ``` + /// + /// - Parameter otherBits: The integer literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise AND operation. func bitAnd(_ otherBits: Int) -> FunctionExpr + + /// Creates an expression applying bitwise AND between this expression and a UInt8 literal (often + /// for byte masks). + /// Assumes `self` evaluates to an Integer or Bytes. + /// - Note: This API is in beta. + /// ```swift + /// // Bitwise AND of 'byteFlags' field and a byte mask + /// Field("byteFlags").bitAnd(0b00001111 as UInt8) + /// ``` + /// - Parameter otherBits: The UInt8 literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise AND operation. func bitAnd(_ otherBits: UInt8) -> FunctionExpr + + /// Creates an expression applying bitwise AND between this expression and another expression. + /// Assumes `self` and `bitsExpression` evaluate to Integer or Bytes. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise AND of 'mask1' and 'mask2' fields + /// Field("mask1").bitAnd(Field("mask2")) + /// ``` + /// - Parameter bitsExpression: The other `Expr` operand. + /// - Returns: A new `FunctionExpr` representing the bitwise AND operation. func bitAnd(_ bitsExpression: Expr) -> FunctionExpr + /// Creates an expression applying bitwise OR between this expression and an integer literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise OR of 'flags' field and 0x01 + /// Field("flags").bitOr(0x01) + /// ``` + /// + /// - Parameter otherBits: The integer literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise OR operation. func bitOr(_ otherBits: Int) -> FunctionExpr + + /// Creates an expression applying bitwise OR between this expression and a UInt8 literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// - Note: This API is in beta. + /// ```swift + /// // Set specific bits in 'controlByte' + /// Field("controlByte").bitOr(0b10000001 as UInt8) + /// ``` + /// - Parameter otherBits: The UInt8 literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise OR operation. func bitOr(_ otherBits: UInt8) -> FunctionExpr + + /// Creates an expression applying bitwise OR between this expression and another expression. + /// Assumes `self` and `bitsExpression` evaluate to Integer or Bytes. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise OR of 'permissionSet1' and 'permissionSet2' fields + /// Field("permissionSet1").bitOr(Field("permissionSet2")) + /// ``` + /// - Parameter bitsExpression: The other `Expr` operand. + /// - Returns: A new `FunctionExpr` representing the bitwise OR operation. func bitOr(_ bitsExpression: Expr) -> FunctionExpr + /// Creates an expression applying bitwise XOR between this expression and an integer literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise XOR of 'toggle' field and 0xFFFF + /// Field("toggle").bitXor(0xFFFF) + /// ``` + /// + /// - Parameter otherBits: The integer literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise XOR operation. func bitXor(_ otherBits: Int) -> FunctionExpr + + /// Creates an expression applying bitwise XOR between this expression and a UInt8 literal. + /// Assumes `self` evaluates to an Integer or Bytes. + /// - Note: This API is in beta. + /// ```swift + /// // Toggle bits in 'statusByte' using a XOR mask + /// Field("statusByte").bitXor(0b01010101 as UInt8) + /// ``` + /// - Parameter otherBits: The UInt8 literal operand. + /// - Returns: A new `FunctionExpr` representing the bitwise XOR operation. func bitXor(_ otherBits: UInt8) -> FunctionExpr + + /// Creates an expression applying bitwise XOR between this expression and another expression. + /// Assumes `self` and `bitsExpression` evaluate to Integer or Bytes. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise XOR of 'key1' and 'key2' fields (assuming Bytes) + /// Field("key1").bitXor(Field("key2")) + /// ``` + /// - Parameter bitsExpression: The other `Expr` operand. + /// - Returns: A new `FunctionExpr` representing the bitwise XOR operation. func bitXor(_ bitsExpression: Expr) -> FunctionExpr + /// Creates an expression applying bitwise NOT to this expression. + /// Assumes `self` evaluates to an Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Bitwise NOT of 'mask' field + /// Field("mask").bitNot() + /// ``` + /// + /// - Returns: A new `FunctionExpr` representing the bitwise NOT operation. func bitNot() -> FunctionExpr + /// Creates an expression applying bitwise left shift to this expression by a literal number of + /// bits. + /// Assumes `self` evaluates to Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Left shift 'value' field by 2 bits + /// Field("value").bitLeftShift(2) + /// ``` + /// + /// - Parameter y: The number of bits (Int literal) to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise left shift operation. func bitLeftShift(_ y: Int) -> FunctionExpr + + /// Creates an expression applying bitwise left shift to this expression by a number of bits + /// specified by an expression. + /// Assumes `self` evaluates to Integer or Bytes, and `numberExpr` evaluates to an Integer. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Left shift 'data' by number of bits in 'shiftCount' field + /// Field("data").bitLeftShift(Field("shiftCount")) + /// ``` + /// - Parameter numberExpr: An `Expr` (evaluating to an Int) for the number of bits to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise left shift operation. func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr + /// Creates an expression applying bitwise right shift to this expression by a literal number of + /// bits. + /// Assumes `self` evaluates to Integer or Bytes. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Right shift 'value' field by 4 bits + /// Field("value").bitRightShift(4) + /// ``` + /// + /// - Parameter y: The number of bits (Int literal) to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise right shift operation. func bitRightShift(_ y: Int) -> FunctionExpr + + /// Creates an expression applying bitwise right shift to this expression by a number of bits + /// specified by an expression. + /// Assumes `self` evaluates to Integer or Bytes, and `numberExpr` evaluates to an Integer. + /// - Note: This API is in beta. + /// + /// ```swift + /// // Right shift 'data' by number of bits in 'shiftCount' field + /// Field("data").bitRightShift(Field("shiftCount")) + /// ``` + /// - Parameter numberExpr: An `Expr` (evaluating to an Int) for the number of bits to shift by. + /// - Returns: A new `FunctionExpr` representing the bitwise right shift operation. func bitRightShift(_ numberExpr: Expr) -> FunctionExpr + /// Creates an expression that returns the result of `catchExpr` if this expression produces an + /// error during evaluation, + /// otherwise returns the result of this expression. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Try dividing 'a' by 'b', return field 'fallbackValue' on error (e.g., division by zero) + /// Field("a").divide(Field("b")).ifError(Field("fallbackValue")) + /// ``` + /// + /// - Parameter catchExpr: The `Expr` to evaluate and return if this expression errors. + /// - Returns: A new `FunctionExpr` representing the 'ifError' operation. func ifError(_ catchExpr: Expr) -> FunctionExpr + + /// Creates an expression that returns the literal `catchValue` if this expression produces an + /// error during evaluation, + /// otherwise returns the result of this expression. + /// + /// - Note: This API is in beta. + /// + /// ```swift + /// // Get first item in 'title' array, or return "Default Title" if error (e.g., empty array) + /// Field("title").arrayOffset(0).ifError("Default Title") + /// ``` + /// + /// - Parameter catchValue: The literal `Sendable` value to return if this expression errors. + /// - Returns: A new `FunctionExpr` representing the 'ifError' operation. func ifError(_ catchValue: Sendable) -> FunctionExpr // MARK: Sorting + /// Creates an `Ordering` object that sorts documents in ascending order based on this expression. + /// + /// ```swift + /// // Sort documents by the 'name' field in ascending order + /// firestore.pipeline().collection("users") + /// .sort(Field("name").ascending()) + /// ``` + /// + /// - Returns: A new `Ordering` instance for ascending sorting. func ascending() -> Ordering - func descending() -> Ordering -} - -public extension Expr { - func `as`(_ name: String) -> ExprWithAlias { - return ExprWithAlias(self, name) - } - - // MARK: Comparison Operators - - func eq(_ other: Expr) -> BooleanExpr { - return BooleanExpr("eq", [self, other]) - } - - func eq(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("eq", [self, Helper.sendableToExpr(other)]) - } - - func neq(_ other: Expr) -> BooleanExpr { - return BooleanExpr("neq", [self, other]) - } - - func neq(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("neq", [self, Helper.sendableToExpr(other)]) - } - - func lt(_ other: Expr) -> BooleanExpr { - return BooleanExpr("lt", [self, other]) - } - - func lt(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("lt", [self, Helper.sendableToExpr(other)]) - } - - func lte(_ other: Expr) -> BooleanExpr { - return BooleanExpr("lte", [self, other]) - } - - func lte(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("lte", [self, Helper.sendableToExpr(other)]) - } - - func gt(_ other: Expr) -> BooleanExpr { - return BooleanExpr("gt", [self, other]) - } - - func gt(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("gt", [self, Helper.sendableToExpr(other)]) - } - - func gte(_ other: Expr) -> BooleanExpr { - return BooleanExpr("gte", [self, other]) - } - - func gte(_ other: Sendable) -> BooleanExpr { - return BooleanExpr("gte", [self, Helper.sendableToExpr(other)]) - } - - // MARK: Arithmetic Operators - - func add(_ second: Expr, _ others: Expr...) -> FunctionExpr { - return FunctionExpr("add", [self, second] + others) - } - - func add(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.sendableToExpr(second)] + others - .map { Helper.sendableToExpr($0) } - return FunctionExpr("add", exprs) - } - - func subtract(_ other: Expr) -> FunctionExpr { - return FunctionExpr("subtract", [self, other]) - } - - func subtract(_ other: Sendable) -> FunctionExpr { - return FunctionExpr("subtract", [self, Helper.sendableToExpr(other)]) - } - - func multiply(_ second: Expr, _ others: Expr...) -> FunctionExpr { - return FunctionExpr("multiply", [self, second] + others) - } - - func multiply(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.sendableToExpr(second)] + others - .map { Helper.sendableToExpr($0) } - return FunctionExpr("multiply", exprs) - } - - func divide(_ other: Expr) -> FunctionExpr { - return FunctionExpr("divide", [self, other]) - } - - func divide(_ other: Sendable) -> FunctionExpr { - return FunctionExpr("divide", [self, Helper.sendableToExpr(other)]) - } - - func mod(_ other: Expr) -> FunctionExpr { - return FunctionExpr("mod", [self, other]) - } - - func mod(_ other: Sendable) -> FunctionExpr { - return FunctionExpr("mod", [self, Helper.sendableToExpr(other)]) - } - - // MARK: Array Operations - - func arrayConcat(_ secondArray: Expr, _ otherArrays: Expr...) -> FunctionExpr { - return FunctionExpr("array_concat", [self, secondArray] + otherArrays) - } - - func arrayConcat(_ secondArray: [Sendable], _ otherArrays: [Sendable]...) -> FunctionExpr { - let exprs = [self] + [Helper.sendableToExpr(secondArray)] + otherArrays - .map { Helper.sendableToExpr($0) } - return FunctionExpr("array_concat", exprs) - } - - func arrayContains(_ element: Expr) -> BooleanExpr { - return BooleanExpr("array_contains", [self, element]) - } - - func arrayContains(_ element: Sendable) -> BooleanExpr { - return BooleanExpr("array_contains", [self, Helper.sendableToExpr(element)]) - } - - func arrayContainsAll(_ values: Expr...) -> BooleanExpr { - return BooleanExpr("array_contains_all", [self] + values) - } - - func arrayContainsAll(_ values: Sendable...) -> BooleanExpr { - let exprValues = values.map { Helper.sendableToExpr($0) } - return BooleanExpr("array_contains_all", [self] + exprValues) - } - - func arrayContainsSendable(_ values: Expr...) -> BooleanExpr { - return BooleanExpr("array_contains_Sendable", [self] + values) - } - - func arrayContainsSendable(_ values: Sendable...) -> BooleanExpr { - let exprValues = values.map { Helper.sendableToExpr($0) } - return BooleanExpr("array_contains_Sendable", [self] + exprValues) - } - func arrayLength() -> FunctionExpr { - return FunctionExpr("array_length", [self]) - } - - func arrayOffset(_ offset: Int) -> FunctionExpr { - return FunctionExpr("array_offset", [self, Helper.sendableToExpr(offset)]) - } - - func arrayOffset(_ offsetExpr: Expr) -> FunctionExpr { - return FunctionExpr("array_offset", [self, offsetExpr]) - } - - // MARK: Equality with Sendable - - func eqSendable(_ others: Expr...) -> BooleanExpr { - return BooleanExpr("eq_Sendable", [self] + others) - } - - func eqSendable(_ others: Sendable...) -> BooleanExpr { - let exprOthers = others.map { Helper.sendableToExpr($0) } - return BooleanExpr("eq_Sendable", [self] + exprOthers) - } - - func notEqSendable(_ others: Expr...) -> BooleanExpr { - return BooleanExpr("not_eq_Sendable", [self] + others) - } - - func notEqSendable(_ others: Sendable...) -> BooleanExpr { - let exprOthers = others.map { Helper.sendableToExpr($0) } - return BooleanExpr("not_eq_Sendable", [self] + exprOthers) - } - - // MARK: Checks - - func isNan() -> BooleanExpr { - return BooleanExpr("is_nan", [self]) - } - - func isNull() -> BooleanExpr { - return BooleanExpr("is_null", [self]) - } - - func exists() -> BooleanExpr { - return BooleanExpr("exists", [self]) - } - - func isError() -> BooleanExpr { - return BooleanExpr("is_error", [self]) - } - - func isAbsent() -> BooleanExpr { - return BooleanExpr("is_absent", [self]) - } - - func isNotNull() -> BooleanExpr { - return BooleanExpr("is_not_null", [self]) - } - - func isNotNan() -> BooleanExpr { - return BooleanExpr("is_not_nan", [self]) - } - - // MARK: String Operations - - func charLength() -> FunctionExpr { - return FunctionExpr("char_length", [self]) - } - - func like(_ pattern: String) -> FunctionExpr { - return FunctionExpr("like", [self, Helper.sendableToExpr(pattern)]) - } - - func like(_ pattern: Expr) -> FunctionExpr { - return FunctionExpr("like", [self, pattern]) - } - - func regexContains(_ pattern: String) -> BooleanExpr { - return BooleanExpr("regex_contains", [self, Helper.sendableToExpr(pattern)]) - } - - func regexContains(_ pattern: Expr) -> BooleanExpr { - return BooleanExpr("regex_contains", [self, pattern]) - } - - func regexMatch(_ pattern: String) -> BooleanExpr { - return BooleanExpr("regex_match", [self, Helper.sendableToExpr(pattern)]) - } - - func regexMatch(_ pattern: Expr) -> BooleanExpr { - return BooleanExpr("regex_match", [self, pattern]) - } - - func strContains(_ substring: String) -> BooleanExpr { - return BooleanExpr("str_contains", [self, Helper.sendableToExpr(substring)]) - } - - func strContains(_ expr: Expr) -> BooleanExpr { - return BooleanExpr("str_contains", [self, expr]) - } - - func startsWith(_ prefix: String) -> BooleanExpr { - return BooleanExpr("starts_with", [self, Helper.sendableToExpr(prefix)]) - } - - func startsWith(_ prefix: Expr) -> BooleanExpr { - return BooleanExpr("starts_with", [self, prefix]) - } - - func endsWith(_ suffix: String) -> BooleanExpr { - return BooleanExpr("ends_with", [self, Helper.sendableToExpr(suffix)]) - } - - func endsWith(_ suffix: Expr) -> BooleanExpr { - return BooleanExpr("ends_with", [self, suffix]) - } - - func lowercased() -> FunctionExpr { - return FunctionExpr("to_lower", [self]) - } - - func uppercased() -> FunctionExpr { - return FunctionExpr("to_upper", [self]) - } - - func trim() -> FunctionExpr { - return FunctionExpr("trim", [self]) - } - - func strConcat(_ secondString: Expr, _ otherStrings: Expr...) -> FunctionExpr { - return FunctionExpr("str_concat", [self, secondString] + otherStrings) - } - - func strConcat(_ secondString: String, _ otherStrings: String...) -> FunctionExpr { - let exprs = [self] + [Helper.sendableToExpr(secondString)] + otherStrings - .map { Helper.sendableToExpr($0) } - return FunctionExpr("str_concat", exprs) - } - - func reverse() -> FunctionExpr { - return FunctionExpr("reverse", [self]) - } - - func replaceFirst(_ find: String, _ replace: String) -> FunctionExpr { - return FunctionExpr( - "replace_first", - [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] - ) - } - - func replaceFirst(_ find: Expr, _ replace: Expr) -> FunctionExpr { - return FunctionExpr("replace_first", [self, find, replace]) - } - - func replaceAll(_ find: String, _ replace: String) -> FunctionExpr { - return FunctionExpr( - "replace_all", - [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] - ) - } - - func replaceAll(_ find: Expr, _ replace: Expr) -> FunctionExpr { - return FunctionExpr("replace_all", [self, find, replace]) - } - - func byteLength() -> FunctionExpr { - return FunctionExpr("byte_length", [self]) - } - - func substr(_ position: Int, _ length: Int? = nil) -> FunctionExpr { - let positionExpr = Helper.sendableToExpr(position) - if let length = length { - return FunctionExpr("substr", [self, positionExpr, Helper.sendableToExpr(length)]) - } else { - return FunctionExpr("substr", [self, positionExpr]) - } - } - - func substr(_ position: Expr, _ length: Expr? = nil) -> FunctionExpr { - if let length = length { - return FunctionExpr("substr", [self, position, length]) - } else { - return FunctionExpr("substr", [self, position]) - } - } - - // MARK: Map Operations - - func mapGet(_ subfield: String) -> FunctionExpr { - return FunctionExpr("map_get", [self, Constant(subfield)]) - } - - func mapRemove(_ key: String) -> FunctionExpr { - return FunctionExpr("map_remove", [self, Helper.sendableToExpr(key)]) - } - - func mapRemove(_ keyExpr: Expr) -> FunctionExpr { - return FunctionExpr("map_remove", [self, keyExpr]) - } - - func mapMerge(_ secondMap: [String: Sendable], - _ otherMaps: [String: Sendable]...) -> FunctionExpr { - let secondMapExpr = Helper.sendableToExpr(secondMap) - let otherMapExprs = otherMaps.map { Helper.sendableToExpr($0) } - return FunctionExpr("map_merge", [self, secondMapExpr] + otherMapExprs) - } - - func mapMerge(_ secondMap: Expr, _ otherMaps: Expr...) -> FunctionExpr { - return FunctionExpr("map_merge", [self, secondMap] + otherMaps) - } - - // MARK: Aggregations - - func count() -> AggregateFunction { - return AggregateFunction("count", [self]) - } - - func sum() -> AggregateFunction { - return AggregateFunction("sum", [self]) - } - - func avg() -> AggregateFunction { - return AggregateFunction("avg", [self]) - } - - func minimum() -> AggregateFunction { - return AggregateFunction("minimum", [self]) - } - - func maximum() -> AggregateFunction { - return AggregateFunction("maximum", [self]) - } - - // MARK: Logical min/max - - func logicalMaximum(_ second: Expr, _ others: Expr...) -> FunctionExpr { - return FunctionExpr("logical_maximum", [self, second] + others) - } - - func logicalMaximum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.sendableToExpr(second)] + others - .map { Helper.sendableToExpr($0) } - return FunctionExpr("logical_maximum", exprs) - } - - func logicalMinimum(_ second: Expr, _ others: Expr...) -> FunctionExpr { - return FunctionExpr("logical_min", [self, second] + others) - } - - func logicalMinimum(_ second: Sendable, _ others: Sendable...) -> FunctionExpr { - let exprs = [self] + [Helper.sendableToExpr(second)] + others - .map { Helper.sendableToExpr($0) } - return FunctionExpr("logical_min", exprs) - } - - // MARK: Vector Operations - - func vectorLength() -> FunctionExpr { - return FunctionExpr("vector_length", [self]) - } - - func cosineDistance(_ other: Expr) -> FunctionExpr { - return FunctionExpr("cosine_distance", [self, other]) - } - - func cosineDistance(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) - } - - func cosineDistance(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("cosine_distance", [self, Helper.sendableToExpr(other)]) - } - - func dotProduct(_ other: Expr) -> FunctionExpr { - return FunctionExpr("dot_product", [self, other]) - } - - func dotProduct(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) - } - - func dotProduct(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("dot_product", [self, Helper.sendableToExpr(other)]) - } - - func euclideanDistance(_ other: Expr) -> FunctionExpr { - return FunctionExpr("euclidean_distance", [self, other]) - } - - func euclideanDistance(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) - } - - func euclideanDistance(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("euclidean_distance", [self, Helper.sendableToExpr(other)]) - } - - func manhattanDistance(_ other: Expr) -> FunctionExpr { - return FunctionExpr("manhattan_distance", [self, other]) - } - - func manhattanDistance(_ other: VectorValue) -> FunctionExpr { - return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) - } - - func manhattanDistance(_ other: [Double]) -> FunctionExpr { - return FunctionExpr("manhattan_distance", [self, Helper.sendableToExpr(other)]) - } - - // MARK: Timestamp operations - - func unixMicrosToTimestamp() -> FunctionExpr { - return FunctionExpr("unix_micros_to_timestamp", [self]) - } - - func timestampToUnixMicros() -> FunctionExpr { - return FunctionExpr("timestamp_to_unix_micros", [self]) - } - - func unixMillisToTimestamp() -> FunctionExpr { - return FunctionExpr("unix_millis_to_timestamp", [self]) - } - - func timestampToUnixMillis() -> FunctionExpr { - return FunctionExpr("timestamp_to_unix_millis", [self]) - } - - func unixSecondsToTimestamp() -> FunctionExpr { - return FunctionExpr("unix_seconds_to_timestamp", [self]) - } - - func timestampToUnixSeconds() -> FunctionExpr { - return FunctionExpr("timestamp_to_unix_seconds", [self]) - } - - func timestampAdd(_ unit: Expr, _ amount: Expr) -> FunctionExpr { - return FunctionExpr("timestamp_add", [self, unit, amount]) - } - - func timestampAdd(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { - return FunctionExpr( - "timestamp_add", - [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] - ) - } - - func timestampSub(_ unit: Expr, _ amount: Expr) -> FunctionExpr { - return FunctionExpr("timestamp_sub", [self, unit, amount]) - } - - func timestampSub(_ unit: TimeUnit, _ amount: Int) -> FunctionExpr { - return FunctionExpr( - "timestamp_sub", - [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] - ) - } - - // MARK: - Bitwise operations - - func bitAnd(_ otherBits: Int) -> FunctionExpr { - return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) - } - - func bitAnd(_ otherBits: UInt8) -> FunctionExpr { - return FunctionExpr("bit_and", [self, Helper.sendableToExpr(otherBits)]) - } - - func bitAnd(_ bitsExpression: Expr) -> FunctionExpr { - return FunctionExpr("bit_and", [self, bitsExpression]) - } - - func bitOr(_ otherBits: Int) -> FunctionExpr { - return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) - } - - func bitOr(_ otherBits: UInt8) -> FunctionExpr { - return FunctionExpr("bit_or", [self, Helper.sendableToExpr(otherBits)]) - } - - func bitOr(_ bitsExpression: Expr) -> FunctionExpr { - return FunctionExpr("bit_or", [self, bitsExpression]) - } - - func bitXor(_ otherBits: Int) -> FunctionExpr { - return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) - } - - func bitXor(_ otherBits: UInt8) -> FunctionExpr { - return FunctionExpr("bit_xor", [self, Helper.sendableToExpr(otherBits)]) - } - - func bitXor(_ bitsExpression: Expr) -> FunctionExpr { - return FunctionExpr("bit_xor", [self, bitsExpression]) - } - - func bitNot() -> FunctionExpr { - return FunctionExpr("bit_not", [self]) - } - - func bitLeftShift(_ y: Int) -> FunctionExpr { - return FunctionExpr("bit_left_shift", [self, Helper.sendableToExpr(y)]) - } - - func bitLeftShift(_ numberExpr: Expr) -> FunctionExpr { - return FunctionExpr("bit_left_shift", [self, numberExpr]) - } - - func bitRightShift(_ y: Int) -> FunctionExpr { - return FunctionExpr("bit_right_shift", [self, Helper.sendableToExpr(y)]) - } - - func bitRightShift(_ numberExpr: Expr) -> FunctionExpr { - return FunctionExpr("bit_right_shift", [self, numberExpr]) - } - - func documentId() -> FunctionExpr { - return FunctionExpr("document_id", [self]) - } - - func ifError(_ catchExpr: Expr) -> FunctionExpr { - return FunctionExpr("if_error", [self, catchExpr]) - } - - func ifError(_ catchValue: Sendable) -> FunctionExpr { - return FunctionExpr("if_error", [self, Helper.sendableToExpr(catchValue)]) - } - - // MARK: Sorting - - func ascending() -> Ordering { - return Ordering(expr: self, direction: .ascending) - } - - func descending() -> Ordering { - return Ordering(expr: self, direction: .descending) - } -} - -extension Expr { - func toBridge() -> ExprBridge { - return (self as! BridgeWrapper).bridge - } -} - -// protocal cannot overwrite operator, since every inheritated class will have this function -// it will lead to error: Generic parameter 'Self' could not be inferred - -public func > (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("gt", [lhs, Helper.sendableToExpr(rhs())]) -} - -public func >= (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("gte", [lhs, Helper.sendableToExpr(rhs())]) -} - -public func < (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("lt", [lhs, Helper.sendableToExpr(rhs())]) -} - -public func <= (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("lte", [lhs, Helper.sendableToExpr(rhs())]) -} - -public func == (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("eq", [lhs, Helper.sendableToExpr(rhs())]) -} - -public func != (lhs: Expr, rhs: @autoclosure () throws -> Sendable) rethrows -> BooleanExpr { - try BooleanExpr("neq", [lhs, Helper.sendableToExpr(rhs())]) + /// Creates an `Ordering` object that sorts documents in descending order based on this + /// expression. + /// + /// ```swift + /// // Sort documents by the 'createdAt' field in descending order + /// firestore.pipeline().collection("users") + /// .sort(Field("createdAt").descending()) + /// ``` + /// + /// - Returns: A new `Ordering` instance for descending sorting. + func descending() -> Ordering } From 237240522a4fb92a80dc52e70f7d7ed1b15e029a Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 6 May 2025 14:33:38 -0400 Subject: [PATCH 31/43] Add documentation to Pipeline.swift --- .../Swift/Source/SwiftAPI/Pipeline/Expr.swift | 2 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 689 +++++++++++++----- 2 files changed, 499 insertions(+), 192 deletions(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift index 79428487f7e..7cd9b0d5adf 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr.swift @@ -8,7 +8,7 @@ // // 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 Sendable KIND, either express or implied. +// 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. diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index dff0f63dd40..e46ada11adb 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -19,6 +19,65 @@ #endif // SWIFT_PACKAGE import Foundation +/// The `Pipeline` class provides a flexible and expressive framework for building complex data +/// transformation and query pipelines for Firestore. +/// +/// A pipeline takes data sources, such as Firestore collections or collection groups, and applies +/// a series of stages that are chained together. Each stage takes the output from the previous +/// stage +/// (or the data source) and produces an output for the next stage (or as the final output of the +/// pipeline). +/// +/// Expressions can be used within each stage to filter and transform data through the stage. +/// +/// NOTE: The chained stages do not prescribe exactly how Firestore will execute the pipeline. +/// Instead, Firestore only guarantees that the result is the same as if the chained stages were +/// executed in order. +/// +/// ## Usage Examples +/// +/// The following examples assume you have a `Firestore` instance named `db`. +/// +/// ```swift +/// import FirebaseFirestore +/// +/// // Example 1: Select specific fields and rename 'rating' to 'bookRating'. +/// // Assumes `Field("rating").as("bookRating")` is a valid `Selectable` expression. +/// do { +/// let results1 = try await db.pipeline().collection("books") +/// .select(Field("title"), Field("author"), Field("rating").as("bookRating")) +/// .execute() +/// print("Results 1: \(results1.documents)") +/// } catch { +/// print("Error in example 1: \(error)") +/// } +/// +/// // Example 2: Filter documents where 'genre' is "Science Fiction" and 'published' is after 1950. +/// // Assumes `Function.eq`, `Function.gt`, and `Function.and` create `BooleanExpr`. +/// do { +/// let results2 = try await db.pipeline().collection("books") +/// .where(Function.and( +/// Function.eq(Field("genre"), "Science Fiction"), +/// Function.gt(Field("published"), 1950) +/// )) +/// .execute() +/// print("Results 2: \(results2.documents)") +/// } catch { +/// print("Error in example 2: \(error)") +/// } +/// +/// // Example 3: Calculate the average rating of books published after 1980. +/// // Assumes `avg()` creates an `Accumulator` and `AggregateWithAlias` is used correctly. +/// do { +/// let results3 = try await db.pipeline().collection("books") +/// .where(Function.gt(Field("published"), 1980)) +/// .aggregate(AggregateWithas(avg(Field("rating")), alias: "averageRating")) +/// .execute() +/// print("Results 3: \(results3.documents)") +/// } catch { +/// print("Error in example 3: \(error)") +/// } +/// ``` @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct Pipeline: @unchecked Sendable { private var stages: [Stage] @@ -31,6 +90,25 @@ public struct Pipeline: @unchecked Sendable { bridge = PipelineBridge(stages: stages.map { $0.bridge }, db: db) } + /// Executes the defined pipeline and returns a `PipelineSnapshot` containing the results. + /// + /// This method asynchronously sends the pipeline definition to Firestore for execution. + /// The resulting documents, transformed and filtered by the pipeline stages, are returned + /// within a `PipelineSnapshot`. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume a pipeline is already configured. + /// do { + /// let snapshot = try await pipeline.execute() + /// // Process snapshot.documents + /// print("Pipeline executed successfully: \(snapshot.documents)") + /// } catch { + /// print("Pipeline execution failed: \(error)") + /// } + /// ``` + /// + /// - Throws: An error if the pipeline execution fails on the backend. + /// - Returns: A `PipelineSnapshot` containing the result of the pipeline execution. public func execute() async throws -> PipelineSnapshot { return try await withCheckedThrowingContinuation { continuation in self.bridge.execute { result, error in @@ -46,24 +124,45 @@ public struct Pipeline: @unchecked Sendable { /// Adds new fields to outputs from previous stages. /// /// This stage allows you to compute values on-the-fly based on existing data from previous - /// stages or constants. You can use this to create new fields or overwrite existing ones. + /// stages or constants. You can use this to create new fields or overwrite existing ones + /// (if there is a name overlap). /// /// The added fields are defined using `Selectable`s, which can be: - /// /// - `Field`: References an existing document field. - /// - `Function`: Performs a calculation using functions like `add`, `multiply` with - /// assigned aliases using `Expr.as`. - /// - /// - Parameter fields: The fields to add to the documents, specified as `Selectable`s. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - `Function`: Performs a calculation using functions like `Function.add` or + /// `Function.multiply`, + /// typically with an assigned alias (e.g., `Function.multiply(Field("price"), + /// 1.1).as("priceWithTax")`). + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline from a collection. + /// let updatedPipeline = pipeline.addFields( + /// Field("rating").as("bookRating"), // Rename 'rating' to 'bookRating'. + /// Function.add(5, Field("quantity")).as("totalQuantityPlusFive") // Calculate + /// 'totalQuantityPlusFive'. + /// ) + /// // let results = try await updatedPipeline.execute() + /// ``` + /// + /// - Parameter field: The first field to add to the documents, specified as a `Selectable`. + /// - Parameter additionalFields: Optional additional fields to add, specified as `Selectable`s. + /// - Returns: A new `Pipeline` object with this stage appended. public func addFields(_ field: Selectable, _ additionalFields: Selectable...) -> Pipeline { let fields = [field] + additionalFields return Pipeline(stages: stages + [AddFields(fields: fields)], db: db) } - /// Remove fields from outputs of previous stages. - /// - Parameter fields: The fields to remove. - /// - Returns: A new Pipeline object with this stage appended to the stage list. + /// Removes fields from outputs of previous stages. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let updatedPipeline = pipeline.removeFields(Field("confidentialData"), Field("internalNotes")) + /// // let results = try await updatedPipeline.execute() + /// ``` + /// + /// - Parameter field: The first field to remove, specified as a `Field` instance. + /// - Parameter additionalFields: Optional additional fields to remove. + /// - Returns: A new `Pipeline` object with this stage appended. public func removeFields(_ field: Field, _ additionalFields: Field...) -> Pipeline { return Pipeline( stages: stages + [RemoveFieldsStage(fields: [field] + additionalFields)], @@ -71,9 +170,18 @@ public struct Pipeline: @unchecked Sendable { ) } - /// Remove fields from outputs of previous stages. - /// - Parameter fields: The fields to remove. - /// - Returns: A new Pipeline object with this stage appended to the stage list. + /// Removes fields from outputs of previous stages using field names. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Removes fields 'rating' and 'cost' from the previous stage outputs. + /// let updatedPipeline = pipeline.removeFields("rating", "cost") + /// // let results = try await updatedPipeline.execute() + /// ``` + /// + /// - Parameter field: The name of the first field to remove. + /// - Parameter additionalFields: Optional additional field names to remove. + /// - Returns: A new `Pipeline` object with this stage appended. public func removeFields(_ field: String, _ additionalFields: String...) -> Pipeline { return Pipeline( stages: stages + [RemoveFieldsStage(fields: [field] + additionalFields)], @@ -84,38 +192,52 @@ public struct Pipeline: @unchecked Sendable { /// Selects or creates a set of fields from the outputs of previous stages. /// /// The selected fields are defined using `Selectable` expressions, which can be: - /// - /// - `String`: Name of an existing field. + /// - `String`: Name of an existing field (implicitly converted to `Field`). /// - `Field`: References an existing field. - /// - `Function`: Represents the result of a function with an assigned alias name using `Expr#as`. - /// - /// If no selections are provided, the output of this stage is empty. Use `addFields` instead if - /// only additions are desired. - /// - /// - Parameter selections: The fields to include in the output documents, specified as - /// `Selectable` expressions. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - `Function`: Represents the result of a function with an assigned alias + /// (e.g., `Function.toUppercase(Field("address")).as("upperAddress")`). + /// + /// If no selections are provided, the output of this stage is typically empty. + /// Use `addFields` if only additions are desired without replacing the existing document + /// structure. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let projectedPipeline = pipeline.select( + /// Field("firstName"), + /// Field("lastName"), + /// Function.toUppercase(Field("address")).as("upperAddress") + /// ) + /// // let results = try await projectedPipeline.execute() + /// ``` + /// + /// - Parameter selection: The first field to include in the output documents, specified as a + /// `Selectable`. + /// - Parameter additionalSelections: Optional additional fields to include, specified as + /// `Selectable`s. + /// - Returns: A new `Pipeline` object with this stage appended. public func select(_ selection: Selectable, _ additionalSelections: Selectable...) -> Pipeline { let selections = [selection] + additionalSelections return Pipeline( - stages: stages + [Select(selections: selections + additionalSelections)], + stages: stages + [Select(selections: selections)], db: db ) } - /// Selects or creates a set of fields from the outputs of previous stages. + /// Selects a set of fields from the outputs of previous stages using field names. /// - /// The selected fields are defined using `Selectable` expressions, which can be: - /// - /// - `String`: Name of an existing field. - /// - `Field`: References an existing field. - /// - `Function`: Represents the result of a function with an assigned alias name using `Expr#as`. + /// The selected fields are specified by their names. If no selections are provided, + /// the output of this stage is typically empty. Use `addFields` if only additions are desired. /// - /// If no selections are provided, the output of this stage is empty. Use `addFields` instead if - /// only additions are desired. + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let projectedPipeline = pipeline.select("title", "author", "yearPublished") + /// // let results = try await projectedPipeline.execute() + /// ``` /// - /// - Parameter selections: `String` values representing field names. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - Parameter selection: The name of the first field to include in the output documents. + /// - Parameter additionalSelections: Optional additional field names to include. + /// - Returns: A new `Pipeline` object with this stage appended. public func select(_ selection: String, _ additionalSelections: String...) -> Pipeline { let selections = ([selection] + additionalSelections).map { Field($0) } return Pipeline( @@ -124,155 +246,216 @@ public struct Pipeline: @unchecked Sendable { ) } - /// Filters the documents from previous stages to only include those matching the specified + /// Filters documents from previous stages, including only those matching the specified /// `BooleanExpr`. /// - /// This stage allows you to apply conditions to the data, similar to a "WHERE" clause - /// in SQL. - /// You can filter documents based on their field values, using implementations of - /// `BooleanExpr`, typically including but not limited to: - /// - /// - field comparators: `Function.eq`, `Function.lt` (less than), `Function.gt` (greater than), - /// etc. - /// - logical operators: `Function.and`, `Function.or`, `Function.not`, - /// etc. - /// - advanced functions: `Function.regexMatch`, `Function.arrayContains`, etc. + /// This stage applies conditions similar to a "WHERE" clause in SQL. + /// Filter documents based on field values using `BooleanExpr` implementations, such as: + /// - Field comparators: `Function.eq`, `Function.lt` (less than), `Function.gt` (greater than). + /// - Logical operators: `Function.and`, `Function.or`, `Function.not`. + /// - Advanced functions: `Function.regexMatch`, `Function.arrayContains`. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// let filteredPipeline = pipeline.where( + /// Field("rating").gt(4.0) // Rating greater than 4.0. + /// && Field("genre").eq("Science Fiction") // Genre is "Science Fiction". + /// ) + /// // let results = try await filteredPipeline.execute() + /// ``` /// /// - Parameter condition: The `BooleanExpr` to apply. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - Returns: A new `Pipeline` object with this stage appended. public func `where`(_ condition: BooleanExpr) -> Pipeline { return Pipeline(stages: stages + [Where(condition: condition)], db: db) } /// Skips the first `offset` number of documents from the results of previous stages. - /// The negative input number will count back from the result set. - /// - /// This stage is useful for implementing pagination in your pipelines, allowing you to - /// retrieve results in chunks. It is typically used in conjunction with `limit` to control the - /// size of each page. /// - /// - Parameter offset: The number of documents to skip. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// A negative input number might count back from the end of the result set, + /// depending on backend behavior. This stage is useful for pagination, + /// typically used with `limit` to control page size. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline, possibly sorted. + /// // Retrieve the second page of 20 results (skip first 20, limit to next 20). + /// let pagedPipeline = pipeline + /// .sort(Ascending("published")) // Example sort. + /// .offset(20) // Skip the first 20 results. + /// .limit(20) // Take the next 20 results. + /// // let results = try await pagedPipeline.execute() + /// ``` + /// + /// - Parameter offset: The number of documents to skip (a `Int32` value). + /// - Returns: A new `Pipeline` object with this stage appended. public func offset(_ offset: Int32) -> Pipeline { return Pipeline(stages: stages + [Offset(offset)], db: db) } /// Limits the maximum number of documents returned by previous stages to `limit`. - /// The negative input number will count back from the result set. - /// - /// This stage is particularly useful when you want to retrieve a controlled - /// subset of data from a potentially large result set. It's often used for: - /// - /// - **Pagination:** In combination with `skip` to retrieve specific pages of results. - /// - **Limiting Data Retrieval:** To prevent excessive data transfer and improve - /// performance, especially when dealing with large collections. /// - /// - Parameter limit: The maximum number of documents to return. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// A negative input number might count back from the end of the result set, + /// depending on backend behavior. This stage helps retrieve a controlled subset of data. + /// It's often used for: + /// - **Pagination:** With `offset` to retrieve specific pages. + /// - **Limiting Data Retrieval:** To improve performance with large collections. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Limit results to the top 10 highest-rated books. + /// let topTenPipeline = pipeline + /// .sort(Descending(Field("rating"))) + /// .limit(10) + /// // let results = try await topTenPipeline.execute() + /// ``` + /// + /// - Parameter limit: The maximum number of documents to return (a `Int32` value). + /// - Returns: A new `Pipeline` object with this stage appended. public func limit(_ limit: Int32) -> Pipeline { return Pipeline(stages: stages + [Limit(limit)], db: db) } - /// Returns a set of distinct `Expr` values from the inputs to this stage. - /// - /// This stage processes the results from previous stages, ensuring that only unique - /// combinations of `Expr` values (such as `Field` and `Function`) are included. - /// - /// The parameters to this stage are defined using `Selectable` expressions or field names: - /// - /// - `String`: The name of an existing field. - /// - `Field`: A reference to an existing document field. - /// - `Function`: Represents the result of a function with an assigned alias using - /// `Expr.alias(_:)`. - /// - /// - Parameter selections: The fields to include in the output documents, specified as - /// `String` values representing field names. + /// Returns a set of distinct documents based on specified grouping field names. + /// + /// This stage ensures that only unique combinations of values for the specified + /// group fields are included from the previous stage's output. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Get a list of unique author and genre combinations. + /// let distinctAuthorsGenresPipeline = pipeline.distinct("author", "genre") + /// // To further select only the author: + /// // .select("author") + /// // let results = try await distinctAuthorsGenresPipeline.execute() + /// ``` + /// + /// - Parameter group: The name of the first field for distinct value combinations. + /// - Parameter additionalGroups: Optional additional field names. + /// - Returns: A new `Pipeline` object with this stage appended. public func distinct(_ group: String, _ additionalGroups: String...) -> Pipeline { let selections = ([group] + additionalGroups).map { Field($0) } return Pipeline(stages: stages + [Distinct(groups: selections)], db: db) } - /// Returns a set of distinct `Expr` values from the inputs to this stage. + /// Returns a set of distinct documents based on specified `Selectable` expressions. /// - /// This stage processes the results from previous stages, ensuring that only unique - /// combinations of `Expr` values (such as `Field` and `Function`) are included. + /// This stage ensures unique combinations of values from evaluated `Selectable` + /// expressions (e.g., `Field` or `Function` results). /// - /// The parameters to this stage are defined using `Selectable` expressions or field names: - /// - /// - `String`: The name of an existing field. + /// `Selectable` expressions can be: /// - `Field`: A reference to an existing document field. - /// - `Function`: Represents the result of a function with an assigned alias using - /// `Expr.alias(_:)`. - /// - /// - Parameter selections: The fields to include in the output documents, specified as - /// `Selectable` expressions. + /// - `Function`: The result of a function with an alias (e.g., + /// `Function.toUppercase(Field("author")).as("authorName")`). + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Get unique uppercase author names and genre combinations. + /// let distinctPipeline = pipeline.distinct( + /// Field("author").uppercased().as("authorName"), + /// Field("genre") + /// ) + /// // To select only the transformed author name: + /// // .select(Field("authorName")) + /// // let results = try await distinctPipeline.execute() + /// ``` + /// + /// - Parameter group: The first `Selectable` expression to consider. + /// - Parameter additionalGroups: Optional additional `Selectable` expressions. + /// - Returns: A new `Pipeline` object with this stage appended. public func distinct(_ group: Selectable, _ additionalGroups: Selectable...) -> Pipeline { let groups = [group] + additionalGroups - return Pipeline(stages: stages + [Distinct(groups: groups + additionalGroups)], db: db) + return Pipeline(stages: stages + [Distinct(groups: groups)], db: db) } - /// Performs aggregation operations on the documents from previous stages. - /// - /// This stage allows you to compute aggregate values over a set of documents. - /// Aggregations are defined using `AccumulatorWithAlias`, which wraps an `Accumulator` - /// and provides a name for the accumulated results. These expressions are typically - /// created by calling `alias(_:)` on `Accumulator` instances. - /// - /// - Parameter accumulators: The `AccumulatorWithAlias` expressions, each wrapping an - /// `Accumulator` and assigning a name to the accumulated results. + /// Performs aggregation operations on all documents from previous stages. + /// + /// Computes aggregate values (e.g., sum, average, count) over the entire set of documents + /// from the previous stage. Aggregations are defined using `AggregateWithAlias`, + /// which pairs an `Accumulator` (e.g., `avg(Field("price"))`) with a result field name. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from a "books" collection. + /// // Calculate the average rating and total number of books. + /// let aggregatedPipeline = pipeline.aggregate( + /// AggregateWithas(aggregate: avg(Field("rating")), alias: "averageRating"), + /// AggregateWithas(aggregate: countAll(), alias: "totalBooks") + /// ) + /// // let results = try await aggregatedPipeline.execute() + /// // results.documents might be: [["averageRating": 4.2, "totalBooks": 150]] + /// ``` + /// + /// - Parameter accumulator: The first `AggregateWithAlias` expression. + /// - Parameter additionalAccumulators: Optional additional `AggregateWithAlias` expressions. + /// - Returns: A new `Pipeline` object with this stage appended. public func aggregate(_ accumulator: AggregateWithAlias, _ additionalAccumulators: AggregateWithAlias...) -> Pipeline { return Pipeline( stages: stages + [Aggregate( accumulators: [accumulator] + additionalAccumulators, - groups: nil + groups: nil // No grouping: aggregate over all documents. )], db: db ) } - /// Performs optionally grouped aggregation operations on the documents from previous stages. - /// - /// This stage calculates aggregate values over a set of documents, optionally grouped by - /// one or more fields or computed expressions. - /// - /// - **Grouping Fields or Expressions:** Defines how documents are grouped. For each - /// unique combination of values in the specified fields or expressions, a separate group - /// is created. If no grouping fields are provided, all documents are placed into a single - /// group. - /// - **Accumulators:** Defines the accumulation operations to perform within each group. - /// These are provided as `AccumulatorWithAlias` expressions, typically created by - /// calling `alias(_:)` on `Accumulator` instances. Each aggregation computes a - /// value (e.g., sum, average, count) based on the documents in its group. + /// Performs optionally grouped aggregation operations on documents from previous stages. + /// + /// Calculates aggregate values, optionally grouping documents by fields or `Selectable` + /// expressions. + /// - **Grouping:** Defined by the `groups` parameter. Each unique combination of values + /// from these `Selectable`s forms a group. If `groups` is `nil` or empty, + /// all documents form a single group. + /// - **Accumulators:** An array of `AggregateWithAlias` defining operations + /// (e.g., sum, average) within each group. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from "books" collection. + /// // Calculate the average rating for each genre. + /// let groupedAggregationPipeline = pipeline.aggregate( + /// [AggregateWithas(aggregate: avg(Field("rating")), alias: "avg_rating")], + /// groups: [Field("genre")] // Group by the "genre" field. + /// ) + /// // let results = try await groupedAggregationPipeline.execute() + /// // results.documents might be: + /// // [ + /// // ["genre": "SciFi", "avg_rating": 4.5], + /// // ["genre": "Fantasy", "avg_rating": 4.2] + /// // ] + /// ``` /// /// - Parameters: - /// - accumulators: A list of `AccumulatorWithAlias` expressions defining the aggregation - /// calculations. - /// - groups: An optional list of grouping fields or expressions. + /// - accumulator: An array of `AggregateWithAlias` expressions for calculations. + /// - groups: Optional array of `Selectable` expressions for grouping. If `nil` or empty, + /// aggregates across all documents. /// - Returns: A new `Pipeline` object with this stage appended. public func aggregate(_ accumulator: [AggregateWithAlias], groups: [Selectable]? = nil) -> Pipeline { return Pipeline(stages: stages + [Aggregate(accumulators: accumulator, groups: groups)], db: db) } - /// Performs optionally grouped aggregation operations on the documents from previous stages. - /// - /// This stage calculates aggregate values over a set of documents, optionally grouped by - /// one or more fields or computed expressions. - /// - /// - **Grouping Fields or Expressions:** Defines how documents are grouped. For each - /// unique combination of values in the specified fields or expressions, a separate group - /// is created. If no grouping fields are provided, all documents are placed into a single - /// group. - /// - **Accumulators:** Defines the accumulation operations to perform within each group. - /// These are provided as `AccumulatorWithAlias` expressions, typically created by - /// calling `alias(_:)` on `Accumulator` instances. Each aggregation computes a - /// value (e.g., sum, average, count) based on the documents in its group. + /// Performs optionally grouped aggregation operations using field names for grouping. + /// + /// Similar to the other `aggregate` method, but `groups` are specified as an array of `String` + /// field names. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from "books" collection. + /// // Count books for each publisher. + /// let groupedByPublisherPipeline = pipeline.aggregate( + /// [AggregateWithas(aggregate: countAll(), alias: "book_count")], + /// groups: ["publisher"] // Group by the "publisher" field name. + /// ) + /// // let results = try await groupedByPublisherPipeline.execute() + /// // results.documents might be: + /// // [ + /// // ["publisher": "Penguin", "book_count": 50], + /// // ["publisher": "HarperCollins", "book_count": 35] + /// // ] + /// ``` /// /// - Parameters: - /// - accumulators: A list of `AccumulatorWithAlias` expressions defining the aggregation - /// calculations. - /// - groups: An optional list of grouping field names. + /// - accumulator: An array of `AggregateWithAlias` expressions. + /// - groups: An optional array of `String` field names for grouping. /// - Returns: A new `Pipeline` object with this stage appended. public func aggregate(_ accumulator: [AggregateWithAlias], groups: [String]? = nil) -> Pipeline { @@ -283,8 +466,31 @@ public struct Pipeline: @unchecked Sendable { ) } - /// Performs a vector similarity search, ordering the result set by most similar to least - /// similar, and returning the first N documents in the result set. + /// Performs a vector similarity search, ordering results by similarity. + /// + /// Returns up to `limit` documents, from most to least similar based on vector embeddings. + /// The distance can optionally be included in a specified field. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from a collection with vector embeddings. + /// let queryVector: [Double] = [0.1, 0.2, ..., 0.8] // Example query vector. + /// let nearestNeighborsPipeline = pipeline.findNearest( + /// field: Field("embedding_field"), // Field containing the vector. + /// vectorValue: queryVector, // Query vector for comparison. + /// distanceMeasure: .COSINE, // Distance metric. + /// limit: 10, // Return top 10 nearest neighbors. + /// distanceField: "similarityScore" // Optional: field for distance score. + /// ) + /// // let results = try await nearestNeighborsPipeline.execute() + /// ``` + /// + /// - Parameters: + /// - field: The `Field` containing vector embeddings. + /// - vectorValue: An array of `Double` representing the query vector. + /// - distanceMeasure: The `DistanceMeasure` (e.g., `.EUCLIDEAN`, `.COSINE`) for comparison. + /// - limit: Optional. Maximum number of similar documents to return. + /// - distanceField: Optional. Name for a new field to store the calculated distance. + /// - Returns: A new `Pipeline` object with this stage appended. public func findNearest(field: Field, vectorValue: [Double], distanceMeasure: DistanceMeasure, @@ -304,107 +510,208 @@ public struct Pipeline: @unchecked Sendable { ) } - /// Sorts the documents from previous stages based on one or more `Ordering` criteria. - /// - /// This stage allows you to order the results of your pipeline. You can specify multiple - /// `Ordering` instances to sort by multiple fields in ascending or descending order. - /// If documents have the same value for a field used for sorting, the next specified ordering - /// will be used. If all orderings result in equal comparison, the documents are considered - /// equal and the order is unspecified. - /// - /// - Parameter orderings: One or more `Ordering` instances specifying the sorting criteria. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// Sorts documents from previous stages based on one or more `Ordering` criteria. + /// + /// Specify multiple `Ordering` instances for multi-field sorting (ascending/descending). + /// If documents are equal by one criterion, the next is used. If all are equal, + /// relative order is unspecified. + /// + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Sort books by rating (descending), then by title (ascending). + /// let sortedPipeline = pipeline.sort( + /// Ascending("rating"), + /// Descending("title") // or Field("title").ascending() for ascending. + /// ) + /// // let results = try await sortedPipeline.execute() + /// ``` + /// + /// - Parameter ordering: The primary `Ordering` criterion. + /// - Parameter additionalOrdering: Optional additional `Ordering` criteria for secondary sorting, + /// etc. + /// - Returns: A new `Pipeline` object with this stage appended. public func sort(_ ordering: Ordering, _ additionalOrdering: Ordering...) -> Pipeline { let orderings = [ordering] + additionalOrdering return Pipeline(stages: stages + [Sort(orderings: orderings)], db: db) } - /// Fully overwrites all fields in a document with those coming from a nested map. + /// Fully overwrites document fields with those from a nested map identified by an `Expr`. + /// + /// "Promotes" a map value (dictionary) from a field to become the new root document. + /// Each key-value pair from the map specified by `expr` becomes a field-value pair + /// in the output document, discarding original document fields. + /// + /// ```swift + /// // Assume input document: + /// // { "id": "user123", "profile": { "name": "Alex", "age": 30 }, "status": "active" } + /// // let pipeline: Pipeline = ... /// - /// This stage allows you to emit a map value as a document. Each key of the map becomes a - /// field on the document that contains the corresponding value. + /// // Replace document with the contents of the 'profile' map. + /// let replacedPipeline = pipeline.replace(with: Field("profile")) /// - /// - Parameter field: The `Expr` field containing the nested map. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// // let results = try await replacedPipeline.execute() + /// // Output document would be: { "name": "Alex", "age": 30 } + /// ``` + /// + /// - Parameter expr: The `Expr` (typically a `Field`) that resolves to the nested map. + /// - Returns: A new `Pipeline` object with this stage appended. public func replace(with expr: Expr) -> Pipeline { return Pipeline(stages: stages + [ReplaceWith(expr: expr)], db: db) } - /// Fully overwrites all fields in a document with those coming from a nested map. + /// Fully overwrites document fields with those from a nested map identified by a field name. + /// + /// "Promotes" a map value (dictionary) from a field to become the new root document. + /// Each key-value pair from the map in `fieldName` becomes a field-value pair + /// in the output document, discarding original document fields. + /// + /// ```swift + /// // Assume input document: + /// // { "id": "user123", "details": { "role": "admin", "department": "tech" }, "joined": + /// "2023-01-15" } + /// // let pipeline: Pipeline = ... /// - /// This stage allows you to emit a map value as a document. Each key of the map becomes a - /// field on the document that contains the corresponding value. + /// // Replace document with the contents of the 'details' map. + /// let replacedPipeline = pipeline.replace(with: "details") /// - /// - Parameter fieldName: The field containing the nested map. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// // let results = try await replacedPipeline.execute() + /// // Output document would be: { "role": "admin", "department": "tech" } + /// ``` + /// + /// - Parameter fieldName: The name of the field containing the nested map. + /// - Returns: A new `Pipeline` object with this stage appended. public func replace(with fieldName: String) -> Pipeline { return Pipeline(stages: stages + [ReplaceWith(fieldName: fieldName)], db: db) } - /// Performs a pseudo-random sampling of the input documents. + /// Performs pseudo-random sampling of input documents, returning a specific count. + /// + /// Filters documents pseudo-randomly. `count` specifies the approximate number + /// to return. The actual number may vary and isn't guaranteed if the input set + /// is smaller than `count`. /// - /// This stage will filter documents pseudo-randomly. The parameter specifies how number of - /// documents to be returned. + /// ```swift + /// // let pipeline: Pipeline = ... // Assume pipeline from a large collection. + /// // Sample 25 books, if available. + /// let sampledPipeline = pipeline.sample(count: 25) + /// // let results = try await sampledPipeline.execute() + /// ``` /// - /// - Parameter count: The number of documents to sample. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - Parameter count: The target number of documents to sample (a `Int64` value). + /// - Returns: A new `Pipeline` object with this stage appended. public func sample(count: Int64) -> Pipeline { return Pipeline(stages: stages + [Sample(count: count)], db: db) } - /// Performs a pseudo-random sampling of the input documents. + /// Performs pseudo-random sampling of input documents, returning a percentage. + /// + /// Filters documents pseudo-randomly. `percentage` (0.0 to 1.0) specifies + /// the approximate fraction of documents to return from the input set. /// - /// This stage will filter documents pseudo-randomly. The `options` parameter specifies how - /// sampling will be performed. See `SampleOptions` for more information. + /// ```swift + /// // let pipeline: Pipeline = ... // Assume initial pipeline. + /// // Sample 50% of books. + /// let sampledPipeline = pipeline.sample(percentage: 0.5) + /// // let results = try await sampledPipeline.execute() + /// ``` /// - /// - Parameter percentage: The percentage of documents to sample. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - Parameter percentage: The percentage of documents to sample (e.g., 0.5 for 50%; a `Double` + /// value). + /// - Returns: A new `Pipeline` object with this stage appended. public func sample(percentage: Double) -> Pipeline { return Pipeline(stages: stages + [Sample(percentage: percentage)], db: db) } - /// Performs union of all documents from two pipelines, including duplicates. + /// Performs a union of all documents from this pipeline and another, including duplicates. + /// + /// Passes through documents from this pipeline's previous stage and also those from + /// the `other` pipeline's previous stage. The order of emitted documents is undefined. + /// Both pipelines should ideally have compatible document structures. + /// + /// ```swift + /// // let db: Firestore = ... + /// // let booksPipeline = db.collection("books").pipeline().select("title", "category") + /// // let magazinesPipeline = db.collection("magazines").pipeline().select("title", + /// Field("topic").as("category")) /// - /// This stage will pass through documents from previous stage, and also pass through documents - /// from previous stage of the `other` Pipeline given in parameter. The order of documents - /// emitted from this stage is undefined. + /// // Emit documents from both "books" and "magazines" collections. + /// let combinedPipeline = booksPipeline.union(magazinesPipeline) + /// // let results = try await combinedPipeline.execute() + /// ``` /// - /// - Parameter other: The other `Pipeline` that is part of union. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - Parameter other: The other `Pipeline` whose documents will be unioned. + /// - Returns: A new `Pipeline` object with this stage appended. public func union(_ other: Pipeline) -> Pipeline { return Pipeline(stages: stages + [Union(other: other)], db: db) } - /// Takes an array field from the input documents and outputs a document for each element - /// with the array field mapped to the alias provided. + /// Takes an array field from input documents and outputs a new document for each element. + /// + /// For each input document, this stage emits zero or more augmented documents based on + /// an array field specified by `field` (a `Selectable`). The `Selectable` for `field` + /// **must** have an alias; this alias becomes the field name in the output document + /// containing the unnested element. /// - /// For each previous stage document, this stage will emit zero or more augmented documents. - /// The input array found in the previous stage document field specified by the `fieldName` - /// parameter, will for each input array element produce an augmented document. The input array - /// element will augment the previous stage document by replacing the field specified by - /// `fieldName` parameter with the element value. + /// The original field containing the array is effectively replaced by the array element + /// under the new alias name in each output document. Other fields from the original document + /// are typically preserved. /// - /// In other words, the field containing the input array will be removed from the augmented - /// document and replaced by the corresponding array element. + /// If `indexField` is provided, a new field with this name is added, containing the + /// zero-based index of the element within its original array. /// - /// - Parameter field: The name of the field containing the array. - /// - Parameter indexField: Optional. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// Behavior for non-array values or empty arrays depends on the backend. + /// + /// ```swift + /// // Assume input document: + /// // { "title": "The Hitchhiker's Guide", "authors": ["Douglas Adams", "Eoin Colfer"] } + /// // let pipeline: Pipeline = ... + /// + /// // Unnest 'authors'. Each author becomes a new document with the author in a "authorName" + /// field. + /// let unnestedPipeline = pipeline.unnest(Field("authors").as("authorName"), indexField: + /// "authorIndex") + /// + /// // let results = try await unnestedPipeline.execute() + /// // Possible Output (other fields like "title" are preserved): + /// // { "title": "The Hitchhiker's Guide", "authorName": "Douglas Adams", "authorIndex": 0 } + /// // { "title": "The Hitchhiker's Guide", "authorName": "Eoin Colfer", "authorIndex": 1 } + /// ``` + /// + /// - Parameters: + /// - field: A `Selectable` resolving to an array field. **Must include an alias** + /// (e.g., `Field("myArray").as("arrayElement")`) to name the output field. + /// - indexField: Optional. If provided, this string names a new field for the element's + /// zero-based index from the original array. + /// - Returns: A new `Pipeline` object with this stage appended. public func unnest(_ field: Selectable, indexField: String? = nil) -> Pipeline { return Pipeline(stages: stages + [Unnest(field: field, indexField: indexField)], db: db) } - /// Adds a stage to the pipeline by specifying the stage name as an argument. This does - /// not offer any type safety on the stage params and requires the caller to know the - /// order (and optionally names) of parameters accepted by the stage. - /// - /// This method provides a way to call stages that are supported by the Firestore backend - /// but that are not implemented in the SDK version being used. + /// Adds a generic stage to the pipeline by specifying its name and parameters. + /// + /// Use this to call backend-supported stages not yet strongly-typed in the SDK. + /// This method does not offer compile-time type safety for stage parameters; + /// the caller must ensure correct name, order, and types. + /// + /// Parameters in `params` and `options` are typically primitive types, `Field`, + /// `Function`, `Expr`, or arrays/dictionaries thereof. + /// + /// ```swift + /// // let pipeline: Pipeline = ... + /// // Example: Assuming a hypothetical backend stage "customFilterV2". + /// let genericPipeline = pipeline.genericStage( + /// name: "customFilterV2", + /// params: [Field("userScore"), 80], // Ordered parameters. + /// options: ["mode": "strict", "logLevel": 2] // Optional named parameters. + /// ) + /// // let results = try await genericPipeline.execute() + /// ``` /// - /// - Parameter name: The unique name of the stage to add. - /// - Parameter params: A list of ordered parameters to configure the stage's behavior. - /// - Parameter options: A list of optional, named parameters to configure the stage's behavior. - /// - Returns: A new `Pipeline` object with this stage appended to the stage list. + /// - Parameters: + /// - name: The unique name of the stage (as recognized by the backend). + /// - params: An array of ordered, `Sendable` parameters for the stage. + /// - options: Optional dictionary of named, `Sendable` parameters. + /// - Returns: A new `Pipeline` object with this stage appended. public func genericStage(name: String, params: [Sendable], options: [String: Sendable]? = nil) -> Pipeline { return Pipeline( From 591816bf03c1e6330c93499a4d416c8296959139 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 7 May 2025 14:08:23 -0400 Subject: [PATCH 32/43] Fix replace with --- Firestore/Source/API/FIRPipelineBridge.mm | 18 +----------------- .../FirebaseFirestore/FIRPipelineBridge.h | 1 - .../Source/SwiftAPI/Pipeline/Pipeline.swift | 2 +- Firestore/Swift/Source/SwiftAPI/Stages.swift | 10 +--------- Firestore/core/src/api/stages.cc | 13 ++----------- Firestore/core/src/api/stages.h | 2 -- 6 files changed, 5 insertions(+), 41 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index 1ddc0314c5c..ac3091249e0 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -625,7 +625,6 @@ - (id)initWithOrderings:(NSArray *)orderings { @implementation FIRReplaceWithStageBridge { FIRExprBridge *_expr; - NSString *_fieldName; Boolean isUserDataRead; std::shared_ptr cpp_replace_with; } @@ -634,17 +633,6 @@ - (id)initWithExpr:(FIRExprBridge *)expr { self = [super init]; if (self) { _expr = expr; - _fieldName = nil; - isUserDataRead = NO; - } - return self; -} - -- (id)initWithFieldName:(NSString *)fieldName { - self = [super init]; - if (self) { - _fieldName = fieldName; - _expr = nil; isUserDataRead = NO; } return self; @@ -652,11 +640,7 @@ - (id)initWithFieldName:(NSString *)fieldName { - (std::shared_ptr)cppStageWithReader:(FSTUserDataReader *)reader { if (!isUserDataRead) { - if (_expr) { - cpp_replace_with = std::make_shared([_expr cppExprWithReader:reader]); - } else { - cpp_replace_with = std::make_shared(MakeString(_fieldName)); - } + cpp_replace_with = std::make_shared([_expr cppExprWithReader:reader]); } isUserDataRead = YES; diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index c8ea8be4ff0..7b8ebf80e9b 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -173,7 +173,6 @@ NS_SWIFT_SENDABLE NS_SWIFT_NAME(ReplaceWithStageBridge) @interface FIRReplaceWithStageBridge : FIRStageBridge - (id)initWithExpr:(FIRExprBridge *)expr; -- (id)initWithFieldName:(NSString *)fieldName; @end NS_SWIFT_SENDABLE diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index e46ada11adb..6a530867711 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -581,7 +581,7 @@ public struct Pipeline: @unchecked Sendable { /// - Parameter fieldName: The name of the field containing the nested map. /// - Returns: A new `Pipeline` object with this stage appended. public func replace(with fieldName: String) -> Pipeline { - return Pipeline(stages: stages + [ReplaceWith(fieldName: fieldName)], db: db) + return Pipeline(stages: stages + [ReplaceWith(expr: Field(fieldName))], db: db) } /// Performs pseudo-random sampling of input documents, returning a specific count. diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index 868d164769b..0180dc6a4bf 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -256,20 +256,12 @@ class Sort: Stage { class ReplaceWith: Stage { var name: String = "replaceWith" var bridge: StageBridge - private var expr: Expr? - private var fieldName: String? + private var expr: Expr init(expr: Expr) { self.expr = expr - fieldName = nil bridge = ReplaceWithStageBridge(expr: expr.toBridge()) } - - init(fieldName: String) { - self.fieldName = fieldName - expr = nil - bridge = ReplaceWithStageBridge(fieldName: fieldName) - } } class Sample: Stage { diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index c4cc0b0edee..7d891398c48 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -312,13 +312,7 @@ google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { result.args_count = 1; result.args = nanopb::MakeArray(1); - if (expr_) { - result.args[0] = expr_->to_proto(); - } else { - result.args[0].which_value_type = - google_firestore_v1_Value_string_value_tag; - result.args[0].string_value = nanopb::MakeBytesArray(field_name_.value()); - } + result.args[0] = expr_->to_proto(); result.options_count = 0; result.options = nullptr; @@ -326,10 +320,7 @@ google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { } ReplaceWith::ReplaceWith(std::shared_ptr expr) - : expr_(std::move(expr)), field_name_(absl::nullopt) { -} -ReplaceWith::ReplaceWith(std::string field_name) - : expr_(nullptr), field_name_(std::move(field_name)) { + : expr_(std::move(expr)) { } Sample::Sample(std::string type, int64_t count, double percentage) diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index 1c512c9b5e7..a3c7303ae92 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -253,13 +253,11 @@ class RemoveFieldsStage : public Stage { class ReplaceWith : public Stage { public: explicit ReplaceWith(std::shared_ptr expr); - explicit ReplaceWith(std::string field_name); ~ReplaceWith() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: std::shared_ptr expr_; - absl::optional field_name_; }; class Sample : public Stage { From 00988291e62ae9882dd54b199b3e093d93cd6821 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 8 May 2025 17:56:03 -0400 Subject: [PATCH 33/43] change tests --- Firestore/Swift/Tests/Integration/PipelineApiTests.swift | 4 ++-- Firestore/Swift/Tests/Integration/PipelineTests.swift | 2 +- Firestore/core/src/api/stages.cc | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index 2e4b71ff0c5..232a1d3076b 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -40,7 +40,7 @@ final class PipelineTests: FSTIntegrationTestCase { func testWhereStage() async throws { _ = db.pipeline().collection("books") .where( - Field("rating") > 4.0 && Field("genre") == "Science Fiction" || ArrayContains( + Field("rating").gt(4.0) && Field("genre").eq("Science Fiction") || ArrayContains( fieldName: "fieldName", values: "rating" ) @@ -327,7 +327,7 @@ final class PipelineTests: FSTIntegrationTestCase { } func testBooleanExpr() async throws { - let isApple: BooleanExpr = Field("type") == "apple" + let isApple: BooleanExpr = Field("type").eq("apple") // USAGE: stage where requires an expression of type BooleanExpr let allAppleOptions: Pipeline = db.pipeline().collection("fruitOptions").where(isApple) diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 51b831ad065..8b6fb9d130c 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -118,7 +118,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let snapshot = try await firestore() .pipeline() .collection("/foo") - .where(Field("foo") == Constant("bar")) + .where(Field("foo").eq(Constant("bar"))) .execute() print(snapshot) diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 7d891398c48..524872fd3a2 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -319,8 +319,7 @@ google_firestore_v1_Pipeline_Stage ReplaceWith::to_proto() const { return result; } -ReplaceWith::ReplaceWith(std::shared_ptr expr) - : expr_(std::move(expr)) { +ReplaceWith::ReplaceWith(std::shared_ptr expr) : expr_(std::move(expr)) { } Sample::Sample(std::string type, int64_t count, double percentage) From 4fb738d9c7a502a140f1874447d93ddf9fbe46aa Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Mon, 12 May 2025 12:29:35 -0400 Subject: [PATCH 34/43] Address feebacks 2 --- Firestore/core/src/remote/serializer.cc | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Firestore/core/src/remote/serializer.cc b/Firestore/core/src/remote/serializer.cc index 5dffd848c09..889767b84ee 100644 --- a/Firestore/core/src/remote/serializer.cc +++ b/Firestore/core/src/remote/serializer.cc @@ -1209,16 +1209,8 @@ Serializer::DecodeCursorValue(google_firestore_v1_Cursor& cursor) const { 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.pipeline = pipeline.to_proto().pipeline_value; result.options_count = 0; result.options = nullptr; From 6ec8b6ca6172991c4c659f04126ebdf5845c5024 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 13 May 2025 12:06:28 -0400 Subject: [PATCH 35/43] Address feedbacks 3 --- .../SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift index f44aa6e8e7d..eac2ef745f1 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift @@ -16,14 +16,14 @@ public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { var bridge: AggregateFunctionBridge let functionName: String - let agrs: [Expr] + let args: [Expr] - public init(_ functionName: String, _ agrs: [Expr]) { + public init(_ functionName: String, _ args: [Expr]) { self.functionName = functionName - self.agrs = agrs + self.args = args bridge = AggregateFunctionBridge( name: functionName, - args: self.agrs.map { $0.toBridge() + args: self.args.map { $0.toBridge() } ) } From fd51b7b2092e5ef12078247e2b565192dde450c1 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 13 May 2025 12:57:31 -0400 Subject: [PATCH 36/43] change mutating fields --- .../Aggregation/AggregateFunction.swift | 2 +- .../SwiftAPI/Pipeline/Expr/Constant.swift | 2 +- .../Source/SwiftAPI/Pipeline/Expr/Field.swift | 2 +- .../SwiftAPI/Pipeline/Expr/FunctionExpr.swift | 2 +- .../Source/SwiftAPI/Pipeline/Ordering.swift | 2 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 2 +- Firestore/Swift/Source/SwiftAPI/Stages.swift | 84 +++++++++---------- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift index eac2ef745f1..ed7c25bd129 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregation/AggregateFunction.swift @@ -13,7 +13,7 @@ // limitations under the License. public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { - var bridge: AggregateFunctionBridge + let bridge: AggregateFunctionBridge let functionName: String let args: [Expr] diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift index cd3c3ffa50d..0d4f30fe463 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift @@ -19,7 +19,7 @@ #endif // SWIFT_PACKAGE public struct Constant: Expr, BridgeWrapper, @unchecked Sendable { - var bridge: ExprBridge + let bridge: ExprBridge let value: Any? diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift index 980695b9e78..fa1dc7d7510 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Field.swift @@ -14,7 +14,7 @@ public class Field: ExprBridge, Expr, Selectable, BridgeWrapper, SelectableWrapper, @unchecked Sendable { - var bridge: ExprBridge + let bridge: ExprBridge var alias: String diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift index 1849b3aacbc..533f6a5ef51 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/FunctionExpr.swift @@ -13,7 +13,7 @@ // limitations under the License. public class FunctionExpr: Expr, BridgeWrapper, @unchecked Sendable { - var bridge: ExprBridge + let bridge: ExprBridge let functionName: String let agrs: [Expr] diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index 70768337b53..9659e95e682 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -17,7 +17,7 @@ public class Ordering: @unchecked Sendable { let expr: Expr let direction: Direction - var bridge: OrderingBridge + let bridge: OrderingBridge init(expr: Expr, direction: Direction) { self.expr = expr diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 6a530867711..4e49c97301b 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -81,7 +81,7 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct Pipeline: @unchecked Sendable { private var stages: [Stage] - var bridge: PipelineBridge + let bridge: PipelineBridge let db: Firestore init(stages: [Stage], db: Firestore) { diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index 0180dc6a4bf..26ff90c39b4 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -23,9 +23,9 @@ protocol Stage { } class CollectionSource: Stage { - var name: String = "collection" + let name: String = "collection" - var bridge: StageBridge + let bridge: StageBridge private var collection: String init(collection: String) { @@ -35,9 +35,9 @@ class CollectionSource: Stage { } class CollectionGroupSource: Stage { - var name: String = "collectionId" + let name: String = "collectionId" - var bridge: StageBridge + let bridge: StageBridge private var collectionId: String init(collectionId: String) { @@ -48,8 +48,8 @@ class CollectionGroupSource: Stage { // Represents the entire database as a source. class DatabaseSource: Stage { - var name: String = "database" - var bridge: StageBridge + let name: String = "database" + let bridge: StageBridge init() { bridge = DatabaseSourceStageBridge() @@ -58,8 +58,8 @@ class DatabaseSource: Stage { // Represents a list of document references as a source. class DocumentsSource: Stage { - var name: String = "documents" - var bridge: StageBridge + let name: String = "documents" + let bridge: StageBridge private var references: [String] // Initialize with an array of String paths @@ -71,8 +71,8 @@ class DocumentsSource: Stage { // Represents an existing Query as a source. class QuerySource: Stage { - var name: String = "query" - var bridge: StageBridge + let name: String = "query" + let bridge: StageBridge private var query: Query init(query: Query) { @@ -84,8 +84,8 @@ class QuerySource: Stage { // Represents an existing AggregateQuery as a source. class AggregateQuerySource: Stage { - var name: String = "aggregateQuery" - var bridge: StageBridge + let name: String = "aggregateQuery" + let bridge: StageBridge private var aggregateQuery: AggregateQuery init(aggregateQuery: AggregateQuery) { @@ -96,9 +96,9 @@ class AggregateQuerySource: Stage { } class Where: Stage { - var name: String = "where" + let name: String = "where" - var bridge: StageBridge + let bridge: StageBridge private var condition: BooleanExpr init(condition: BooleanExpr) { @@ -108,9 +108,9 @@ class Where: Stage { } class Limit: Stage { - var name: String = "limit" + let name: String = "limit" - var bridge: StageBridge + let bridge: StageBridge private var limit: Int32 init(_ limit: Int32) { @@ -120,9 +120,9 @@ class Limit: Stage { } class Offset: Stage { - var name: String = "offset" + let name: String = "offset" - var bridge: StageBridge + let bridge: StageBridge private var offset: Int32 init(_ offset: Int32) { @@ -132,8 +132,8 @@ class Offset: Stage { } class AddFields: Stage { - var name: String = "addFields" - var bridge: StageBridge + let name: String = "addFields" + let bridge: StageBridge private var fields: [Selectable] init(fields: [Selectable]) { @@ -150,8 +150,8 @@ class AddFields: Stage { } class RemoveFieldsStage: Stage { - var name: String = "removeFields" - var bridge: StageBridge + let name: String = "removeFields" + let bridge: StageBridge private var fields: [String] init(fields: [String]) { @@ -166,8 +166,8 @@ class RemoveFieldsStage: Stage { } class Select: Stage { - var name: String = "select" - var bridge: StageBridge + let name: String = "select" + let bridge: StageBridge private var selections: [Selectable] init(selections: [Selectable]) { @@ -179,8 +179,8 @@ class Select: Stage { } class Distinct: Stage { - var name: String = "distinct" - var bridge: StageBridge + let name: String = "distinct" + let bridge: StageBridge private var groups: [Selectable] init(groups: [Selectable]) { @@ -192,8 +192,8 @@ class Distinct: Stage { } class Aggregate: Stage { - var name: String = "aggregate" - var bridge: StageBridge + let name: String = "aggregate" + let bridge: StageBridge private var accumulators: [AggregateWithAlias] private var groups: [String: Expr] = [:] @@ -214,8 +214,8 @@ class Aggregate: Stage { } class FindNearest: Stage { - var name: String = "findNearest" - var bridge: StageBridge + let name: String = "findNearest" + let bridge: StageBridge private var field: Field private var vectorValue: [Double] private var distanceMeasure: DistanceMeasure @@ -243,8 +243,8 @@ class FindNearest: Stage { } class Sort: Stage { - var name: String = "sort" - var bridge: StageBridge + let name: String = "sort" + let bridge: StageBridge private var orderings: [Ordering] init(orderings: [Ordering]) { @@ -254,8 +254,8 @@ class Sort: Stage { } class ReplaceWith: Stage { - var name: String = "replaceWith" - var bridge: StageBridge + let name: String = "replaceWith" + let bridge: StageBridge private var expr: Expr init(expr: Expr) { @@ -265,8 +265,8 @@ class ReplaceWith: Stage { } class Sample: Stage { - var name: String = "sample" - var bridge: StageBridge + let name: String = "sample" + let bridge: StageBridge private var count: Int64? private var percentage: Double? @@ -284,8 +284,8 @@ class Sample: Stage { } class Union: Stage { - var name: String = "union" - var bridge: StageBridge + let name: String = "union" + let bridge: StageBridge private var other: Pipeline init(other: Pipeline) { @@ -295,8 +295,8 @@ class Union: Stage { } class Unnest: Stage { - var name: String = "unnest" - var bridge: StageBridge + let name: String = "unnest" + let bridge: StageBridge private var field: Selectable private var indexField: String? @@ -311,8 +311,8 @@ class Unnest: Stage { } class GenericStage: Stage { - var name: String - var bridge: StageBridge + let name: String + let bridge: StageBridge private var params: [Sendable] private var options: [String: Sendable]? From 469b7680260777f7e15a8ea06d2874636e4cf52e Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 13 May 2025 13:04:41 -0400 Subject: [PATCH 37/43] [Firestore] On SPM, import FirebaseFirestoreInternalWrapper (#14848) --- Firestore/Swift/Source/SwiftAPI/Stages.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index 26ff90c39b4..ceb56cc22b3 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -14,9 +14,14 @@ * limitations under the License. */ -import FirebaseFirestoreInternal import Foundation +#if SWIFT_PACKAGE + @_exported import FirebaseFirestoreInternalWrapper +#else + @_exported import FirebaseFirestoreInternal +#endif // SWIFT_PACKAGE + protocol Stage { var name: String { get } var bridge: StageBridge { get } From ba94c32fa1b6cb53522e537f1834c4b88a9853c6 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Tue, 13 May 2025 13:16:05 -0400 Subject: [PATCH 38/43] add version restrictions --- Firestore/Swift/Source/SwiftAPI/Stages.swift | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index ceb56cc22b3..65796af8471 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -22,11 +22,13 @@ import Foundation @_exported import FirebaseFirestoreInternal #endif // SWIFT_PACKAGE +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) protocol Stage { var name: String { get } var bridge: StageBridge { get } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class CollectionSource: Stage { let name: String = "collection" @@ -39,6 +41,7 @@ class CollectionSource: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class CollectionGroupSource: Stage { let name: String = "collectionId" @@ -52,6 +55,7 @@ class CollectionGroupSource: Stage { } // Represents the entire database as a source. +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class DatabaseSource: Stage { let name: String = "database" let bridge: StageBridge @@ -62,6 +66,7 @@ class DatabaseSource: Stage { } // Represents a list of document references as a source. +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class DocumentsSource: Stage { let name: String = "documents" let bridge: StageBridge @@ -75,6 +80,7 @@ class DocumentsSource: Stage { } // 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 @@ -88,6 +94,7 @@ class QuerySource: Stage { } // 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 @@ -100,6 +107,7 @@ class AggregateQuerySource: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Where: Stage { let name: String = "where" @@ -112,6 +120,7 @@ class Where: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Limit: Stage { let name: String = "limit" @@ -124,6 +133,7 @@ class Limit: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Offset: Stage { let name: String = "offset" @@ -136,6 +146,7 @@ class Offset: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AddFields: Stage { let name: String = "addFields" let bridge: StageBridge @@ -154,6 +165,7 @@ class AddFields: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class RemoveFieldsStage: Stage { let name: String = "removeFields" let bridge: StageBridge @@ -170,6 +182,7 @@ class RemoveFieldsStage: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Select: Stage { let name: String = "select" let bridge: StageBridge @@ -183,6 +196,7 @@ class Select: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Distinct: Stage { let name: String = "distinct" let bridge: StageBridge @@ -196,6 +210,7 @@ class Distinct: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Aggregate: Stage { let name: String = "aggregate" let bridge: StageBridge @@ -218,6 +233,7 @@ class Aggregate: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class FindNearest: Stage { let name: String = "findNearest" let bridge: StageBridge @@ -247,6 +263,7 @@ class FindNearest: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Sort: Stage { let name: String = "sort" let bridge: StageBridge @@ -258,6 +275,7 @@ class Sort: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class ReplaceWith: Stage { let name: String = "replaceWith" let bridge: StageBridge @@ -269,6 +287,7 @@ class ReplaceWith: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Sample: Stage { let name: String = "sample" let bridge: StageBridge @@ -288,6 +307,7 @@ class Sample: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Union: Stage { let name: String = "union" let bridge: StageBridge @@ -299,6 +319,7 @@ class Union: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class Unnest: Stage { let name: String = "unnest" let bridge: StageBridge @@ -315,6 +336,7 @@ class Unnest: Stage { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class GenericStage: Stage { let name: String let bridge: StageBridge From d1d1a895d21a4537c54cbd18967561fa06c6182f Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 15 May 2025 13:24:25 -0400 Subject: [PATCH 39/43] Fix CI tests --- .../xcschemes/Firestore_Example_iOS.xcscheme | 12 ++++++++++++ .../Example/Tests/Util/FSTIntegrationTestCase.h | 2 ++ .../Example/Tests/Util/FSTIntegrationTestCase.mm | 5 +++++ .../Source/SwiftAPI/Pipeline/PipelineSource.swift | 5 +++-- .../Swift/Tests/Integration/PipelineApiTests.swift | 5 +++++ .../Swift/Tests/Integration/PipelineTests.swift | 5 +++++ Firestore/core/src/api/stages.cc | 4 ++-- 7 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme index 1df610c09a8..279780fe448 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme @@ -89,6 +89,18 @@ ReferencedContainer = "container:Firestore.xcodeproj"> + + + + + + Pipeline { - let paths = docs.map { $0.path } + let paths = docs.map { $0.path.hasPrefix("/") ? $0.path : "/" + $0.path } return Pipeline(stages: [DocumentsSource(paths: paths)], db: db) } public func documents(_ paths: [String]) -> Pipeline { - return Pipeline(stages: [DocumentsSource(paths: paths)], db: db) + let normalizedPaths = paths.map { $0.hasPrefix("/") ? $0 : "/" + $0 } + return Pipeline(stages: [DocumentsSource(paths: normalizedPaths)], db: db) } public func create(from query: Query) -> Pipeline { diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index 232a1d3076b..25ca1f09a6d 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -18,6 +18,11 @@ import XCTest import FirebaseFirestore final class PipelineTests: FSTIntegrationTestCase { + override func setUp() { + FSTIntegrationTestCase.switchToEnterpriseMode() + super.setUp() + } + func testCreatePipeline() async throws { let pipelineSource: PipelineSource = db.pipeline() diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 8b6fb9d130c..7bfdc8525a1 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -113,6 +113,11 @@ private let bookDocs: [String: [String: Any]] = [ @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class PipelineIntegrationTests: FSTIntegrationTestCase { + override func setUp() { + FSTIntegrationTestCase.switchToEnterpriseMode() + super.setUp() + } + func testCount() async throws { try await firestore().collection("foo").document("bar").setData(["foo": "bar", "x": 42]) let snapshot = try await firestore() diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index 524872fd3a2..afa943c8cb0 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -89,8 +89,8 @@ google_firestore_v1_Pipeline_Stage DocumentsSource::to_proto() const { for (size_t i = 0; i < documents_.size(); ++i) { result.args[i].which_value_type = - google_firestore_v1_Value_string_value_tag; - result.args[i].string_value = nanopb::MakeBytesArray(documents_[i]); + google_firestore_v1_Value_reference_value_tag; + result.args[i].reference_value = nanopb::MakeBytesArray(documents_[i]); } result.options_count = 0; From 65475b636751c3b57d79d16c0b817ca921f1a40e Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 15 May 2025 15:36:44 -0400 Subject: [PATCH 40/43] enable nightly build --- .github/workflows/firestore-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/firestore-nightly.yml b/.github/workflows/firestore-nightly.yml index 36beeadb4ac..86e48efea04 100644 --- a/.github/workflows/firestore-nightly.yml +++ b/.github/workflows/firestore-nightly.yml @@ -15,6 +15,8 @@ name: firestore_nightly on: + pull_request: + branches: [ "cheryllin/pplapi", "cheryllin/ppl" ] workflow_dispatch: concurrency: From f4b00ab0165ed51d0fe876fac20660f83331524a Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Thu, 15 May 2025 15:39:46 -0400 Subject: [PATCH 41/43] revert settings --- .../xcschemes/Firestore_Example_iOS.xcscheme | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme index 279780fe448..1df610c09a8 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme @@ -89,18 +89,6 @@ ReferencedContainer = "container:Firestore.xcodeproj"> - - - - - - Date: Fri, 23 May 2025 13:15:45 -0400 Subject: [PATCH 42/43] add tests and fix bugs --- Firestore/Source/API/FIRPipelineBridge.mm | 48 +- .../FirebaseFirestore/FIRPipelineBridge.h | 8 +- .../Source/SwiftAPI/Pipeline/Pipeline.swift | 8 +- .../SwiftAPI/Pipeline/PipelineSnapshot.swift | 8 +- .../SwiftAPI/Pipeline/PipelineSource.swift | 16 +- Firestore/Swift/Source/SwiftAPI/Stages.swift | 22 +- .../Tests/Integration/PipelineApiTests.swift | 14 +- .../Tests/Integration/PipelineTests.swift | 410 +++++++++++++++++- .../core/src/api/aggregate_expressions.cc | 2 +- Firestore/core/src/api/stages.cc | 4 +- Firestore/core/src/api/stages.h | 10 +- 11 files changed, 474 insertions(+), 76 deletions(-) diff --git a/Firestore/Source/API/FIRPipelineBridge.mm b/Firestore/Source/API/FIRPipelineBridge.mm index ac3091249e0..6e3a47f2511 100644 --- a/Firestore/Source/API/FIRPipelineBridge.mm +++ b/Firestore/Source/API/FIRPipelineBridge.mm @@ -20,6 +20,7 @@ #include +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" @@ -39,11 +40,13 @@ #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/comparison.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::AddFields; +using firebase::firestore::api::AddStage; using firebase::firestore::api::AggregateFunction; using firebase::firestore::api::AggregateStage; using firebase::firestore::api::CollectionGroupSource; @@ -57,7 +60,6 @@ using firebase::firestore::api::Field; using firebase::firestore::api::FindNearestStage; using firebase::firestore::api::FunctionExpr; -using firebase::firestore::api::GenericStage; using firebase::firestore::api::LimitStage; using firebase::firestore::api::MakeFIRTimestamp; using firebase::firestore::api::OffsetStage; @@ -73,6 +75,7 @@ using firebase::firestore::api::Where; using firebase::firestore::model::FieldPath; using firebase::firestore::nanopb::SharedMessage; +using firebase::firestore::util::ComparisonResult; using firebase::firestore::util::MakeCallback; using firebase::firestore::util::MakeNSString; using firebase::firestore::util::MakeString; @@ -80,6 +83,13 @@ NS_ASSUME_NONNULL_BEGIN +inline std::string EnsureLeadingSlash(const std::string &path) { + if (!path.empty() && path[0] == '/') { + return path; + } + return "/" + path; +} + @implementation FIRExprBridge @end @@ -216,10 +226,19 @@ @implementation FIRCollectionSourceStageBridge { std::shared_ptr collection_source; } -- (id)initWithPath:(NSString *)path { +- (id)initWithRef:(FIRCollectionReference *)ref firestore:(FIRFirestore *)db { self = [super init]; if (self) { - collection_source = std::make_shared(MakeString(path)); + if (ref.firestore.databaseID.CompareTo(db.databaseID) != ComparisonResult::Same) { + ThrowInvalidArgument( + "Invalid CollectionReference. The project ID (\"%s\") or the database (\"%s\") does not " + "match " + "the project ID (\"%s\") and database (\"%s\") of the target database of this Pipeline.", + ref.firestore.databaseID.project_id(), ref.firestore.databaseID.database_id(), + db.databaseID.project_id(), db.databaseID.project_id()); + } + collection_source = + std::make_shared(EnsureLeadingSlash(MakeString(ref.path))); } return self; } @@ -270,12 +289,21 @@ @implementation FIRDocumentsSourceStageBridge { std::shared_ptr cpp_document_source; } -- (id)initWithDocuments:(NSArray *)documents { +- (id)initWithDocuments:(NSArray *)documents firestore:(FIRFirestore *)db { self = [super init]; if (self) { std::vector cpp_documents; - for (NSString *doc in documents) { - cpp_documents.push_back(MakeString(doc)); + for (FIRDocumentReference *doc in documents) { + if (doc.firestore.databaseID.CompareTo(db.databaseID) != ComparisonResult::Same) { + ThrowInvalidArgument("Invalid DocumentReference. The project ID (\"%s\") or the database " + "(\"%s\") does not match " + "the project ID (\"%s\") and database (\"%s\") of the target database " + "of this Pipeline.", + doc.firestore.databaseID.project_id(), + doc.firestore.databaseID.database_id(), db.databaseID.project_id(), + db.databaseID.project_id()); + } + cpp_documents.push_back(EnsureLeadingSlash(MakeString(doc.path))); } cpp_document_source = std::make_shared(std::move(cpp_documents)); } @@ -754,12 +782,12 @@ - (id)initWithField:(FIRExprBridge *)field indexField:(NSString *_Nullable)index @end -@implementation FIRGenericStageBridge { +@implementation FIRAddStageBridge { NSString *_name; NSArray *_params; NSDictionary *_Nullable _options; Boolean isUserDataRead; - std::shared_ptr cpp_generic_stage; + std::shared_ptr cpp_generic_stage; } - (id)initWithName:(NSString *)name @@ -787,8 +815,8 @@ - (id)initWithName:(NSString *)name cpp_options[MakeString(key)] = [_options[key] cppExprWithReader:reader]; } } - cpp_generic_stage = std::make_shared(MakeString(_name), std::move(cpp_params), - std::move(cpp_options)); + cpp_generic_stage = std::make_shared(MakeString(_name), std::move(cpp_params), + std::move(cpp_options)); } isUserDataRead = YES; diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h index 7b8ebf80e9b..ed17953d76d 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h @@ -70,7 +70,7 @@ NS_SWIFT_SENDABLE NS_SWIFT_NAME(CollectionSourceStageBridge) @interface FIRCollectionSourceStageBridge : FIRStageBridge -- (id)initWithPath:(NSString *)path; +- (id)initWithRef:(FIRCollectionReference *)ref firestore:(FIRFirestore *)db; @end @@ -94,7 +94,7 @@ NS_SWIFT_SENDABLE NS_SWIFT_NAME(DocumentsSourceStageBridge) @interface FIRDocumentsSourceStageBridge : FIRStageBridge -- (id)initWithDocuments:(NSArray *)documents; +- (id)initWithDocuments:(NSArray *)documents firestore:(FIRFirestore *)db; @end @@ -195,8 +195,8 @@ NS_SWIFT_NAME(UnnestStageBridge) @end NS_SWIFT_SENDABLE -NS_SWIFT_NAME(GenericStageBridge) -@interface FIRGenericStageBridge : FIRStageBridge +NS_SWIFT_NAME(AddStageBridge) +@interface FIRAddStageBridge : FIRStageBridge - (id)initWithName:(NSString *)name params:(NSArray *)params options:(NSDictionary *_Nullable)options; diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 4e49c97301b..129ecb1df73 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -699,7 +699,7 @@ public struct Pipeline: @unchecked Sendable { /// ```swift /// // let pipeline: Pipeline = ... /// // Example: Assuming a hypothetical backend stage "customFilterV2". - /// let genericPipeline = pipeline.genericStage( + /// let genericPipeline = pipeline.addStage( /// name: "customFilterV2", /// params: [Field("userScore"), 80], // Ordered parameters. /// options: ["mode": "strict", "logLevel": 2] // Optional named parameters. @@ -712,10 +712,10 @@ public struct Pipeline: @unchecked Sendable { /// - params: An array of ordered, `Sendable` parameters for the stage. /// - options: Optional dictionary of named, `Sendable` parameters. /// - Returns: A new `Pipeline` object with this stage appended. - public func genericStage(name: String, params: [Sendable], - options: [String: Sendable]? = nil) -> Pipeline { + public func addStage(name: String, params: [Sendable], + options: [String: Sendable]? = nil) -> Pipeline { return Pipeline( - stages: stages + [GenericStage(name: name, params: params, options: options)], + stages: stages + [AddStage(name: name, params: params, options: options)], db: db ) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift index e25191b8ad2..a260cc55cee 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSnapshot.swift @@ -25,7 +25,7 @@ public struct PipelineSnapshot: Sendable { public let pipeline: Pipeline /// An array of all the results in the `PipelineSnapshot`. - let results_cache: [PipelineResult] + public let results: [PipelineResult] /// The time at which the pipeline producing this result was executed. public let executionTime: Timestamp @@ -36,10 +36,6 @@ public struct PipelineSnapshot: Sendable { self.bridge = bridge self.pipeline = pipeline executionTime = self.bridge.execution_time - results_cache = self.bridge.results.map { PipelineResult($0) } - } - - public func results() -> [PipelineResult] { - return results_cache + results = self.bridge.results.map { PipelineResult($0) } } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index da0b5b5b1b4..6a0026340a2 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -21,8 +21,12 @@ public struct PipelineSource: @unchecked Sendable { } public func collection(_ path: String) -> Pipeline { - let normalizedPath = path.hasPrefix("/") ? path : "/" + path - return Pipeline(stages: [CollectionSource(collection: normalizedPath)], db: db) + return Pipeline(stages: [CollectionSource(collection: db.collection(path), db: db)], db: db) + } + + public func collection(_ ref: CollectionReference) -> Pipeline { + let collectionStage = CollectionSource(collection: ref, db: db) + return Pipeline(stages: [collectionStage], db: db) } public func collectionGroup(_ collectionId: String) -> Pipeline { @@ -37,13 +41,13 @@ public struct PipelineSource: @unchecked Sendable { } public func documents(_ docs: [DocumentReference]) -> Pipeline { - let paths = docs.map { $0.path.hasPrefix("/") ? $0.path : "/" + $0.path } - return Pipeline(stages: [DocumentsSource(paths: paths)], db: db) + return Pipeline(stages: [DocumentsSource(docs: docs, db: db)], db: db) } public func documents(_ paths: [String]) -> Pipeline { - let normalizedPaths = paths.map { $0.hasPrefix("/") ? $0 : "/" + $0 } - return Pipeline(stages: [DocumentsSource(paths: normalizedPaths)], db: db) + let docs = paths.map { db.document($0) } + let documentsStage = DocumentsSource(docs: docs, db: db) + return Pipeline(stages: [documentsStage], db: db) } public func create(from query: Query) -> Pipeline { diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/SwiftAPI/Stages.swift index 65796af8471..fe01725a97a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Stages.swift +++ b/Firestore/Swift/Source/SwiftAPI/Stages.swift @@ -33,11 +33,13 @@ class CollectionSource: Stage { let name: String = "collection" let bridge: StageBridge - private var collection: String + private var collection: CollectionReference + private let db: Firestore - init(collection: String) { + init(collection: CollectionReference, db: Firestore) { self.collection = collection - bridge = CollectionSourceStageBridge(path: collection) + self.db = db + bridge = CollectionSourceStageBridge(ref: collection, firestore: db) } } @@ -70,12 +72,14 @@ class DatabaseSource: Stage { class DocumentsSource: Stage { let name: String = "documents" let bridge: StageBridge - private var references: [String] + private var docs: [DocumentReference] + private let db: Firestore // Initialize with an array of String paths - init(paths: [String]) { - references = paths - bridge = DocumentsSourceStageBridge(documents: paths) + init(docs: [DocumentReference], db: Firestore) { + self.docs = docs + self.db = db + bridge = DocumentsSourceStageBridge(documents: docs, firestore: db) } } @@ -337,7 +341,7 @@ class Unnest: Stage { } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -class GenericStage: Stage { +class AddStage: Stage { let name: String let bridge: StageBridge private var params: [Sendable] @@ -349,6 +353,6 @@ class GenericStage: Stage { self.options = options let bridgeParams = params.map { Helper.sendableToExpr($0).toBridge() } let bridgeOptions = options?.mapValues { Helper.sendableToExpr($0).toBridge() } - bridge = GenericStageBridge(name: name, params: bridgeParams, options: bridgeOptions) + bridge = AddStageBridge(name: name, params: bridgeParams, options: bridgeOptions) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index 25ca1f09a6d..2920860e5e1 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -263,22 +263,22 @@ final class PipelineTests: FSTIntegrationTestCase { // ... } } - func testGenericStage() async throws { + func testAddStage() async throws { // Assume we don't have a built-in "where" stage, the customer could still - // add this stage by calling genericStage, passing the name of the stage "where", + // add this stage by calling addStage, passing the name of the stage "where", // and providing positional argument values. _ = db.pipeline().collection("books") - .genericStage(name: "where", - params: [Field("published").lt(1900)]) + .addStage(name: "where", + params: [Field("published").lt(1900)]) .select("title", "author") // In cases where the stage also supports named argument values, then these can be // provided with a third argument that maps the argument name to value. // Note that these named arguments are always optional in the stage definition. _ = db.pipeline().collection("books") - .genericStage(name: "where", - params: [Field("published").lt(1900)], - options: ["someOptionalParamName": "the argument value for this param"]) + .addStage(name: "where", + params: [Field("published").lt(1900)], + options: ["someOptionalParamName": "the argument value for this param"]) .select("title", "author") } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 7bfdc8525a1..3e89c99ba9b 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -14,8 +14,10 @@ * limitations under the License. */ +import FirebaseCore // For FirebaseApp management import FirebaseFirestore import Foundation +import XCTest // For XCTFail, XCTAssertEqual etc. private let bookDocs: [String: [String: Any]] = [ "book1": [ @@ -24,9 +26,10 @@ private let bookDocs: [String: [String: Any]] = [ "genre": "Science Fiction", "published": 1979, "rating": 4.2, - "tags": ["comedy", "space", "adventure"], // Array literal - "awards": ["hugo": true, "nebula": false], // Dictionary literal - "nestedField": ["level.1": ["level.2": true]], // Nested dictionary literal + "tags": ["comedy", "space", "adventure"], + "awards": ["hugo": true, "nebula": false, "others": ["unknown": ["year": 1980]]], // Corrected + "nestedField": ["level.1": ["level.2": true]], + "embedding": VectorValue([10, 1, 1, 1, 1, 1, 1, 1, 1, 1]), ], "book2": [ "title": "Pride and Prejudice", @@ -36,6 +39,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.5, "tags": ["classic", "social commentary", "love"], "awards": ["none": true], + "embedding": VectorValue([1, 10, 1, 1, 1, 1, 1, 1, 1, 1]), // Added ], "book3": [ "title": "One Hundred Years of Solitude", @@ -45,6 +49,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.3, "tags": ["family", "history", "fantasy"], "awards": ["nobel": true, "nebula": false], + "embedding": VectorValue([1, 1, 10, 1, 1, 1, 1, 1, 1, 1]), ], "book4": [ "title": "The Lord of the Rings", @@ -54,6 +59,9 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.7, "tags": ["adventure", "magic", "epic"], "awards": ["hugo": false, "nebula": false], + "remarks": NSNull(), // Added + "cost": Double.nan, // Added + "embedding": VectorValue([1, 1, 1, 10, 1, 1, 1, 1, 1, 1]), // Added ], "book5": [ "title": "The Handmaid's Tale", @@ -63,6 +71,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.1, "tags": ["feminism", "totalitarianism", "resistance"], "awards": ["arthur c. clarke": true, "booker prize": false], + "embedding": VectorValue([1, 1, 1, 1, 10, 1, 1, 1, 1, 1]), // Added ], "book6": [ "title": "Crime and Punishment", @@ -72,6 +81,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.3, "tags": ["philosophy", "crime", "redemption"], "awards": ["none": true], + "embedding": VectorValue([1, 1, 1, 1, 1, 10, 1, 1, 1, 1]), // Added ], "book7": [ "title": "To Kill a Mockingbird", @@ -81,6 +91,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.2, "tags": ["racism", "injustice", "coming-of-age"], "awards": ["pulitzer": true], + "embedding": VectorValue([1, 1, 1, 1, 1, 1, 10, 1, 1, 1]), // Added ], "book8": [ "title": "1984", @@ -90,6 +101,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.2, "tags": ["surveillance", "totalitarianism", "propaganda"], "awards": ["prometheus": true], + "embedding": VectorValue([1, 1, 1, 1, 1, 1, 1, 10, 1, 1]), // Added ], "book9": [ "title": "The Great Gatsby", @@ -99,6 +111,7 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.0, "tags": ["wealth", "american dream", "love"], "awards": ["none": true], + "embedding": VectorValue([1, 1, 1, 1, 1, 1, 1, 1, 10, 1]), // Added ], "book10": [ "title": "Dune", @@ -108,9 +121,46 @@ private let bookDocs: [String: [String: Any]] = [ "rating": 4.6, "tags": ["politics", "desert", "ecology"], "awards": ["hugo": true, "nebula": true], + "embedding": VectorValue([1, 1, 1, 1, 1, 1, 1, 1, 1, 10]), // Added ], ] +func expectResults(_ snapshot: PipelineSnapshot, + expectedCount: Int, + file: StaticString = #file, + line: UInt = #line) { + XCTAssertEqual( + snapshot.results.count, + expectedCount, + "Snapshot results count mismatch", + file: file, + line: line + ) +} + +func expectResults(_ snapshot: PipelineSnapshot, + expectedIDs: [String], + file: StaticString = #file, + line: UInt = #line) { + let results = snapshot.results + XCTAssertEqual( + results.count, + expectedIDs.count, + "Snapshot document IDs count mismatch. Expected \(expectedIDs.count), got \(results.count). Actual IDs: \(results.map { $0.id })", + file: file, + line: line + ) + + let actualIDs = results.map { $0.id! }.sorted() + XCTAssertEqual( + actualIDs, + expectedIDs.sorted(), + "Snapshot document IDs mismatch. Expected (sorted): \(expectedIDs.sorted()), got (sorted): \(actualIDs)", + file: file, + line: line + ) +} + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class PipelineIntegrationTests: FSTIntegrationTestCase { override func setUp() { @@ -118,17 +168,6 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { super.setUp() } - func testCount() async throws { - try await firestore().collection("foo").document("bar").setData(["foo": "bar", "x": 42]) - let snapshot = try await firestore() - .pipeline() - .collection("/foo") - .where(Field("foo").eq(Constant("bar"))) - .execute() - - print(snapshot) - } - func testEmptyResults() async throws { let collRef = collectionRef( withDocuments: bookDocs @@ -141,7 +180,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .limit(0) .execute() - XCTAssertTrue(snapshot.results().isEmpty) + expectResults(snapshot, expectedCount: 0) } func testFullResults() async throws { @@ -155,14 +194,341 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .collection(collRef.path) .execute() - let results = snapshot.results() - XCTAssertEqual(results.count, 10) + // expectResults(snapshot, expectedCount: 10) // This is implicitly checked by expectedIDs + // version + expectResults( + snapshot, + expectedIDs: [ + "book1", "book10", "book2", "book3", "book4", + "book5", "book6", "book7", "book8", "book9", + ] + ) + } + + func testReturnsExecutionTime() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let startTime = Date().timeIntervalSince1970 + + let pipeline = db.pipeline().collection(collRef.path) + let snapshot = try await pipeline.execute() + + let endTime = Date().timeIntervalSince1970 + + XCTAssertEqual(snapshot.results.count, bookDocs.count, "Should fetch all documents") + + let executionTimeValue = snapshot.executionTime.dateValue().timeIntervalSince1970 + + XCTAssertGreaterThanOrEqual( + executionTimeValue, + startTime, + "Execution time should be at or after start time" + ) + XCTAssertLessThanOrEqual( + executionTimeValue, + endTime, + "Execution time should be at or before end time" + ) + XCTAssertGreaterThan(executionTimeValue, 0, "Execution time should be positive and not zero") + } + + func testReturnsExecutionTimeForEmptyQuery() async throws { + let collRef = + collectionRef(withDocuments: bookDocs) // Using bookDocs is fine, limit(0) makes it empty + let db = collRef.firestore + + let startTime = Date().timeIntervalSince1970 + + let pipeline = db.pipeline().collection(collRef.path).limit(0) + let snapshot = try await pipeline.execute() + + let endTime = Date().timeIntervalSince1970 + + expectResults(snapshot, expectedCount: 0) + + let executionTimeValue = snapshot.executionTime.dateValue().timeIntervalSince1970 + XCTAssertGreaterThanOrEqual( + executionTimeValue, + startTime, + "Execution time should be at or after start time" + ) + XCTAssertLessThanOrEqual( + executionTimeValue, + endTime, + "Execution time should be at or before end time" + ) + XCTAssertGreaterThan(executionTimeValue, 0, "Execution time should be positive and not zero") + } + + func testReturnsCreateAndUpdateTimeForEachDocument() async throws { + let beforeInitialExecute = Date().timeIntervalSince1970 + let collRef = collectionRef(withDocuments: bookDocs) + let afterInitialExecute = Date().timeIntervalSince1970 + + let db = collRef.firestore + let pipeline = db.pipeline().collection(collRef.path) + var snapshot = try await pipeline.execute() + + XCTAssertEqual( + snapshot.results.count, + bookDocs.count, + "Initial fetch should return all documents" + ) + for doc in snapshot.results { + XCTAssertNotNil( + doc.createTime, + "Document \(String(describing: doc.id)) should have createTime" + ) + XCTAssertNotNil( + doc.updateTime, + "Document \(String(describing: doc.id)) should have updateTime" + ) + if let createTime = doc.createTime, let updateTime = doc.updateTime { + let createTimestamp = createTime.dateValue().timeIntervalSince1970 + let updateTimestamp = updateTime.dateValue().timeIntervalSince1970 + + XCTAssertEqual(createTimestamp, + updateTimestamp, + "Initial createTime and updateTime should be equal for \(String(describing: doc.id))") + + XCTAssertGreaterThanOrEqual(createTimestamp, beforeInitialExecute, + "Initial createTime for \(String(describing: doc.id)) should be at or after start time") + XCTAssertLessThanOrEqual(createTimestamp, afterInitialExecute, + "Initial createTime for \(String(describing: doc.id)) should be positive and not zero") + } + } + + // Update documents + let batch = db.batch() + for doc in snapshot.results { + batch + .updateData( + ["newField": "value"], + forDocument: doc.ref! + ) + } + + try await batch.commit() + + snapshot = try await pipeline.execute() + XCTAssertEqual( + snapshot.results.count, + bookDocs.count, + "Fetch after update should return all documents" + ) + + for doc in snapshot.results { + XCTAssertNotNil( + doc.createTime, + "Document \(String(describing: doc.id)) should still have createTime after update" + ) + XCTAssertNotNil( + doc.updateTime, + "Document \(String(describing: doc.id)) should still have updateTime after update" + ) + if let createTime = doc.createTime, let updateTime = doc.updateTime { + let createTimestamp = createTime.dateValue().timeIntervalSince1970 + let updateTimestamp = updateTime.dateValue().timeIntervalSince1970 + + XCTAssertLessThan(createTimestamp, + updateTimestamp, + "updateTime (\(updateTimestamp)) should be after createTime (\(createTimestamp)) for \(String(describing: doc.id))") + } + } + } + + func testReturnsExecutionTimeForAggregateQuery() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let startTime = Date().timeIntervalSince1970 + + let pipeline = db.pipeline() + .collection(collRef.path) + .aggregate(Field("rating").avg().as("avgRating")) + let snapshot = try await pipeline.execute() + + let endTime = Date().timeIntervalSince1970 + + XCTAssertEqual(snapshot.results.count, 1, "Aggregate query should return a single result") - let actualIDs = Set(results.map { $0.id }) - let expectedIDs = Set([ - "book1", "book2", "book3", "book4", "book5", - "book6", "book7", "book8", "book9", "book10", + let executionTimeValue = snapshot.executionTime.dateValue().timeIntervalSince1970 + XCTAssertGreaterThanOrEqual(executionTimeValue, startTime) + XCTAssertLessThanOrEqual(executionTimeValue, endTime) + XCTAssertGreaterThan(executionTimeValue, 0, "Execution time should be positive") + } + + func testTimestampsAreNilForAggregateQueryResults() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .aggregate( + [Field("rating").avg().as("avgRating")], + groups: ["genre"] + ) // Make sure 'groupBy' and 'average' are correct + let snapshot = try await pipeline.execute() + + // There are 8 unique genres in bookDocs + XCTAssertEqual(snapshot.results.count, 8, "Should return one result per genre") + + for doc in snapshot.results { + XCTAssertNil( + doc.createTime, + "createTime should be nil for aggregate result (docID: \(String(describing: doc.id)), data: \(doc.data))" + ) + XCTAssertNil( + doc.updateTime, + "updateTime should be nil for aggregate result (docID: \(String(describing: doc.id)), data: \(doc.data))" + ) + } + } + + func testSupportsCollectionReferenceAsSource() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline().collection(collRef) + let snapshot = try await pipeline.execute() + + expectResults(snapshot, expectedCount: bookDocs.count) + } + + func testSupportsListOfDocumentReferencesAsSource() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let docRefs: [DocumentReference] = [ + collRef.document("book1"), + collRef.document("book2"), + collRef.document("book3"), + ] + let pipeline = db.pipeline().documents(docRefs) + let snapshot = try await pipeline.execute() + + expectResults(snapshot, expectedIDs: ["book1", "book2", "book3"]) + } + + func testSupportsListOfDocumentPathsAsSource() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let docPaths: [String] = [ + collRef.document("book1").path, + collRef.document("book2").path, + collRef.document("book3").path, + ] + let pipeline = db.pipeline().documents(docPaths) + let snapshot = try await pipeline.execute() + + expectResults(snapshot, expectedIDs: ["book1", "book2", "book3"]) + } + + func testRejectsCollectionReferenceFromAnotherDB() async throws { + let db1 = firestore() // Primary DB + + let db2 = Firestore.firestore(app: db1.app, database: "db2") + + let collRefDb2 = db2.collection("foo") + + XCTAssertTrue(FSTNSExceptionUtil.testForException({ + _ = db1.pipeline().collection(collRefDb2) + }, reasonContains: "Invalid CollectionReference")) + } + + func testRejectsDocumentReferenceFromAnotherDB() async throws { + let db1 = firestore() // Primary DB + + let db2 = Firestore.firestore(app: db1.app, database: "db2") + + let docRefDb2 = db2.collection("foo").document("bar") + + XCTAssertTrue(FSTNSExceptionUtil.testForException({ + _ = db1.pipeline().documents([docRefDb2]) + }, reasonContains: "Invalid DocumentReference")) + } + + func testSupportsCollectionGroupAsSource() async throws { + let db = firestore() + + let rootCollForTest = collectionRef() + + let randomSubCollectionId = String(UUID().uuidString.prefix(8)) + + // Create parent documents first to ensure they exist before creating subcollections. + let doc1Ref = rootCollForTest.document("book1").collection(randomSubCollectionId) + .document("translation") + try await doc1Ref.setData(["order": 1]) + + let doc2Ref = rootCollForTest.document("book2").collection(randomSubCollectionId) + .document("translation") + try await doc2Ref.setData(["order": 2]) + + let pipeline = db.pipeline() + .collectionGroup(randomSubCollectionId) + .sort(Field("order").ascending()) + + let snapshot = try await pipeline.execute() + + // Assert that only the two documents from the targeted subCollectionId are fetched, in the + // correct order. + expectResults(snapshot, expectedIDs: [doc1Ref.documentID, doc2Ref.documentID]) + } + + func testSupportsDatabaseAsSource() async throws { + let db = firestore() + let testRootCol = collectionRef() // Provides a unique root path for this test + + let randomIDValue = UUID().uuidString.prefix(8) + + // Document 1 + let collADocRef = testRootCol.document("docA") // Using specific IDs for clarity in debugging + try await collADocRef.setData(["order": 1, "randomId": randomIDValue, "name": "DocInCollA"]) + + // Document 2 + let collBDocRef = testRootCol.document("docB") // Using specific IDs for clarity in debugging + try await collBDocRef.setData(["order": 2, "randomId": randomIDValue, "name": "DocInCollB"]) + + // Document 3 (control, should not be fetched by the main query due to different randomId) + let collCDocRef = testRootCol.document("docC") + try await collCDocRef.setData([ + "order": 3, + "randomId": "\(UUID().uuidString)", + "name": "DocInCollC", ]) - XCTAssertEqual(actualIDs, expectedIDs) + + // Document 4 (control, no randomId, should not be fetched) + let collDDocRef = testRootCol.document("docD") + try await collDDocRef.setData(["order": 4, "name": "DocInCollDNoRandomId"]) + + // Document 5 (control, correct randomId but in a sub-sub-collection to test depth) + // This also helps ensure the database() query scans deeply. + let subSubCollDocRef = testRootCol.document("parentForSubSub").collection("subSubColl") + .document("docE") + try await subSubCollDocRef.setData([ + "order": 0, + "randomId": randomIDValue, + "name": "DocInSubSubColl", + ]) + + let pipeline = db.pipeline() + .database() // Source is the entire database + .where(Field("randomId").eq(randomIDValue)) + .sort(Ascending("order")) + let snapshot = try await pipeline.execute() + + // We expect 3 documents: docA, docB, and docE (from sub-sub-collection) + XCTAssertEqual( + snapshot.results.count, + 3, + "Should fetch the three documents with the correct randomId" + ) + // Order should be docE (order 0), docA (order 1), docB (order 2) + expectResults( + snapshot, + expectedIDs: [subSubCollDocRef.documentID, collADocRef.documentID, collBDocRef.documentID] + ) } } diff --git a/Firestore/core/src/api/aggregate_expressions.cc b/Firestore/core/src/api/aggregate_expressions.cc index fb58918833c..8509dfda59a 100644 --- a/Firestore/core/src/api/aggregate_expressions.cc +++ b/Firestore/core/src/api/aggregate_expressions.cc @@ -25,7 +25,7 @@ namespace api { google_firestore_v1_Value AggregateFunction::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_); result.function_value.args_count = static_cast(params_.size()); result.function_value.args = nanopb::MakeArray( diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index afa943c8cb0..d7a694999c6 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -393,7 +393,7 @@ google_firestore_v1_Pipeline_Stage Unnest::to_proto() const { return result; } -GenericStage::GenericStage( +AddStage::AddStage( std::string name, std::vector> params, std::unordered_map> options) @@ -402,7 +402,7 @@ GenericStage::GenericStage( options_(std::move(options)) { } -google_firestore_v1_Pipeline_Stage GenericStage::to_proto() const { +google_firestore_v1_Pipeline_Stage AddStage::to_proto() const { google_firestore_v1_Pipeline_Stage result; result.name = nanopb::MakeBytesArray(name_); diff --git a/Firestore/core/src/api/stages.h b/Firestore/core/src/api/stages.h index a3c7303ae92..2a7a29812bb 100644 --- a/Firestore/core/src/api/stages.h +++ b/Firestore/core/src/api/stages.h @@ -293,12 +293,12 @@ class Unnest : public Stage { absl::optional index_field_; }; -class GenericStage : public Stage { +class AddStage : public Stage { public: - GenericStage(std::string name, - std::vector> params, - std::unordered_map> options); - ~GenericStage() override = default; + AddStage(std::string name, + std::vector> params, + std::unordered_map> options); + ~AddStage() override = default; google_firestore_v1_Pipeline_Stage to_proto() const override; private: From 71820c764bc9425d92402e7b807f2d461762e264 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Fri, 23 May 2025 16:55:54 -0400 Subject: [PATCH 43/43] fix warning --- Firestore/core/src/api/stages.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/core/src/api/stages.cc b/Firestore/core/src/api/stages.cc index d7a694999c6..7182dd552ff 100644 --- a/Firestore/core/src/api/stages.cc +++ b/Firestore/core/src/api/stages.cc @@ -84,7 +84,7 @@ google_firestore_v1_Pipeline_Stage DocumentsSource::to_proto() const { result.name = nanopb::MakeBytesArray("documents"); - result.args_count = documents_.size(); + result.args_count = static_cast(documents_.size()); result.args = nanopb::MakeArray(result.args_count); for (size_t i = 0; i < documents_.size(); ++i) {