From bfc427407faff91b56b725ca469c7deffcd92b26 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 17:44:54 -0700 Subject: [PATCH 01/12] added new expressions --- .../firestore_v1/pipeline_expressions.py | 86 +++++++++++++++++++ .../tests/system/pipeline_e2e/general.yaml | 56 ++++++++++++ .../tests/system/pipeline_e2e/logical.yaml | 32 +++++++ .../unit/v1/test_pipeline_expressions.py | 41 +++++++++ 4 files changed, 215 insertions(+) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 969ddf2794a5..cccb71cbf70b 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1006,6 +1006,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. + """ + args = [self] + args.extend( + [Expression._cast_to_expr_or_convert_to_constant(x) for x in others] + ) + return FunctionExpression("coalesce", args) + + @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 switchOn operation. + """ + args = [self, Expression._cast_to_expr_or_convert_to_constant(result)] + args.extend( + [Expression._cast_to_expr_or_convert_to_constant(x) for x in others] + ) + return FunctionExpression("switch_on", args) + + @expose_as_static + def parent(self) -> "Expression": + """Creates an expression that returns the parent document of a document reference. + + Example: + >>> # Get the parent document of a document reference. + >>> Field.of("__path__").parent() + + Returns: + An Expression representing the parent operation. + """ + return FunctionExpression("parent", [self]) + + @expose_as_static def sum(self) -> "Expression": """Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. @@ -2808,6 +2878,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. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index b14e636d1ebc..0221112f478f 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -765,3 +765,59 @@ tests: res: fieldReferenceValue: res name: select + - description: testCoalesce + pipeline: + - Literals: + - res: + FunctionExpression.coalesce: + - Constant: null + - Constant: "B" + - Select: + - res + assert_results: + - res: "B" + - description: testSwitchOn + pipeline: + - Literals: + - res: + FunctionExpression.switch_on: + - FunctionExpression.equal: + - Constant: 1 + - Constant: 2 + - Constant: "A" + - FunctionExpression.equal: + - Constant: 1 + - Constant: 1 + - Constant: "B" + - Constant: "C" + - Select: + - res + assert_results: + - res: "B" + - description: testParent + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.parent: + - Field: __path__ + - res + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: 1 + name: limit + - args: + - mapValue: + fields: + res: + functionValue: + args: + - fieldReferenceValue: __path__ + name: parent + name: select diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml index a3de3a4a07fe..206935f5b816 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml @@ -759,3 +759,35 @@ tests: - "value_or_default" assert_results: - value_or_default: "1984" + - description: whereByNorCondition + pipeline: + - Collection: books + - Where: + - Nor: + - FunctionExpression.greater_than: + - Field: rating + - Constant: 4.5 + - FunctionExpression.equal: + - Field: genre + - Constant: Science Fiction + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: rating + - doubleValue: 4.5 + name: greater_than + - functionValue: + args: + - fieldReferenceValue: genre + - stringValue: Science Fiction + name: equal + name: nor + name: where diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index 98db3c3a8f17..5af54403fdb9 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -735,6 +735,14 @@ def test_or(self): assert instance.params == [arg1, arg2] assert repr(instance) == "Or(Arg1, Arg2)" + def test_nor(self): + arg1 = self._make_arg("Arg1") + arg2 = self._make_arg("Arg2") + instance = expr.Nor(arg1, arg2) + assert instance.name == "nor" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Nor(Arg1, Arg2)" + def test_array_get(self): arg1 = self._make_arg("ArrayField") arg2 = self._make_arg("Offset") @@ -904,6 +912,39 @@ def test_if_error(self): infix_instance = arg1.if_error(arg2) assert infix_instance == instance + def test_coalesce(self): + arg1 = self._make_arg("Arg1") + arg2 = self._make_arg("Arg2") + arg3 = self._make_arg("Arg3") + instance = Expression.coalesce(arg1, arg2, arg3) + assert instance.name == "coalesce" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "Arg1.coalesce(Arg2, Arg3)" + infix_instance = arg1.coalesce(arg2, arg3) + assert infix_instance == instance + + def test_switch_on(self): + arg1 = self._make_arg("Condition1") + arg2 = self._make_arg("Result1") + arg3 = self._make_arg("Condition2") + arg4 = self._make_arg("Result2") + arg5 = self._make_arg("Default") + instance = Expression.switch_on(arg1, arg2, arg3, arg4, arg5) + assert instance.name == "switch_on" + assert instance.params == [arg1, arg2, arg3, arg4, arg5] + assert repr(instance) == "Condition1.switch_on(Result1, Condition2, Result2, Default)" + infix_instance = arg1.switch_on(arg2, arg3, arg4, arg5) + assert infix_instance == instance + + def test_parent(self): + arg1 = self._make_arg("Input") + instance = Expression.parent(arg1) + assert instance.name == "parent" + assert instance.params == [arg1] + assert repr(instance) == "Input.parent()" + infix_instance = arg1.parent() + assert infix_instance == instance + def test_not(self): arg1 = self._make_arg("Condition") instance = expr.Not(arg1) From 1712ad7d47795ad4b100ec0bb2e6cad7798339c7 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 18:05:38 -0700 Subject: [PATCH 02/12] added offset --- .../firestore_v1/pipeline_expressions.py | 24 ++++ .../tests/system/pipeline_e2e/array.yaml | 122 ++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 10 ++ 3 files changed, 156 insertions(+) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index cccb71cbf70b..6dc8aed8824d 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -761,6 +761,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) @@ -774,6 +777,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 @@ -1436,6 +1459,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") diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml index e29ef0d6c2ed..cbe1a5104707 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -424,6 +424,90 @@ tests: - integerValue: '0' name: array_get name: select + - description: testArrayGet_NonArray + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.is_error: + - FunctionExpression.array_get: + - Field: title + - Constant: 0 + - "isError" + assert_results: + - isError: true + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + isError: + functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: title + - integerValue: '0' + name: array_get + name: is_error + name: select + - description: testOffset_NonArray + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.is_absent: + - FunctionExpression.offset: + - Field: title + - Constant: 0 + - "isAbsent" + assert_results: + - isAbsent: true + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + isAbsent: + functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: title + - integerValue: '0' + name: offset + name: is_absent + name: select - description: testArrayGet_NegativeOffset pipeline: - Collection: books @@ -462,6 +546,44 @@ tests: - integerValue: '-1' name: array_get name: select + - description: testOffset + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.offset: + - Field: tags + - Constant: -1 + - "lastTag" + assert_results: + - lastTag: "adventure" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + lastTag: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '-1' + name: offset + name: select - description: testArrayFirst pipeline: - Collection: books diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index 5af54403fdb9..a4d563e6f385 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -753,6 +753,16 @@ def test_array_get(self): infix_istance = arg1.array_get(arg2) assert infix_istance == instance + def test_offset(self): + arg1 = self._make_arg("ArrayField") + arg2 = self._make_arg("Offset") + instance = Expression.offset(arg1, arg2) + assert instance.name == "offset" + assert instance.params == [arg1, arg2] + assert repr(instance) == "ArrayField.offset(Offset)" + infix_istance = arg1.offset(arg2) + assert infix_istance == instance + def test_array_contains(self): arg1 = self._make_arg("ArrayField") arg2 = self._make_arg("Element") From c825560d642a6df076c79db42c13e26c97c15310 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 18:17:05 -0700 Subject: [PATCH 03/12] fixed e2e tests --- .../tests/system/pipeline_e2e/array.yaml | 40 +++---------------- .../tests/system/pipeline_e2e/general.yaml | 2 +- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml index cbe1a5104707..d7440f1be8bf 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -473,41 +473,11 @@ tests: - FunctionExpression.equal: - Field: title - Constant: "The Hitchhiker's Guide to the Galaxy" - - Select: - - AliasedExpression: - - FunctionExpression.is_absent: - - FunctionExpression.offset: - - Field: title - - Constant: 0 - - "isAbsent" - assert_results: - - isAbsent: true - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - functionValue: - args: - - fieldReferenceValue: title - - stringValue: "The Hitchhiker's Guide to the Galaxy" - name: equal - name: where - - args: - - mapValue: - fields: - isAbsent: - functionValue: - args: - - functionValue: - args: - - fieldReferenceValue: title - - integerValue: '0' - name: offset - name: is_absent - name: select + - Where: + - FunctionExpression.offset: + - Field: title + - Constant: 0 + assert_count: 0 - description: testArrayGet_NegativeOffset pipeline: - Collection: books diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index 0221112f478f..9ab493d20e11 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -810,7 +810,7 @@ tests: - referenceValue: /books name: collection - args: - - integerValue: 1 + - integerValue: '1' name: limit - args: - mapValue: From 0397b5cd6d481cff6ef9fc8f3f4f335dd9fabfbd Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 18:19:06 -0700 Subject: [PATCH 04/12] fixed lint --- .../google/cloud/firestore_v1/pipeline_expressions.py | 1 - .../tests/unit/v1/test_pipeline_expressions.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 6dc8aed8824d..83a8970b3f29 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1098,7 +1098,6 @@ def parent(self) -> "Expression": """ return FunctionExpression("parent", [self]) - @expose_as_static def sum(self) -> "Expression": """Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index a4d563e6f385..ea0aa3c04806 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -942,7 +942,10 @@ def test_switch_on(self): instance = Expression.switch_on(arg1, arg2, arg3, arg4, arg5) assert instance.name == "switch_on" assert instance.params == [arg1, arg2, arg3, arg4, arg5] - assert repr(instance) == "Condition1.switch_on(Result1, Condition2, Result2, Default)" + assert ( + repr(instance) + == "Condition1.switch_on(Result1, Condition2, Result2, Default)" + ) infix_instance = arg1.switch_on(arg2, arg3, arg4, arg5) assert infix_instance == instance From 63bdc38428cd76b0d3faee54134ab2244f9caf12 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 20:16:12 -0700 Subject: [PATCH 05/12] added new expressions --- .../firestore_v1/pipeline_expressions.py | 130 ++++++++++++++++- .../tests/system/pipeline_e2e/array.yaml | 132 ++++++++++++++++++ .../tests/system/pipeline_e2e/general.yaml | 112 +++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 105 +++++++++++++- 4 files changed, 474 insertions(+), 5 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 83a8970b3f29..e684a04c769b 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -188,7 +188,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. @@ -900,6 +900,68 @@ 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(Field.of("tag").equal("comedy"), "tag") + >>> # Filter the 'tags' array to only include elements after the first element (index > 0) + >>> Field.of("tags").array_filter(Field.of("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 within the filter expression. + index_alias: An optional string or string constant used to refer to the index + of the current array element 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(Field.of("tag").to_upper(), "tag") + >>> # Append the index to each tag in the 'tags' array + >>> Field.of("tags").array_transform(Field.of("tag").string_concat(Field.of("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 within the transform expression. + index_alias: An optional string or string constant used to refer to the index of the current array element 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 @@ -961,7 +1023,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]) @@ -1098,6 +1160,70 @@ def parent(self) -> "Expression": """ return FunctionExpression("parent", [self]) + @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 namespace(self) -> "Expression": + """Extracts the namespace from a document reference. + + Example: + >>> Field.of("__path__").namespace() + + Returns: + A new `Expression` representing the namespace extraction. + """ + return FunctionExpression("namespace", [self]) + + @expose_as_static + def has_ancestor(self, ancestor: Expression | CONSTANT_TYPE) -> "BooleanExpression": + """Checks if the current document has the specified ancestor. + + Example: + >>> Field.of("__path__").has_ancestor("projects/my-project/databases/(default)/documents/users/user1") + + Args: + ancestor: The DocumentReference of the potential ancestor. + + Returns: + A new `BooleanExpression` representing the has_ancestor check. + """ + return BooleanExpression( + "has_ancestor", [self, self._cast_to_expr_or_convert_to_constant(ancestor)] + ) + + @expose_as_static + def reference_slice( + self, start: int | Expression, end: int | Expression | None = None + ) -> "Expression": + """Extracts a slice of the path segments from a document reference. + + Example: + >>> Field.of("__path__").reference_slice(1, 3) + + Args: + start: The starting index of the path segment. + end: The ending index (exclusive) of the path segment. If None, slices to the end. + + Returns: + A new `Expression` representing the sliced path. + """ + args = [self, self._cast_to_expr_or_convert_to_constant(start)] + if end is not None: + args.append(self._cast_to_expr_or_convert_to_constant(end)) + return FunctionExpression("reference_slice", args) + @expose_as_static def sum(self) -> "Expression": """Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml index d7440f1be8bf..94454ee7505a 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -892,3 +892,135 @@ tests: - stringValue: "Science Fiction" name: array_index_of_all name: select + - description: testArrayFilter + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_filter: + - Field: tags + - "tag" + - FunctionExpression.equal: + - Field: tag + - Constant: comedy + - "comedyTag" + assert_results: + - comedyTag: ["comedy"] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + comedyTag: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "tag" + - functionValue: + args: + - fieldReferenceValue: tag + - stringValue: "comedy" + name: equal + name: array_filter + name: select + - description: testArrayTransform + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_transform: + - Field: tags + - "tag" + - FunctionExpression.to_upper: + - Field: tag + - "upperTags" + assert_results: + - upperTags: ["COMEDY", "SPACE", "ADVENTURE"] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + upperTags: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "tag" + - functionValue: + args: + - fieldReferenceValue: tag + name: to_upper + name: array_transform + name: select + - description: testArrayTransformWithIndex + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_transform: + - Field: tags + - "tag" + - Field: i + - index_alias: "i" + - "indexedTags" + assert_results: + - indexedTags: [0, 1, 2] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + indexedTags: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "tag" + - stringValue: "i" + - fieldReferenceValue: i + name: array_transform + name: select diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index 9ab493d20e11..196791846081 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -821,3 +821,115 @@ tests: - fieldReferenceValue: __path__ name: parent name: select + - description: testStorageSize + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.storage_size: + - Field: __path__ + - res + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + res: + functionValue: + args: + - fieldReferenceValue: __path__ + name: storage_size + name: select + - description: testNamespace + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.namespace: + - Field: __path__ + - res + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + res: + functionValue: + args: + - fieldReferenceValue: __path__ + name: namespace + name: select + - description: testHasAncestor + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.has_ancestor: + - Field: __path__ + - Constant: "projects/test/databases/(default)/documents" + - res + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + res: + functionValue: + args: + - fieldReferenceValue: __path__ + - stringValue: "projects/test/databases/(default)/documents" + name: has_ancestor + name: select + - description: testReferenceSlice + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.reference_slice: + - Field: __path__ + - Constant: 1 + - res + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + res: + functionValue: + args: + - fieldReferenceValue: __path__ + - integerValue: '1' + name: reference_slice + name: select diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index ea0aa3c04806..09bebf5e5578 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -958,6 +958,45 @@ def test_parent(self): infix_instance = arg1.parent() assert infix_instance == instance + def test_storage_size(self): + arg1 = self._make_arg("Input") + instance = Expression.storage_size(arg1) + assert instance.name == "storage_size" + assert instance.params == [arg1] + assert repr(instance) == "Input.storage_size()" + infix_instance = arg1.storage_size() + assert infix_instance == instance + + def test_namespace(self): + arg1 = self._make_arg("Input") + instance = Expression.namespace(arg1) + assert instance.name == "namespace" + assert instance.params == [arg1] + assert repr(instance) == "Input.namespace()" + infix_instance = arg1.namespace() + assert infix_instance == instance + + def test_has_ancestor(self): + arg1 = self._make_arg("Input") + arg2 = self._make_arg("Ancestor") + instance = Expression.has_ancestor(arg1, arg2) + assert instance.name == "has_ancestor" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Input.has_ancestor(Ancestor)" + infix_instance = arg1.has_ancestor(arg2) + assert infix_instance == instance + + def test_reference_slice(self): + arg1 = self._make_arg("Input") + arg2 = self._make_arg("Start") + arg3 = self._make_arg("End") + instance = Expression.reference_slice(arg1, arg2, arg3) + assert instance.name == "reference_slice" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "Input.reference_slice(Start, End)" + infix_instance = arg1.reference_slice(arg2, arg3) + assert infix_instance == instance + def test_not(self): arg1 = self._make_arg("Condition") instance = expr.Not(arg1) @@ -1649,14 +1688,74 @@ def test_array_length(self): assert infix_instance == instance def test_array_reverse(self): - arg1 = self._make_arg("Array") + arg1 = self._make_arg("ArrayField") instance = Expression.array_reverse(arg1) assert instance.name == "array_reverse" assert instance.params == [arg1] - assert repr(instance) == "Array.array_reverse()" - infix_instance = arg1.array_reverse() + assert repr(instance) == "ArrayField.array_reverse()" + infix_istance = arg1.array_reverse() + assert infix_istance == instance + + def test_array_filter(self): + arg1 = self._make_arg("ArrayField") + arg2 = "element_alias" + arg3 = self._make_arg("FilterExpr", expr_type=BooleanExpression) + instance = Expression.array_filter(arg1, arg3, arg2) + assert instance.name == "array_filter" + assert instance.params == [arg1, Constant.of(arg2), arg3] + assert ( + repr(instance) + == "ArrayField.array_filter(Constant.of('element_alias'), FilterExpr)" + ) + infix_instance = arg1.array_filter(arg3, arg2) assert infix_instance == instance + arg4 = "index_alias" + instance_with_idx = Expression.array_filter(arg1, arg3, arg2, arg4) + assert instance_with_idx.name == "array_filter" + assert instance_with_idx.params == [ + arg1, + Constant.of(arg2), + Constant.of(arg4), + arg3, + ] + assert ( + repr(instance_with_idx) + == "ArrayField.array_filter(Constant.of('element_alias'), FilterExpr, Constant.of('index_alias'))" + ) + infix_instance_with_idx = arg1.array_filter(arg3, arg2, arg4) + assert infix_instance_with_idx == instance_with_idx + + def test_array_transform(self): + arg1 = self._make_arg("ArrayField") + arg2 = "element_alias" + arg3 = self._make_arg("TransformExpr") + instance = Expression.array_transform(arg1, arg3, arg2) + assert instance.name == "array_transform" + assert instance.params == [arg1, Constant.of(arg2), arg3] + assert ( + repr(instance) + == "ArrayField.array_transform(Constant.of('element_alias'), TransformExpr)" + ) + infix_instance = arg1.array_transform(arg3, arg2) + assert infix_instance == instance + + arg4 = "index_alias" + instance_with_idx = Expression.array_transform(arg1, arg3, arg2, arg4) + assert instance_with_idx.name == "array_transform" + assert instance_with_idx.params == [ + arg1, + Constant.of(arg2), + Constant.of(arg4), + arg3, + ] + assert ( + repr(instance_with_idx) + == "ArrayField.array_transform(Constant.of('element_alias'), TransformExpr, Constant.of('index_alias'))" + ) + infix_instance_with_idx = arg1.array_transform(arg3, arg2, arg4) + assert infix_instance_with_idx == instance_with_idx + def test_array_concat(self): arg1 = self._make_arg("ArrayRef1") arg2 = self._make_arg("ArrayRef2") From 9f5d842775741f5725658ec832d89638ed1d36d0 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 20:30:53 -0700 Subject: [PATCH 06/12] updated tests --- .../tests/system/pipeline_e2e/array.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml index 94454ee7505a..3e0f1ba22762 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -903,10 +903,10 @@ tests: - AliasedExpression: - FunctionExpression.array_filter: - Field: tags - - "tag" - FunctionExpression.equal: - Field: tag - Constant: comedy + - "tag" - "comedyTag" assert_results: - comedyTag: ["comedy"] @@ -949,9 +949,9 @@ tests: - AliasedExpression: - FunctionExpression.array_transform: - Field: tags - - "tag" - FunctionExpression.to_upper: - Field: tag + - "tag" - "upperTags" assert_results: - upperTags: ["COMEDY", "SPACE", "ADVENTURE"] @@ -993,9 +993,9 @@ tests: - AliasedExpression: - FunctionExpression.array_transform: - Field: tags + - Field: "i" - "tag" - - Field: i - - index_alias: "i" + - "i" - "indexedTags" assert_results: - indexedTags: [0, 1, 2] From e0f5275b627d2b7073d676ef392a60a5c2112c79 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 20:46:03 -0700 Subject: [PATCH 07/12] fix e2e tests by using variables --- .../cloud/firestore_v1/pipeline_expressions.py | 18 ++++++++++-------- .../tests/system/pipeline_e2e/array.yaml | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index e684a04c769b..e755575c5c59 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -911,16 +911,16 @@ def array_filter( Example: >>> # Filter the 'tags' array to only include the tag "comedy" - >>> Field.of("tags").array_filter(Field.of("tag").equal("comedy"), "tag") + >>> 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(Field.of("i").greater_than(0), element_alias="tag", index_alias="i") + >>> 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 within the filter expression. + 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 within the filter expression. + of the current array element as a variable within the filter expression. Returns: A new `Expression` representing the filtered array. @@ -943,14 +943,16 @@ def array_transform( Example: >>> # Convert each tag in the 'tags' array to uppercase - >>> Field.of("tags").array_transform(Field.of("tag").to_upper(), "tag") + >>> 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(Field.of("tag").string_concat(Field.of("i")), element_alias="tag", index_alias="i") + >>> 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 within the transform expression. - index_alias: An optional string or string constant used to refer to the index of the current array element within the transform expression. + 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. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml index 3e0f1ba22762..5b1a7f1aa05a 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -904,7 +904,7 @@ tests: - FunctionExpression.array_filter: - Field: tags - FunctionExpression.equal: - - Field: tag + - Variable: tag - Constant: comedy - "tag" - "comedyTag" @@ -950,7 +950,7 @@ tests: - FunctionExpression.array_transform: - Field: tags - FunctionExpression.to_upper: - - Field: tag + - Variable: tag - "tag" - "upperTags" assert_results: @@ -993,7 +993,7 @@ tests: - AliasedExpression: - FunctionExpression.array_transform: - Field: tags - - Field: "i" + - Variable: "i" - "tag" - "i" - "indexedTags" From d2d4cfef6d69cfc2109699c608e7d9c28fb00f31 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 20:53:25 -0700 Subject: [PATCH 08/12] removed unneded expressions --- .../firestore_v1/pipeline_expressions.py | 29 ---------- .../tests/system/pipeline_e2e/general.yaml | 56 ------------------- .../unit/v1/test_pipeline_expressions.py | 21 +------ 3 files changed, 1 insertion(+), 105 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index e755575c5c59..4ae5fd922b87 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1176,35 +1176,6 @@ def storage_size(self) -> "Expression": """ return FunctionExpression("storage_size", [self]) - @expose_as_static - def namespace(self) -> "Expression": - """Extracts the namespace from a document reference. - - Example: - >>> Field.of("__path__").namespace() - - Returns: - A new `Expression` representing the namespace extraction. - """ - return FunctionExpression("namespace", [self]) - - @expose_as_static - def has_ancestor(self, ancestor: Expression | CONSTANT_TYPE) -> "BooleanExpression": - """Checks if the current document has the specified ancestor. - - Example: - >>> Field.of("__path__").has_ancestor("projects/my-project/databases/(default)/documents/users/user1") - - Args: - ancestor: The DocumentReference of the potential ancestor. - - Returns: - A new `BooleanExpression` representing the has_ancestor check. - """ - return BooleanExpression( - "has_ancestor", [self, self._cast_to_expr_or_convert_to_constant(ancestor)] - ) - @expose_as_static def reference_slice( self, start: int | Expression, end: int | Expression | None = None diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index 196791846081..dbfc51759be1 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -848,62 +848,6 @@ tests: - fieldReferenceValue: __path__ name: storage_size name: select - - description: testNamespace - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.namespace: - - Field: __path__ - - res - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - integerValue: '1' - name: limit - - args: - - mapValue: - fields: - res: - functionValue: - args: - - fieldReferenceValue: __path__ - name: namespace - name: select - - description: testHasAncestor - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.has_ancestor: - - Field: __path__ - - Constant: "projects/test/databases/(default)/documents" - - res - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - integerValue: '1' - name: limit - - args: - - mapValue: - fields: - res: - functionValue: - args: - - fieldReferenceValue: __path__ - - stringValue: "projects/test/databases/(default)/documents" - name: has_ancestor - name: select - description: testReferenceSlice pipeline: - Collection: books diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index 09bebf5e5578..e7576e4abd58 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -967,25 +967,6 @@ def test_storage_size(self): infix_instance = arg1.storage_size() assert infix_instance == instance - def test_namespace(self): - arg1 = self._make_arg("Input") - instance = Expression.namespace(arg1) - assert instance.name == "namespace" - assert instance.params == [arg1] - assert repr(instance) == "Input.namespace()" - infix_instance = arg1.namespace() - assert infix_instance == instance - - def test_has_ancestor(self): - arg1 = self._make_arg("Input") - arg2 = self._make_arg("Ancestor") - instance = Expression.has_ancestor(arg1, arg2) - assert instance.name == "has_ancestor" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Input.has_ancestor(Ancestor)" - infix_instance = arg1.has_ancestor(arg2) - assert infix_instance == instance - def test_reference_slice(self): arg1 = self._make_arg("Input") arg2 = self._make_arg("Start") @@ -1751,7 +1732,7 @@ def test_array_transform(self): ] assert ( repr(instance_with_idx) - == "ArrayField.array_transform(Constant.of('element_alias'), TransformExpr, Constant.of('index_alias'))" + == "ArrayField.array_transform(TransformExpr, Constant.of('element_alias'), Constant.of('index_alias'))" ) infix_instance_with_idx = arg1.array_transform(arg3, arg2, arg4) assert infix_instance_with_idx == instance_with_idx From 730aa009e05135e2d0a13b329a3ffa3905297574 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 2 Apr 2026 21:42:44 -0700 Subject: [PATCH 09/12] fixing tests --- .../firestore_v1/pipeline_expressions.py | 22 ++++---- .../tests/system/pipeline_e2e/general.yaml | 35 ++++++++---- .../tests/system/pipeline_e2e/logical.yaml | 55 ++++++++++++++++--- .../unit/v1/test_pipeline_expressions.py | 6 +- 4 files changed, 87 insertions(+), 31 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 4ae5fd922b87..fc5d61188309 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1155,10 +1155,10 @@ def parent(self) -> "Expression": Example: >>> # Get the parent document of a document reference. - >>> Field.of("__path__").parent() + >>> Field.of("__name__").parent() Returns: - An Expression representing the parent operation. + An Expression representing that returns a reference to the parent document. """ return FunctionExpression("parent", [self]) @@ -1178,23 +1178,25 @@ def storage_size(self) -> "Expression": @expose_as_static def reference_slice( - self, start: int | Expression, end: int | Expression | None = None + self, offset: int | Expression, length: int | Expression ) -> "Expression": """Extracts a slice of the path segments from a document reference. Example: - >>> Field.of("__path__").reference_slice(1, 3) + >>> Field.of("__name__").reference_slice(1, 2) Args: - start: The starting index of the path segment. - end: The ending index (exclusive) of the path segment. If None, slices to the end. + offset: The starting index of the path segment. + length: The number of segments to include in the slice. Returns: - A new `Expression` representing the sliced path. + A new `Expression` that returns a reference to the sliced portion of the document path. """ - args = [self, self._cast_to_expr_or_convert_to_constant(start)] - if end is not None: - args.append(self._cast_to_expr_or_convert_to_constant(end)) + args = [ + self, + self._cast_to_expr_or_convert_to_constant(offset), + self._cast_to_expr_or_convert_to_constant(length), + ] return FunctionExpression("reference_slice", args) @expose_as_static diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index dbfc51759be1..d9fd862e7b84 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -801,8 +801,10 @@ tests: - Select: - AliasedExpression: - FunctionExpression.parent: - - Field: __path__ + - Field: __name__ - res + assert_results: + - res: "/books/book1" assert_proto: pipeline: stages: @@ -818,7 +820,7 @@ tests: res: functionValue: args: - - fieldReferenceValue: __path__ + - fieldReferenceValue: __name__ name: parent name: select - description: testStorageSize @@ -828,8 +830,10 @@ tests: - Select: - AliasedExpression: - FunctionExpression.storage_size: - - Field: __path__ + - Field: __name__ - res + assert_results: + - res: 29 assert_proto: pipeline: stages: @@ -845,19 +849,25 @@ tests: res: functionValue: args: - - fieldReferenceValue: __path__ + - fieldReferenceValue: __name__ name: storage_size name: select - description: testReferenceSlice pipeline: - Collection: books - - Limit: 1 + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "1984" - Select: - AliasedExpression: - FunctionExpression.reference_slice: - - Field: __path__ - - Constant: 1 + - Field: __name__ + - 0 + - 1 - res + assert_results: + - res: "/books/book8" assert_proto: pipeline: stages: @@ -865,15 +875,20 @@ tests: - referenceValue: /books name: collection - args: - - integerValue: '1' - name: limit + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: '1984' + name: equal + name: where - args: - mapValue: fields: res: functionValue: args: - - fieldReferenceValue: __path__ + - fieldReferenceValue: __name__ + - integerValue: '4' - integerValue: '1' name: reference_slice name: select diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml index 206935f5b816..775c7c78ea80 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml @@ -764,12 +764,27 @@ tests: - Collection: books - Where: - Nor: - - FunctionExpression.greater_than: - - Field: rating - - Constant: 4.5 - FunctionExpression.equal: - Field: genre - - Constant: Science Fiction + - Constant: Romance + - FunctionExpression.equal: + - Field: genre + - Constant: Dystopian + - FunctionExpression.equal: + - Field: genre + - Constant: Fantasy + - FunctionExpression.greater_than: + - Field: published + - Constant: 1949 + - Select: + - title + - Sort: + - Ordering: + - Field: title + - ASCENDING + assert_results: + - title: "Crime and Punishment" + - title: "The Great Gatsby" assert_proto: pipeline: stages: @@ -781,13 +796,37 @@ tests: args: - functionValue: args: - - fieldReferenceValue: rating - - doubleValue: 4.5 - name: greater_than + - fieldReferenceValue: genre + - stringValue: Romance + name: equal - functionValue: args: - fieldReferenceValue: genre - - stringValue: Science Fiction + - stringValue: Dystopian + name: equal + - functionValue: + args: + - fieldReferenceValue: genre + - stringValue: Fantasy name: equal + - functionValue: + args: + - fieldReferenceValue: published + - integerValue: '1949' + name: greater_than name: nor name: where + - args: + - mapValue: + fields: + title: + fieldReferenceValue: title + name: select + - args: + - mapValue: + fields: + direction: + stringValue: ascending + expression: + fieldReferenceValue: title + name: sort diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index e7576e4abd58..c6f35a240296 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -969,12 +969,12 @@ def test_storage_size(self): def test_reference_slice(self): arg1 = self._make_arg("Input") - arg2 = self._make_arg("Start") - arg3 = self._make_arg("End") + arg2 = self._make_arg("Offset") + arg3 = self._make_arg("Length") instance = Expression.reference_slice(arg1, arg2, arg3) assert instance.name == "reference_slice" assert instance.params == [arg1, arg2, arg3] - assert repr(instance) == "Input.reference_slice(Start, End)" + assert repr(instance) == "Input.reference_slice(Offset, Length)" infix_instance = arg1.reference_slice(arg2, arg3) assert infix_instance == instance From a2e64548c7890b26bb8740c6b3cb4b430c5d28ac Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 3 Apr 2026 15:03:35 -0700 Subject: [PATCH 10/12] fixed e2e test --- .../tests/system/pipeline_e2e/general.yaml | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index d9fd862e7b84..f1fe80b2dd7e 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -861,13 +861,14 @@ tests: - Constant: "1984" - Select: - AliasedExpression: - - FunctionExpression.reference_slice: - - Field: __name__ - - 0 - - 1 + - FunctionExpression.document_id: + - FunctionExpression.reference_slice: + - Field: __name__ + - Constant: 0 + - Constant: 1 - res assert_results: - - res: "/books/book8" + - res: "book8" assert_proto: pipeline: stages: @@ -887,8 +888,11 @@ tests: res: functionValue: args: - - fieldReferenceValue: __name__ - - integerValue: '4' - - integerValue: '1' - name: reference_slice + - functionValue: + args: + - fieldReferenceValue: __name__ + - integerValue: '0' + - integerValue: '1' + name: reference_slice + name: document_id name: select From c3eb82c57da02f6817b9e75cd1e78594a0d49108 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 3 Apr 2026 16:53:11 -0700 Subject: [PATCH 11/12] removed parent and reference_slice --- .../firestore_v1/pipeline_expressions.py | 36 ------ .../tests/system/pipeline_e2e/general.yaml | 120 ------------------ .../tests/system/pipeline_e2e/references.yaml | 49 +++++++ .../unit/v1/test_pipeline_expressions.py | 20 --- 4 files changed, 49 insertions(+), 176 deletions(-) create mode 100644 packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index fc5d61188309..d4363d5c5ffb 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1149,19 +1149,6 @@ def switch_on( ) return FunctionExpression("switch_on", args) - @expose_as_static - def parent(self) -> "Expression": - """Creates an expression that returns the parent document of a document reference. - - Example: - >>> # Get the parent document of a document reference. - >>> Field.of("__name__").parent() - - Returns: - An Expression representing that returns a reference to the parent document. - """ - return FunctionExpression("parent", [self]) - @expose_as_static def storage_size(self) -> "Expression": """Calculates the Firestore storage size of a given value. @@ -1176,29 +1163,6 @@ def storage_size(self) -> "Expression": """ return FunctionExpression("storage_size", [self]) - @expose_as_static - def reference_slice( - self, offset: int | Expression, length: int | Expression - ) -> "Expression": - """Extracts a slice of the path segments from a document reference. - - Example: - >>> Field.of("__name__").reference_slice(1, 2) - - Args: - offset: The starting index of the path segment. - length: The number of segments to include in the slice. - - Returns: - A new `Expression` that returns a reference to the sliced portion of the document path. - """ - args = [ - self, - self._cast_to_expr_or_convert_to_constant(offset), - self._cast_to_expr_or_convert_to_constant(length), - ] - return FunctionExpression("reference_slice", args) - @expose_as_static def sum(self) -> "Expression": """Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index f1fe80b2dd7e..15ad164fa575 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -295,53 +295,6 @@ tests: - Pipeline: - Collection: books assert_count: 20 # Results will be duplicated - - description: testDocumentId - pipeline: - - Collection: books - - Where: - - FunctionExpression.equal: - - Field: title - - Constant: "The Hitchhiker's Guide to the Galaxy" - - Select: - - AliasedExpression: - - FunctionExpression.document_id: - - Field: __name__ - - "doc_id" - assert_results: - - doc_id: "book1" - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - functionValue: - args: - - fieldReferenceValue: title - - stringValue: "The Hitchhiker's Guide to the Galaxy" - name: equal - name: where - - args: - - mapValue: - fields: - doc_id: - functionValue: - name: document_id - args: - - fieldReferenceValue: __name__ - name: select - - description: testCollectionId - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.collection_id: - - Field: __name__ - - "collectionName" - assert_results: - - collectionName: "books" - description: testCollectionGroup pipeline: - CollectionGroup: books @@ -794,35 +747,6 @@ tests: - res assert_results: - res: "B" - - description: testParent - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.parent: - - Field: __name__ - - res - assert_results: - - res: "/books/book1" - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - integerValue: '1' - name: limit - - args: - - mapValue: - fields: - res: - functionValue: - args: - - fieldReferenceValue: __name__ - name: parent - name: select - description: testStorageSize pipeline: - Collection: books @@ -852,47 +776,3 @@ tests: - fieldReferenceValue: __name__ name: storage_size name: select - - description: testReferenceSlice - pipeline: - - Collection: books - - Where: - - FunctionExpression.equal: - - Field: title - - Constant: "1984" - - Select: - - AliasedExpression: - - FunctionExpression.document_id: - - FunctionExpression.reference_slice: - - Field: __name__ - - Constant: 0 - - Constant: 1 - - res - assert_results: - - res: "book8" - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - functionValue: - args: - - fieldReferenceValue: title - - stringValue: '1984' - name: equal - name: where - - args: - - mapValue: - fields: - res: - functionValue: - args: - - functionValue: - args: - - fieldReferenceValue: __name__ - - integerValue: '0' - - integerValue: '1' - name: reference_slice - name: document_id - name: select diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml new file mode 100644 index 000000000000..ed29330f811d --- /dev/null +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml @@ -0,0 +1,49 @@ +tests: + - description: testDocumentId + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.document_id: + - Field: __name__ + - "doc_id" + assert_results: + - doc_id: "book1" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + doc_id: + functionValue: + name: document_id + args: + - fieldReferenceValue: __name__ + name: select + - description: testCollectionId + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.collection_id: + - Field: __name__ + - "collectionName" + assert_results: + - collectionName: "books" + diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index c6f35a240296..c907f8bfbe25 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -949,15 +949,6 @@ def test_switch_on(self): infix_instance = arg1.switch_on(arg2, arg3, arg4, arg5) assert infix_instance == instance - def test_parent(self): - arg1 = self._make_arg("Input") - instance = Expression.parent(arg1) - assert instance.name == "parent" - assert instance.params == [arg1] - assert repr(instance) == "Input.parent()" - infix_instance = arg1.parent() - assert infix_instance == instance - def test_storage_size(self): arg1 = self._make_arg("Input") instance = Expression.storage_size(arg1) @@ -967,17 +958,6 @@ def test_storage_size(self): infix_instance = arg1.storage_size() assert infix_instance == instance - def test_reference_slice(self): - arg1 = self._make_arg("Input") - arg2 = self._make_arg("Offset") - arg3 = self._make_arg("Length") - instance = Expression.reference_slice(arg1, arg2, arg3) - assert instance.name == "reference_slice" - assert instance.params == [arg1, arg2, arg3] - assert repr(instance) == "Input.reference_slice(Offset, Length)" - infix_instance = arg1.reference_slice(arg2, arg3) - assert infix_instance == instance - def test_not(self): arg1 = self._make_arg("Condition") instance = expr.Not(arg1) From 98284be5d2efe7c54fc7a1c71883c86fe13e80fa Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 3 Apr 2026 16:59:29 -0700 Subject: [PATCH 12/12] gemini comments --- .../cloud/firestore_v1/pipeline_expressions.py | 18 +++++++++--------- .../tests/unit/v1/test_pipeline_expressions.py | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index d4363d5c5ffb..d2d6637f013a 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1113,11 +1113,11 @@ def coalesce(self, *others: Expression | CONSTANT_TYPE) -> "Expression": Returns: An Expression representing the coalesce operation. """ - args = [self] - args.extend( - [Expression._cast_to_expr_or_convert_to_constant(x) for x in others] + return FunctionExpression( + "coalesce", + [self] + + [Expression._cast_to_expr_or_convert_to_constant(x) for x in others], ) - return FunctionExpression("coalesce", args) @expose_as_static def switch_on( @@ -1141,13 +1141,13 @@ def switch_on( *others: Additional alternating conditions and results, optionally followed by a default value. Returns: - An Expression representing the switchOn operation. + An Expression representing the "switch_on" operation. """ - args = [self, Expression._cast_to_expr_or_convert_to_constant(result)] - args.extend( - [Expression._cast_to_expr_or_convert_to_constant(x) for x in others] + 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], ) - return FunctionExpression("switch_on", args) @expose_as_static def storage_size(self) -> "Expression": diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index c907f8bfbe25..51a510d2b78f 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -750,8 +750,8 @@ def test_array_get(self): assert instance.name == "array_get" assert instance.params == [arg1, arg2] assert repr(instance) == "ArrayField.array_get(Offset)" - infix_istance = arg1.array_get(arg2) - assert infix_istance == instance + infix_instance = arg1.array_get(arg2) + assert infix_instance == instance def test_offset(self): arg1 = self._make_arg("ArrayField") @@ -760,8 +760,8 @@ def test_offset(self): assert instance.name == "offset" assert instance.params == [arg1, arg2] assert repr(instance) == "ArrayField.offset(Offset)" - infix_istance = arg1.offset(arg2) - assert infix_istance == instance + infix_instance = arg1.offset(arg2) + assert infix_instance == instance def test_array_contains(self): arg1 = self._make_arg("ArrayField") @@ -1654,8 +1654,8 @@ def test_array_reverse(self): assert instance.name == "array_reverse" assert instance.params == [arg1] assert repr(instance) == "ArrayField.array_reverse()" - infix_istance = arg1.array_reverse() - assert infix_istance == instance + infix_instance = arg1.array_reverse() + assert infix_instance == instance def test_array_filter(self): arg1 = self._make_arg("ArrayField")