Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class Expression(ABC):
"""Represents an expression that can be evaluated to a value within the
execution of a pipeline.

Expressionessions are the building blocks for creating complex queries and
Expressions are the building blocks for creating complex queries and
transformations in Firestore pipelines. They can represent:

- **Field references:** Access values from document fields.
Expand Down Expand Up @@ -786,6 +786,9 @@ def array_get(self, offset: Expression | int) -> "FunctionExpression":
Creates an expression that indexes into an array from the beginning or end and returns the
element. A negative offset starts from the end.

If the expression is evaluated against a non-array type, it evaluates to an error. See `offset`
for an alternative that evaluates to unset instead.

Example:
>>> Array([1,2,3]).array_get(0)

Expand All @@ -799,6 +802,26 @@ def array_get(self, offset: Expression | int) -> "FunctionExpression":
"array_get", [self, self._cast_to_expr_or_convert_to_constant(offset)]
)

@expose_as_static
def offset(self, offset: Expression | int) -> "FunctionExpression":
"""
Creates an expression that indexes into an array from the beginning or end and returns the
element. A negative offset starts from the end.
If the expression is evaluated against a non-array type, it evaluates to unset.

Example:
>>> Array([1,2,3]).offset(0)

Args:
offset: the index of the element to return

Returns:
A new `Expression` representing the `offset` operation.
"""
return FunctionExpression(
"offset", [self, self._cast_to_expr_or_convert_to_constant(offset)]
)

@expose_as_static
def array_contains(
self, element: Expression | CONSTANT_TYPE
Expand Down Expand Up @@ -902,6 +925,70 @@ def array_reverse(self) -> "Expression":
"""
return FunctionExpression("array_reverse", [self])

@expose_as_static
def array_filter(
self,
filter_expr: "BooleanExpression",
element_alias: str | Constant[str],
index_alias: str | Constant[str] | None = None,
) -> "Expression":
"""Filters an array based on a predicate.

Example:
>>> # Filter the 'tags' array to only include the tag "comedy"
>>> Field.of("tags").array_filter(Variable("tag").equal("comedy"), "tag")
>>> # Filter the 'tags' array to only include elements after the first element (index > 0)
>>> Field.of("tags").array_filter(Variable("i").greater_than(0), element_alias="tag", index_alias="i")

Args:
filter_expr: The predicate boolean expression used to filter the elements.
element_alias: A string or string constant used to refer to the current array
element as a variable within the filter expression.
index_alias: An optional string or string constant used to refer to the index
of the current array element as a variable within the filter expression.

Returns:
A new `Expression` representing the filtered array.
"""
args = [self, self._cast_to_expr_or_convert_to_constant(element_alias)]
if index_alias is not None:
args.append(self._cast_to_expr_or_convert_to_constant(index_alias))
args.append(filter_expr)

return FunctionExpression("array_filter", args)

@expose_as_static
def array_transform(
self,
transform_expr: "Expression",
element_alias: str | Constant[str],
index_alias: str | Constant[str] | None = None,
) -> "Expression":
"""Creates an expression that applies a provided transformation to each element in an array.

Example:
>>> # Convert each tag in the 'tags' array to uppercase
>>> Field.of("tags").array_transform(Variable("tag").to_upper(), "tag")
>>> # Append the index to each tag in the 'tags' array
>>> Field.of("tags").array_transform(Variable("tag").string_concat(Variable("i")), element_alias="tag", index_alias="i")

Args:
transform_expr: The expression used to transform the elements.
element_alias: A string or string constant used to refer to the current array
element as a variable within the transform expression.
index_alias: An optional string or string constant used to refer to the index
of the current array element as a variable within the transform expression.

Returns:
A new `Expression` representing the transformed array.
"""
args = [self, self._cast_to_expr_or_convert_to_constant(element_alias)]
if index_alias is not None:
args.append(self._cast_to_expr_or_convert_to_constant(index_alias))
args.append(transform_expr)

return FunctionExpression("array_transform", args)

@expose_as_static
def array_concat(
self, *other_arrays: Array | list[Expression | CONSTANT_TYPE] | Expression
Expand Down Expand Up @@ -963,7 +1050,7 @@ def is_absent(self) -> "BooleanExpression":
>>> Field.of("email").is_absent()

Returns:
A new `BooleanExpressionession` representing the isAbsent operation.
A new `BooleanExpression` representing the isAbsent operation.
"""
return BooleanExpression("is_absent", [self])

Expand Down Expand Up @@ -1031,6 +1118,76 @@ def exists(self) -> "BooleanExpression":
"""
return BooleanExpression("exists", [self])

@expose_as_static
def coalesce(self, *others: Expression | CONSTANT_TYPE) -> "Expression":
"""Creates an expression that evaluates to the first non-null, non-error value.

Example:
>>> # Return the "preferredName" field if it exists.
>>> # Otherwise, check the "fullName" field.
>>> # Otherwise, return the literal string "Anonymous".
>>> Field.of("preferredName").coalesce(Field.of("fullName"), "Anonymous")

>>> # Equivalent static call:
>>> Expression.coalesce(Field.of("preferredName"), Field.of("fullName"), "Anonymous")

Args:
*others: Additional expressions or constants to evaluate if the current
expression evaluates to null or error.

Returns:
An Expression representing the coalesce operation.
"""
return FunctionExpression(
"coalesce",
[self]
+ [Expression._cast_to_expr_or_convert_to_constant(x) for x in others],
)

@expose_as_static
def switch_on(
self, result: Expression | CONSTANT_TYPE, *others: Expression | CONSTANT_TYPE
) -> "Expression":
"""Creates an expression that evaluates to the result corresponding to the first true condition.

This function behaves like a `switch` statement. It accepts an alternating sequence of
conditions and their corresponding results. If an odd number of arguments is provided, the
final argument serves as a default fallback result. If no default is provided and no condition
evaluates to true, it throws an error.

Example:
>>> # Return "Pending" if status is 1, "Active" if status is 2, otherwise "Unknown"
>>> Field.of("status").equal(1).switch_on(
... "Pending", Field.of("status").equal(2), "Active", "Unknown"
... )

Args:
result: The result to return if this condition is true.
*others: Additional alternating conditions and results, optionally followed by a default value.

Returns:
An Expression representing the "switch_on" operation.
"""
return FunctionExpression(
"switch_on",
[self, Expression._cast_to_expr_or_convert_to_constant(result)]
+ [Expression._cast_to_expr_or_convert_to_constant(x) for x in others],
)

@expose_as_static
def storage_size(self) -> "Expression":
"""Calculates the Firestore storage size of a given value.

Mirrors the sizing rules detailed in Firebase/Firestore documentation.

Example:
>>> Field.of("content").storage_size()

Returns:
A new `Expression` representing the storage size.
"""
return FunctionExpression("storage_size", [self])

@expose_as_static
def sum(self) -> "Expression":
"""Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs.
Expand Down Expand Up @@ -1391,6 +1548,7 @@ def join(self, delimeter: Expression | str) -> "Expression":
@expose_as_static
def map_get(self, key: str | Constant[str]) -> "Expression":
"""Accesses a value from the map produced by evaluating this expression.
If the expression is evaluated against a non-map type, it evaluates to an error.

Example:
>>> Map({"city": "London"}).map_get("city")
Expand Down Expand Up @@ -2844,6 +3002,22 @@ def __init__(self, *conditions: "BooleanExpression"):
super().__init__("or", conditions, use_infix_repr=False)


class Nor(BooleanExpression):
"""
Represents an expression that performs a logical 'NOR' operation on multiple filter conditions.

Example:
>>> # Check if neither the 'age' field is greater than 18 nor the 'city' field is "London"
>>> Nor(Field.of("age").greater_than(18), Field.of("city").equal("London"))

Args:
*conditions: The filter conditions to 'NOR' together.
"""

def __init__(self, *conditions: "BooleanExpression"):
super().__init__("nor", conditions, use_infix_repr=False)


class Xor(BooleanExpression):
"""
Represents an expression that performs a logical 'XOR' (exclusive OR) operation on multiple filter conditions.
Expand Down
Loading
Loading