From 8a02c167a811c9bcb52c38dcfc56097a73957a16 Mon Sep 17 00:00:00 2001 From: Linchin Date: Wed, 18 Mar 2026 23:04:46 +0000 Subject: [PATCH 01/13] feat: support array_first, array_last, array_first_n, array_last_n --- .../firestore_v1/pipeline_expressions.py | 52 ++++++ .../tests/system/pipeline_e2e/array.yaml | 148 ++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 42 +++++ 3 files changed, 242 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 325789f2358a..458671b8130b 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 @@ -1490,6 +1490,58 @@ def last(self) -> "Expression": """ return AggregateFunction("last", [self]) + @expose_as_static + def array_first(self) -> "Expression": + """Creates an expression that returns the first element of an array. + + Example: + >>> # Select the first element of array 'colors' + >>> Field.of("colors").array_first() + + Returns: + A new `Expression` representing the first element of the array. + """ + return FunctionExpression("array_first", [self]) + + @expose_as_static + def array_last(self) -> "Expression": + """Creates an expression that returns the last element of an array. + + Example: + >>> # Select the last element of array 'colors' + >>> Field.of("colors").array_last() + + Returns: + A new `Expression` representing the last element of the array. + """ + return FunctionExpression("array_last", [self]) + + @expose_as_static + def array_first_n(self, n: int | "Expression") -> "Expression": + """Creates an expression that returns the first `n` elements of an array. + + Example: + >>> # Select the first 2 elements of array 'colors' + >>> Field.of("colors").array_first_n(2) + + Returns: + A new `Expression` representing the first `n` elements of the array. + """ + return FunctionExpression("array_first_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + + @expose_as_static + def array_last_n(self, n: int | "Expression") -> "Expression": + """Creates an expression that returns the last `n` elements of an array. + + Example: + >>> # Select the last 2 elements of array 'colors' + >>> Field.of("colors").array_last_n(2) + + Returns: + A new `Expression` representing the last `n` elements of the array. + """ + return FunctionExpression("array_last_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 f82f1cbc1564..416ebb2eb63a 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -461,4 +461,152 @@ tests: - fieldReferenceValue: tags - integerValue: '-1' name: array_get + name: select + - description: testArrayFirst + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_first: + - Field: tags + - "firstTag" + assert_results: + - firstTag: "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: + firstTag: + functionValue: + args: + - fieldReferenceValue: tags + name: array_first + name: select + - description: testArrayLast + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_last: + - Field: tags + - "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 + name: array_last + name: select + - description: testArrayFirstN + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_first_n: + - Field: tags + - Constant: 2 + - "firstTags" + assert_results: + - firstTags: ["comedy", "space"] + 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: + firstTags: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '2' + name: array_first_n + name: select + - description: testArrayLastN + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_last_n: + - Field: tags + - Constant: 2 + - "lastTags" + assert_results: + - lastTags: ["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: + lastTags: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '2' + name: array_last_n name: select \ No newline at end of file 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 80738799f975..cd40c5594b71 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 @@ -1619,3 +1619,45 @@ def test_last(self): assert repr(instance) == "Value.last()" infix_instance = arg1.last() assert infix_instance == instance + + def test_array_first(self): + arg1 = self._make_arg("Value") + instance = Expression.array_first(arg1) + assert instance.name == "array_first" + assert instance.params == [arg1] + assert repr(instance) == "Value.array_first()" + infix_instance = arg1.array_first() + assert infix_instance == instance + + def test_array_last(self): + arg1 = self._make_arg("Value") + instance = Expression.array_last(arg1) + assert instance.name == "array_last" + assert instance.params == [arg1] + assert repr(instance) == "Value.array_last()" + infix_instance = arg1.array_last() + assert infix_instance == instance + + def test_array_first_n(self): + arg1 = self._make_arg("Value") + n = 2 + instance = Expression.array_first_n(arg1, n) + assert instance.name == "array_first_n" + assert isinstance(instance.params[0], Constant) + assert instance.params[0].value == n + assert instance.params[1] == arg1 + assert repr(instance) == "Constant.of(2).array_first_n(Value)" + infix_instance = arg1.array_first_n(n) + assert infix_instance == instance + + def test_array_last_n(self): + arg1 = self._make_arg("Value") + n = 2 + instance = Expression.array_last_n(arg1, n) + assert instance.name == "array_last_n" + assert isinstance(instance.params[0], Constant) + assert instance.params[0].value == n + assert instance.params[1] == arg1 + assert repr(instance) == "Constant.of(2).array_last_n(Value)" + infix_instance = arg1.array_last_n(n) + assert infix_instance == instance From bb7ddf55517e6616dc7409b260af8d278587a3d5 Mon Sep 17 00:00:00 2001 From: Linchin Date: Wed, 18 Mar 2026 23:13:15 +0000 Subject: [PATCH 02/13] fix unit test --- .../tests/unit/v1/test_pipeline_expressions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 cd40c5594b71..e8e439371e00 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 @@ -1643,10 +1643,10 @@ def test_array_first_n(self): n = 2 instance = Expression.array_first_n(arg1, n) assert instance.name == "array_first_n" - assert isinstance(instance.params[0], Constant) - assert instance.params[0].value == n - assert instance.params[1] == arg1 - assert repr(instance) == "Constant.of(2).array_first_n(Value)" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == n + assert repr(instance) == "Value.array_first_n(Constant.of(2))" infix_instance = arg1.array_first_n(n) assert infix_instance == instance @@ -1655,9 +1655,9 @@ def test_array_last_n(self): n = 2 instance = Expression.array_last_n(arg1, n) assert instance.name == "array_last_n" - assert isinstance(instance.params[0], Constant) - assert instance.params[0].value == n - assert instance.params[1] == arg1 - assert repr(instance) == "Constant.of(2).array_last_n(Value)" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == n + assert repr(instance) == "Value.array_last_n(Constant.of(2))" infix_instance = arg1.array_last_n(n) assert infix_instance == instance From 975c7e0224b6aeb535ec357adba220799b2c2b6e Mon Sep 17 00:00:00 2001 From: Linchin Date: Wed, 18 Mar 2026 23:26:49 +0000 Subject: [PATCH 03/13] support array_max, array_max_n, array_min, array_min_n --- .../firestore_v1/pipeline_expressions.py | 62 ++++++++++++++++++ .../tests/system/pipeline_e2e/array.yaml | 64 ++++++++++++++++++- .../unit/v1/test_pipeline_expressions.py | 42 ++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) 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 458671b8130b..fbedbbf7215e 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 @@ -1542,6 +1542,68 @@ def array_last_n(self, n: int | "Expression") -> "Expression": """ return FunctionExpression("array_last_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + @expose_as_static + def array_maximum(self) -> "Expression": + """Creates an expression that returns the maximum element of an array. + + Example: + >>> # Select the maximum element of array 'scores' + >>> Field.of("scores").array_maximum() + + Returns: + A new `Expression` representing the maximum element of the array. + """ + return FunctionExpression("maximum", [self]) + + @expose_as_static + def array_minimum(self) -> "Expression": + """Creates an expression that returns the minimum element of an array. + + Example: + >>> # Select the minimum element of array 'scores' + >>> Field.of("scores").array_minimum() + + Returns: + A new `Expression` representing the minimum element of the array. + """ + return FunctionExpression("minimum", [self]) + + @expose_as_static + def array_maximum_n(self, n: int | "Expression") -> "Expression": + """Creates an expression that returns the maximum `n` elements of an array. + + Example: + >>> # Select the maximum 2 elements of array 'scores' + >>> Field.of("scores").array_maximum_n(2) + + Note: + Returns the n largest non-null elements in the array, in descending + order. This does not use a stable sort, meaning the order of equivalent + elements is undefined. + + Returns: + A new `Expression` representing the maximum `n` elements of the array. + """ + return FunctionExpression("maximum_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + + @expose_as_static + def array_minimum_n(self, n: int | "Expression") -> "Expression": + """Creates an expression that returns the minimum `n` elements of an array. + + Example: + >>> # Select the minimum 2 elements of array 'scores' + >>> Field.of("scores").array_minimum_n(2) + + Note: + Returns the n smallest non-null elements in the array, in ascending + order. This does not use a stable sort, meaning the order of equivalent + elements is undefined. + + Returns: + A new `Expression` representing the minimum `n` elements of the array. + """ + return FunctionExpression("minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 416ebb2eb63a..0bcc14be8e65 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -609,4 +609,66 @@ tests: - fieldReferenceValue: tags - integerValue: '2' name: array_last_n - name: select \ No newline at end of file + name: select + + - description: testArrayMaximum + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_maximum: + - Field: tags + - "maxTag" + assert_results: + - maxTag: "space" + + - description: testArrayMinimum + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_minimum: + - Field: tags + - "minTag" + assert_results: + - minTag: "adventure" + + - description: testArrayMaximumN + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_maximum_n: + - Field: tags + - Constant: 2 + - "maxTags" + assert_results: + - maxTags: ["space", "comedy"] + + - description: testArrayMinimumN + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_minimum_n: + - Field: tags + - Constant: 2 + - "minTags" + assert_results: + - minTags: ["adventure", "comedy"] 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 e8e439371e00..87c450e9bb0d 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 @@ -1661,3 +1661,45 @@ def test_array_last_n(self): assert repr(instance) == "Value.array_last_n(Constant.of(2))" infix_instance = arg1.array_last_n(n) assert infix_instance == instance + + def test_array_maximum(self): + arg1 = self._make_arg("Value") + instance = Expression.array_maximum(arg1) + assert instance.name == "maximum" + assert instance.params == [arg1] + assert repr(instance) == "Value.maximum()" + infix_instance = arg1.array_maximum() + assert infix_instance == instance + + def test_array_minimum(self): + arg1 = self._make_arg("Value") + instance = Expression.array_minimum(arg1) + assert instance.name == "minimum" + assert instance.params == [arg1] + assert repr(instance) == "Value.minimum()" + infix_instance = arg1.array_minimum() + assert infix_instance == instance + + def test_array_maximum_n(self): + arg1 = self._make_arg("Value") + n = 2 + instance = Expression.array_maximum_n(arg1, n) + assert instance.name == "maximum_n" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == n + assert repr(instance) == "Value.maximum_n(Constant.of(2))" + infix_instance = arg1.array_maximum_n(n) + assert infix_instance == instance + + def test_array_minimum_n(self): + arg1 = self._make_arg("Value") + n = 2 + instance = Expression.array_minimum_n(arg1, n) + assert instance.name == "minimum_n" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == n + assert repr(instance) == "Value.minimum_n(Constant.of(2))" + infix_instance = arg1.array_minimum_n(n) + assert infix_instance == instance From 35eba8d9e151851278c8d73c89f2f65d2f565649 Mon Sep 17 00:00:00 2001 From: Linchin Date: Wed, 18 Mar 2026 23:37:57 +0000 Subject: [PATCH 04/13] support array_slice --- .../firestore_v1/pipeline_expressions.py | 24 ++++++++++++++ .../tests/system/pipeline_e2e/array.yaml | 33 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 29 ++++++++++++++++ 3 files changed, 86 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 fbedbbf7215e..fc2e85617ce8 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 @@ -1604,6 +1604,30 @@ def array_minimum_n(self, n: int | "Expression") -> "Expression": """ return FunctionExpression("minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + @expose_as_static + def array_slice(self, offset: int | "Expression", length: int | "Expression" | None = None) -> "Expression": + """Creates an expression that returns a slice of an array. + + Example: + >>> # Slice array 'scores' starting at index 1 with length 2 + >>> Field.of("scores").array_slice(1, 2) + + Note: + Both offset and length allow negative values to represent wrap-around from the end + of the array. + + Args: + offset: The starting index of the slice. + length: The number of elements to include in the slice. If omitted, slices to the end. + + Returns: + A new `Expression` representing the slice of the array. + """ + args = [self, self._cast_to_expr_or_convert_to_constant(offset)] + if length is not None: + args.append(self._cast_to_expr_or_convert_to_constant(length)) + return FunctionExpression("array_slice", args) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 0bcc14be8e65..3e9a6108a8c9 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -672,3 +672,36 @@ tests: - "minTags" assert_results: - minTags: ["adventure", "comedy"] + + - description: testArraySlice1Arg + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_slice: + - Field: tags + - Constant: 1 + - "sliced" + assert_results: + - sliced: ["space", "adventure"] + + - description: testArraySlice2Args + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_slice: + - Field: tags + - Constant: 1 + - Constant: 1 + - "sliced" + assert_results: + - sliced: ["space"] 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 87c450e9bb0d..bd855c1cbac1 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 @@ -1703,3 +1703,32 @@ def test_array_minimum_n(self): assert repr(instance) == "Value.minimum_n(Constant.of(2))" infix_instance = arg1.array_minimum_n(n) assert infix_instance == instance + + def test_array_slice_1_arg(self): + arg1 = self._make_arg("Value") + offset = 1 + instance = Expression.array_slice(arg1, offset) + assert instance.name == "array_slice" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == offset + assert len(instance.params) == 2 + assert repr(instance) == "Value.array_slice(Constant.of(1))" + infix_instance = arg1.array_slice(offset) + assert infix_instance == instance + + def test_array_slice_2_args(self): + arg1 = self._make_arg("Value") + offset = 1 + length = 2 + instance = Expression.array_slice(arg1, offset, length) + assert instance.name == "array_slice" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == offset + assert isinstance(instance.params[2], Constant) + assert instance.params[2].value == length + assert len(instance.params) == 3 + assert repr(instance) == "Value.array_slice(Constant.of(1), Constant.of(2))" + infix_instance = arg1.array_slice(offset, length) + assert infix_instance == instance From dad5c0ded30fa8237529bb14db573ce17db9286a Mon Sep 17 00:00:00 2001 From: Linchin Date: Fri, 20 Mar 2026 23:48:32 +0000 Subject: [PATCH 05/13] update docstring for array_slice --- .../google/cloud/firestore_v1/pipeline_expressions.py | 9 +++------ 1 file changed, 3 insertions(+), 6 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 fc2e85617ce8..8ce6873d1f6d 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 @@ -1606,18 +1606,15 @@ def array_minimum_n(self, n: int | "Expression") -> "Expression": @expose_as_static def array_slice(self, offset: int | "Expression", length: int | "Expression" | None = None) -> "Expression": - """Creates an expression that returns a slice of an array. + """Ccreates an expression that returns a slice of an array starting from the specified + offset with a given length. Example: >>> # Slice array 'scores' starting at index 1 with length 2 >>> Field.of("scores").array_slice(1, 2) - Note: - Both offset and length allow negative values to represent wrap-around from the end - of the array. - Args: - offset: The starting index of the slice. + offset: the 0-based index of the first element to include. length: The number of elements to include in the slice. If omitted, slices to the end. Returns: From 4e1e84730d3569406f8ee26e71af265e73cd5397 Mon Sep 17 00:00:00 2001 From: Linchin Date: Sat, 21 Mar 2026 00:09:02 +0000 Subject: [PATCH 06/13] feat: add array_index_of expression and corresponding unit and system tests. --- .../firestore_v1/pipeline_expressions.py | 27 +++++++++++++ .../tests/system/pipeline_e2e/array.yaml | 40 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 15 +++++++ 3 files changed, 82 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 8ce6873d1f6d..f33ee17d9c08 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 @@ -1625,6 +1625,33 @@ def array_slice(self, offset: int | "Expression", length: int | "Expression" | N args.append(self._cast_to_expr_or_convert_to_constant(length)) return FunctionExpression("array_slice", args) + @expose_as_static + def array_index_of( + self, search: "Expression" | CONSTANT_TYPE + ) -> "Expression": + """Creates an expression that returns the index of a value in an array. + + Returns -1 if the value is not found. + + Example: + >>> # Get the index of "comedy" in the 'tags' array + >>> Field.of("tags").array_index_of("comedy") + + Args: + search: The element (expression or constant) to find the index of. + + Returns: + A new `Expression` representing the 'array_index_of' value. + """ + return FunctionExpression( + "array_index_of", + [ + self, + self._cast_to_expr_or_convert_to_constant(search), + self._cast_to_expr_or_convert_to_constant("first") + ] + ) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 3e9a6108a8c9..bad1c67b3f81 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -705,3 +705,43 @@ tests: - "sliced" assert_results: - sliced: ["space"] + + - description: testArrayIndexOf + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_index_of: + - Field: tags + - Constant: "space" + - "spaceIndex" + assert_results: + - spaceIndex: 1 + 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: + spaceIndex: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "space" + - stringValue: "first" + name: array_index_of + 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 bd855c1cbac1..c35534ab45b1 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 @@ -1732,3 +1732,18 @@ def test_array_slice_2_args(self): assert repr(instance) == "Value.array_slice(Constant.of(1), Constant.of(2))" infix_instance = arg1.array_slice(offset, length) assert infix_instance == instance + + def test_array_index_of(self): + arg1 = self._make_arg("Value") + value = "comedy" + instance = Expression.array_index_of(arg1, value) + assert instance.name == "array_index_of" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == value + assert isinstance(instance.params[2], Constant) + assert instance.params[2].value == "first" + assert len(instance.params) == 3 + assert repr(instance) == "Value.array_index_of(Constant.of('comedy'), Constant.of('first'))" + infix_instance = arg1.array_index_of(value) + assert infix_instance == instance From 166f18026cc54b6a6a48118512b7a0da786a4c3d Mon Sep 17 00:00:00 2001 From: Linchin Date: Sat, 21 Mar 2026 00:28:19 +0000 Subject: [PATCH 07/13] feat: add `array_index_of_all` expression for pipeline queries with unit and system tests. --- .../firestore_v1/pipeline_expressions.py | 25 +++++++++ .../tests/system/pipeline_e2e/array.yaml | 55 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 13 +++++ 3 files changed, 93 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 f33ee17d9c08..0c87752dc04a 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 @@ -1652,6 +1652,31 @@ def array_index_of( ] ) + @expose_as_static + def array_index_of_all( + self, search: "Expression" | CONSTANT_TYPE + ) -> "Expression": + """Creates an expression that returns all indices of a value in an array. + Returns an empty array if the value is not found. + + Example: + >>> # Get all indices of "comedy" in the 'tags' array + >>> Field.of("tags").array_index_of_all("comedy") + + Args: + search: The element (expression or constant) to find the indices of. + + Returns: + A new `Expression` representing the 'array_index_of_all' value. + """ + return FunctionExpression( + "array_index_of_all", + [ + self, + self._cast_to_expr_or_convert_to_constant(search) + ] + ) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 bad1c67b3f81..e29ef0d6c2ed 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -745,3 +745,58 @@ tests: - stringValue: "first" name: array_index_of name: select + + - description: testArrayIndexOfAll + pipeline: + - Collection: books + - Sort: + - Ordering: + - Field: title + - ASCENDING + - Aggregate: + - AliasedExpression: + - FunctionExpression.array_agg: + - Field: genre + - "genre_array" + - Select: + - AliasedExpression: + - FunctionExpression.array_index_of_all: + - Field: genre_array + - Constant: "Science Fiction" + - "sciFiIndices" + assert_results: + - sciFiIndices: [2, 7] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + direction: + stringValue: ascending + expression: + fieldReferenceValue: title + name: sort + - args: + - mapValue: + fields: + genre_array: + functionValue: + name: array_agg + args: + - fieldReferenceValue: genre + - mapValue: {} + name: aggregate + - args: + - mapValue: + fields: + sciFiIndices: + functionValue: + args: + - fieldReferenceValue: genre_array + - stringValue: "Science Fiction" + name: array_index_of_all + 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 c35534ab45b1..0f25ccb3270f 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 @@ -1747,3 +1747,16 @@ def test_array_index_of(self): assert repr(instance) == "Value.array_index_of(Constant.of('comedy'), Constant.of('first'))" infix_instance = arg1.array_index_of(value) assert infix_instance == instance + + def test_array_index_of_all(self): + arg1 = self._make_arg("Value") + value = "comedy" + instance = Expression.array_index_of_all(arg1, value) + assert instance.name == "array_index_of_all" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == value + assert len(instance.params) == 2 + assert repr(instance) == "Value.array_index_of_all(Constant.of('comedy'))" + infix_instance = arg1.array_index_of_all(value) + assert infix_instance == instance From 755e643e9e66710fd0e1c577d193456f24fb0b8f Mon Sep 17 00:00:00 2001 From: Linchin Date: Sat, 21 Mar 2026 00:45:50 +0000 Subject: [PATCH 08/13] feat: Add `array_transform` function to `Expression` for transforming array elements with optional index. --- .../firestore_v1/pipeline_expressions.py | 33 +++++++ .../tests/system/pipeline_e2e/array.yaml | 98 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 34 +++++++ 3 files changed, 165 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 0c87752dc04a..a43e46eeb444 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 @@ -1677,6 +1677,39 @@ def array_index_of_all( ] ) + @expose_as_static + def array_transform( + self, element_alias: str, body: "Expression", index_alias: str | None = None + ) -> "Expression": + """Creates an expression that transforms elements of an array. + + Example: + >>> # Transform each element by adding 1. + >>> Field.of("nums").array_transform("e", Field.of("e").add(1)) + >>> + >>> # Transform each element by adding its index to it. + >>> Field.of("nums").array_transform("e", Field.of("e").add(Field.of("i")), index_alias="i") + + Args: + element_alias: The variable name to use for the current element within the body expression. + body: The expression to apply to each element. + index_alias: The variable name to use for the current index within the body expression. + + Returns: + A new `Expression` applying the transformation to the array elements. + """ + 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(self._cast_to_expr_or_convert_to_constant(body)) + + return FunctionExpression("array_transform", args) + + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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..7c4d1fea3a4e 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -800,3 +800,101 @@ tests: - stringValue: "Science Fiction" name: array_index_of_all name: select + + - description: testArrayTransform + pipeline: + - Collection: books + - Select: + - AliasedExpression: + - FunctionExpression.array_transform: + - Array: + - 1 + - 10 + - 100 + - "num" + - FunctionExpression.add: + - Field: "num" + - Constant: 1 + - "transformed" + - Limit: 1 + assert_results: + - transformed: [2, 11, 101] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + transformed: + functionValue: + name: array_transform + args: + - functionValue: + name: array + args: + - integerValue: '1' + - integerValue: '10' + - integerValue: '100' + - stringValue: num + - functionValue: + name: add + args: + - fieldReferenceValue: num + - integerValue: '1' + name: select + - args: + - integerValue: '1' + name: limit + + - description: testArrayTransformWithIndex + pipeline: + - Collection: books + - Select: + - AliasedExpression: + - FunctionExpression.array_transform: + - Array: + - 1 + - 10 + - 100 + - "num" + - FunctionExpression.multiply: # body + - Field: "num" + - Field: "idx" + - "idx" # index_alias + - "transformed" + - Limit: 1 + assert_results: + - transformed: [0, 10, 200] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + transformed: + functionValue: + name: array_transform + args: + - functionValue: + name: array + args: + - integerValue: '1' + - integerValue: '10' + - integerValue: '100' + - stringValue: num + - stringValue: idx + - functionValue: + name: multiply + args: + - fieldReferenceValue: num + - fieldReferenceValue: idx + name: select + - args: + - integerValue: '1' + name: limit 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 0f25ccb3270f..bc55e3fa8e27 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 @@ -1760,3 +1760,37 @@ def test_array_index_of_all(self): assert repr(instance) == "Value.array_index_of_all(Constant.of('comedy'))" infix_instance = arg1.array_index_of_all(value) assert infix_instance == instance + + def test_array_transform(self): + arg1 = self._make_arg("Value") + element_alias = "e" + body = self._make_arg("BodyValue") + instance = Expression.array_transform(arg1, element_alias, body) + assert instance.name == "array_transform" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == element_alias + assert instance.params[2] == body + assert len(instance.params) == 3 + assert repr(instance) == "Value.array_transform(Constant.of('e'), BodyValue)" + infix_instance = arg1.array_transform(element_alias, body) + assert infix_instance == instance + + def test_array_transform_with_index(self): + arg1 = self._make_arg("Value") + element_alias = "e" + index_alias = "i" + body = self._make_arg("BodyValue") + instance = Expression.array_transform(arg1, element_alias, body, index_alias=index_alias) + assert instance.name == "array_transform" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == element_alias + assert isinstance(instance.params[2], Constant) + assert instance.params[2].value == index_alias + assert instance.params[3] == body + assert len(instance.params) == 4 + assert repr(instance) == "Value.array_transform(Constant.of('e'), Constant.of('i'), BodyValue)" + infix_instance = arg1.array_transform(element_alias, body, index_alias=index_alias) + assert infix_instance == instance + From 2bd56860b3e73ea01e85c54bac8e6b341cd5f074 Mon Sep 17 00:00:00 2001 From: Linchin Date: Sat, 21 Mar 2026 01:02:03 +0000 Subject: [PATCH 09/13] implement `array_filter` --- .../firestore_v1/pipeline_expressions.py | 22 +++++++++ .../tests/system/pipeline_e2e/array.yaml | 48 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 15 ++++++ 3 files changed, 85 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 a43e46eeb444..904e147cfac4 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 @@ -1709,6 +1709,28 @@ def array_transform( return FunctionExpression("array_transform", args) + @expose_as_static + def array_filter(self, element_alias: str, body: "Expression") -> "Expression": + """ + Takes an array, evaluates a boolean expression on each element, and returns a new + array containing only the elements for which the expression evaluates to True. + + Args: + element_alias: Element variable name. + body: Boolean expression applied to each element. + + Returns: + Expression: The created FunctionExpression AST node. + """ + return FunctionExpression( + "array_filter", + [ + self, + self._cast_to_expr_or_convert_to_constant(element_alias), + self._cast_to_expr_or_convert_to_constant(body), + ], + ) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": 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 7c4d1fea3a4e..0e7f4b303eef 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -898,3 +898,51 @@ tests: - args: - integerValue: '1' name: limit + + - description: testArrayFilter + pipeline: + - Collection: books + - Select: + - AliasedExpression: + - FunctionExpression.array_filter: + - Array: + - 1 + - 10 + - 100 + - "num" + - FunctionExpression.greater_than: + - Field: "num" + - Constant: 9 + - "filtered" + - Limit: 1 + assert_results: + - filtered: [10, 100] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + filtered: + functionValue: + name: array_filter + args: + - functionValue: + name: array + args: + - integerValue: '1' + - integerValue: '10' + - integerValue: '100' + - stringValue: num + - functionValue: + name: greater_than + args: + - fieldReferenceValue: num + - integerValue: '9' + name: select + - args: + - integerValue: '1' + name: limit 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 bc55e3fa8e27..c67da914f6ec 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 @@ -1794,3 +1794,18 @@ def test_array_transform_with_index(self): infix_instance = arg1.array_transform(element_alias, body, index_alias=index_alias) assert infix_instance == instance + def test_array_filter(self): + arg1 = self._make_arg("Value") + element_alias = "e" + body = self._make_arg("BodyValue") + instance = Expression.array_filter(arg1, element_alias, body) + assert instance.name == "array_filter" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == element_alias + assert instance.params[2] == body + assert len(instance.params) == 3 + assert repr(instance) == "Value.array_filter(Constant.of('e'), BodyValue)" + infix_instance = arg1.array_filter(element_alias, body) + assert infix_instance == instance + From 496d8e2a086dd81d4ce19a34a02635f206b5f731 Mon Sep 17 00:00:00 2001 From: Linchin Date: Sat, 21 Mar 2026 01:03:13 +0000 Subject: [PATCH 10/13] lint --- .../firestore_v1/pipeline_expressions.py | 46 ++++++++++--------- .../unit/v1/test_pipeline_expressions.py | 19 ++++++-- 2 files changed, 38 insertions(+), 27 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 904e147cfac4..a97cd9feb73f 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 @@ -1527,7 +1527,9 @@ def array_first_n(self, n: int | "Expression") -> "Expression": Returns: A new `Expression` representing the first `n` elements of the array. """ - return FunctionExpression("array_first_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + return FunctionExpression( + "array_first_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) @expose_as_static def array_last_n(self, n: int | "Expression") -> "Expression": @@ -1540,7 +1542,9 @@ def array_last_n(self, n: int | "Expression") -> "Expression": Returns: A new `Expression` representing the last `n` elements of the array. """ - return FunctionExpression("array_last_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + return FunctionExpression( + "array_last_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) @expose_as_static def array_maximum(self) -> "Expression": @@ -1584,7 +1588,9 @@ def array_maximum_n(self, n: int | "Expression") -> "Expression": Returns: A new `Expression` representing the maximum `n` elements of the array. """ - return FunctionExpression("maximum_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + return FunctionExpression( + "maximum_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) @expose_as_static def array_minimum_n(self, n: int | "Expression") -> "Expression": @@ -1602,10 +1608,14 @@ def array_minimum_n(self, n: int | "Expression") -> "Expression": Returns: A new `Expression` representing the minimum `n` elements of the array. """ - return FunctionExpression("minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)]) + return FunctionExpression( + "minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) @expose_as_static - def array_slice(self, offset: int | "Expression", length: int | "Expression" | None = None) -> "Expression": + def array_slice( + self, offset: int | "Expression", length: int | "Expression" | None = None + ) -> "Expression": """Ccreates an expression that returns a slice of an array starting from the specified offset with a given length. @@ -1626,9 +1636,7 @@ def array_slice(self, offset: int | "Expression", length: int | "Expression" | N return FunctionExpression("array_slice", args) @expose_as_static - def array_index_of( - self, search: "Expression" | CONSTANT_TYPE - ) -> "Expression": + def array_index_of(self, search: "Expression" | CONSTANT_TYPE) -> "Expression": """Creates an expression that returns the index of a value in an array. Returns -1 if the value is not found. @@ -1644,18 +1652,16 @@ def array_index_of( A new `Expression` representing the 'array_index_of' value. """ return FunctionExpression( - "array_index_of", + "array_index_of", [ - self, + self, self._cast_to_expr_or_convert_to_constant(search), - self._cast_to_expr_or_convert_to_constant("first") - ] + self._cast_to_expr_or_convert_to_constant("first"), + ], ) @expose_as_static - def array_index_of_all( - self, search: "Expression" | CONSTANT_TYPE - ) -> "Expression": + def array_index_of_all(self, search: "Expression" | CONSTANT_TYPE) -> "Expression": """Creates an expression that returns all indices of a value in an array. Returns an empty array if the value is not found. @@ -1670,11 +1676,8 @@ def array_index_of_all( A new `Expression` representing the 'array_index_of_all' value. """ return FunctionExpression( - "array_index_of_all", - [ - self, - self._cast_to_expr_or_convert_to_constant(search) - ] + "array_index_of_all", + [self, self._cast_to_expr_or_convert_to_constant(search)], ) @expose_as_static @@ -1704,7 +1707,7 @@ def array_transform( ] if index_alias is not None: args.append(self._cast_to_expr_or_convert_to_constant(index_alias)) - + args.append(self._cast_to_expr_or_convert_to_constant(body)) return FunctionExpression("array_transform", args) @@ -1731,7 +1734,6 @@ def array_filter(self, element_alias: str, body: "Expression") -> "Expression": ], ) - @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 c67da914f6ec..2f4cf9459bb6 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 @@ -1744,7 +1744,10 @@ def test_array_index_of(self): assert isinstance(instance.params[2], Constant) assert instance.params[2].value == "first" assert len(instance.params) == 3 - assert repr(instance) == "Value.array_index_of(Constant.of('comedy'), Constant.of('first'))" + assert ( + repr(instance) + == "Value.array_index_of(Constant.of('comedy'), Constant.of('first'))" + ) infix_instance = arg1.array_index_of(value) assert infix_instance == instance @@ -1781,7 +1784,9 @@ def test_array_transform_with_index(self): element_alias = "e" index_alias = "i" body = self._make_arg("BodyValue") - instance = Expression.array_transform(arg1, element_alias, body, index_alias=index_alias) + instance = Expression.array_transform( + arg1, element_alias, body, index_alias=index_alias + ) assert instance.name == "array_transform" assert instance.params[0] == arg1 assert isinstance(instance.params[1], Constant) @@ -1790,8 +1795,13 @@ def test_array_transform_with_index(self): assert instance.params[2].value == index_alias assert instance.params[3] == body assert len(instance.params) == 4 - assert repr(instance) == "Value.array_transform(Constant.of('e'), Constant.of('i'), BodyValue)" - infix_instance = arg1.array_transform(element_alias, body, index_alias=index_alias) + assert ( + repr(instance) + == "Value.array_transform(Constant.of('e'), Constant.of('i'), BodyValue)" + ) + infix_instance = arg1.array_transform( + element_alias, body, index_alias=index_alias + ) assert infix_instance == instance def test_array_filter(self): @@ -1808,4 +1818,3 @@ def test_array_filter(self): assert repr(instance) == "Value.array_filter(Constant.of('e'), BodyValue)" infix_instance = arg1.array_filter(element_alias, body) assert infix_instance == instance - From cabf3c534015872a713e8b98a237c3fcba44c820 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 18:32:07 +0000 Subject: [PATCH 11/13] remove transform and filter --- .../firestore_v1/pipeline_expressions.py | 68 +------- .../tests/system/pipeline_e2e/array.yaml | 146 ------------------ .../unit/v1/test_pipeline_expressions.py | 55 ------- 3 files changed, 6 insertions(+), 263 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 a97cd9feb73f..612c9936926d 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 @@ -1637,19 +1637,18 @@ def array_slice( @expose_as_static def array_index_of(self, search: "Expression" | CONSTANT_TYPE) -> "Expression": - """Creates an expression that returns the index of a value in an array. - - Returns -1 if the value is not found. + """Creates an expression that returns the first index of the search value in the array, + or -1 if not found. Example: >>> # Get the index of "comedy" in the 'tags' array >>> Field.of("tags").array_index_of("comedy") Args: - search: The element (expression or constant) to find the index of. + search: An expression evaluating to the value to search for. Returns: - A new `Expression` representing the 'array_index_of' value. + A new `Expression` representing the index. """ return FunctionExpression( "array_index_of", @@ -1663,77 +1662,22 @@ def array_index_of(self, search: "Expression" | CONSTANT_TYPE) -> "Expression": @expose_as_static def array_index_of_all(self, search: "Expression" | CONSTANT_TYPE) -> "Expression": """Creates an expression that returns all indices of a value in an array. - Returns an empty array if the value is not found. Example: >>> # Get all indices of "comedy" in the 'tags' array >>> Field.of("tags").array_index_of_all("comedy") Args: - search: The element (expression or constant) to find the indices of. + search: An expression evaluating to the value to search for. Returns: - A new `Expression` representing the 'array_index_of_all' value. + A new `Expression` representing the indices. """ return FunctionExpression( "array_index_of_all", [self, self._cast_to_expr_or_convert_to_constant(search)], ) - @expose_as_static - def array_transform( - self, element_alias: str, body: "Expression", index_alias: str | None = None - ) -> "Expression": - """Creates an expression that transforms elements of an array. - - Example: - >>> # Transform each element by adding 1. - >>> Field.of("nums").array_transform("e", Field.of("e").add(1)) - >>> - >>> # Transform each element by adding its index to it. - >>> Field.of("nums").array_transform("e", Field.of("e").add(Field.of("i")), index_alias="i") - - Args: - element_alias: The variable name to use for the current element within the body expression. - body: The expression to apply to each element. - index_alias: The variable name to use for the current index within the body expression. - - Returns: - A new `Expression` applying the transformation to the array elements. - """ - 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(self._cast_to_expr_or_convert_to_constant(body)) - - return FunctionExpression("array_transform", args) - - @expose_as_static - def array_filter(self, element_alias: str, body: "Expression") -> "Expression": - """ - Takes an array, evaluates a boolean expression on each element, and returns a new - array containing only the elements for which the expression evaluates to True. - - Args: - element_alias: Element variable name. - body: Boolean expression applied to each element. - - Returns: - Expression: The created FunctionExpression AST node. - """ - return FunctionExpression( - "array_filter", - [ - self, - self._cast_to_expr_or_convert_to_constant(element_alias), - self._cast_to_expr_or_convert_to_constant(body), - ], - ) - @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 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 0e7f4b303eef..e29ef0d6c2ed 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -800,149 +800,3 @@ tests: - stringValue: "Science Fiction" name: array_index_of_all name: select - - - description: testArrayTransform - pipeline: - - Collection: books - - Select: - - AliasedExpression: - - FunctionExpression.array_transform: - - Array: - - 1 - - 10 - - 100 - - "num" - - FunctionExpression.add: - - Field: "num" - - Constant: 1 - - "transformed" - - Limit: 1 - assert_results: - - transformed: [2, 11, 101] - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - mapValue: - fields: - transformed: - functionValue: - name: array_transform - args: - - functionValue: - name: array - args: - - integerValue: '1' - - integerValue: '10' - - integerValue: '100' - - stringValue: num - - functionValue: - name: add - args: - - fieldReferenceValue: num - - integerValue: '1' - name: select - - args: - - integerValue: '1' - name: limit - - - description: testArrayTransformWithIndex - pipeline: - - Collection: books - - Select: - - AliasedExpression: - - FunctionExpression.array_transform: - - Array: - - 1 - - 10 - - 100 - - "num" - - FunctionExpression.multiply: # body - - Field: "num" - - Field: "idx" - - "idx" # index_alias - - "transformed" - - Limit: 1 - assert_results: - - transformed: [0, 10, 200] - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - mapValue: - fields: - transformed: - functionValue: - name: array_transform - args: - - functionValue: - name: array - args: - - integerValue: '1' - - integerValue: '10' - - integerValue: '100' - - stringValue: num - - stringValue: idx - - functionValue: - name: multiply - args: - - fieldReferenceValue: num - - fieldReferenceValue: idx - name: select - - args: - - integerValue: '1' - name: limit - - - description: testArrayFilter - pipeline: - - Collection: books - - Select: - - AliasedExpression: - - FunctionExpression.array_filter: - - Array: - - 1 - - 10 - - 100 - - "num" - - FunctionExpression.greater_than: - - Field: "num" - - Constant: 9 - - "filtered" - - Limit: 1 - assert_results: - - filtered: [10, 100] - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - mapValue: - fields: - filtered: - functionValue: - name: array_filter - args: - - functionValue: - name: array - args: - - integerValue: '1' - - integerValue: '10' - - integerValue: '100' - - stringValue: num - - functionValue: - name: greater_than - args: - - fieldReferenceValue: num - - integerValue: '9' - name: select - - args: - - integerValue: '1' - name: limit 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 2f4cf9459bb6..e5c1f7109326 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 @@ -1763,58 +1763,3 @@ def test_array_index_of_all(self): assert repr(instance) == "Value.array_index_of_all(Constant.of('comedy'))" infix_instance = arg1.array_index_of_all(value) assert infix_instance == instance - - def test_array_transform(self): - arg1 = self._make_arg("Value") - element_alias = "e" - body = self._make_arg("BodyValue") - instance = Expression.array_transform(arg1, element_alias, body) - assert instance.name == "array_transform" - assert instance.params[0] == arg1 - assert isinstance(instance.params[1], Constant) - assert instance.params[1].value == element_alias - assert instance.params[2] == body - assert len(instance.params) == 3 - assert repr(instance) == "Value.array_transform(Constant.of('e'), BodyValue)" - infix_instance = arg1.array_transform(element_alias, body) - assert infix_instance == instance - - def test_array_transform_with_index(self): - arg1 = self._make_arg("Value") - element_alias = "e" - index_alias = "i" - body = self._make_arg("BodyValue") - instance = Expression.array_transform( - arg1, element_alias, body, index_alias=index_alias - ) - assert instance.name == "array_transform" - assert instance.params[0] == arg1 - assert isinstance(instance.params[1], Constant) - assert instance.params[1].value == element_alias - assert isinstance(instance.params[2], Constant) - assert instance.params[2].value == index_alias - assert instance.params[3] == body - assert len(instance.params) == 4 - assert ( - repr(instance) - == "Value.array_transform(Constant.of('e'), Constant.of('i'), BodyValue)" - ) - infix_instance = arg1.array_transform( - element_alias, body, index_alias=index_alias - ) - assert infix_instance == instance - - def test_array_filter(self): - arg1 = self._make_arg("Value") - element_alias = "e" - body = self._make_arg("BodyValue") - instance = Expression.array_filter(arg1, element_alias, body) - assert instance.name == "array_filter" - assert instance.params[0] == arg1 - assert isinstance(instance.params[1], Constant) - assert instance.params[1].value == element_alias - assert instance.params[2] == body - assert len(instance.params) == 3 - assert repr(instance) == "Value.array_filter(Constant.of('e'), BodyValue)" - infix_instance = arg1.array_filter(element_alias, body) - assert infix_instance == instance From 59be60e49d8933e132b3080b3218b4c84fc64e9a Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 18:46:54 +0000 Subject: [PATCH 12/13] use infix_name_override --- .../cloud/firestore_v1/pipeline_expressions.py | 16 ++++++++++++---- .../tests/unit/v1/test_pipeline_expressions.py | 8 ++++---- 2 files changed, 16 insertions(+), 8 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 612c9936926d..4028f1722de5 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 @@ -1557,7 +1557,9 @@ def array_maximum(self) -> "Expression": Returns: A new `Expression` representing the maximum element of the array. """ - return FunctionExpression("maximum", [self]) + return FunctionExpression( + "maximum", [self], infix_name_override="array_maximum" + ) @expose_as_static def array_minimum(self) -> "Expression": @@ -1570,7 +1572,9 @@ def array_minimum(self) -> "Expression": Returns: A new `Expression` representing the minimum element of the array. """ - return FunctionExpression("minimum", [self]) + return FunctionExpression( + "minimum", [self], infix_name_override="array_minimum" + ) @expose_as_static def array_maximum_n(self, n: int | "Expression") -> "Expression": @@ -1589,7 +1593,9 @@ def array_maximum_n(self, n: int | "Expression") -> "Expression": A new `Expression` representing the maximum `n` elements of the array. """ return FunctionExpression( - "maximum_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + "maximum_n", + [self, self._cast_to_expr_or_convert_to_constant(n)], + infix_name_override="array_maximum_n", ) @expose_as_static @@ -1609,7 +1615,9 @@ def array_minimum_n(self, n: int | "Expression") -> "Expression": A new `Expression` representing the minimum `n` elements of the array. """ return FunctionExpression( - "minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + "minimum_n", + [self, self._cast_to_expr_or_convert_to_constant(n)], + infix_name_override="array_minimum_n", ) @expose_as_static 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 e5c1f7109326..bd75d368ea4a 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 @@ -1667,7 +1667,7 @@ def test_array_maximum(self): instance = Expression.array_maximum(arg1) assert instance.name == "maximum" assert instance.params == [arg1] - assert repr(instance) == "Value.maximum()" + assert repr(instance) == "Value.array_maximum()" infix_instance = arg1.array_maximum() assert infix_instance == instance @@ -1676,7 +1676,7 @@ def test_array_minimum(self): instance = Expression.array_minimum(arg1) assert instance.name == "minimum" assert instance.params == [arg1] - assert repr(instance) == "Value.minimum()" + assert repr(instance) == "Value.array_minimum()" infix_instance = arg1.array_minimum() assert infix_instance == instance @@ -1688,7 +1688,7 @@ def test_array_maximum_n(self): assert instance.params[0] == arg1 assert isinstance(instance.params[1], Constant) assert instance.params[1].value == n - assert repr(instance) == "Value.maximum_n(Constant.of(2))" + assert repr(instance) == "Value.array_maximum_n(Constant.of(2))" infix_instance = arg1.array_maximum_n(n) assert infix_instance == instance @@ -1700,7 +1700,7 @@ def test_array_minimum_n(self): assert instance.params[0] == arg1 assert isinstance(instance.params[1], Constant) assert instance.params[1].value == n - assert repr(instance) == "Value.minimum_n(Constant.of(2))" + assert repr(instance) == "Value.array_minimum_n(Constant.of(2))" infix_instance = arg1.array_minimum_n(n) assert infix_instance == instance From 0828e534c7e94f83167501478848da4e82a63259 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 23:45:37 +0000 Subject: [PATCH 13/13] typo --- .../google/cloud/firestore_v1/pipeline_expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4028f1722de5..66dcf0684ace 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 @@ -1624,7 +1624,7 @@ def array_minimum_n(self, n: int | "Expression") -> "Expression": def array_slice( self, offset: int | "Expression", length: int | "Expression" | None = None ) -> "Expression": - """Ccreates an expression that returns a slice of an array starting from the specified + """Creates an expression that returns a slice of an array starting from the specified offset with a given length. Example: