diff --git a/Firestore/Swift/Source/ExpressionImplementation.swift b/Firestore/Swift/Source/ExpressionImplementation.swift index 112b23a9710..d1f6b4cfaba 100644 --- a/Firestore/Swift/Source/ExpressionImplementation.swift +++ b/Firestore/Swift/Source/ExpressionImplementation.swift @@ -30,7 +30,10 @@ extension Expression { /// - Parameter otherBits: The integer literal operand. /// - Returns: A new "FunctionExpression" representing the bitwise AND operation. func bitAnd(_ otherBits: Int) -> FunctionExpression { - return FunctionExpression("bit_and", [self, Helper.sendableToExpr(otherBits)]) + return FunctionExpression( + functionName: "bit_and", + args: [self, Helper.sendableToExpr(otherBits)] + ) } /// Creates an expression applying bitwise AND between this expression and a UInt8 literal (often @@ -44,7 +47,10 @@ extension Expression { /// - Parameter otherBits: The UInt8 literal operand. /// - Returns: A new "FunctionExpression" representing the bitwise AND operation. func bitAnd(_ otherBits: UInt8) -> FunctionExpression { - return FunctionExpression("bit_and", [self, Helper.sendableToExpr(otherBits)]) + return FunctionExpression( + functionName: "bit_and", + args: [self, Helper.sendableToExpr(otherBits)] + ) } /// Creates an expression applying bitwise AND between this expression and another expression. @@ -58,7 +64,7 @@ extension Expression { /// - Parameter bitsExpression: The other `Expr` operand. /// - Returns: A new "FunctionExpression" representing the bitwise AND operation. func bitAnd(_ bitsExpression: Expression) -> FunctionExpression { - return FunctionExpression("bit_and", [self, bitsExpression]) + return FunctionExpression(functionName: "bit_and", args: [self, bitsExpression]) } /// Creates an expression applying bitwise OR between this expression and an integer literal. @@ -74,7 +80,10 @@ extension Expression { /// - Parameter otherBits: The integer literal operand. /// - Returns: A new "FunctionExpression" representing the bitwise OR operation. func bitOr(_ otherBits: Int) -> FunctionExpression { - return FunctionExpression("bit_or", [self, Helper.sendableToExpr(otherBits)]) + return FunctionExpression( + functionName: "bit_or", + args: [self, Helper.sendableToExpr(otherBits)] + ) } /// Creates an expression applying bitwise OR between this expression and a UInt8 literal. @@ -87,7 +96,10 @@ extension Expression { /// - Parameter otherBits: The UInt8 literal operand. /// - Returns: A new "FunctionExpression" representing the bitwise OR operation. func bitOr(_ otherBits: UInt8) -> FunctionExpression { - return FunctionExpression("bit_or", [self, Helper.sendableToExpr(otherBits)]) + return FunctionExpression( + functionName: "bit_or", + args: [self, Helper.sendableToExpr(otherBits)] + ) } /// Creates an expression applying bitwise OR between this expression and another expression. @@ -101,7 +113,7 @@ extension Expression { /// - Parameter bitsExpression: The other `Expr` operand. /// - Returns: A new "FunctionExpression" representing the bitwise OR operation. func bitOr(_ bitsExpression: Expression) -> FunctionExpression { - return FunctionExpression("bit_or", [self, bitsExpression]) + return FunctionExpression(functionName: "bit_or", args: [self, bitsExpression]) } /// Creates an expression applying bitwise XOR between this expression and an integer literal. @@ -117,7 +129,10 @@ extension Expression { /// - Parameter otherBits: The integer literal operand. /// - Returns: A new "FunctionExpression" representing the bitwise XOR operation. func bitXor(_ otherBits: Int) -> FunctionExpression { - return FunctionExpression("bit_xor", [self, Helper.sendableToExpr(otherBits)]) + return FunctionExpression( + functionName: "bit_xor", + args: [self, Helper.sendableToExpr(otherBits)] + ) } /// Creates an expression applying bitwise XOR between this expression and a UInt8 literal. @@ -130,7 +145,10 @@ extension Expression { /// - Parameter otherBits: The UInt8 literal operand. /// - Returns: A new "FunctionExpression" representing the bitwise XOR operation. func bitXor(_ otherBits: UInt8) -> FunctionExpression { - return FunctionExpression("bit_xor", [self, Helper.sendableToExpr(otherBits)]) + return FunctionExpression( + functionName: "bit_xor", + args: [self, Helper.sendableToExpr(otherBits)] + ) } /// Creates an expression applying bitwise XOR between this expression and another expression. @@ -144,7 +162,7 @@ extension Expression { /// - Parameter bitsExpression: The other `Expr` operand. /// - Returns: A new "FunctionExpression" representing the bitwise XOR operation. func bitXor(_ bitsExpression: Expression) -> FunctionExpression { - return FunctionExpression("bit_xor", [self, bitsExpression]) + return FunctionExpression(functionName: "bit_xor", args: [self, bitsExpression]) } /// Creates an expression applying bitwise NOT to this expression. @@ -159,7 +177,7 @@ extension Expression { /// /// - Returns: A new "FunctionExpression" representing the bitwise NOT operation. func bitNot() -> FunctionExpression { - return FunctionExpression("bit_not", [self]) + return FunctionExpression(functionName: "bit_not", args: [self]) } /// Creates an expression applying bitwise left shift to this expression by a literal number of @@ -176,7 +194,10 @@ extension Expression { /// - Parameter y: The number of bits (Int literal) to shift by. /// - Returns: A new "FunctionExpression" representing the bitwise left shift operation. func bitLeftShift(_ y: Int) -> FunctionExpression { - return FunctionExpression("bit_left_shift", [self, Helper.sendableToExpr(y)]) + return FunctionExpression( + functionName: "bit_left_shift", + args: [self, Helper.sendableToExpr(y)] + ) } /// Creates an expression applying bitwise left shift to this expression by a number of bits @@ -191,7 +212,7 @@ extension Expression { /// - Parameter numberExpr: An `Expr` (evaluating to an Int) for the number of bits to shift by. /// - Returns: A new "FunctionExpression" representing the bitwise left shift operation. func bitLeftShift(_ numberExpression: Expression) -> FunctionExpression { - return FunctionExpression("bit_left_shift", [self, numberExpression]) + return FunctionExpression(functionName: "bit_left_shift", args: [self, numberExpression]) } /// Creates an expression applying bitwise right shift to this expression by a literal number of @@ -208,7 +229,10 @@ extension Expression { /// - Parameter y: The number of bits (Int literal) to shift by. /// - Returns: A new "FunctionExpression" representing the bitwise right shift operation. func bitRightShift(_ y: Int) -> FunctionExpression { - return FunctionExpression("bit_right_shift", [self, Helper.sendableToExpr(y)]) + return FunctionExpression( + functionName: "bit_right_shift", + args: [self, Helper.sendableToExpr(y)] + ) } /// Creates an expression applying bitwise right shift to this expression by a number of bits @@ -223,7 +247,7 @@ extension Expression { /// - Parameter numberExpr: An `Expr` (evaluating to an Int) for the number of bits to shift by. /// - Returns: A new "FunctionExpression" representing the bitwise right shift operation. func bitRightShift(_ numberExpression: Expression) -> FunctionExpression { - return FunctionExpression("bit_right_shift", [self, numberExpression]) + return FunctionExpression(functionName: "bit_right_shift", args: [self, numberExpression]) } /// Calculates the Manhattan (L1) distance between this vector expression and another vector @@ -240,7 +264,7 @@ extension Expression { /// - Parameter expression: The other vector as an `Expr` to compare against. /// - Returns: A new `FunctionExpression` representing the Manhattan distance. func manhattanDistance(_ expression: Expression) -> FunctionExpression { - return FunctionExpression("manhattan_distance", [self, expression]) + return FunctionExpression(functionName: "manhattan_distance", args: [self, expression]) } /// Calculates the Manhattan (L1) distance between this vector expression and another vector @@ -254,7 +278,10 @@ extension Expression { /// - Parameter vector: The other vector as a `VectorValue` to compare against. /// - Returns: A new `FunctionExpression` representing the Manhattan distance. func manhattanDistance(_ vector: VectorValue) -> FunctionExpression { - return FunctionExpression("manhattan_distance", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "manhattan_distance", + args: [self, Helper.sendableToExpr(vector)] + ) } /// Calculates the Manhattan (L1) distance between this vector expression and another vector @@ -269,7 +296,10 @@ extension Expression { /// - Parameter vector: The other vector as `[Double]` to compare against. /// - Returns: A new `FunctionExpression` representing the Manhattan distance. func manhattanDistance(_ vector: [Double]) -> FunctionExpression { - return FunctionExpression("manhattan_distance", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "manhattan_distance", + args: [self, Helper.sendableToExpr(vector)] + ) } /// Creates an expression that replaces the first occurrence of a literal substring within this @@ -286,8 +316,8 @@ extension Expression { /// - Returns: A new `FunctionExpr` representing the string with the first occurrence replaced. func replaceFirst(_ find: String, with replace: String) -> FunctionExpression { return FunctionExpression( - "replace_first", - [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] + functionName: "replace_first", + args: [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] ) } @@ -305,7 +335,7 @@ extension Expression { /// occurrence with. /// - Returns: A new `FunctionExpr` representing the string with the first occurrence replaced. func replaceFirst(_ find: Expression, with replace: Expression) -> FunctionExpression { - return FunctionExpression("replace_first", [self, find, replace]) + return FunctionExpression(functionName: "replace_first", args: [self, find, replace]) } /// Creates an expression that replaces all occurrences of a literal substring within this string @@ -322,8 +352,8 @@ extension Expression { /// - Returns: A new `FunctionExpr` representing the string with all occurrences replaced. func stringReplace(_ find: String, with replace: String) -> FunctionExpression { return FunctionExpression( - "string_replace", - [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] + functionName: "string_replace", + args: [self, Helper.sendableToExpr(find), Helper.sendableToExpr(replace)] ) } @@ -341,7 +371,7 @@ extension Expression { /// occurrences with. /// - Returns: A new `FunctionExpression` representing the string with all occurrences replaced. func stringReplace(_ find: Expression, with replace: Expression) -> FunctionExpression { - return FunctionExpression("string_replace", [self, find, replace]) + return FunctionExpression(functionName: "string_replace", args: [self, find, replace]) } // MARK: Equivalence Operations @@ -352,7 +382,7 @@ extension Expression { /// - Parameter other: The value to compare against. /// - Returns: A `BooleanExpr` that can be used in `where` clauses. func equivalent(_ other: Sendable) -> BooleanExpression { - return BooleanExpression("equivalent", [self, Helper.sendableToExpr(other)]) + return BooleanExpression(functionName: "equivalent", args: [self, Helper.sendableToExpr(other)]) } } @@ -364,215 +394,221 @@ public extension Expression { // MARK: Arithmetic Operators func abs() -> FunctionExpression { - return FunctionExpression("abs", [self]) + return FunctionExpression(functionName: "abs", args: [self]) } func ceil() -> FunctionExpression { - return FunctionExpression("ceil", [self]) + return FunctionExpression(functionName: "ceil", args: [self]) } func floor() -> FunctionExpression { - return FunctionExpression("floor", [self]) + return FunctionExpression(functionName: "floor", args: [self]) } func ln() -> FunctionExpression { - return FunctionExpression("ln", [self]) + return FunctionExpression(functionName: "ln", args: [self]) } func pow(_ exponent: Sendable) -> FunctionExpression { - return FunctionExpression("pow", [self, Helper.sendableToExpr(exponent)]) + return FunctionExpression(functionName: "pow", args: [self, Helper.sendableToExpr(exponent)]) } func pow(_ exponent: Expression) -> FunctionExpression { - return FunctionExpression("pow", [self, exponent]) + return FunctionExpression(functionName: "pow", args: [self, exponent]) } func round() -> FunctionExpression { - return FunctionExpression("round", [self]) + return FunctionExpression(functionName: "round", args: [self]) } func sqrt() -> FunctionExpression { - return FunctionExpression("sqrt", [self]) + return FunctionExpression(functionName: "sqrt", args: [self]) } func exp() -> FunctionExpression { - return FunctionExpression("exp", [self]) + return FunctionExpression(functionName: "exp", args: [self]) } func add(_ value: Expression) -> FunctionExpression { - return FunctionExpression("add", [self, value]) + return FunctionExpression(functionName: "add", args: [self, value]) } func add(_ value: Sendable) -> FunctionExpression { - return FunctionExpression("add", [self, Helper.sendableToExpr(value)]) + return FunctionExpression(functionName: "add", args: [self, Helper.sendableToExpr(value)]) } func subtract(_ other: Expression) -> FunctionExpression { - return FunctionExpression("subtract", [self, other]) + return FunctionExpression(functionName: "subtract", args: [self, other]) } func subtract(_ other: Sendable) -> FunctionExpression { - return FunctionExpression("subtract", [self, Helper.sendableToExpr(other)]) + return FunctionExpression(functionName: "subtract", args: [self, Helper.sendableToExpr(other)]) } func multiply(_ value: Expression) -> FunctionExpression { - return FunctionExpression("multiply", [self, value]) + return FunctionExpression(functionName: "multiply", args: [self, value]) } func multiply(_ value: Sendable) -> FunctionExpression { - return FunctionExpression("multiply", [self, Helper.sendableToExpr(value)]) + return FunctionExpression(functionName: "multiply", args: [self, Helper.sendableToExpr(value)]) } func divide(_ other: Expression) -> FunctionExpression { - return FunctionExpression("divide", [self, other]) + return FunctionExpression(functionName: "divide", args: [self, other]) } func divide(_ other: Sendable) -> FunctionExpression { - return FunctionExpression("divide", [self, Helper.sendableToExpr(other)]) + return FunctionExpression(functionName: "divide", args: [self, Helper.sendableToExpr(other)]) } func mod(_ other: Expression) -> FunctionExpression { - return FunctionExpression("mod", [self, other]) + return FunctionExpression(functionName: "mod", args: [self, other]) } func mod(_ other: Sendable) -> FunctionExpression { - return FunctionExpression("mod", [self, Helper.sendableToExpr(other)]) + return FunctionExpression(functionName: "mod", args: [self, Helper.sendableToExpr(other)]) } // MARK: Array Operations func arrayReverse() -> FunctionExpression { - return FunctionExpression("array_reverse", [self]) + return FunctionExpression(functionName: "array_reverse", args: [self]) } func arrayConcat(_ arrays: [Expression]) -> FunctionExpression { - return FunctionExpression("array_concat", [self] + arrays) + return FunctionExpression(functionName: "array_concat", args: [self] + arrays) } func arrayConcat(_ arrays: [[Sendable]]) -> FunctionExpression { let exprs = [self] + arrays.map { Helper.sendableToExpr($0) } - return FunctionExpression("array_concat", exprs) + return FunctionExpression(functionName: "array_concat", args: exprs) } func arrayContains(_ element: Expression) -> BooleanExpression { - return BooleanExpression("array_contains", [self, element]) + return BooleanExpression(functionName: "array_contains", args: [self, element]) } func arrayContains(_ element: Sendable) -> BooleanExpression { - return BooleanExpression("array_contains", [self, Helper.sendableToExpr(element)]) + return BooleanExpression( + functionName: "array_contains", + args: [self, Helper.sendableToExpr(element)] + ) } func arrayContainsAll(_ values: [Expression]) -> BooleanExpression { - return BooleanExpression("array_contains_all", [self, Helper.array(values)]) + return BooleanExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) } func arrayContainsAll(_ values: [Sendable]) -> BooleanExpression { - return BooleanExpression("array_contains_all", [self, Helper.array(values)]) + return BooleanExpression(functionName: "array_contains_all", args: [self, Helper.array(values)]) } func arrayContainsAll(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression("array_contains_all", [self, arrayExpression]) + return BooleanExpression(functionName: "array_contains_all", args: [self, arrayExpression]) } func arrayContainsAny(_ values: [Expression]) -> BooleanExpression { - return BooleanExpression("array_contains_any", [self, Helper.array(values)]) + return BooleanExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) } func arrayContainsAny(_ values: [Sendable]) -> BooleanExpression { - return BooleanExpression("array_contains_any", [self, Helper.array(values)]) + return BooleanExpression(functionName: "array_contains_any", args: [self, Helper.array(values)]) } func arrayContainsAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression("array_contains_any", [self, arrayExpression]) + return BooleanExpression(functionName: "array_contains_any", args: [self, arrayExpression]) } func arrayLength() -> FunctionExpression { - return FunctionExpression("array_length", [self]) + return FunctionExpression(functionName: "array_length", args: [self]) } func arrayGet(_ offset: Int) -> FunctionExpression { - return FunctionExpression("array_get", [self, Helper.sendableToExpr(offset)]) + return FunctionExpression( + functionName: "array_get", + args: [self, Helper.sendableToExpr(offset)] + ) } func arrayGet(_ offsetExpression: Expression) -> FunctionExpression { - return FunctionExpression("array_get", [self, offsetExpression]) + return FunctionExpression(functionName: "array_get", args: [self, offsetExpression]) } func greaterThan(_ other: Expression) -> BooleanExpression { - return BooleanExpression("greater_than", [self, other]) + return BooleanExpression(functionName: "greater_than", args: [self, other]) } func greaterThan(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression("greater_than", [self, exprOther]) + return BooleanExpression(functionName: "greater_than", args: [self, exprOther]) } func greaterThanOrEqual(_ other: Expression) -> BooleanExpression { - return BooleanExpression("greater_than_or_equal", [self, other]) + return BooleanExpression(functionName: "greater_than_or_equal", args: [self, other]) } func greaterThanOrEqual(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression("greater_than_or_equal", [self, exprOther]) + return BooleanExpression(functionName: "greater_than_or_equal", args: [self, exprOther]) } func lessThan(_ other: Expression) -> BooleanExpression { - return BooleanExpression("less_than", [self, other]) + return BooleanExpression(functionName: "less_than", args: [self, other]) } func lessThan(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression("less_than", [self, exprOther]) + return BooleanExpression(functionName: "less_than", args: [self, exprOther]) } func lessThanOrEqual(_ other: Expression) -> BooleanExpression { - return BooleanExpression("less_than_or_equal", [self, other]) + return BooleanExpression(functionName: "less_than_or_equal", args: [self, other]) } func lessThanOrEqual(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression("less_than_or_equal", [self, exprOther]) + return BooleanExpression(functionName: "less_than_or_equal", args: [self, exprOther]) } func equal(_ other: Expression) -> BooleanExpression { - return BooleanExpression("equal", [self, other]) + return BooleanExpression(functionName: "equal", args: [self, other]) } func equal(_ other: Sendable) -> BooleanExpression { let exprOther = Helper.sendableToExpr(other) - return BooleanExpression("equal", [self, exprOther]) + return BooleanExpression(functionName: "equal", args: [self, exprOther]) } func notEqual(_ other: Expression) -> BooleanExpression { - return BooleanExpression("not_equal", [self, other]) + return BooleanExpression(functionName: "not_equal", args: [self, other]) } func notEqual(_ other: Sendable) -> BooleanExpression { - return BooleanExpression("not_equal", [self, Helper.sendableToExpr(other)]) + return BooleanExpression(functionName: "not_equal", args: [self, Helper.sendableToExpr(other)]) } func equalAny(_ others: [Expression]) -> BooleanExpression { - return BooleanExpression("equal_any", [self, Helper.array(others)]) + return BooleanExpression(functionName: "equal_any", args: [self, Helper.array(others)]) } func equalAny(_ others: [Sendable]) -> BooleanExpression { - return BooleanExpression("equal_any", [self, Helper.array(others)]) + return BooleanExpression(functionName: "equal_any", args: [self, Helper.array(others)]) } func equalAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression("equal_any", [self, arrayExpression]) + return BooleanExpression(functionName: "equal_any", args: [self, arrayExpression]) } func notEqualAny(_ others: [Expression]) -> BooleanExpression { - return BooleanExpression("not_equal_any", [self, Helper.array(others)]) + return BooleanExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) } func notEqualAny(_ others: [Sendable]) -> BooleanExpression { - return BooleanExpression("not_equal_any", [self, Helper.array(others)]) + return BooleanExpression(functionName: "not_equal_any", args: [self, Helper.array(others)]) } func notEqualAny(_ arrayExpression: Expression) -> BooleanExpression { - return BooleanExpression("not_equal_any", [self, arrayExpression]) + return BooleanExpression(functionName: "not_equal_any", args: [self, arrayExpression]) } // MARK: Checks @@ -580,322 +616,395 @@ public extension Expression { // --- Added Type Check Operations --- func isNan() -> BooleanExpression { - return BooleanExpression("is_nan", [self]) + return BooleanExpression(functionName: "is_nan", args: [self]) } func isNil() -> BooleanExpression { - return BooleanExpression("is_null", [self]) + return BooleanExpression(functionName: "is_null", args: [self]) } func exists() -> BooleanExpression { - return BooleanExpression("exists", [self]) + return BooleanExpression(functionName: "exists", args: [self]) } func isError() -> BooleanExpression { - return BooleanExpression("is_error", [self]) + return BooleanExpression(functionName: "is_error", args: [self]) } func isAbsent() -> BooleanExpression { - return BooleanExpression("is_absent", [self]) + return BooleanExpression(functionName: "is_absent", args: [self]) } func isNotNil() -> BooleanExpression { - return BooleanExpression("is_not_null", [self]) + return BooleanExpression(functionName: "is_not_null", args: [self]) } func isNotNan() -> BooleanExpression { - return BooleanExpression("is_not_nan", [self]) + return BooleanExpression(functionName: "is_not_nan", args: [self]) } // --- Added String Operations --- func join(delimiter: String) -> FunctionExpression { - return FunctionExpression("join", [self, Constant(delimiter)]) + return FunctionExpression(functionName: "join", args: [self, Constant(delimiter)]) } func length() -> FunctionExpression { - return FunctionExpression("length", [self]) + return FunctionExpression(functionName: "length", args: [self]) } func charLength() -> FunctionExpression { - return FunctionExpression("char_length", [self]) + return FunctionExpression(functionName: "char_length", args: [self]) } func like(_ pattern: String) -> BooleanExpression { - return BooleanExpression("like", [self, Helper.sendableToExpr(pattern)]) + return BooleanExpression(functionName: "like", args: [self, Helper.sendableToExpr(pattern)]) } func like(_ pattern: Expression) -> BooleanExpression { - return BooleanExpression("like", [self, pattern]) + return BooleanExpression(functionName: "like", args: [self, pattern]) } func regexContains(_ pattern: String) -> BooleanExpression { - return BooleanExpression("regex_contains", [self, Helper.sendableToExpr(pattern)]) + return BooleanExpression( + functionName: "regex_contains", + args: [self, Helper.sendableToExpr(pattern)] + ) } func regexContains(_ pattern: Expression) -> BooleanExpression { - return BooleanExpression("regex_contains", [self, pattern]) + return BooleanExpression(functionName: "regex_contains", args: [self, pattern]) } func regexMatch(_ pattern: String) -> BooleanExpression { - return BooleanExpression("regex_match", [self, Helper.sendableToExpr(pattern)]) + return BooleanExpression( + functionName: "regex_match", + args: [self, Helper.sendableToExpr(pattern)] + ) } func regexMatch(_ pattern: Expression) -> BooleanExpression { - return BooleanExpression("regex_match", [self, pattern]) + return BooleanExpression(functionName: "regex_match", args: [self, pattern]) } func stringContains(_ substring: String) -> BooleanExpression { - return BooleanExpression("string_contains", [self, Helper.sendableToExpr(substring)]) + return BooleanExpression( + functionName: "string_contains", + args: [self, Helper.sendableToExpr(substring)] + ) } func stringContains(_ expression: Expression) -> BooleanExpression { - return BooleanExpression("string_contains", [self, expression]) + return BooleanExpression(functionName: "string_contains", args: [self, expression]) } func startsWith(_ prefix: String) -> BooleanExpression { - return BooleanExpression("starts_with", [self, Helper.sendableToExpr(prefix)]) + return BooleanExpression( + functionName: "starts_with", + args: [self, Helper.sendableToExpr(prefix)] + ) } func startsWith(_ prefix: Expression) -> BooleanExpression { - return BooleanExpression("starts_with", [self, prefix]) + return BooleanExpression(functionName: "starts_with", args: [self, prefix]) } func endsWith(_ suffix: String) -> BooleanExpression { - return BooleanExpression("ends_with", [self, Helper.sendableToExpr(suffix)]) + return BooleanExpression(functionName: "ends_with", args: [self, Helper.sendableToExpr(suffix)]) } func endsWith(_ suffix: Expression) -> BooleanExpression { - return BooleanExpression("ends_with", [self, suffix]) + return BooleanExpression(functionName: "ends_with", args: [self, suffix]) } func toLower() -> FunctionExpression { - return FunctionExpression("to_lower", [self]) + return FunctionExpression(functionName: "to_lower", args: [self]) } func toUpper() -> FunctionExpression { - return FunctionExpression("to_upper", [self]) + return FunctionExpression(functionName: "to_upper", args: [self]) } func trim() -> FunctionExpression { - return FunctionExpression("trim", [self]) + return FunctionExpression(functionName: "trim", args: [self]) } func stringConcat(_ strings: [Expression]) -> FunctionExpression { - return FunctionExpression("string_concat", [self] + strings) + return FunctionExpression(functionName: "string_concat", args: [self] + strings) } func stringConcat(_ strings: [Sendable]) -> FunctionExpression { let exprs = [self] + strings.map { Helper.sendableToExpr($0) } - return FunctionExpression("string_concat", exprs) + return FunctionExpression(functionName: "string_concat", args: exprs) } func reverse() -> FunctionExpression { - return FunctionExpression("reverse", [self]) + return FunctionExpression(functionName: "reverse", args: [self]) } func stringReverse() -> FunctionExpression { - return FunctionExpression("string_reverse", [self]) + return FunctionExpression(functionName: "string_reverse", args: [self]) } func byteLength() -> FunctionExpression { - return FunctionExpression("byte_length", [self]) + return FunctionExpression(functionName: "byte_length", args: [self]) } func substring(position: Int, length: Int? = nil) -> FunctionExpression { let positionExpr = Helper.sendableToExpr(position) if let length = length { - return FunctionExpression("substring", [self, positionExpr, Helper.sendableToExpr(length)]) + return FunctionExpression( + functionName: "substring", + args: [self, positionExpr, Helper.sendableToExpr(length)] + ) } else { - return FunctionExpression("substring", [self, positionExpr]) + return FunctionExpression(functionName: "substring", args: [self, positionExpr]) } } func substring(position: Expression, length: Expression? = nil) -> FunctionExpression { if let length = length { - return FunctionExpression("substring", [self, position, length]) + return FunctionExpression(functionName: "substring", args: [self, position, length]) } else { - return FunctionExpression("substring", [self, position]) + return FunctionExpression(functionName: "substring", args: [self, position]) } } // --- Added Map Operations --- func mapGet(_ subfield: String) -> FunctionExpression { - return FunctionExpression("map_get", [self, Constant(subfield)]) + return FunctionExpression(functionName: "map_get", args: [self, Constant(subfield)]) } func mapRemove(_ key: String) -> FunctionExpression { - return FunctionExpression("map_remove", [self, Helper.sendableToExpr(key)]) + return FunctionExpression(functionName: "map_remove", args: [self, Helper.sendableToExpr(key)]) } func mapRemove(_ keyExpression: Expression) -> FunctionExpression { - return FunctionExpression("map_remove", [self, keyExpression]) + return FunctionExpression(functionName: "map_remove", args: [self, keyExpression]) } func mapMerge(_ maps: [[String: Sendable]]) -> FunctionExpression { let mapExprs = maps.map { Helper.sendableToExpr($0) } - return FunctionExpression("map_merge", [self] + mapExprs) + return FunctionExpression(functionName: "map_merge", args: [self] + mapExprs) } func mapMerge(_ maps: [Expression]) -> FunctionExpression { - return FunctionExpression("map_merge", [self] + maps) + return FunctionExpression(functionName: "map_merge", args: [self] + maps) + } + + func mapSet(key: Expression, value: Sendable) -> FunctionExpression { + return FunctionExpression( + functionName: "map_set", + args: [self, key, Helper.sendableToExpr(value)] + ) + } + + func mapSet(key: String, value: Sendable) -> FunctionExpression { + return FunctionExpression( + functionName: "map_set", + args: [self, Helper.sendableToExpr(key), Helper.sendableToExpr(value)] + ) } // --- Added Aggregate Operations (on Expr) --- func countDistinct() -> AggregateFunction { - return AggregateFunction("count_distinct", [self]) + return AggregateFunction(functionName: "count_distinct", args: [self]) } func count() -> AggregateFunction { - return AggregateFunction("count", [self]) + return AggregateFunction(functionName: "count", args: [self]) } func sum() -> AggregateFunction { - return AggregateFunction("sum", [self]) + return AggregateFunction(functionName: "sum", args: [self]) } func average() -> AggregateFunction { - return AggregateFunction("average", [self]) + return AggregateFunction(functionName: "average", args: [self]) } func minimum() -> AggregateFunction { - return AggregateFunction("minimum", [self]) + return AggregateFunction(functionName: "minimum", args: [self]) } func maximum() -> AggregateFunction { - return AggregateFunction("maximum", [self]) + return AggregateFunction(functionName: "maximum", args: [self]) } // MARK: Logical min/max func logicalMaximum(_ expressions: [Expression]) -> FunctionExpression { - return FunctionExpression("maximum", [self] + expressions) + return FunctionExpression(functionName: "maximum", args: [self] + expressions) } func logicalMaximum(_ values: [Sendable]) -> FunctionExpression { let exprs = [self] + values.map { Helper.sendableToExpr($0) } - return FunctionExpression("maximum", exprs) + return FunctionExpression(functionName: "maximum", args: exprs) } func logicalMinimum(_ expressions: [Expression]) -> FunctionExpression { - return FunctionExpression("minimum", [self] + expressions) + return FunctionExpression(functionName: "minimum", args: [self] + expressions) } func logicalMinimum(_ values: [Sendable]) -> FunctionExpression { let exprs = [self] + values.map { Helper.sendableToExpr($0) } - return FunctionExpression("minimum", exprs) + return FunctionExpression(functionName: "minimum", args: exprs) } // MARK: Vector Operations func vectorLength() -> FunctionExpression { - return FunctionExpression("vector_length", [self]) + return FunctionExpression(functionName: "vector_length", args: [self]) } func cosineDistance(_ expression: Expression) -> FunctionExpression { - return FunctionExpression("cosine_distance", [self, expression]) + return FunctionExpression(functionName: "cosine_distance", args: [self, expression]) } func cosineDistance(_ vector: VectorValue) -> FunctionExpression { - return FunctionExpression("cosine_distance", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "cosine_distance", + args: [self, Helper.sendableToExpr(vector)] + ) } func cosineDistance(_ vector: [Double]) -> FunctionExpression { - return FunctionExpression("cosine_distance", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "cosine_distance", + args: [self, Helper.sendableToExpr(vector)] + ) } func dotProduct(_ expression: Expression) -> FunctionExpression { - return FunctionExpression("dot_product", [self, expression]) + return FunctionExpression(functionName: "dot_product", args: [self, expression]) } func dotProduct(_ vector: VectorValue) -> FunctionExpression { - return FunctionExpression("dot_product", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "dot_product", + args: [self, Helper.sendableToExpr(vector)] + ) } func dotProduct(_ vector: [Double]) -> FunctionExpression { - return FunctionExpression("dot_product", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "dot_product", + args: [self, Helper.sendableToExpr(vector)] + ) } func euclideanDistance(_ expression: Expression) -> FunctionExpression { - return FunctionExpression("euclidean_distance", [self, expression]) + return FunctionExpression(functionName: "euclidean_distance", args: [self, expression]) } func euclideanDistance(_ vector: VectorValue) -> FunctionExpression { - return FunctionExpression("euclidean_distance", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "euclidean_distance", + args: [self, Helper.sendableToExpr(vector)] + ) } func euclideanDistance(_ vector: [Double]) -> FunctionExpression { - return FunctionExpression("euclidean_distance", [self, Helper.sendableToExpr(vector)]) + return FunctionExpression( + functionName: "euclidean_distance", + args: [self, Helper.sendableToExpr(vector)] + ) } // MARK: Timestamp operations func unixMicrosToTimestamp() -> FunctionExpression { - return FunctionExpression("unix_micros_to_timestamp", [self]) + return FunctionExpression(functionName: "unix_micros_to_timestamp", args: [self]) } func timestampToUnixMicros() -> FunctionExpression { - return FunctionExpression("timestamp_to_unix_micros", [self]) + return FunctionExpression(functionName: "timestamp_to_unix_micros", args: [self]) } func unixMillisToTimestamp() -> FunctionExpression { - return FunctionExpression("unix_millis_to_timestamp", [self]) + return FunctionExpression(functionName: "unix_millis_to_timestamp", args: [self]) } func timestampToUnixMillis() -> FunctionExpression { - return FunctionExpression("timestamp_to_unix_millis", [self]) + return FunctionExpression(functionName: "timestamp_to_unix_millis", args: [self]) } func unixSecondsToTimestamp() -> FunctionExpression { - return FunctionExpression("unix_seconds_to_timestamp", [self]) + return FunctionExpression(functionName: "unix_seconds_to_timestamp", args: [self]) } func timestampToUnixSeconds() -> FunctionExpression { - return FunctionExpression("timestamp_to_unix_seconds", [self]) + return FunctionExpression(functionName: "timestamp_to_unix_seconds", args: [self]) } - func timestampAdd(amount: Expression, unit: Expression) -> FunctionExpression { - return FunctionExpression("timestamp_add", [self, unit, amount]) + func timestampTruncate(granularity: TimeUnit) -> FunctionExpression { + return FunctionExpression( + functionName: "timestamp_trunc", + args: [self, Helper.sendableToExpr(granularity.rawValue)] + ) + } + + func timestampTruncate(granularity: Sendable) -> FunctionExpression { + return FunctionExpression( + functionName: "timestamp_trunc", + args: [self, Helper.sendableToExpr(granularity)] + ) } func timestampAdd(_ amount: Int, _ unit: TimeUnit) -> FunctionExpression { return FunctionExpression( - "timestamp_add", - [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + functionName: "timestamp_add", + args: [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] ) } - func timestampSubtract(amount: Expression, unit: Expression) -> FunctionExpression { - return FunctionExpression("timestamp_subtract", [self, unit, amount]) + func timestampAdd(amount: Expression, unit: Sendable) -> FunctionExpression { + return FunctionExpression( + functionName: "timestamp_add", + args: [self, Helper.sendableToExpr(unit), amount] + ) } func timestampSubtract(_ amount: Int, _ unit: TimeUnit) -> FunctionExpression { return FunctionExpression( - "timestamp_subtract", - [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + functionName: "timestamp_subtract", + args: [self, Helper.sendableToExpr(unit), Helper.sendableToExpr(amount)] + ) + } + + func timestampSubtract(amount: Expression, unit: Sendable) -> FunctionExpression { + return FunctionExpression( + functionName: "timestamp_subtract", + args: [self, Helper.sendableToExpr(unit), amount] ) } func documentId() -> FunctionExpression { - return FunctionExpression("document_id", [self]) + return FunctionExpression(functionName: "document_id", args: [self]) } func collectionId() -> FunctionExpression { - return FunctionExpression("collection_id", [self]) + return FunctionExpression(functionName: "collection_id", args: [self]) } func ifError(_ catchExpression: Expression) -> FunctionExpression { - return FunctionExpression("if_error", [self, catchExpression]) + return FunctionExpression(functionName: "if_error", args: [self, catchExpression]) } func ifError(_ catchValue: Sendable) -> FunctionExpression { - return FunctionExpression("if_error", [self, Helper.sendableToExpr(catchValue)]) + return FunctionExpression( + functionName: "if_error", + args: [self, Helper.sendableToExpr(catchValue)] + ) } func ifAbsent(_ defaultValue: Sendable) -> FunctionExpression { - return FunctionExpression("if_absent", [self, Helper.sendableToExpr(defaultValue)]) + return FunctionExpression( + functionName: "if_absent", + args: [self, Helper.sendableToExpr(defaultValue)] + ) } // MARK: Sorting @@ -910,6 +1019,6 @@ public extension Expression { func concat(_ values: [Sendable]) -> FunctionExpression { let exprs = [self] + values.map { Helper.sendableToExpr($0) } - return FunctionExpression("concat", exprs) + return FunctionExpression(functionName: "concat", args: exprs) } } diff --git a/Firestore/Swift/Source/Helper/PipelineHelper.swift b/Firestore/Swift/Source/Helper/PipelineHelper.swift index b5b38e8dbfe..197a5c530cb 100644 --- a/Firestore/Swift/Source/Helper/PipelineHelper.swift +++ b/Firestore/Swift/Source/Helper/PipelineHelper.swift @@ -47,14 +47,14 @@ enum Helper { result.append(Constant(key)) result.append(sendableToExpr(value)) } - return FunctionExpression("map", result) + return FunctionExpression(functionName: "map", args: result) } static func array(_ elements: [Sendable?]) -> FunctionExpression { let transformedElements = elements.map { element in sendableToExpr(element) } - return FunctionExpression("array", transformedElements) + return FunctionExpression(functionName: "array", args: transformedElements) } // This function is used to convert Swift type into Objective-C type. diff --git a/Firestore/Swift/Source/SwiftAPI/Stages.swift b/Firestore/Swift/Source/Stages.swift similarity index 100% rename from Firestore/Swift/Source/SwiftAPI/Stages.swift rename to Firestore/Swift/Source/Stages.swift diff --git a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift index d270c316f62..797874a6e55 100644 --- a/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Firestore+Pipeline.swift @@ -22,6 +22,32 @@ import Foundation @objc public extension Firestore { + /// Creates a `PipelineSource` that can be used to build and execute a pipeline of operations on + /// the Firestore database. + /// + /// A pipeline is a sequence of stages that are executed in order. Each stage can perform an + /// operation on the data, such as filtering, sorting, or transforming it. + /// + /// Example usage: + /// ```swift + /// let db = Firestore.firestore() + /// let pipeline = db.pipeline() + /// .collection("books") + /// .where(Field("rating").isGreaterThan(4.5)) + /// .sort([Field("rating").descending()]) + /// .limit(2) + /// + /// do { + /// let snapshot = try await pipeline.execute() + /// for doc in snapshot.results { + /// print(doc.data()) + /// } + /// } catch { + /// print("Error executing pipeline: \(error)") + /// } + /// ``` + /// + /// - Returns: A `PipelineSource` that can be used to build and execute a pipeline. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @nonobjc func pipeline() -> PipelineSource { return PipelineSource(db: self) { stages, db in @@ -29,6 +55,11 @@ import Foundation } } + /// Creates a `RealtimePipelineSource` for building and executing a realtime pipeline. + /// + /// This is an internal method and should not be used directly. + /// + /// - Returns: A `RealtimePipelineSource` for building a realtime pipeline. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @nonobjc internal func realtimePipeline() -> RealtimePipelineSource { return RealtimePipelineSource(db: self) { stages, db in diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AggregateFunction.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AggregateFunction.swift index 3adf83239db..d4e224b7028 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AggregateFunction.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AggregateFunction.swift @@ -18,13 +18,23 @@ extension AggregateFunction { } } +/// Represents an aggregate function in a pipeline. +/// +/// An `AggregateFunction` is a function that computes a single value from a set of input values. +/// +/// `AggregateFunction`s are typically used in the `aggregate` stage of a pipeline. public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { let bridge: AggregateFunctionBridge let functionName: String let args: [Expression] - public init(_ functionName: String, _ args: [Expression]) { + /// Creates a new `AggregateFunction`. + /// + /// - Parameters: + /// - functionName: The name of the aggregate function. + /// - args: The arguments to the aggregate function. + public init(functionName: String, args: [Expression]) { self.functionName = functionName self.args = args bridge = AggregateFunctionBridge( @@ -34,6 +44,10 @@ public class AggregateFunction: AggregateBridgeWrapper, @unchecked Sendable { ) } + /// Creates an `AliasedAggregate` from this aggregate function. + /// + /// - Parameter name: The alias for the aggregate function. + /// - Returns: An `AliasedAggregate` with the given alias. public func `as`(_ name: String) -> AliasedAggregate { return AliasedAggregate(aggregate: self, alias: name) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AliasedAggregate.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AliasedAggregate.swift index 5c16126c6a8..d8c6aee1c4b 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AliasedAggregate.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/AliasedAggregate.swift @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// An `AggregateFunction` that has been given an alias. public struct AliasedAggregate { - public let aggregate: AggregateFunction - public let alias: String + let aggregate: AggregateFunction + + let alias: String } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/CountAll.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/CountAll.swift index 2c08f8e31d0..2fad4903d0d 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/CountAll.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Aggregates/CountAll.swift @@ -38,6 +38,6 @@ public class CountAll: AggregateFunction, @unchecked Sendable { /// Initializes a new `CountAll` aggregation. public init() { - super.init("count", []) + super.init(functionName: "count", args: []) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift index a4946946485..39e6cdd3321 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/DistanceMeasure.swift @@ -20,6 +20,7 @@ import Foundation +/// Represents the distance measure to be used in a vector similarity search. public struct DistanceMeasure: Sendable, Equatable, Hashable { let kind: Kind @@ -29,10 +30,13 @@ public struct DistanceMeasure: Sendable, Equatable, Hashable { case dotProduct = "dot_product" } + /// The Euclidean distance measure. public static let euclidean: DistanceMeasure = .init(kind: .euclidean) + /// The Cosine distance measure. public static let cosine: DistanceMeasure = .init(kind: .cosine) + /// The Dot Product distance measure. public static let dotProduct: DistanceMeasure = .init(kind: .dotProduct) init(kind: Kind) { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/AliasedExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/AliasedExpression.swift index 0468edd4a44..f19232d7f07 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/AliasedExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/AliasedExpression.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// An `Expression` that has been given an alias. public struct AliasedExpression: Selectable, SelectableWrapper, Sendable { let alias: String diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift index 8e483a85c7a..ac8b85a036c 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Expression.swift @@ -27,7 +27,7 @@ public protocol Expression: Sendable { /// /// ```swift /// // Calculate total price and alias it "totalPrice" - /// Field("price").multiply(Field("quantity")).`as`("totalPrice") + /// Field("price").multiply(Field("quantity")).as("totalPrice") /// ``` /// /// - Parameter name: The alias to assign to this expression. @@ -56,7 +56,7 @@ public protocol Expression: Sendable { /// - Returns: A new `FunctionExpression` representing the square root of the number. func sqrt() -> FunctionExpression - /// Creates an expression that returns the value of self raised to the power of Y. + /// Creates an expression that returns the value of self raised to the power of self. /// /// Returns zero on underflow. /// @@ -69,7 +69,7 @@ public protocol Expression: Sendable { /// - Returns: A new `FunctionExpression` representing the power of the number. func pow(_ exponent: Sendable) -> FunctionExpression - /// Creates an expression that returns the value of self raised to the power of Y. + /// Creates an expression that returns the value of self raised to the power of self. /// /// Returns zero on underflow. /// @@ -455,98 +455,98 @@ public protocol Expression: Sendable { /// Field("tags").arrayGet(Field("favoriteTagIndex")) /// ``` /// - /// - Parameter offsetExpr: An `Expression` (evaluating to an Int) representing the offset of the + /// - Parameter offsetExpression: An `Expression` (evaluating to an Int) representing the offset + /// of the /// element to return. /// - Returns: A new `FunctionExpression` representing the "arrayGet" operation. func arrayGet(_ offsetExpression: Expression) -> FunctionExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is greater + /// Creates a `BooleanExpression` that returns `true` if this expression is greater /// than the given expression. /// /// - Parameter other: The expression to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func greaterThan(_ other: Expression) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is greater + /// Creates a `BooleanExpression` that returns `true` if this expression is greater /// than the given value. /// /// - Parameter other: The value to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func greaterThan(_ other: Sendable) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is + /// Creates a `BooleanExpression` that returns `true` if this expression is /// greater than or equal to the given expression. /// /// - Parameter other: The expression to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func greaterThanOrEqual(_ other: Expression) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is + /// Creates a `BooleanExpression` that returns `true` if this expression is /// greater than or equal to the given value. /// /// - Parameter other: The value to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func greaterThanOrEqual(_ other: Sendable) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is less + /// Creates a `BooleanExpression` that returns `true` if this expression is less /// than the given expression. /// /// - Parameter other: The expression to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func lessThan(_ other: Expression) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is less + /// Creates a `BooleanExpression` that returns `true` if this expression is less /// than the given value. /// /// - Parameter other: The value to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func lessThan(_ other: Sendable) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is less + /// Creates a `BooleanExpression` that returns `true` if this expression is less /// than or equal to the given expression. /// /// - Parameter other: The expression to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func lessThanOrEqual(_ other: Expression) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is less + /// Creates a `BooleanExpression` that returns `true` if this expression is less /// than or equal to the given value. /// /// - Parameter other: The value to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func lessThanOrEqual(_ other: Sendable) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is equal + /// Creates a `BooleanExpression` that returns `true` if this expression is equal /// to the given expression. /// /// - Parameter other: The expression to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func equal(_ other: Expression) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is equal + /// Creates a `BooleanExpression` that returns `true` if this expression is equal /// to the given value. /// /// - Parameter other: The value to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func equal(_ other: Sendable) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is not + /// Creates a `BooleanExpression` that returns `true` if this expression is not /// equal to the given expression. /// /// - Parameter other: The expression to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func notEqual(_ other: Expression) -> BooleanExpression - /// Creates a `BooleanExpr` that returns `true` if this expression is not + /// Creates a `BooleanExpression` that returns `true` if this expression is not /// equal to the given value. /// /// - Parameter other: The value to compare against. - /// - Returns: A `BooleanExpr` that can be used in `where` clauses. + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func notEqual(_ other: Sendable) -> BooleanExpression /// 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 @@ -554,12 +554,11 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter others: An array of at least one `Expression` value to check against. - /// - Returns: A new `BooleanExpr` representing the "IN" comparison (eq_any). + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func equalAny(_ others: [Expression]) -> BooleanExpression /// 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" @@ -567,12 +566,11 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter others: An array of at least one `Sendable` literal value to check against. - /// - Returns: A new `BooleanExpr` representing the "IN" comparison (eq_any). + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func equalAny(_ others: [Sendable]) -> BooleanExpression /// 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 any of "categoryIDs" fields @@ -580,12 +578,11 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter arrayExpression: An `Expression` elements evaluated to be array. - /// - Returns: A new `BooleanExpr` representing the "IN" comparison (eq_any). + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func equalAny(_ arrayExpression: Expression) -> BooleanExpression /// 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 @@ -593,12 +590,11 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter others: An array of at least one `Expression` value to check against. - /// - Returns: A new `BooleanExpr` representing the "NOT IN" comparison (not_eq_any). + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func notEqualAny(_ others: [Expression]) -> BooleanExpression /// 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" @@ -606,12 +602,11 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter others: An array of at least one `Sendable` literal value to check against. - /// - Returns: A new `BooleanExpr` representing the "NOT IN" comparison (not_eq_any). + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func notEqualAny(_ others: [Sendable]) -> BooleanExpression /// 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 not equal to any of "categoryIDs" fields @@ -619,18 +614,17 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter arrayExpression: An `Expression` elements evaluated to be array. - /// - Returns: A new `BooleanExpr` representing the "IN" comparison (eq_any). + /// - Returns: A `BooleanExpression` that can be used in `where` clauses. func notEqualAny(_ arrayExpression: Expression) -> BooleanExpression /// 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. + /// - Returns: A new `BooleanExpression` representing the "isNaN" check. func isNan() -> BooleanExpression /// Creates an expression that checks if this expression evaluates to "Nil". @@ -640,46 +634,38 @@ public protocol Expression: Sendable { /// Field("optionalField").isNil() /// ``` /// - /// - Returns: A new `BooleanExpr` representing the "isNil" check. + /// - Returns: A new `BooleanExpression` representing the "isNil" check. func isNil() -> BooleanExpression /// 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. + /// - Returns: A new `BooleanExpression` representing the "exists" check. func exists() -> BooleanExpression /// 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").arrayGet(100).isError() /// ``` /// - /// - Returns: A new `BooleanExpr` representing the "isError" check. + /// - Returns: A new `BooleanExpression` representing the "isError" check. func isError() -> BooleanExpression /// 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. + /// is absent (e.g., a field does not exist in a map). Otherwise, returns `false`. /// /// ```swift /// // Check if the field `value` is absent. /// Field("value").isAbsent() /// ``` /// - /// - Returns: A new `BooleanExpr` representing the "isAbsent" check. + /// - Returns: A new `BooleanExpression` representing the "isAbsent" check. func isAbsent() -> BooleanExpression /// Creates an expression that checks if the result of this expression is not null. @@ -689,12 +675,11 @@ public protocol Expression: Sendable { /// Field("name").isNotNil() /// ``` /// - /// - Returns: A new `BooleanExpr` representing the "isNotNil" check. + /// - Returns: A new `BooleanExpression` representing the "isNotNil" check. func isNotNil() -> BooleanExpression /// 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 @@ -750,7 +735,7 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter pattern: The literal string pattern to search for. Use "%" as a wildcard. - /// - Returns: A new `BooleanExpr` representing the "like" comparison. + /// - Returns: A new `BooleanExpression` representing the "like" comparison. func like(_ pattern: String) -> BooleanExpression /// Creates an expression that performs a case-sensitive string comparison using wildcards against @@ -763,14 +748,13 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter pattern: An `Expression` (evaluating to a string) representing the pattern to - /// search - /// for. - /// - Returns: A new `BooleanExpr` representing the "like" comparison. + /// search for. + /// - Returns: A new `BooleanExpression` representing the "like" comparison. func like(_ pattern: Expression) -> BooleanExpression /// 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. + /// Assumes `self` evaluates to a string. /// /// ```swift /// // Check if "description" contains "example" (case-insensitive) @@ -778,12 +762,12 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter pattern: The literal string regular expression to use for the search. - /// - Returns: A new `BooleanExpr` representing the "regex_contains" comparison. + /// - Returns: A new `BooleanExpression` representing the "regex_contains" comparison. func regexContains(_ pattern: String) -> BooleanExpression /// 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. + /// Assumes `self` evaluates to a string, and `pattern` evaluates to a string. /// /// ```swift /// // Check if "logEntry" contains a pattern from "errorPattern" field @@ -791,14 +775,13 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter pattern: An `Expression` (evaluating to a string) representing the regular - /// expression to - /// use for the search. - /// - Returns: A new `BooleanExpr` representing the "regex_contains" comparison. + /// expression to use for the search. + /// - Returns: A new `BooleanExpression` representing the "regex_contains" comparison. func regexContains(_ pattern: Expression) -> BooleanExpression /// 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. + /// Assumes `self` evaluates to a string. /// /// ```swift /// // Check if the "email" field matches a valid email pattern @@ -806,12 +789,12 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter pattern: The literal string regular expression to use for the match. - /// - Returns: A new `BooleanExpr` representing the regular expression match. + /// - Returns: A new `BooleanExpression` representing the regular expression match. func regexMatch(_ pattern: String) -> BooleanExpression /// 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. + /// Assumes `self` evaluates to a string, and `pattern` evaluates to a string. /// /// ```swift /// // Check if "input" matches the regex stored in "validationRegex" @@ -819,9 +802,8 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter pattern: An `Expression` (evaluating to a string) representing the regular - /// expression to - /// use for the match. - /// - Returns: A new `BooleanExpr` representing the regular expression match. + /// expression to use for the match. + /// - Returns: A new `BooleanExpression` representing the regular expression match. func regexMatch(_ pattern: Expression) -> BooleanExpression /// Creates an expression that checks if a string (from `self`) contains a specified literal @@ -834,7 +816,7 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter substring: The literal string substring to search for. - /// - Returns: A new `BooleanExpr` representing the "stringContains" comparison. + /// - Returns: A new `BooleanExpression` representing the "stringContains" comparison. func stringContains(_ substring: String) -> BooleanExpression /// Creates an expression that checks if a string (from `self`) contains a specified substring @@ -848,7 +830,7 @@ public protocol Expression: Sendable { /// /// - Parameter expression: An `Expression` (evaluating to a string) representing the substring to /// search for. - /// - Returns: A new `BooleanExpr` representing the "str_contains" comparison. + /// - Returns: A new `BooleanExpression` representing the "str_contains" comparison. func stringContains(_ expression: Expression) -> BooleanExpression /// Creates an expression that checks if a string (from `self`) starts with a given literal prefix @@ -902,7 +884,7 @@ public protocol Expression: Sendable { /// /// - Parameter suffix: An `Expression` (evaluating to a string) representing the suffix to check /// for. - /// - Returns: A new `BooleanExpr` representing the "ends_with" comparison. + /// - Returns: A new `BooleanExpression` representing the "ends_with" comparison. func endsWith(_ suffix: Expression) -> BooleanExpression /// Creates an expression that converts a string (from `self`) to lowercase. @@ -965,7 +947,7 @@ public protocol Expression: Sendable { /// - Returns: A new `FunctionExpression` representing the concatenated string. func stringConcat(_ strings: [Expression]) -> FunctionExpression - /// Creates an expression that reverses this string expression. + /// Creates an expression that reverses this expression. /// Assumes `self` evaluates to a string. /// /// ```swift @@ -973,7 +955,7 @@ public protocol Expression: Sendable { /// Field("myString").reverse() /// ``` /// - /// - Returns: A new `FunctionExpr` representing the reversed string. + /// - Returns: A new `FunctionExpression` representing the reversed string. func reverse() -> FunctionExpression /// Creates an expression that reverses this string expression. @@ -984,11 +966,11 @@ public protocol Expression: Sendable { /// Field("myString").stringReverse() /// ``` /// - /// - Returns: A new `FunctionExpr` representing the reversed string. + /// - Returns: A new `FunctionExpression` representing the reversed string. func stringReverse() -> FunctionExpression - /// Creates an expression that calculates the length of this string or bytes expression in bytes. - /// Assumes `self` evaluates to a string or bytes. + /// Creates an expression that calculates the length of this expression in bytes. + /// Assumes `self` evaluates to a string. /// /// ```swift /// // Calculate the length of the "myString" field in bytes. @@ -998,48 +980,44 @@ public protocol Expression: Sendable { /// Field("avatar").byteLength() /// ``` /// - /// - Returns: A new `FunctionExpr` representing the length in bytes. + /// - Returns: A new `FunctionExpression` representing the length in bytes. func byteLength() -> FunctionExpression - /// Creates an expression that returns a substring of this expression (String or Bytes) using + /// Creates an expression that returns a substring of this expression 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. + /// Indexing is 0-based. Assumes `self` evaluates to a string. /// /// ```swift /// // Get substring from index 5 with length 10 /// Field("myString").substring(5, 10) /// /// // Get substring from "myString" starting at index 3 to the end - /// Field("myString").substring(3, nil) + /// Field("myString").substring(3) // Default 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. + /// - Returns: A new `FunctionExpression` representing the substring. func substring(position: Int, length: Int?) -> FunctionExpression - /// Creates an expression that returns a substring of this expression (String or Bytes) using + /// Creates an expression that returns a substring of this expression using /// expressions for position and optional length. - /// Indexing is 0-based. Assumes `self` evaluates to a string or bytes, and parameters evaluate to + /// Indexing is 0-based. Assumes `self` evaluates to a string, 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").substring(Field("start"), Field("len")) /// /// // Get substring from index calculated by Field("start") to the end - /// Field("myString").substring(Field("start"), nil) // Passing nil for optional Expr length + /// Field("myString").substring(Field("start")) // Default nil for optional Expression 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. + /// - Parameter position: An `Expression` (evaluating to an Int) for the index of the first + /// character. + /// - Parameter length: Optional `Expression` (evaluating to an Int) for the length of the + /// substring. If `nil`, goes to the end. + /// - Returns: A new `FunctionExpression` representing the substring. func substring(position: Expression, length: Expression?) -> FunctionExpression // MARK: Map Operations @@ -1053,45 +1031,39 @@ public protocol Expression: Sendable { /// ``` /// /// - Parameter subfield: The literal string key to access in the map. - /// - Returns: A new `FunctionExpr` representing the value associated with the given key. + /// - Returns: A new `FunctionExpression` representing the value associated with the given key. func mapGet(_ subfield: String) -> FunctionExpression /// 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. + /// - Returns: A new `FunctionExpression` representing the "map_remove" operation. func mapRemove(_ key: String) -> FunctionExpression /// 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. + /// Assumes `self` evaluates to a Map, and `keyExpression` evaluates to a string. /// /// ```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. + /// - Parameter keyExpression: An `Expression` (evaluating to a string) representing the key to + /// remove from the map. + /// - Returns: A new `FunctionExpression` representing the "map_remove" operation. func mapRemove(_ keyExpression: Expression) -> FunctionExpression /// 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]) @@ -1099,7 +1071,7 @@ public protocol Expression: Sendable { /// /// - Parameter maps: Maps (dictionary literals with `Sendable` values) /// to merge. - /// - Returns: A new `FunctionExpr` representing the "map_merge" operation. + /// - Returns: A new `FunctionExpression` representing the "map_merge" operation. func mapMerge(_ maps: [[String: Sendable]]) -> FunctionExpression @@ -1107,17 +1079,43 @@ public protocol Expression: Sendable { /// 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 maps: Additional `Expression` (evaluating to Maps) to merge. - /// - Returns: A new `FunctionExpr` representing the "map_merge" operation. + /// - Returns: A new `FunctionExpression` representing the "map_merge" operation. func mapMerge(_ maps: [Expression]) -> FunctionExpression + /// Creates an expression that adds or updates a specified field in a map. + /// Assumes `self` evaluates to a Map, `key` evaluates to a string, and `value` can be + /// any type. + /// + /// ```swift + /// // Set a field using a key from another field + /// Field("config").mapSet(key: Field("keyName"), value: Field("keyValue")) + /// ``` + /// + /// - Parameter key: An `Expression` (evaluating to a string) representing the key of + /// the field to set or update. + /// - Parameter value: The `Expression` representing the value to set for the field. + /// - Returns: A new `FunctionExpression` representing the map with the updated field. + func mapSet(key: Expression, value: Sendable) -> FunctionExpression + + /// Creates an expression that adds or updates a specified field in a map. + /// Assumes `self` evaluates to a Map. + /// + /// ```swift + /// // Set the "status" field to "active" in the "order" map + /// Field("order").mapSet(key: "status", value: "active") + /// ``` + /// + /// - Parameter key: The literal string key of the field to set or update. + /// - Parameter value: The `Sendable` literal value to set for the field. + /// - Returns: A new `FunctionExpression` representing the map with the updated field. + func mapSet(key: String, value: Sendable) -> FunctionExpression + // MARK: Aggregations /// Creates an aggregation that counts the number of distinct values of this expression. @@ -1187,8 +1185,6 @@ public protocol Expression: Sendable { /// - 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. /// @@ -1433,22 +1429,29 @@ public protocol Expression: Sendable { /// - Returns: A new `FunctionExpression` representing the number of seconds. func timestampToUnixSeconds() -> FunctionExpression - /// 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. + /// Creates an expression that truncates a timestamp to a specified granularity. + /// Assumes `self` evaluates to a Timestamp. /// /// ```swift - /// // Add duration from "unitField"/"amountField" to "timestamp" - /// Field("timestamp").timestampAdd(amount: Field("amountField"), unit: Field("unitField")) + /// // Truncate "timestamp" field to the nearest day. + /// Field("timestamp").timestampTruncate(granularity: .day) /// ``` /// - /// - 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 "FunctionExpression" representing the resulting timestamp. - func timestampAdd(amount: Expression, unit: Expression) -> FunctionExpression + /// - Parameter granularity: A `TimeUnit` enum representing the truncation unit. + /// - Returns: A new `FunctionExpression` representing the truncated timestamp. + func timestampTruncate(granularity: TimeUnit) -> FunctionExpression + + /// Creates an expression that truncates a timestamp to a specified granularity. + /// Assumes `self` evaluates to a Timestamp, and `granularity` is a literal string. + /// + /// ```swift + /// // Truncate "timestamp" field to the nearest day using a literal string. + /// Field("timestamp").timestampTruncate(granularity: "day") + /// ``` + /// + /// - Parameter granularity: A `Sendable` literal string specifying the truncation unit. + /// - Returns: A new `FunctionExpression` representing the truncated timestamp. + func timestampTruncate(granularity: Sendable) -> FunctionExpression /// Creates an expression that adds a specified amount of time to this timestamp expression, /// where unit and amount are provided as literals. @@ -1464,27 +1467,25 @@ public protocol Expression: Sendable { /// - Returns: A new "FunctionExpression" representing the resulting timestamp. func timestampAdd(_ amount: Int, _ unit: TimeUnit) -> FunctionExpression - /// 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. + /// Creates an expression that adds a specified amount of time to this timestamp expression, + /// where unit and amount are provided as an expression for amount and a literal for unit. + /// Assumes `self` evaluates to a Timestamp, `amount` evaluates to an integer, and `unit` + /// evaluates to a string. /// /// ```swift - /// // Subtract duration from "unitField"/"amountField" from "timestamp" - /// Field("timestamp").timestampSubtract(amount: Field("amountField"), unit: Field("unitField")) + /// // Add duration from "amountField" to "timestamp" with a literal unit "day". + /// Field("timestamp").timestampAdd(amount: Field("amountField"), unit: "day") /// ``` /// - /// - Parameter unit: An `Expression` evaluating to the unit of time string (e.g., "day", "hour"). + /// - Parameter unit: A `Sendable` literal string specifying the unit of time. /// Valid units are "microsecond", "millisecond", "second", "minute", "hour", /// "day". - /// - Parameter amount: An `Expression` evaluating to the amount (Int) of the unit to subtract. + /// - Parameter amount: An `Expression` evaluating to the amount (Int) of the unit to add. /// - Returns: A new "FunctionExpression" representing the resulting timestamp. - func timestampSubtract(amount: Expression, unit: Expression) -> FunctionExpression + func timestampAdd(amount: Expression, unit: Sendable) -> FunctionExpression /// Creates an expression that subtracts a specified amount of time from this timestamp - /// expression, - /// where unit and amount are provided as literals. + /// expression, where unit and amount are provided as literals. /// Assumes `self` evaluates to a Timestamp. /// /// ```swift @@ -1497,9 +1498,25 @@ public protocol Expression: Sendable { /// - Returns: A new "FunctionExpression" representing the resulting timestamp. func timestampSubtract(_ amount: Int, _ unit: TimeUnit) -> FunctionExpression - /// Creates an expression that returns the document ID from a path. + /// Creates an expression that subtracts a specified amount of time from this timestamp + /// expression, where unit and amount are provided as an expression for amount and a literal for + /// unit. + /// Assumes `self` evaluates to a Timestamp, `amount` evaluates to an integer, and `unit` + /// evaluates to a string. /// - /// - Note: This API is in beta. + /// ```swift + /// // Subtract duration from "amountField" from "timestamp" with a literal unit "day". + /// Field("timestamp").timestampSubtract(amount: Field("amountField"), unit: "day") + /// ``` + /// + /// - Parameter unit: A `Sendable` literal string specifying the unit of time. + /// Valid units are "microsecond", "millisecond", "second", "minute", "hour", + /// "day". + /// - Parameter amount: An `Expression` evaluating to the amount (Int) of the unit to subtract. + /// - Returns: A new "FunctionExpression" representing the resulting timestamp. + func timestampSubtract(amount: Expression, unit: Sendable) -> FunctionExpression + + /// Creates an expression that returns the document ID from a path. /// /// ```swift /// // Get the document ID from a path. @@ -1514,26 +1531,21 @@ public protocol Expression: Sendable { /// root itself. func collectionId() -> FunctionExpression - /// 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. + /// Creates an expression that returns the result of `catchExpression` if this expression produces + /// an error during evaluation, otherwise returns the result of this expression. /// /// ```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 `Expression` to evaluate and return if this expression errors. + /// - Parameter catchExpression: The `Expression` to evaluate and return if this expression + /// errors. /// - Returns: A new "FunctionExpression" representing the "ifError" operation. - func ifError(_ catchExpr: Expression) -> FunctionExpression + func ifError(_ catchExpression: Expression) -> FunctionExpression /// 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. + /// error during evaluation, otherwise returns the result of this expression. /// /// ```swift /// // Get first item in "title" array, or return "Default Title" if error (e.g., empty array) @@ -1548,8 +1560,6 @@ public protocol Expression: Sendable { /// absent (e.g., a field does not exist in a map). /// Otherwise, returns the result of this expression. /// - /// - Note: This API is in beta. - /// /// ```swift /// // If the "optionalField" is absent, return "default value". /// Field("optionalField").ifAbsent("default value") diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Field.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Field.swift index a2b0c74fc77..45607ec3f7a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Field.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/Field.swift @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -/// /// A `Field` is an `Expression` that represents a field in a Firestore document. /// /// It is a central component for building queries and transformations in Firestore pipelines. @@ -42,9 +41,12 @@ public struct Field: Expression, Selectable, BridgeWrapper, SelectableWrapper, return self } + /// The name of the field. public let fieldName: String /// Creates a new `Field` expression from a field name. + /// + /// - Parameter name: The name of the field. public init(_ name: String) { let fieldBridge = FieldBridge(name: name) bridge = fieldBridge @@ -53,6 +55,8 @@ public struct Field: Expression, Selectable, BridgeWrapper, SelectableWrapper, } /// Creates a new `Field` expression from a `FieldPath`. + /// + /// - Parameter path: The `FieldPath` of the field. public init(_ path: FieldPath) { let fieldBridge = FieldBridge(path: path) bridge = fieldBridge diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ArrayExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ArrayExpression.swift index 25fc28b3d89..e5c8e4426b4 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ArrayExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ArrayExpression.swift @@ -38,6 +38,6 @@ public class ArrayExpression: FunctionExpression, @unchecked Sendable { result.append(Helper.sendableToExpr(element)) } - super.init("array", result) + super.init(functionName: "array", args: result) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift index c29703bf881..700d4aa0476 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/BooleanExpression.swift @@ -31,8 +31,8 @@ import Foundation /// ) /// ``` public class BooleanExpression: FunctionExpression, @unchecked Sendable { - override public init(_ functionName: String, _ agrs: [Expression]) { - super.init(functionName, agrs) + override public init(functionName: String, args: [Expression]) { + super.init(functionName: functionName, args: args) } /// Creates an aggregation that counts the number of documents for which this boolean expression @@ -52,8 +52,8 @@ public class BooleanExpression: FunctionExpression, @unchecked Sendable { /// ``` /// /// - Returns: An `AggregateFunction` that performs the conditional count. - func countIf() -> AggregateFunction { - return AggregateFunction("count_if", [self]) + public func countIf() -> AggregateFunction { + return AggregateFunction(functionName: "count_if", args: [self]) } /// Creates a conditional expression that returns one of two specified expressions based on the @@ -79,7 +79,10 @@ public class BooleanExpression: FunctionExpression, @unchecked Sendable { /// - Returns: A new `FunctionExpression` representing the conditional logic. public func then(_ thenExpression: Expression, else elseExpression: Expression) -> FunctionExpression { - return FunctionExpression("conditional", [self, thenExpression, elseExpression]) + return FunctionExpression( + functionName: "conditional", + args: [self, thenExpression, elseExpression] + ) } /// Combines two boolean expressions with a logical AND (`&&`). @@ -103,7 +106,7 @@ public class BooleanExpression: FunctionExpression, @unchecked Sendable { public static func && (lhs: BooleanExpression, rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { - try BooleanExpression("and", [lhs, rhs()]) + try BooleanExpression(functionName: "and", args: [lhs, rhs()]) } /// Combines two boolean expressions with a logical OR (`||`). @@ -127,7 +130,7 @@ public class BooleanExpression: FunctionExpression, @unchecked Sendable { public static func || (lhs: BooleanExpression, rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { - try BooleanExpression("or", [lhs, rhs()]) + try BooleanExpression(functionName: "or", args: [lhs, rhs()]) } /// Combines two boolean expressions with a logical XOR (`^`). @@ -151,7 +154,7 @@ public class BooleanExpression: FunctionExpression, @unchecked Sendable { public static func ^ (lhs: BooleanExpression, rhs: @autoclosure () throws -> BooleanExpression) rethrows -> BooleanExpression { - try BooleanExpression("xor", [lhs, rhs()]) + try BooleanExpression(functionName: "xor", args: [lhs, rhs()]) } /// Negates a boolean expression with a logical NOT (`!`). @@ -168,6 +171,6 @@ public class BooleanExpression: FunctionExpression, @unchecked Sendable { /// - Parameter lhs: The boolean expression to negate. /// - Returns: A new `BooleanExpression` representing the logical NOT. public static prefix func ! (lhs: BooleanExpression) -> BooleanExpression { - return BooleanExpression("not", [lhs]) + return BooleanExpression(functionName: "not", args: [lhs]) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ConditionalExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ConditionalExpression.swift index 93638f5d916..fb5b01a0237 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ConditionalExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ConditionalExpression.swift @@ -41,9 +41,9 @@ public class ConditionalExpression: FunctionExpression, @unchecked Sendable { /// - expression: The `BooleanExpression` to evaluate. /// - thenExpression: The `Expression` to evaluate if the boolean expression is `true`. /// - elseExpression: The `Expression` to evaluate if the boolean expression is `false`. - public init(_ expr: BooleanExpression, + public init(_ expression: BooleanExpression, then thenExpression: Expression, else elseExpression: Expression) { - super.init("conditional", [expr, thenExpression, elseExpression]) + super.init(functionName: "conditional", args: [expression, thenExpression, elseExpression]) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/CurrentTimestamp.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/CurrentTimestamp.swift index 914394a4147..5ce275c2f61 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/CurrentTimestamp.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/CurrentTimestamp.swift @@ -25,6 +25,6 @@ import Foundation /// ``` public class CurrentTimestamp: FunctionExpression, @unchecked Sendable { public init() { - super.init("current_timestamp", []) + super.init(functionName: "current_timestamp", args: []) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ErrorExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ErrorExpression.swift index 7e045ffbf50..8926905677a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ErrorExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/ErrorExpression.swift @@ -23,6 +23,6 @@ import Foundation /// ``` public class ErrorExpression: FunctionExpression, @unchecked Sendable { public init(_ errorMessage: String) { - super.init("error", [Constant(errorMessage)]) + super.init(functionName: "error", args: [Constant(errorMessage)]) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/FunctionExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/FunctionExpression.swift index 825487c9a56..2f1bac5814f 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/FunctionExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/FunctionExpression.swift @@ -12,18 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// Represents a function call in a pipeline. +/// +/// A `FunctionExpression` is an expression that represents a function call with a given name and +/// arguments. +/// +/// `FunctionExpression`s are typically used to perform operations on data in a pipeline, such as +/// mathematical calculations, string manipulations, or array operations. public class FunctionExpression: Expression, BridgeWrapper, @unchecked Sendable { let bridge: ExprBridge let functionName: String - let agrs: [Expression] + let args: [Expression] - public init(_ functionName: String, _ agrs: [Expression]) { + /// Creates a new `FunctionExpression`. + /// + /// - Parameters: + /// - functionName: The name of the function. + /// - args: The arguments to the function. + public init(functionName: String, args: [Expression]) { self.functionName = functionName - self.agrs = agrs + self.args = args bridge = FunctionExprBridge( name: functionName, - args: self.agrs.map { $0.toBridge() + args: self.args.map { $0.toBridge() } ) } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/MapExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/MapExpression.swift index f7bd9628bc0..8501c28f9ee 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/MapExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/MapExpression.swift @@ -36,6 +36,6 @@ public class MapExpression: FunctionExpression, @unchecked Sendable { result.append(Helper.sendableToExpr(element.value)) } - super.init("map", result) + super.init(functionName: "map", args: result) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/RandomExpression.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/RandomExpression.swift index 9a4ff22a958..27615cec877 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/RandomExpression.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Expressions/FunctionExpressions/RandomExpression.swift @@ -26,9 +26,9 @@ /// .collection("users") /// .where(RandomExpression().lessThan(0.1)) /// ``` -public class RandomExpression: FunctionExpression, @unchecked Sendable { +class RandomExpression: FunctionExpression, @unchecked Sendable { /// Creates a new `RandomExpression` that generates a random number. - public init() { - super.init("rand", []) + init() { + super.init(functionName: "rand", args: []) } } diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift index f9090e8dd41..c62f349c23c 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Ordering.swift @@ -14,9 +14,13 @@ * limitations under the License. */ +/// An ordering for the documents in a pipeline. public struct Ordering: @unchecked Sendable { + /// The expression to order by. public let expression: Expression + /// The direction to order in. public let direction: Direction + let bridge: OrderingBridge init(expression: Expression, direction: Direction) { @@ -26,6 +30,7 @@ public struct Ordering: @unchecked Sendable { } } +/// A direction to order results in. public struct Direction: Sendable, Equatable, Hashable { let kind: Kind public let rawValue: String @@ -35,8 +40,10 @@ public struct Direction: Sendable, Equatable, Hashable { case descending } + /// The ascending direction. static let ascending = Direction(kind: .ascending, rawValue: "ascending") + /// The descending direction. static let descending = Direction(kind: .descending, rawValue: "descending") init(kind: Kind, rawValue: String) { diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift index 593d16fb669..32fcb1ec64a 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift @@ -24,16 +24,11 @@ import Foundation /// /// 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). +/// 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`. @@ -88,6 +83,7 @@ public struct Pipeline: @unchecked Sendable { bridge = PipelineBridge(stages: stages.map { $0.bridge }, db: db) } + /// A `Pipeline.Snapshot` contains the results of a pipeline execution. public struct Snapshot: Sendable { /// An array of all the results in the `Pipeline.Snapshot`. public let results: [PipelineResult] @@ -114,8 +110,8 @@ public struct Pipeline: @unchecked Sendable { /// // 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)") + /// // Process snapshot.results + /// print("Pipeline executed successfully: \(snapshot.results)") /// } catch { /// print("Pipeline execution failed: \(error)") /// } @@ -305,7 +301,7 @@ public struct Pipeline: @unchecked Sendable { /// // let pipeline: Pipeline = ... // Assume initial pipeline. /// // Limit results to the top 10 highest-rated books. /// let topTenPipeline = pipeline - /// .sort(Descending(Field("rating"))) + /// .sort([Field("rating").descending()]) /// .limit(10) /// // let results = try await topTenPipeline.execute() /// ``` @@ -324,7 +320,7 @@ public struct Pipeline: @unchecked Sendable { /// ```swift /// // let pipeline: Pipeline = ... // Assume initial pipeline. /// // Get a list of unique author and genre combinations. - /// let distinctAuthorsGenresPipeline = pipeline.distinct("author", "genre") + /// let distinctAuthorsGenresPipeline = pipeline.distinct(["author", "genre"]) /// // To further select only the author: /// // .select("author") /// // let results = try await distinctAuthorsGenresPipeline.execute() @@ -379,11 +375,11 @@ public struct Pipeline: @unchecked Sendable { /// // let pipeline: Pipeline = ... // Assume pipeline from "books" collection. /// // Calculate the average rating for each genre. /// let groupedAggregationPipeline = pipeline.aggregate( - /// [AggregateWithas(aggregate: average(Field("rating")), alias: "avg_rating")], + /// [Field("rating").average().as("avg_rating")], /// groups: [Field("genre")] // Group by the "genre" field. /// ) /// // let results = try await groupedAggregationPipeline.execute() - /// // results.documents might be: + /// // snapshot.results might be: /// // [ /// // ["genre": "SciFi", "avg_rating": 4.5], /// // ["genre": "Fantasy", "avg_rating": 4.2] @@ -486,8 +482,8 @@ public struct Pipeline: @unchecked Sendable { /// /// - Parameter expression: 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: Expression) -> Pipeline { - return Pipeline(stages: stages + [ReplaceWith(expr: expr)], db: db) + public func replace(with expression: Expression) -> Pipeline { + return Pipeline(stages: stages + [ReplaceWith(expr: expression)], db: db) } /// Fully overwrites document fields with those from a nested map identified by a field name. @@ -566,7 +562,7 @@ public struct Pipeline: @unchecked Sendable { /// // Field("topic").as("category")]) /// /// // Emit documents from both "books" and "magazines" collections. - /// let combinedPipeline = booksPipeline.union(with: [magazinesPipeline]) + /// let combinedPipeline = booksPipeline.union(with: magazinesPipeline) /// // let results = try await combinedPipeline.execute() /// ``` /// @@ -625,7 +621,7 @@ public struct Pipeline: @unchecked Sendable { /// 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. + /// `Function`, `Expression`, or arrays/dictionaries thereof. /// /// ```swift /// // let pipeline: Pipeline = ... diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift index 5c58feb9c7c..b7b1347c3a2 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineSource.swift @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// A `PipelineSource` is the entry point for building a Firestore pipeline. It allows you to +/// specify the source of the data for the pipeline, which can be a collection, a collection group, +/// a list of documents, or the entire database. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public struct PipelineSource: @unchecked Sendable { let db: Firestore @@ -22,14 +25,26 @@ public struct PipelineSource: @unchecked Sendable { self.factory = factory } + /// Specifies a collection as the data source for the pipeline. + /// + /// - Parameter path: The path to the collection. + /// - Returns: A `Pipeline` with the specified collection as its source. public func collection(_ path: String) -> Pipeline { return factory([CollectionSource(collection: db.collection(path), db: db)], db) } + /// Specifies a collection as the data source for the pipeline. + /// + /// - Parameter coll: The `CollectionReference` of the collection. + /// - Returns: A `Pipeline` with the specified collection as its source. public func collection(_ coll: CollectionReference) -> Pipeline { return factory([CollectionSource(collection: coll, db: db)], db) } + /// Specifies a collection group as the data source for the pipeline. + /// + /// - Parameter collectionId: The ID of the collection group. + /// - Returns: A `Pipeline` with the specified collection group as its source. public func collectionGroup(_ collectionId: String) -> Pipeline { return factory( [CollectionGroupSource(collectionId: collectionId)], @@ -37,19 +52,37 @@ public struct PipelineSource: @unchecked Sendable { ) } + /// Specifies the entire database as the data source for the pipeline. + /// + /// - Returns: A `Pipeline` with the entire database as its source. public func database() -> Pipeline { return factory([DatabaseSource()], db) } + /// Specifies a list of documents as the data source for the pipeline. + /// + /// - Parameter docs: An array of `DocumentReference` objects. + /// - Returns: A `Pipeline` with the specified documents as its source. public func documents(_ docs: [DocumentReference]) -> Pipeline { return factory([DocumentsSource(docs: docs, db: db)], db) } + /// Specifies a list of documents as the data source for the pipeline. + /// + /// - Parameter paths: An array of document paths. + /// - Returns: A `Pipeline` with the specified documents as its source. public func documents(_ paths: [String]) -> Pipeline { let docs = paths.map { db.document($0) } return factory([DocumentsSource(docs: docs, db: db)], db) } + /// Creates a `Pipeline` from an existing `Query`. + /// + /// This allows you to convert a standard Firestore query into a pipeline, which can then be + /// further modified with additional pipeline stages. + /// + /// - Parameter query: The `Query` to convert into a pipeline. + /// - Returns: A `Pipeline` that is equivalent to the given query. public func create(from query: Query) -> Pipeline { let stageBridges = PipelineBridge.createStageBridges(from: query) let stages: [Stage] = stageBridges.map { bridge in diff --git a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift index a9c655f4e6a..e2a800d55f9 100644 --- a/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift +++ b/Firestore/Swift/Source/SwiftAPI/Pipeline/Selectable.swift @@ -12,4 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// A protocol for expressions that have a name. +/// +/// `Selectable` is adopted by expressions that can be used in pipeline stages where a named output +/// is required, such as `select` and `distinct`. +/// +/// A `Field` is a `Selectable` where the name is the field path. +/// +/// An expression can be made `Selectable` by giving it an alias using the `.as()` method. public protocol Selectable: Sendable {} diff --git a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift index fb6f8193d56..20096529f97 100644 --- a/Firestore/Swift/Tests/Integration/PipelineApiTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineApiTests.swift @@ -403,12 +403,12 @@ final class PipelineApiTests: FSTIntegrationTestCase { func testGeneric() async throws { // This is the same of the logicalMin('price', 0)', if it did not exist - _ = FunctionExpression("logicalMin", [Field("price"), Constant(0)]) + _ = FunctionExpression(functionName: "logicalMin", args: [Field("price"), Constant(0)]) // Create a generic BooleanExpr for use where BooleanExpr is required - _ = BooleanExpression("eq", [Field("price"), Constant(10)]) + _ = BooleanExpression(functionName: "eq", args: [Field("price"), Constant(10)]) // Create a generic AggregateFunction for use where AggregateFunction is required - _ = AggregateFunction("sum", [Field("price")]) + _ = AggregateFunction(functionName: "sum", args: [Field("price")]) } } diff --git a/Firestore/Swift/Tests/Integration/PipelineTests.swift b/Firestore/Swift/Tests/Integration/PipelineTests.swift index 9a201cd7866..d1f8edbfb06 100644 --- a/Firestore/Swift/Tests/Integration/PipelineTests.swift +++ b/Firestore/Swift/Tests/Integration/PipelineTests.swift @@ -849,27 +849,26 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { } } - // Hide this test due to `.countIf()` design is incomplete. -// func testReturnsCountIfAccumulation() async throws { -// let collRef = collectionRef(withDocuments: bookDocs) -// let db = collRef.firestore -// -// let expectedCount = 3 -// let expectedResults: [String: Sendable] = ["count": expectedCount] -// let condition = Field("rating").greaterThan(4.3) -// -// let pipeline = db.pipeline() -// .collection(collRef.path) -// .aggregate([condition.countIf().as("count")]) -// let snapshot = try await pipeline.execute() -// -// XCTAssertEqual(snapshot.results.count, 1, "countIf aggregate should return a single document") -// if let result = snapshot.results.first { -// TestHelper.compare(pipelineResult: result, expected: expectedResults) -// } else { -// XCTFail("No result for countIf aggregation") -// } -// } + func testReturnsCountIfAccumulation() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let expectedCount = 3 + let expectedResults: [String: Sendable] = ["count": expectedCount] + let condition = Field("rating").greaterThan(4.3) + + let pipeline = db.pipeline() + .collection(collRef.path) + .aggregate([condition.countIf().as("count")]) + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1, "countIf aggregate should return a single document") + if let result = snapshot.results.first { + TestHelper.compare(pipelineResult: result, expected: expectedResults) + } else { + XCTFail("No result for countIf aggregation") + } + } func testDistinctStage() async throws { let collRef = collectionRef(withDocuments: bookDocs) @@ -2405,9 +2404,12 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { Field("value").exp().as("expValue"), ]) - let snapshot = try await pipeline.execute() - XCTAssertEqual(snapshot.results.count, 1) - XCTAssertNil(snapshot.results.first!.get("expValue")) + do { + let _ = try await pipeline.execute() + XCTFail("The pipeline should have thrown an error, but it did not.") + } catch { + XCTAssert(true, "Successfully caught expected error from exponent overflow.") + } } func testCollectionIdWorks() async throws { @@ -2499,8 +2501,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let collRef = collectionRef(withDocuments: bookDocs) let db = collRef.firestore - // Part 1 - var pipeline = db.pipeline() + let pipeline = db.pipeline() .collection(collRef.path) .sort([Field("rating").descending()]) .limit(1) @@ -2508,8 +2509,6 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { [ Field("rating").isNil().as("ratingIsNull"), Field("rating").isNan().as("ratingIsNaN"), - Field("title").arrayGet(0).isError().as("isError"), - Field("title").arrayGet(0).ifError(Constant("was error")).as("ifError"), Field("foo").isAbsent().as("isAbsent"), Field("title").isNotNil().as("titleIsNotNull"), Field("cost").isNotNan().as("costIsNotNan"), @@ -2518,15 +2517,13 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { ] ) - var snapshot = try await pipeline.execute() - XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document for checks part 1") + let snapshot = try await pipeline.execute() + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document for checks") if let resultDoc = snapshot.results.first { let expectedResults: [String: Sendable?] = [ "ratingIsNull": false, "ratingIsNaN": false, - "isError": true, - "ifError": "was error", "isAbsent": true, "titleIsNotNull": true, "costIsNotNan": false, @@ -2535,42 +2532,61 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { ] TestHelper.compare(pipelineResult: resultDoc, expected: expectedResults) } else { - XCTFail("No document retrieved for checks part 1") + XCTFail("No document retrieved for checks") } + } - // Part 2 - pipeline = db.pipeline() + func testIsError() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() .collection(collRef.path) .sort([Field("rating").descending()]) .limit(1) .select( [ - Field("rating").isNil().as("ratingIsNull"), - Field("rating").isNan().as("ratingIsNaN"), - Field("title").arrayGet(0).isError().as("isError"), - Field("title").arrayGet(0).ifError(Constant("was error")).as("ifError"), - Field("foo").isAbsent().as("isAbsent"), - Field("title").isNotNil().as("titleIsNotNull"), - Field("cost").isNotNan().as("costIsNotNan"), + Field("title").arrayLength().isError().as("isError"), ] ) - snapshot = try await pipeline.execute() - XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document for checks part 2") + let snapshot = try await pipeline.execute() + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document for test") if let resultDoc = snapshot.results.first { let expectedResults: [String: Sendable?] = [ - "ratingIsNull": false, - "ratingIsNaN": false, "isError": true, + ] + TestHelper.compare(pipelineResult: resultDoc, expected: expectedResults) + } else { + XCTFail("No document retrieved for test") + } + } + + func testIfError() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .sort([Field("rating").descending()]) + .limit(1) + .select( + [ + Field("title").arrayLength().ifError(Constant("was error")).as("ifError"), + ] + ) + + let snapshot = try await pipeline.execute() + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document for test") + + if let resultDoc = snapshot.results.first { + let expectedResults: [String: Sendable?] = [ "ifError": "was error", - "isAbsent": true, - "titleIsNotNull": true, - "costIsNotNan": false, ] TestHelper.compare(pipelineResult: resultDoc, expected: expectedResults) } else { - XCTFail("No document retrieved for checks part 2") + XCTFail("No document retrieved for test") } } @@ -2758,7 +2774,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .limit(1) .select( [ - FunctionExpression("add", [Field("rating"), Constant(1)]).as( + FunctionExpression(functionName: "add", args: [Field("rating"), Constant(1)]).as( "rating" ), ] @@ -2786,9 +2802,9 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let pipeline = db.pipeline() .collection(collRef.path) .where( - BooleanExpression("and", [Field("rating").greaterThan(0), - Field("title").charLength().lessThan(5), - Field("tags").arrayContains("propaganda")]) + BooleanExpression(functionName: "and", args: [Field("rating").greaterThan(0), + Field("title").charLength().lessThan(5), + Field("tags").arrayContains("propaganda")]) ) .select(["title"]) @@ -2810,8 +2826,8 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let pipeline = db.pipeline() .collection(collRef.path) .where(BooleanExpression( - "array_contains_any", - [Field("tags"), ArrayExpression(["politics"])] + functionName: "array_contains_any", + args: [Field("tags"), ArrayExpression(["politics"])] )) .select([Field("title")]) @@ -2832,8 +2848,13 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { let pipeline = db.pipeline() .collection(collRef.path) - .aggregate([AggregateFunction("count_if", [Field("rating").greaterThanOrEqual(4.5)]) - .as("countOfBest")]) + .aggregate( + [AggregateFunction( + functionName: "count_if", + args: [Field("rating").greaterThanOrEqual(4.5)] + ) + .as("countOfBest")] + ) let snapshot = try await pipeline.execute() @@ -2858,7 +2879,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { .collection(collRef.path) .sort( [ - FunctionExpression("char_length", [Field("title")]).ascending(), + FunctionExpression(functionName: "char_length", args: [Field("title")]).ascending(), Field("__name__").descending(), ] ) @@ -2903,36 +2924,37 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { TestHelper.compare(snapshot: snapshot, expected: expectedResults, enforceOrder: false) } - func testSupportsRand() async throws { - let collRef = collectionRef(withDocuments: bookDocs) - let db = collRef.firestore - - let pipeline = db.pipeline() - .collection(collRef.path) - .limit(10) - .select([RandomExpression().as("result")]) - - let snapshot = try await pipeline.execute() - - XCTAssertEqual(snapshot.results.count, 10, "Should fetch 10 documents") - - for doc in snapshot.results { - guard let resultValue = doc.get("result") else { - XCTFail("Document \(doc.id ?? "unknown") should have a 'result' field") - continue - } - guard let doubleValue = resultValue as? Double else { - XCTFail("Result value for document \(doc.id ?? "unknown") is not a Double: \(resultValue)") - continue - } - XCTAssertGreaterThanOrEqual( - doubleValue, - 0.0, - "Result for \(doc.id ?? "unknown") should be >= 0.0" - ) - XCTAssertLessThan(doubleValue, 1.0, "Result for \(doc.id ?? "unknown") should be < 1.0") - } - } +// func testSupportsRand() async throws { +// let collRef = collectionRef(withDocuments: bookDocs) +// let db = collRef.firestore +// +// let pipeline = db.pipeline() +// .collection(collRef.path) +// .limit(10) +// .select([RandomExpression().as("result")]) +// +// let snapshot = try await pipeline.execute() +// +// XCTAssertEqual(snapshot.results.count, 10, "Should fetch 10 documents") +// +// for doc in snapshot.results { +// guard let resultValue = doc.get("result") else { +// XCTFail("Document \(doc.id ?? "unknown") should have a 'result' field") +// continue +// } +// guard let doubleValue = resultValue as? Double else { +// XCTFail("Result value for document \(doc.id ?? "unknown") is not a Double: +// \(resultValue)") +// continue +// } +// XCTAssertGreaterThanOrEqual( +// doubleValue, +// 0.0, +// "Result for \(doc.id ?? "unknown") should be >= 0.0" +// ) +// XCTAssertLessThan(doubleValue, 1.0, "Result for \(doc.id ?? "unknown") should be < 1.0") +// } +// } func testSupportsArray() async throws { let db = firestore() @@ -3104,6 +3126,142 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { } } + func testMapSetAddsNewField() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("title").equal("The Hitchhiker's Guide to the Galaxy")) + .select([ + Field("awards").mapSet(key: "newAward", value: true).as("modifiedAwards"), + Field("title"), + ]) + + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document") + if let resultDoc = snapshot.results.first { + let expectedAwards: [String: Sendable?] = [ + "hugo": true, + "nebula": false, + "others": ["unknown": ["year": 1980]], + "newAward": true, + ] + let expectedResult: [String: Sendable?] = [ + "title": "The Hitchhiker's Guide to the Galaxy", + "modifiedAwards": expectedAwards, + ] + TestHelper.compare(pipelineResult: resultDoc, expected: expectedResult) + } else { + XCTFail("No document retrieved for testMapSetAddsNewField") + } + } + + func testMapSetUpdatesExistingField() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("title").equal("The Hitchhiker's Guide to the Galaxy")) + .select([ + Field("awards").mapSet(key: "hugo", value: false).as("modifiedAwards"), + Field("title"), + ]) + + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document") + if let resultDoc = snapshot.results.first { + let expectedAwards: [String: Sendable?] = [ + "hugo": false, + "nebula": false, + "others": ["unknown": ["year": 1980]], + ] + let expectedResult: [String: Sendable?] = [ + "title": "The Hitchhiker's Guide to the Galaxy", + "modifiedAwards": expectedAwards, + ] + TestHelper.compare(pipelineResult: resultDoc, expected: expectedResult) + } else { + XCTFail("No document retrieved for testMapSetUpdatesExistingField") + } + } + + func testMapSetWithExpressionValue() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("title").equal("The Hitchhiker's Guide to the Galaxy")) + .select( + [ + Field("awards") + .mapSet( + key: "ratingCategory", + value: Field("rating").greaterThan(4.0).then(Constant("high"), else: Constant("low")) + ) + .as("modifiedAwards"), + Field("title"), + ] + ) + + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document") + if let resultDoc = snapshot.results.first { + let expectedAwards: [String: Sendable?] = [ + "hugo": true, + "nebula": false, + "others": ["unknown": ["year": 1980]], + "ratingCategory": "high", + ] + let expectedResult: [String: Sendable?] = [ + "title": "The Hitchhiker's Guide to the Galaxy", + "modifiedAwards": expectedAwards, + ] + TestHelper.compare(pipelineResult: resultDoc, expected: expectedResult) + } else { + XCTFail("No document retrieved for testMapSetWithExpressionValue") + } + } + + func testMapSetWithExpressionKey() async throws { + let collRef = collectionRef(withDocuments: bookDocs) + let db = collRef.firestore + + let pipeline = db.pipeline() + .collection(collRef.path) + .where(Field("title").equal("The Hitchhiker's Guide to the Galaxy")) + .select([ + Field("awards") + .mapSet(key: Constant("dynamicKey"), value: "dynamicValue") + .as("modifiedAwards"), + Field("title"), + ]) + + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document") + if let resultDoc = snapshot.results.first { + let expectedAwards: [String: Sendable?] = [ + "hugo": true, + "nebula": false, + "others": ["unknown": ["year": 1980]], + "dynamicKey": "dynamicValue", + ] + let expectedResult: [String: Sendable?] = [ + "title": "The Hitchhiker's Guide to the Galaxy", + "modifiedAwards": expectedAwards, + ] + TestHelper.compare(pipelineResult: resultDoc, expected: expectedResult) + } else { + XCTFail("No document retrieved for testMapSetWithExpressionKey") + } + } + func testSupportsTimestampConversions() async throws { let db = firestore() let randomCol = collectionRef() // Unique collection for this test @@ -3172,12 +3330,16 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { Field("timestamp").timestampAdd(10, .second).as("plus10seconds"), Field("timestamp").timestampAdd(10, .microsecond).as("plus10micros"), Field("timestamp").timestampAdd(10, .millisecond).as("plus10millis"), + Field("timestamp").timestampAdd(amount: Constant(10), unit: "day") + .as("plus10daysExprUnitSendable"), Field("timestamp").timestampSubtract(10, .day).as("minus10days"), Field("timestamp").timestampSubtract(10, .hour).as("minus10hours"), Field("timestamp").timestampSubtract(10, .minute).as("minus10minutes"), Field("timestamp").timestampSubtract(10, .second).as("minus10seconds"), Field("timestamp").timestampSubtract(10, .microsecond).as("minus10micros"), Field("timestamp").timestampSubtract(10, .millisecond).as("minus10millis"), + Field("timestamp").timestampSubtract(amount: Constant(10), unit: "day") + .as("minus10daysExprUnitSendable"), ] ) @@ -3190,12 +3352,14 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { "plus10seconds": Timestamp(seconds: 1_741_380_245, nanoseconds: 0), "plus10micros": Timestamp(seconds: 1_741_380_235, nanoseconds: 10000), "plus10millis": Timestamp(seconds: 1_741_380_235, nanoseconds: 10_000_000), + "plus10daysExprUnitSendable": Timestamp(seconds: 1_742_244_235, nanoseconds: 0), "minus10days": Timestamp(seconds: 1_740_516_235, nanoseconds: 0), "minus10hours": Timestamp(seconds: 1_741_344_235, nanoseconds: 0), "minus10minutes": Timestamp(seconds: 1_741_379_635, nanoseconds: 0), "minus10seconds": Timestamp(seconds: 1_741_380_225, nanoseconds: 0), "minus10micros": Timestamp(seconds: 1_741_380_234, nanoseconds: 999_990_000), "minus10millis": Timestamp(seconds: 1_741_380_234, nanoseconds: 990_000_000), + "minus10daysExprUnitSendable": Timestamp(seconds: 1_740_516_235, nanoseconds: 0), ] XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document") @@ -3206,6 +3370,56 @@ class PipelineIntegrationTests: FSTIntegrationTestCase { } } + func testTimestampTruncWorks() async throws { + let db = firestore() + let randomCol = collectionRef() + try await randomCol.document("dummyDoc").setData(["field": "value"]) + + let baseTimestamp = Timestamp(seconds: 1_741_380_235, nanoseconds: 123_456_000) + + let pipeline = db.pipeline() + .collection(randomCol.path) + .limit(1) + .select( + [ + Constant(baseTimestamp).timestampTruncate(granularity: "nanosecond").as("truncNano"), + Constant(baseTimestamp).timestampTruncate(granularity: .microsecond).as("truncMicro"), + Constant(baseTimestamp).timestampTruncate(granularity: .millisecond).as("truncMilli"), + Constant(baseTimestamp).timestampTruncate(granularity: .second).as("truncSecond"), + Constant(baseTimestamp).timestampTruncate(granularity: .minute).as("truncMinute"), + Constant(baseTimestamp).timestampTruncate(granularity: .hour).as("truncHour"), + Constant(baseTimestamp).timestampTruncate(granularity: .day).as("truncDay"), + Constant(baseTimestamp).timestampTruncate(granularity: "month").as("truncMonth"), + Constant(baseTimestamp).timestampTruncate(granularity: "year").as("truncYear"), + Constant(baseTimestamp).timestampTruncate(granularity: Constant("day")) + .as("truncDayExpr"), + ] + ) + + let snapshot = try await pipeline.execute() + + XCTAssertEqual(snapshot.results.count, 1, "Should retrieve one document") + + let expectedResults: [String: Timestamp] = [ + "truncNano": Timestamp(seconds: 1_741_380_235, nanoseconds: 123_456_000), + "truncMicro": Timestamp(seconds: 1_741_380_235, nanoseconds: 123_456_000), + "truncMilli": Timestamp(seconds: 1_741_380_235, nanoseconds: 123_000_000), + "truncSecond": Timestamp(seconds: 1_741_380_235, nanoseconds: 0), + "truncMinute": Timestamp(seconds: 1_741_380_180, nanoseconds: 0), + "truncHour": Timestamp(seconds: 1_741_377_600, nanoseconds: 0), + "truncDay": Timestamp(seconds: 1_741_305_600, nanoseconds: 0), // Assuming UTC day start + "truncMonth": Timestamp(seconds: 1_740_787_200, nanoseconds: 0), // Assuming UTC month start + "truncYear": Timestamp(seconds: 1_735_689_600, nanoseconds: 0), // Assuming UTC year start + "truncDayExpr": Timestamp(seconds: 1_741_305_600, nanoseconds: 0), // Assuming UTC day start + ] + + if let resultDoc = snapshot.results.first { + TestHelper.compare(pipelineResult: resultDoc, expected: expectedResults) + } else { + XCTFail("No document retrieved for timestamp trunc test") + } + } + func testCurrentTimestampWorks() async throws { let collRef = collectionRef(withDocuments: ["doc1": ["foo": 1]]) let db = collRef.firestore