From b34463a3a774a248d44e9f8705e4ec3317b181ba Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 26 Nov 2025 13:48:36 -0500 Subject: [PATCH 1/6] add Expression.asBoolean --- .../xcschemes/Firestore_Example_iOS.xcscheme | 12 ++ .../Source/ExpressionImplementation.swift | 99 +++++---- .../Pipeline/Expressions/Expression.swift | 5 + .../BooleanExpression.swift | 201 +++++------------- .../Tests/Integration/PipelineApiTests.swift | 3 - .../Tests/Integration/PipelineTests.swift | 9 +- 6 files changed, 133 insertions(+), 196 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"> + + + + + + BooleanExpression { + if let boolExpr = self as? BooleanExpression { + return boolExpr + } + if let constant = self as? Constant { + return BooleanConstant(constant) + } + if let field = self as? Field { + return BooleanField(field) + } + if let funcExpr = self as? FunctionExpression { + return BooleanFunctionExpression(funcExpr) + } + // This should be unreachable if all expression types are handled. + fatalError("Unknown expression type \(Swift.type(of: self)) cannot be converted to BooleanExpression") + } + func `as`(_ name: String) -> AliasedExpression { return AliasedExpression(self, name) } @@ -474,38 +491,38 @@ public extension Expression { } func arrayContains(_ element: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains", args: [self, element]) + return BooleanFunctionExpression(functionName: "array_contains", args: [self, element]) } func arrayContains(_ element: Sendable) -> BooleanExpression { - return BooleanExpression( + return BooleanFunctionExpression( functionName: "array_contains", args: [self, Helper.sendableToExpr(element)] ) } func arrayContainsAll(_ values: [Expression]) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) + return BooleanFunctionExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) } func arrayContainsAll(_ values: [Sendable]) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) + return BooleanFunctionExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) } func arrayContainsAll(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains_all", args: [self, arrayExpression]) + return BooleanFunctionExpression(functionName: "array_contains_all", args: [self, arrayExpression]) } func arrayContainsAny(_ values: [Expression]) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) + return BooleanFunctionExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) } func arrayContainsAny(_ values: [Sendable]) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) + return BooleanFunctionExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) } func arrayContainsAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "array_contains_any", args: [self, arrayExpression]) + return BooleanFunctionExpression(functionName: "array_contains_any", args: [self, arrayExpression]) } func arrayLength() -> FunctionExpression { @@ -532,80 +549,80 @@ public extension Expression { } func greaterThan(_ other: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "greater_than", args: [self, other]) + return BooleanFunctionExpression(functionName: "greater_than", args: [self, other]) } func greaterThan(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression(functionName: "greater_than", args: [self, exprOther]) + return BooleanFunctionExpression(functionName: "greater_than", args: [self, exprOther]) } func greaterThanOrEqual(_ other: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "greater_than_or_equal", args: [self, other]) + return BooleanFunctionExpression(functionName: "greater_than_or_equal", args: [self, other]) } func greaterThanOrEqual(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression(functionName: "greater_than_or_equal", args: [self, exprOther]) + return BooleanFunctionExpression(functionName: "greater_than_or_equal", args: [self, exprOther]) } func lessThan(_ other: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "less_than", args: [self, other]) + return BooleanFunctionExpression(functionName: "less_than", args: [self, other]) } func lessThan(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression(functionName: "less_than", args: [self, exprOther]) + return BooleanFunctionExpression(functionName: "less_than", args: [self, exprOther]) } func lessThanOrEqual(_ other: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "less_than_or_equal", args: [self, other]) + return BooleanFunctionExpression(functionName: "less_than_or_equal", args: [self, other]) } func lessThanOrEqual(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression(functionName: "less_than_or_equal", args: [self, exprOther]) + return BooleanFunctionExpression(functionName: "less_than_or_equal", args: [self, exprOther]) } func equal(_ other: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "equal", args: [self, other]) + return BooleanFunctionExpression(functionName: "equal", args: [self, other]) } func equal(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression(functionName: "equal", args: [self, exprOther]) + return BooleanFunctionExpression(functionName: "equal", args: [self, exprOther]) } func notEqual(_ other: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "not_equal", args: [self, other]) + return BooleanFunctionExpression(functionName: "not_equal", args: [self, other]) } func notEqual(_ other: Sendable) -> BooleanExpression { - return BooleanExpression(functionName: "not_equal", args: [self, Helper.sendableToExpr(other)]) + return BooleanFunctionExpression(functionName: "not_equal", args: [self, Helper.sendableToExpr(other)]) } func equalAny(_ others: [Expression]) -> BooleanExpression { - return BooleanExpression(functionName: "equal_any", args: [self, Helper.array(others)]) + return BooleanFunctionExpression(functionName: "equal_any", args: [self, Helper.array(others)]) } func equalAny(_ others: [Sendable]) -> BooleanExpression { - return BooleanExpression(functionName: "equal_any", args: [self, Helper.array(others)]) + return BooleanFunctionExpression(functionName: "equal_any", args: [self, Helper.array(others)]) } func equalAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "equal_any", args: [self, arrayExpression]) + return BooleanFunctionExpression(functionName: "equal_any", args: [self, arrayExpression]) } func notEqualAny(_ others: [Expression]) -> BooleanExpression { - return BooleanExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) + return BooleanFunctionExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) } func notEqualAny(_ others: [Sendable]) -> BooleanExpression { - return BooleanExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) + return BooleanFunctionExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) } func notEqualAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "not_equal_any", args: [self, arrayExpression]) + return BooleanFunctionExpression(functionName: "not_equal_any", args: [self, arrayExpression]) } // MARK: Checks @@ -613,15 +630,15 @@ public extension Expression { // --- Added Type Check Operations --- func exists() -> BooleanExpression { - return BooleanExpression(functionName: "exists", args: [self]) + return BooleanFunctionExpression(functionName: "exists", args: [self]) } func isError() -> BooleanExpression { - return BooleanExpression(functionName: "is_error", args: [self]) + return BooleanFunctionExpression(functionName: "is_error", args: [self]) } func isAbsent() -> BooleanExpression { - return BooleanExpression(functionName: "is_absent", args: [self]) + return BooleanFunctionExpression(functionName: "is_absent", args: [self]) } // --- Added String Operations --- @@ -647,63 +664,63 @@ public extension Expression { } func like(_ pattern: String) -> BooleanExpression { - return BooleanExpression(functionName: "like", args: [self, Helper.sendableToExpr(pattern)]) + return BooleanFunctionExpression(functionName: "like", args: [self, Helper.sendableToExpr(pattern)]) } func like(_ pattern: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "like", args: [self, pattern]) + return BooleanFunctionExpression(functionName: "like", args: [self, pattern]) } func regexContains(_ pattern: String) -> BooleanExpression { - return BooleanExpression( + return BooleanFunctionExpression( functionName: "regex_contains", args: [self, Helper.sendableToExpr(pattern)] ) } func regexContains(_ pattern: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "regex_contains", args: [self, pattern]) + return BooleanFunctionExpression(functionName: "regex_contains", args: [self, pattern]) } func regexMatch(_ pattern: String) -> BooleanExpression { - return BooleanExpression( + return BooleanFunctionExpression( functionName: "regex_match", args: [self, Helper.sendableToExpr(pattern)] ) } func regexMatch(_ pattern: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "regex_match", args: [self, pattern]) + return BooleanFunctionExpression(functionName: "regex_match", args: [self, pattern]) } func stringContains(_ substring: String) -> BooleanExpression { - return BooleanExpression( + return BooleanFunctionExpression( functionName: "string_contains", args: [self, Helper.sendableToExpr(substring)] ) } func stringContains(_ expression: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "string_contains", args: [self, expression]) + return BooleanFunctionExpression(functionName: "string_contains", args: [self, expression]) } func startsWith(_ prefix: String) -> BooleanExpression { - return BooleanExpression( + return BooleanFunctionExpression( functionName: "starts_with", args: [self, Helper.sendableToExpr(prefix)] ) } func startsWith(_ prefix: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "starts_with", args: [self, prefix]) + return BooleanFunctionExpression(functionName: "starts_with", args: [self, prefix]) } func endsWith(_ suffix: String) -> BooleanExpression { - return BooleanExpression(functionName: "ends_with", args: [self, Helper.sendableToExpr(suffix)]) + return BooleanFunctionExpression(functionName: "ends_with", args: [self, Helper.sendableToExpr(suffix)]) } func endsWith(_ suffix: Expression) -> BooleanExpression { - return BooleanExpression(functionName: "ends_with", args: [self, suffix]) + return BooleanFunctionExpression(functionName: "ends_with", args: [self, suffix]) } func toLower() -> FunctionExpression { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift index 8b3367b299c..fb2475f3140 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift @@ -20,6 +20,11 @@ import Foundation public protocol Expression: Sendable { + /// Casts the expression to a `BooleanExpression`. + /// + /// - Returns: A `BooleanExpression` representing the same expression. + func asBoolean() -> BooleanExpression + /// Assigns an alias to this expression. /// /// Aliases are useful for renaming fields in the output of a stage or for giving meaningful diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift index 700d4aa0476..80ebe316774 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift @@ -14,163 +14,68 @@ import Foundation -/// -/// A `BooleanExpression` is a specialized `FunctionExpression` that evaluates to a boolean value. -/// -/// It is used to construct conditional logic within Firestore pipelines, such as in `where` -/// clauses or `ConditionalExpression`. `BooleanExpression` instances can be combined using standard -/// logical operators (`&&`, `||`, `!`, `^`) to create complex conditions. -/// -/// Example usage in a `where` clause: -/// ```swift -/// firestore.pipeline() -/// .collection("products") -/// .where( -/// Field("price").greaterThan(100) && -/// (Field("category").equal("electronics") || Field("on_sale").equal(true)) -/// ) -/// ``` -public class BooleanExpression: FunctionExpression, @unchecked Sendable { - override public init(functionName: String, args: [Expression]) { - super.init(functionName: functionName, args: args) - } +public protocol BooleanExpression: Expression {} - /// Creates an aggregation that counts the number of documents for which this boolean expression - /// evaluates to `true`. - /// - /// This is useful for counting documents that meet a specific condition without retrieving the - /// documents themselves. - /// - /// ```swift - /// // Count how many books were published after 1980 - /// let post1980Condition = Field("published").greaterThan(1980) - /// firestore.pipeline() - /// .collection("books") - /// .aggregate([ - /// post1980Condition.countIf().as("modernBooksCount") - /// ]) - /// ``` - /// - /// - Returns: An `AggregateFunction` that performs the conditional count. - public func countIf() -> AggregateFunction { - return AggregateFunction(functionName: "count_if", args: [self]) +internal struct BooleanFunctionExpression: BooleanExpression, BridgeWrapper { + internal let expr: FunctionExpression + public var bridge: ExprBridge { return expr.bridge } + + internal init(_ expr: FunctionExpression) { + self.expr = expr } - /// Creates a conditional expression that returns one of two specified expressions based on the - /// result of this boolean expression. - /// - /// This is equivalent to a ternary operator (`condition ? then : else`). - /// - /// ```swift - /// // Create a new field "status" based on the "rating" field. - /// // If rating > 4.5, status is "top_rated", otherwise "regular". - /// firestore.pipeline() - /// .collection("products") - /// .addFields([ - /// Field("rating").greaterThan(4.5) - /// .then(Constant("top_rated"), else: Constant("regular")) - /// .as("status") - /// ]) - /// ``` - /// - /// - Parameters: - /// - thenExpression: The `Expression` to evaluate if this boolean expression is `true`. - /// - elseExpression: The `Expression` to evaluate if this boolean expression is `false`. - /// - Returns: A new `FunctionExpression` representing the conditional logic. - public func then(_ thenExpression: Expression, - else elseExpression: Expression) -> FunctionExpression { - return FunctionExpression( - functionName: "conditional", - args: [self, thenExpression, elseExpression] - ) + internal init(functionName: String, args: [Expression]) { + expr = FunctionExpression(functionName: functionName, args: args) } +} + +internal struct BooleanConstant: BooleanExpression, BridgeWrapper { + private let constant: Constant + public var bridge: ExprBridge { return constant.bridge } - /// Combines two boolean expressions with a logical AND (`&&`). - /// - /// The resulting expression is `true` only if both the left-hand side (`lhs`) and the right-hand - /// side (`rhs`) are `true`. - /// - /// ```swift - /// // Find books in the "Fantasy" genre with a rating greater than 4.5 - /// firestore.pipeline() - /// .collection("books") - /// .where( - /// Field("genre").equal("Fantasy") && Field("rating").greaterThan(4.5) - /// ) - /// ``` - /// - /// - Parameters: - /// - lhs: The left-hand boolean expression. - /// - rhs: The right-hand boolean expression. - /// - Returns: A new `BooleanExpression` representing the logical AND. - public static func && (lhs: BooleanExpression, - rhs: @autoclosure () throws -> BooleanExpression) rethrows - -> BooleanExpression { - try BooleanExpression(functionName: "and", args: [lhs, rhs()]) + internal init(_ constant: Constant) { + self.constant = constant } +} - /// Combines two boolean expressions with a logical OR (`||`). - /// - /// The resulting expression is `true` if either the left-hand side (`lhs`) or the right-hand - /// side (`rhs`) is `true`. - /// - /// ```swift - /// // Find books that are either in the "Romance" genre or were published before 1900 - /// firestore.pipeline() - /// .collection("books") - /// .where( - /// Field("genre").equal("Romance") || Field("published").lessThan(1900) - /// ) - /// ``` - /// - /// - Parameters: - /// - lhs: The left-hand boolean expression. - /// - rhs: The right-hand boolean expression. - /// - Returns: A new `BooleanExpression` representing the logical OR. - public static func || (lhs: BooleanExpression, - rhs: @autoclosure () throws -> BooleanExpression) rethrows - -> BooleanExpression { - try BooleanExpression(functionName: "or", args: [lhs, rhs()]) +internal struct BooleanField: BooleanExpression, BridgeWrapper { + private let field: Field + public var bridge: ExprBridge { return field.bridge } + + internal init(_ field: Field) { + self.field = field } +} + +public func && (lhs: BooleanExpression, + rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { + return try BooleanFunctionExpression(functionName: "and", args: [lhs, rhs()]) +} + +public func || (lhs: BooleanExpression, + rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { + return try BooleanFunctionExpression(functionName: "or", args: [lhs, rhs()]) +} + +public func ^ (lhs: BooleanExpression, + rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { + return try BooleanFunctionExpression(functionName: "xor", args: [lhs, rhs()]) +} + +public prefix func ! (lhs: BooleanExpression) -> BooleanExpression { + return BooleanFunctionExpression(functionName: "not", args: [lhs]) +} - /// Combines two boolean expressions with a logical XOR (`^`). - /// - /// The resulting expression is `true` if the left-hand side (`lhs`) and the right-hand side - /// (`rhs`) have different boolean values. - /// - /// ```swift - /// // Find books that are in the "Dystopian" genre OR have a rating of 5.0, but not both. - /// firestore.pipeline() - /// .collection("books") - /// .where( - /// Field("genre").equal("Dystopian") ^ Field("rating").equal(5.0) - /// ) - /// ``` - /// - /// - Parameters: - /// - lhs: The left-hand boolean expression. - /// - rhs: The right-hand boolean expression. - /// - Returns: A new `BooleanExpression` representing the logical XOR. - public static func ^ (lhs: BooleanExpression, - rhs: @autoclosure () throws -> BooleanExpression) rethrows - -> BooleanExpression { - try BooleanExpression(functionName: "xor", args: [lhs, rhs()]) +public extension BooleanExpression { + func countIf() -> AggregateFunction { + return AggregateFunction(functionName: "count_if", args: [self]) } - /// Negates a boolean expression with a logical NOT (`!`). - /// - /// The resulting expression is `true` if the original expression is `false`, and vice versa. - /// - /// ```swift - /// // Find books that are NOT in the "Science Fiction" genre - /// firestore.pipeline() - /// .collection("books") - /// .where(!Field("genre").equal("Science Fiction")) - /// ``` - /// - /// - Parameter lhs: The boolean expression to negate. - /// - Returns: A new `BooleanExpression` representing the logical NOT. - public static prefix func ! (lhs: BooleanExpression) -> BooleanExpression { - return BooleanExpression(functionName: "not", args: [lhs]) + func then(_ thenExpression: Expression, + else elseExpression: Expression) -> FunctionExpression { + return FunctionExpression( + functionName: "conditional", + args: [self, thenExpression, elseExpression] + ) } -} +} \ No newline at end of file diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index 20096529f97..e4434b97830 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -405,9 +405,6 @@ final class PipelineApiTests: FSTIntegrationTestCase { // This is the same of the logicalMin('price', 0)', if it did not exist _ = FunctionExpression(functionName: "logicalMin", args: [Field("price"), Constant(0)]) - // Create a generic BooleanExpr for use where BooleanExpr is required - _ = BooleanExpression(functionName: "eq", args: [Field("price"), Constant(10)]) - // Create a generic AggregateFunction for use where AggregateFunction is required _ = AggregateFunction(functionName: "sum", args: [Field("price")]) } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 9eb545cb617..2e5ba23c332 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -2783,9 +2783,10 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let pipeline = db.pipeline() .collection(collRef.path) .where( - BooleanExpression(functionName: "and", args: [Field("rating").greaterThan(0), + FunctionExpression(functionName: "and", args: [Field("rating").greaterThan(0), Field("title").charLength().lessThan(5), - Field("tags").arrayContains("propaganda")]) + Field("tags") + .arrayContains("propaganda")]).asBoolean() ) .select(["title"]) @@ -2806,10 +2807,10 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let pipeline = db.pipeline() .collection(collRef.path) - .where(BooleanExpression( + .where(FunctionExpression( functionName: "array_contains_any", args: [Field("tags"), ArrayExpression(["politics"])] - )) + ).asBoolean()) .select([Field("title")]) let snapshot = try await pipeline.execute() From dfe49762ea192ee2eb0a5d15af77475134532221 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 26 Nov 2025 15:47:03 -0500 Subject: [PATCH 2/6] add tests and format code --- .../Source/ExpressionImplementation.swift | 59 +++++++++++++++---- .../BooleanExpression.swift | 24 ++++---- .../Tests/Integration/PipelineTests.swift | 35 ++++++++++- 3 files changed, 92 insertions(+), 26 deletions(-) diff --git a/Firestore/Swift/Source/ExpressionImplementation.swift b/Firestore/Swift/Source/ExpressionImplementation.swift index aea684fb159..83dd834f11a 100644 --- a/Firestore/Swift/Source/ExpressionImplementation.swift +++ b/Firestore/Swift/Source/ExpressionImplementation.swift @@ -390,7 +390,9 @@ public extension Expression { return BooleanFunctionExpression(funcExpr) } // This should be unreachable if all expression types are handled. - fatalError("Unknown expression type \(Swift.type(of: self)) cannot be converted to BooleanExpression") + fatalError( + "Unknown expression type \(Swift.type(of: self)) cannot be converted to BooleanExpression" + ) } func `as`(_ name: String) -> AliasedExpression { @@ -502,27 +504,45 @@ public extension Expression { } func arrayContainsAll(_ values: [Expression]) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) + return BooleanFunctionExpression( + functionName: "array_contains_all", + args: [self, Helper.array(values)] + ) } func arrayContainsAll(_ values: [Sendable]) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) + return BooleanFunctionExpression( + functionName: "array_contains_all", + args: [self, Helper.array(values)] + ) } func arrayContainsAll(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "array_contains_all", args: [self, arrayExpression]) + return BooleanFunctionExpression( + functionName: "array_contains_all", + args: [self, arrayExpression] + ) } func arrayContainsAny(_ values: [Expression]) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) + return BooleanFunctionExpression( + functionName: "array_contains_any", + args: [self, Helper.array(values)] + ) } func arrayContainsAny(_ values: [Sendable]) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) + return BooleanFunctionExpression( + functionName: "array_contains_any", + args: [self, Helper.array(values)] + ) } func arrayContainsAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "array_contains_any", args: [self, arrayExpression]) + return BooleanFunctionExpression( + functionName: "array_contains_any", + args: [self, arrayExpression] + ) } func arrayLength() -> FunctionExpression { @@ -598,7 +618,10 @@ public extension Expression { } func notEqual(_ other: Sendable) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "not_equal", args: [self, Helper.sendableToExpr(other)]) + return BooleanFunctionExpression( + functionName: "not_equal", + args: [self, Helper.sendableToExpr(other)] + ) } func equalAny(_ others: [Expression]) -> BooleanExpression { @@ -614,11 +637,17 @@ public extension Expression { } func notEqualAny(_ others: [Expression]) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) + return BooleanFunctionExpression( + functionName: "not_equal_any", + args: [self, Helper.array(others)] + ) } func notEqualAny(_ others: [Sendable]) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) + return BooleanFunctionExpression( + functionName: "not_equal_any", + args: [self, Helper.array(others)] + ) } func notEqualAny(_ arrayExpression: Expression) -> BooleanExpression { @@ -664,7 +693,10 @@ public extension Expression { } func like(_ pattern: String) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "like", args: [self, Helper.sendableToExpr(pattern)]) + return BooleanFunctionExpression( + functionName: "like", + args: [self, Helper.sendableToExpr(pattern)] + ) } func like(_ pattern: Expression) -> BooleanExpression { @@ -716,7 +748,10 @@ public extension Expression { } func endsWith(_ suffix: String) -> BooleanExpression { - return BooleanFunctionExpression(functionName: "ends_with", args: [self, Helper.sendableToExpr(suffix)]) + return BooleanFunctionExpression( + functionName: "ends_with", + args: [self, Helper.sendableToExpr(suffix)] + ) } func endsWith(_ suffix: Expression) -> BooleanExpression { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift index 80ebe316774..d7d7c1f8e9d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift @@ -16,49 +16,49 @@ import Foundation public protocol BooleanExpression: Expression {} -internal struct BooleanFunctionExpression: BooleanExpression, BridgeWrapper { - internal let expr: FunctionExpression +struct BooleanFunctionExpression: BooleanExpression, BridgeWrapper { + let expr: FunctionExpression public var bridge: ExprBridge { return expr.bridge } - internal init(_ expr: FunctionExpression) { + init(_ expr: FunctionExpression) { self.expr = expr } - internal init(functionName: String, args: [Expression]) { + init(functionName: String, args: [Expression]) { expr = FunctionExpression(functionName: functionName, args: args) } } -internal struct BooleanConstant: BooleanExpression, BridgeWrapper { +struct BooleanConstant: BooleanExpression, BridgeWrapper { private let constant: Constant public var bridge: ExprBridge { return constant.bridge } - internal init(_ constant: Constant) { + init(_ constant: Constant) { self.constant = constant } } -internal struct BooleanField: BooleanExpression, BridgeWrapper { +struct BooleanField: BooleanExpression, BridgeWrapper { private let field: Field public var bridge: ExprBridge { return field.bridge } - internal init(_ field: Field) { + init(_ field: Field) { self.field = field } } public func && (lhs: BooleanExpression, - rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { + rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { return try BooleanFunctionExpression(functionName: "and", args: [lhs, rhs()]) } public func || (lhs: BooleanExpression, - rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { + rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { return try BooleanFunctionExpression(functionName: "or", args: [lhs, rhs()]) } public func ^ (lhs: BooleanExpression, - rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { + rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { return try BooleanFunctionExpression(functionName: "xor", args: [lhs, rhs()]) } @@ -78,4 +78,4 @@ public extension BooleanExpression { args: [self, thenExpression, elseExpression] ) } -} \ No newline at end of file +} diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 2e5ba23c332..050fd173068 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -2784,9 +2784,9 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .collection(collRef.path) .where( FunctionExpression(functionName: "and", args: [Field("rating").greaterThan(0), - Field("title").charLength().lessThan(5), + Field("title").charLength().lessThan(5), Field("tags") - .arrayContains("propaganda")]).asBoolean() + .arrayContains("propaganda")]).asBoolean() ) .select(["title"]) @@ -3910,4 +3910,35 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { ] TestHelper.compare(snapshot: snapshot, expected: expectedResults, enforceOrder: true) } + + func testFieldAndConstantAsBooleanExpression() async throws { + let collRef = collectionRef(withDocuments: [ + "doc1": ["a": true], + "doc2": ["a": false], + "doc3": ["b": true], + ]) + let db = collRef.firestore + + var pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("a").asBoolean()) + var snapshot = try await pipeline.execute() + TestHelper.compare(snapshot: snapshot, expectedIDs: ["doc1"], enforceOrder: false) + + pipeline = db.pipeline() + .collection(collRef.path) + .where(Constant(true).asBoolean()) + snapshot = try await pipeline.execute() + TestHelper.compare( + snapshot: snapshot, + expectedIDs: ["doc1", "doc2", "doc3"], + enforceOrder: false + ) + + pipeline = db.pipeline() + .collection(collRef.path) + .where(Constant(false).asBoolean()) + snapshot = try await pipeline.execute() + TestHelper.compare(snapshot: snapshot, expectedCount: 0) + } } From 9da9f0e30d72650969578aab273960f80aafd1d9 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 26 Nov 2025 16:25:14 -0500 Subject: [PATCH 3/6] add documentation --- .../xcschemes/Firestore_Example_iOS.xcscheme | 12 -- .../BooleanExpression.swift | 121 ++++++++++++++++++ 2 files changed, 121 insertions(+), 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"> - - - - - - BooleanExpression) rethrows -> BooleanExpression { return try BooleanFunctionExpression(functionName: "and", args: [lhs, rhs()]) } +/// Combines two boolean expressions with a logical OR (`||`). +/// +/// The resulting expression is `true` if either the left-hand side (`lhs`) or the right-hand +/// side (`rhs`) is `true`. +/// +/// ```swift +/// // Find books that are either in the "Romance" genre or were published before 1900 +/// firestore.pipeline() +/// .collection("books") +/// .where( +/// Field("genre").equal("Romance") || Field("published").lessThan(1900) +/// ) +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand boolean expression. +/// - rhs: The right-hand boolean expression. +/// - Returns: A new `BooleanExpression` representing the logical OR. public func || (lhs: BooleanExpression, rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { return try BooleanFunctionExpression(functionName: "or", args: [lhs, rhs()]) } +/// Combines two boolean expressions with a logical XOR (`^`). +/// +/// The resulting expression is `true` if the left-hand side (`lhs`) and the right-hand side +/// (`rhs`) have different boolean values. +/// +/// ```swift +/// // Find books that are in the "Dystopian" genre OR have a rating of 5.0, but not both. +/// firestore.pipeline() +/// .collection("books") +/// .where( +/// Field("genre").equal("Dystopian") ^ Field("rating").equal(5.0) +/// ) +/// ``` +/// +/// - Parameters: +/// - lhs: The left-hand boolean expression. +/// - rhs: The right-hand boolean expression. +/// - Returns: A new `BooleanExpression` representing the logical XOR. public func ^ (lhs: BooleanExpression, rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { return try BooleanFunctionExpression(functionName: "xor", args: [lhs, rhs()]) } +/// Negates a boolean expression with a logical NOT (`!`). +/// +/// The resulting expression is `true` if the original expression is `false`, and vice versa. +/// +/// ```swift +/// // Find books that are NOT in the "Science Fiction" genre +/// firestore.pipeline() +/// .collection("books") +/// .where(!Field("genre").equal("Science Fiction")) +/// ``` +/// +/// - Parameter lhs: The boolean expression to negate. +/// - Returns: A new `BooleanExpression` representing the logical NOT. public prefix func ! (lhs: BooleanExpression) -> BooleanExpression { return BooleanFunctionExpression(functionName: "not", args: [lhs]) } public extension BooleanExpression { + /// Creates an aggregation that counts the number of documents for which this boolean expression + /// evaluates to `true`. + /// + /// This is useful for counting documents that meet a specific condition without retrieving the + /// documents themselves. + /// + /// ```swift + /// // Count how many books were published after 1980 + /// let post1980Condition = Field("published").greaterThan(1980) + /// firestore.pipeline() + /// .collection("books") + /// .aggregate([ + /// post1980Condition.countIf().as("modernBooksCount") + /// ]) + /// ``` + /// + /// - Returns: An `AggregateFunction` that performs the conditional count. func countIf() -> AggregateFunction { return AggregateFunction(functionName: "count_if", args: [self]) } + /// Creates a conditional expression that returns one of two specified expressions based on the + /// result of this boolean expression. + /// + /// This is equivalent to a ternary operator (`condition ? then : else`). + /// + /// ```swift + /// // Create a new field "status" based on the "rating" field. + /// // If rating > 4.5, status is "top_rated", otherwise "regular". + /// firestore.pipeline() + /// .collection("products") + /// .addFields([ + /// Field("rating").greaterThan(4.5) + /// .then(Constant("top_rated"), else: Constant("regular")) + /// .as("status") + /// ]) + /// ``` + /// + /// - Parameters: + /// - thenExpression: The `Expression` to evaluate if this boolean expression is `true`. + /// - elseExpression: The `Expression` to evaluate if this boolean expression is `false`. + /// - Returns: A new `FunctionExpression` representing the conditional logic. func then(_ thenExpression: Expression, else elseExpression: Expression) -> FunctionExpression { return FunctionExpression( From 01d743007ab8f51306107333dd5c92e076620b7c Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:29:46 -0500 Subject: [PATCH 4/6] Update Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../Expressions/FunctionExpressions/BooleanExpression.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift index 85d436d0e91..a3dcb23e03e 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift @@ -167,7 +167,7 @@ public extension BooleanExpression { /// ``` /// /// - Returns: An `AggregateFunction` that performs the conditional count. - func countIf() -> AggregateFunction { + public func countIf() -> AggregateFunction { return AggregateFunction(functionName: "count_if", args: [self]) } From 233d9d60aeef7ad302653ef88306f376da0e47d3 Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:29:54 -0500 Subject: [PATCH 5/6] Update Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../Expressions/FunctionExpressions/BooleanExpression.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift index a3dcb23e03e..8e796eae2cc 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift @@ -192,7 +192,7 @@ public extension BooleanExpression { /// - thenExpression: The `Expression` to evaluate if this boolean expression is `true`. /// - elseExpression: The `Expression` to evaluate if this boolean expression is `false`. /// - Returns: A new `FunctionExpression` representing the conditional logic. - func then(_ thenExpression: Expression, + public func then(_ thenExpression: Expression, else elseExpression: Expression) -> FunctionExpression { return FunctionExpression( functionName: "conditional", From dfc9c5febff391c830cebc64c045ccc110b3b932 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 26 Nov 2025 16:57:46 -0500 Subject: [PATCH 6/6] format --- .../Source/ExpressionImplementation.swift | 21 +++++++++---------- .../BooleanExpression.swift | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Firestore/Swift/Source/ExpressionImplementation.swift b/Firestore/Swift/Source/ExpressionImplementation.swift index 83dd834f11a..aecfb3c75b1 100644 --- a/Firestore/Swift/Source/ExpressionImplementation.swift +++ b/Firestore/Swift/Source/ExpressionImplementation.swift @@ -377,22 +377,21 @@ extension Expression { public extension Expression { func asBoolean() -> BooleanExpression { - if let boolExpr = self as? BooleanExpression { + switch self { + case let boolExpr as BooleanExpression: return boolExpr - } - if let constant = self as? Constant { + case let constant as Constant: return BooleanConstant(constant) - } - if let field = self as? Field { + case let field as Field: return BooleanField(field) - } - if let funcExpr = self as? FunctionExpression { + case let funcExpr as FunctionExpression: return BooleanFunctionExpression(funcExpr) + default: + // This should be unreachable if all expression types are handled. + fatalError( + "Unknown expression type \(Swift.type(of: self)) cannot be converted to BooleanExpression" + ) } - // This should be unreachable if all expression types are handled. - fatalError( - "Unknown expression type \(Swift.type(of: self)) cannot be converted to BooleanExpression" - ) } func `as`(_ name: String) -> AliasedExpression { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift index 8e796eae2cc..85d436d0e91 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift @@ -167,7 +167,7 @@ public extension BooleanExpression { /// ``` /// /// - Returns: An `AggregateFunction` that performs the conditional count. - public func countIf() -> AggregateFunction { + func countIf() -> AggregateFunction { return AggregateFunction(functionName: "count_if", args: [self]) } @@ -192,7 +192,7 @@ public extension BooleanExpression { /// - thenExpression: The `Expression` to evaluate if this boolean expression is `true`. /// - elseExpression: The `Expression` to evaluate if this boolean expression is `false`. /// - Returns: A new `FunctionExpression` representing the conditional logic. - public func then(_ thenExpression: Expression, + func then(_ thenExpression: Expression, else elseExpression: Expression) -> FunctionExpression { return FunctionExpression( functionName: "conditional",