From 15e608bf90fbf8e521de0a3ba73c0ac81a24f393 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 23 Mar 2026 16:16:31 -0700 Subject: [PATCH 01/10] feat(firestore): add new array-based pipeline expressions --- .../firestore_v1/pipeline_expressions.py | 198 +++++++++ .../tests/system/pipeline_e2e/array.yaml | 413 ++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 118 +++++ 3 files changed, 729 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 2edd510070ca..82a926719c9b 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 @@ -781,6 +781,204 @@ def array_reverse(self) -> "Expression": """ return FunctionExpression("array_reverse", [self]) + @expose_as_static + def array_maximum(self) -> "Expression": + """Creates an expression that finds the maximum element in a numeric array. + + Example: + >>> # Get the maximum value in the 'scores' array + >>> Field.of("scores").array_maximum() + + Returns: + A new `Expression` representing the maximum element. + """ + return FunctionExpression("maximum", [self]) + + @expose_as_static + def array_minimum(self) -> "Expression": + """Creates an expression that finds the minimum element in a numeric array. + + Example: + >>> # Get the minimum value in the 'scores' array + >>> Field.of("scores").array_minimum() + + Returns: + A new `Expression` representing the minimum element. + """ + return FunctionExpression("minimum", [self]) + + @expose_as_static + def array_first(self) -> "Expression": + """Creates an expression that returns the first element of an array. + + Example: + >>> # Get the first value in the 'scores' array + >>> Field.of("scores").array_first() + + Returns: + A new `Expression` representing the first element. + """ + return FunctionExpression("array_first", [self]) + + @expose_as_static + def array_first_n(self, n: Expression | int) -> "Expression": + """Creates an expression that returns the first N elements of an array. + + Example: + >>> # Get the first 3 values in the 'scores' array + >>> Field.of("scores").array_first_n(3) + + Args: + n: The number of elements to return. Can be an integer or an Expression. + + Returns: + A new `Expression` representing the first N elements. + """ + return FunctionExpression( + "array_first_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) + + @expose_as_static + def array_last(self) -> "Expression": + """Creates an expression that returns the last element of an array. + + Example: + >>> # Get the last value in the 'scores' array + >>> Field.of("scores").array_last() + + Returns: + A new `Expression` representing the last element. + """ + return FunctionExpression("array_last", [self]) + + @expose_as_static + def array_last_n(self, n: Expression | int) -> "Expression": + """Creates an expression that returns the last N elements of an array. + + Example: + >>> # Get the last 3 values in the 'scores' array + >>> Field.of("scores").array_last_n(3) + + Args: + n: The number of elements to return. Can be an integer or an Expression. + + Returns: + A new `Expression` representing the last N elements. + """ + return FunctionExpression( + "array_last_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) + + @expose_as_static + def array_maximum_n(self, n: Expression | int) -> "Expression": + """Creates an expression that finds the N maximum elements in an array. + + Example: + >>> # Get the 3 highest scores + >>> Field.of("scores").array_maximum_n(3) + + Note: This does not use a stable sort, meaning the order of equivalent + elements is undefined. + + Args: + n: The number of elements to return. Can be an integer or an Expression. + + Returns: + A new `Expression` representing the maximum N elements. + """ + return FunctionExpression( + "maximum_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) + + @expose_as_static + def array_minimum_n(self, n: Expression | int) -> "Expression": + """Creates an expression that finds the N minimum elements in an array. + + Example: + >>> # Get the 3 lowest scores + >>> Field.of("scores").array_minimum_n(3) + + Note: This does not use a stable sort, meaning the order of equivalent + elements is undefined. + + Args: + n: The number of elements to return. Can be an integer or an Expression. + + Returns: + A new `Expression` representing the minimum N elements. + """ + return FunctionExpression( + "minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)] + ) + + @expose_as_static + def array_index_of( + self, search: Expression | CONSTANT_TYPE + ) -> "Expression": + """Creates an expression that returns the first index of a search value in an array. + + Returns the first index of the search value in the array, or -1 if not found. + + Example: + >>> # Get the first position of 'user_1' in an array + >>> Field.of("users").array_index_of("user_1") + + Args: + search: The value to search for. Can be an Expression or a constant. + + Returns: + A new `Expression` representing the index of the element. + """ + 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 array_index_of_all(self, search: Expression | CONSTANT_TYPE) -> "Expression": + """Creates an expression that returns the indices of all occurrences of a search value in an array. + + Example: + >>> # Get all positions of 'user_1' in an array + >>> Field.of("users").array_index_of_all("user_1") + + Args: + search: The value to search for. Can be an Expression or a constant. + + Returns: + 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_slice( + self, offset: Expression | int, length: Expression | int | None = None + ) -> "Expression": + """Creates an expression that returns a slice of an array starting from the specified offset. + + Example: + >>> # Get a slice of the array starting at index 1 and taking 2 elements + >>> Field.of("scores").array_slice(1, 2) + + Args: + offset: The 0-based index of the first element to include. + length: The number of elements to include. If omitted, takes all remaining. + + Returns: + A new `Expression` representing the sliced 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 array_concat( self, *other_arrays: Array | list[Expression | CONSTANT_TYPE] | 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 f82f1cbc1564..159d8f1db8d6 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,417 @@ tests: - fieldReferenceValue: tags - integerValue: '-1' name: array_get + 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" + 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: + maxTag: + functionValue: + args: + - fieldReferenceValue: tags + name: maximum + name: select + - 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" + 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: + minTag: + functionValue: + args: + - fieldReferenceValue: tags + name: minimum + 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 + - "val" + assert_results: + - val: "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: + val: + functionValue: + args: + - fieldReferenceValue: tags + name: array_first + 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 + - "val" + assert_results: + - val: ["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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '2' + name: array_first_n + 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 + - "val" + assert_results: + - val: "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: + val: + functionValue: + args: + - fieldReferenceValue: tags + name: array_last + 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 + - "val" + assert_results: + - val: ["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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '2' + name: array_last_n + name: select + - 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 + - "val" + assert_results: + - val: ["space", "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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '2' + name: maximum_n + name: select + - 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 + - "val" + assert_results: + - val: ["adventure", "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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '2' + name: minimum_n + name: select + - 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" + - "val" + assert_results: + - val: 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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "space" + - stringValue: "first" + name: array_index_of + name: select + - description: testArrayIndexOfAll + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_index_of_all: + - Field: tags + - Constant: "space" + - "val" + assert_results: + - val: [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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "space" + name: array_index_of_all + name: select + - description: testArraySlice + 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: 2 + - "val" + assert_results: + - val: ["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: + val: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '1' + - integerValue: '2' + name: array_slice 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 f4804da25e14..489e31a23305 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 @@ -1453,6 +1453,124 @@ def test_array_reverse(self): infix_instance = arg1.array_reverse() assert infix_instance == instance + def test_array_maximum(self): + arg1 = self._make_arg("Array") + instance = Expression.array_maximum(arg1) + assert instance.name == "maximum" + assert instance.params == [arg1] + assert repr(instance) == "Array.array_maximum()" + infix_instance = arg1.array_maximum() + assert infix_instance == instance + + def test_array_minimum(self): + arg1 = self._make_arg("Array") + instance = Expression.array_minimum(arg1) + assert instance.name == "minimum" + assert instance.params == [arg1] + assert repr(instance) == "Array.array_minimum()" + infix_instance = arg1.array_minimum() + assert infix_instance == instance + + def test_array_first(self): + arg1 = self._make_arg("Array") + instance = Expression.array_first(arg1) + assert instance.name == "array_first" + assert instance.params == [arg1] + assert repr(instance) == "Array.array_first()" + infix_instance = arg1.array_first() + assert infix_instance == instance + + def test_array_first_n(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("N") + instance = Expression.array_first_n(arg1, arg2) + assert instance.name == "array_first_n" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Array.array_first_n(N)" + infix_instance = arg1.array_first_n(arg2) + assert infix_instance == instance + + def test_array_last(self): + arg1 = self._make_arg("Array") + instance = Expression.array_last(arg1) + assert instance.name == "array_last" + assert instance.params == [arg1] + assert repr(instance) == "Array.array_last()" + infix_instance = arg1.array_last() + assert infix_instance == instance + + def test_array_last_n(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("N") + instance = Expression.array_last_n(arg1, arg2) + assert instance.name == "array_last_n" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Array.array_last_n(N)" + infix_instance = arg1.array_last_n(arg2) + assert infix_instance == instance + + def test_array_maximum_n(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("N") + instance = Expression.array_maximum_n(arg1, arg2) + assert instance.name == "maximum_n" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Array.array_maximum_n(N)" + infix_instance = arg1.array_maximum_n(arg2) + assert infix_instance == instance + + def test_array_minimum_n(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("N") + instance = Expression.array_minimum_n(arg1, arg2) + assert instance.name == "minimum_n" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Array.array_minimum_n(N)" + infix_instance = arg1.array_minimum_n(arg2) + assert infix_instance == instance + + def test_array_index_of(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("Search") + instance = Expression.array_index_of(arg1, arg2) + assert instance.name == "array_index_of" + assert len(instance.params) == 3 + assert instance.params[0] == arg1 + assert instance.params[1] == arg2 + infix_instance = arg1.array_index_of(arg2) + assert infix_instance == instance + + def test_array_index_of_all(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("Search") + instance = Expression.array_index_of_all(arg1, arg2) + assert instance.name == "array_index_of_all" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Array.array_index_of_all(Search)" + infix_instance = arg1.array_index_of_all(arg2) + assert infix_instance == instance + + def test_array_slice(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("Offset") + instance = Expression.array_slice(arg1, arg2) + assert instance.name == "array_slice" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Array.array_slice(Offset)" + infix_instance = arg1.array_slice(arg2) + assert infix_instance == instance + + def test_array_slice_with_length(self): + arg1 = self._make_arg("Array") + arg2 = self._make_arg("Offset") + arg3 = self._make_arg("Length") + instance = Expression.array_slice(arg1, arg2, arg3) + assert instance.name == "array_slice" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "Array.array_slice(Offset, Length)" + infix_instance = arg1.array_slice(arg2, arg3) + assert infix_instance == instance + def test_array_concat(self): arg1 = self._make_arg("ArrayRef1") arg2 = self._make_arg("ArrayRef2") From 4a2de3a1c9220e20f73019abfaa7ca3fd0cbb9b5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 23 Mar 2026 16:48:06 -0700 Subject: [PATCH 02/10] fixed infix naming --- .../google/cloud/firestore_v1/pipeline_expressions.py | 8 ++++---- .../tests/unit/v1/test_pipeline_expressions.py | 1 + 2 files changed, 5 insertions(+), 4 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 82a926719c9b..355219a4fefe 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 @@ -792,7 +792,7 @@ def array_maximum(self) -> "Expression": Returns: A new `Expression` representing the maximum element. """ - return FunctionExpression("maximum", [self]) + return FunctionExpression("maximum", [self], infix_name_override="array_maximum") @expose_as_static def array_minimum(self) -> "Expression": @@ -805,7 +805,7 @@ def array_minimum(self) -> "Expression": Returns: A new `Expression` representing the minimum element. """ - return FunctionExpression("minimum", [self]) + return FunctionExpression("minimum", [self], infix_name_override="array_minimum") @expose_as_static def array_first(self) -> "Expression": @@ -887,7 +887,7 @@ def array_maximum_n(self, n: Expression | int) -> "Expression": A new `Expression` representing the maximum N elements. """ 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 @@ -908,7 +908,7 @@ def array_minimum_n(self, n: Expression | int) -> "Expression": A new `Expression` representing the minimum N elements. """ 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 489e31a23305..337f4a10d32a 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 @@ -1537,6 +1537,7 @@ def test_array_index_of(self): assert len(instance.params) == 3 assert instance.params[0] == arg1 assert instance.params[1] == arg2 + assert instance.params[2] == "first" infix_instance = arg1.array_index_of(arg2) assert infix_instance == instance From 4d273ee74ce4c28ce0f9cfab3b90101a73fccd5c Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 23 Mar 2026 16:48:42 -0700 Subject: [PATCH 03/10] ran format --- .../firestore_v1/pipeline_expressions.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 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 355219a4fefe..1e36e9014fb7 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 @@ -792,7 +792,9 @@ def array_maximum(self) -> "Expression": Returns: A new `Expression` representing the maximum element. """ - return FunctionExpression("maximum", [self], infix_name_override="array_maximum") + return FunctionExpression( + "maximum", [self], infix_name_override="array_maximum" + ) @expose_as_static def array_minimum(self) -> "Expression": @@ -805,7 +807,9 @@ def array_minimum(self) -> "Expression": Returns: A new `Expression` representing the minimum element. """ - return FunctionExpression("minimum", [self], infix_name_override="array_minimum") + return FunctionExpression( + "minimum", [self], infix_name_override="array_minimum" + ) @expose_as_static def array_first(self) -> "Expression": @@ -887,7 +891,9 @@ def array_maximum_n(self, n: Expression | int) -> "Expression": A new `Expression` representing the maximum N elements. """ return FunctionExpression( - "maximum_n", [self, self._cast_to_expr_or_convert_to_constant(n)], infix_name_override="array_maximum_n" + "maximum_n", + [self, self._cast_to_expr_or_convert_to_constant(n)], + infix_name_override="array_maximum_n", ) @expose_as_static @@ -908,13 +914,13 @@ def array_minimum_n(self, n: Expression | int) -> "Expression": A new `Expression` representing the minimum N elements. """ return FunctionExpression( - "minimum_n", [self, self._cast_to_expr_or_convert_to_constant(n)], infix_name_override="array_minimum_n" + "minimum_n", + [self, self._cast_to_expr_or_convert_to_constant(n)], + infix_name_override="array_minimum_n", ) @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 first index of a search value in an array. Returns the first index of the search value in the array, or -1 if not found. From 49812e1786ca9f0e24e4696319b61f6bb4bd858a Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 23 Mar 2026 17:17:21 -0700 Subject: [PATCH 04/10] added new expressions --- .../firestore_v1/pipeline_expressions.py | 183 ++++++++++++++++++ .../system/pipeline_e2e/date_and_time.yaml | 24 +++ .../tests/system/pipeline_e2e/general.yaml | 19 +- .../tests/system/pipeline_e2e/logical.yaml | 34 ++++ .../tests/system/pipeline_e2e/map.yaml | 54 ++++++ .../unit/v1/test_pipeline_expressions.py | 127 ++++++++++++ 6 files changed, 440 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 1e36e9014fb7..d695ebac1e38 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 @@ -1917,6 +1917,189 @@ def as_(self, alias: str) -> "AliasedExpression": """ return AliasedExpression(self, alias) + @expose_as_static + def cmp(self, other: "Expression | CONSTANT_TYPE") -> "Expression": + """Creates an expression that compares this expression to another expression. + + Returns an integer: + * -1 if this expression is less than the other + * 0 if they are equal + * 1 if this expression is greater than the other + + Example: + >>> # Compare the 'price' field to 10 + >>> Field.of("price").cmp(10) + + Returns: + A new `Expression` representing the comparison operation. + """ + return FunctionExpression("cmp", [self, self._cast_to_expr_or_convert_to_constant(other)]) + + @expose_as_static + def timestamp_trunc( + self, granularity: "Expression | str", timezone: "Expression | str | None" = None + ) -> "Expression": + """Creates an expression that truncates a timestamp to a specified granularity. + + Example: + >>> # Truncate the 'createdAt' field to the day + >>> Field.of("createdAt").timestamp_trunc("day") + + Returns: + A new `Expression` representing the timestamp_trunc operation. + """ + args = [self, self._cast_to_expr_or_convert_to_constant(granularity)] + if timezone is not None: + args.append(self._cast_to_expr_or_convert_to_constant(timezone)) + return FunctionExpression("timestamp_trunc", args) + + @expose_as_static + def timestamp_extract( + self, part: "Expression | str", timezone: "Expression | str | None" = None + ) -> "Expression": + """Creates an expression that extracts a part of a timestamp. + + Example: + >>> # Extract the year from the 'createdAt' field + >>> Field.of("createdAt").timestamp_extract("year") + + Returns: + A new `Expression` representing the timestamp_extract operation. + """ + args = [self, self._cast_to_expr_or_convert_to_constant(part)] + if timezone is not None: + args.append(self._cast_to_expr_or_convert_to_constant(timezone)) + return FunctionExpression("timestamp_extract", args) + + @expose_as_static + def timestamp_diff( + self, start: "Expression | CONSTANT_TYPE", unit: "Expression | str" + ) -> "Expression": + """Creates an expression that computes the difference between two timestamps in the specified unit. + + Example: + >>> # Compute the difference in days between the 'end' field and the 'start' field + >>> Field.of("end").timestamp_diff(Field.of("start"), "day") + + Returns: + A new `Expression` representing the timestamp_diff operation. + """ + return FunctionExpression( + "timestamp_diff", + [ + self, + self._cast_to_expr_or_convert_to_constant(start), + self._cast_to_expr_or_convert_to_constant(unit), + ], + ) + + @expose_as_static + def if_null(self, *others: "Expression | CONSTANT_TYPE") -> "Expression": + """Creates an expression that returns the first non-null expression from the provided arguments. + + Example: + >>> # Return the 'nickname' field if not null, otherwise return 'firstName' + >>> Field.of("nickname").if_null(Field.of("firstName")) + + Returns: + A new `Expression` representing the if_null operation. + """ + return FunctionExpression( + "if_null", + [self] + [self._cast_to_expr_or_convert_to_constant(o) for o in others], + ) + + @expose_as_static + def map_set( + self, + key: "Expression | CONSTANT_TYPE", + value: "Expression | CONSTANT_TYPE", + *more_key_values: "Expression | CONSTANT_TYPE", + ) -> "Expression": + """Creates an expression that returns a new Map with the specified entries added or updated. + + Example: + >>> # Update the 'city' key in a map to "San Francisco" + >>> Map({"city": "Los Angeles"}).map_set("city", "San Francisco") + + Returns: + A new `Expression` representing the map_set operation as a Map. + """ + args = [ + self, + self._cast_to_expr_or_convert_to_constant(key), + self._cast_to_expr_or_convert_to_constant(value), + ] + args.extend([self._cast_to_expr_or_convert_to_constant(o) for o in more_key_values]) + return FunctionExpression("map_set", args) + + @expose_as_static + def map_keys(self) -> "Expression": + """Creates an expression that returns the keys of a map as an Array. + + Example: + >>> # Get the keys of a map + >>> Map({"city": "Los Angeles"}).map_keys() + + Returns: + A new `Expression` representing the map_keys operation as an Array. + """ + return FunctionExpression("map_keys", [self]) + + @expose_as_static + def map_values(self) -> "Expression": + """Creates an expression that returns the values of a map as an Array. + + Example: + >>> # Get the values from a map + >>> Map({"city": "Los Angeles"}).map_values() + + Returns: + A new `Expression` representing the map_values operation as an Array. + """ + return FunctionExpression("map_values", [self]) + + @expose_as_static + def map_entries(self) -> "Expression": + """Creates an expression that returns the entries of a map as an Array of structured Maps. + + Example: + >>> # Get the entries of a map + >>> Map({"city": "Los Angeles"}).map_entries() + + Returns: + A new `Expression` representing the map_entries operation as an Array of Maps (containing 'key' and 'value' fields). + """ + return FunctionExpression("map_entries", [self]) + + @expose_as_static + def type(self) -> "Expression": + """Creates an expression that returns the data type of this expression's result as a string. + + Example: + >>> # Get the type of the 'title' field + >>> Field.of("title").type() + + Returns: + A new `Expression` representing the type operation. + """ + return FunctionExpression("type", [self]) + + @expose_as_static + def is_type(self, type_val: "Expression | str") -> "BooleanExpression": + """Creates an expression that checks if the result is of the specified type. + + Example: + >>> # Check if the 'price' field is a number + >>> Field.of("price").is_type("number") + + Returns: + A new `BooleanExpression` representing the is_type operation. + """ + return BooleanExpression( + "is_type", [self, self._cast_to_expr_or_convert_to_constant(type_val)] + ) + class Constant(Expression, Generic[CONSTANT_TYPE]): """Represents a constant literal value in an expression.""" diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml index 2319b333bf58..1eaf25d37d23 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml @@ -101,3 +101,27 @@ tests: from_seconds: "1993-04-28T12:01:00.000000+00:00" plus_day: "1993-04-29T12:01:00.654321+00:00" minus_hour: "1993-04-28T11:01:00.654321+00:00" + - description: testTimestampTruncExtractDiff + pipeline: + - Collection: timestamps + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.timestamp_trunc: + - Field: time + - Constant: "day" + - "trunc_day" + - AliasedExpression: + - FunctionExpression.timestamp_extract: + - Field: time + - Constant: "year" + - "extract_year" + - AliasedExpression: + - FunctionExpression.timestamp_diff: + - FunctionExpression.timestamp_add: + - Field: time + - Constant: "day" + - Constant: 1 + - Field: time + - Constant: "hour" + - "diff_hours" 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 46a10cd4d1af..c645157f07a8 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -684,4 +684,21 @@ tests: - args: - fieldReferenceValue: awards - stringValue: full_replace - name: replace_with \ No newline at end of file + name: replace_with + - description: testTypeAndIsType + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.type: + - Constant: "hello" + - "val_type" + - AliasedExpression: + - FunctionExpression.is_type: + - Constant: "hello" + - Constant: "string" + - "val_is_string" + assert_results: + - val_type: "string" + val_is_string: true \ No newline at end of file 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 296cfda146c2..d0b91264b385 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml @@ -688,3 +688,37 @@ tests: conditional_field: "Dystopian" - title: "Dune" conditional_field: "Frank Herbert" + - description: testCmp + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - FunctionExpression.cmp: + - Field: rating + - Constant: 4.5 + - Constant: 1 + - Select: + - title + - Sort: + - Ordering: + - Field: title + - ASCENDING + assert_results: + - title: Dune + - title: The Lord of the Rings + - description: testIfNull + pipeline: + - Collection: errors + - Select: + - AliasedExpression: + - FunctionExpression.if_null: + - Field: value + - Constant: "default" + - "value_or_default" + - Sort: + - Ordering: + - Field: value_or_default + - ASCENDING + assert_results: + - value_or_default: "NaN" + - value_or_default: "default" diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml index 3e5e5de12e9d..d9f5b20431d9 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml @@ -267,3 +267,57 @@ tests: a: "orig" b: "new" c: "new" + - description: testMapSet + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.map_set: + - Map: + elements: {"a": "orig"} + - Constant: "b" + - Constant: "new" + - "merged" + assert_results: + - merged: + a: "orig" + b: "new" + - description: testMapKeys + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.map_keys: + - Map: + elements: {"a": "1", "b": "2"} + - "keys" + assert_results: + - keys: ["a", "b"] + - description: testMapValues + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.map_values: + - Map: + elements: {"a": "1", "b": "2"} + - "values" + assert_results: + - values: ["1", "2"] + - description: testMapEntries + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.map_entries: + - Map: + elements: {"a": "1"} + - "entries" + assert_results: + - entries: + - k: "a" + v: "1" 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 337f4a10d32a..b7e91d0ae7fa 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 @@ -1761,3 +1761,130 @@ def test_last(self): assert repr(instance) == "Value.last()" infix_instance = arg1.last() assert infix_instance == instance + + def test_cmp(self): + arg1 = self._make_arg("Value") + arg2 = self._make_arg("Other") + instance = Expression.cmp(arg1, arg2) + assert instance.name == "cmp" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Value.cmp(Other)" + infix_instance = arg1.cmp(arg2) + assert infix_instance == instance + + def test_timestamp_trunc(self): + arg1 = self._make_arg("Timestamp") + arg2 = self._make_arg("Granularity") + arg3 = self._make_arg("Timezone") + instance = Expression.timestamp_trunc(arg1, arg2) + assert instance.name == "timestamp_trunc" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Timestamp.timestamp_trunc(Granularity)" + infix_instance = arg1.timestamp_trunc(arg2) + assert infix_instance == instance + + instance_tz = Expression.timestamp_trunc(arg1, arg2, arg3) + assert instance_tz.name == "timestamp_trunc" + assert instance_tz.params == [arg1, arg2, arg3] + assert repr(instance_tz) == "Timestamp.timestamp_trunc(Granularity, Timezone)" + infix_instance_tz = arg1.timestamp_trunc(arg2, arg3) + assert infix_instance_tz == instance_tz + + def test_timestamp_extract(self): + arg1 = self._make_arg("Timestamp") + arg2 = self._make_arg("Part") + arg3 = self._make_arg("Timezone") + instance = Expression.timestamp_extract(arg1, arg2) + assert instance.name == "timestamp_extract" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Timestamp.timestamp_extract(Part)" + infix_instance = arg1.timestamp_extract(arg2) + assert infix_instance == instance + + instance_tz = Expression.timestamp_extract(arg1, arg2, arg3) + assert instance_tz.name == "timestamp_extract" + assert instance_tz.params == [arg1, arg2, arg3] + assert repr(instance_tz) == "Timestamp.timestamp_extract(Part, Timezone)" + infix_instance_tz = arg1.timestamp_extract(arg2, arg3) + assert infix_instance_tz == instance_tz + + def test_timestamp_diff(self): + arg1 = self._make_arg("End") + arg2 = self._make_arg("Start") + arg3 = self._make_arg("Unit") + instance = Expression.timestamp_diff(arg1, arg2, arg3) + assert instance.name == "timestamp_diff" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "End.timestamp_diff(Start, Unit)" + infix_instance = arg1.timestamp_diff(arg2, arg3) + assert infix_instance == instance + + def test_if_null(self): + arg1 = self._make_arg("Field1") + arg2 = self._make_arg("Field2") + arg3 = self._make_arg("Field3") + instance = Expression.if_null(arg1, arg2, arg3) + assert instance.name == "if_null" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "Field1.if_null(Field2, Field3)" + infix_instance = arg1.if_null(arg2, arg3) + assert infix_instance == instance + + def test_map_set(self): + arg1 = self._make_arg("Map1") + arg2 = self._make_arg("Key") + arg3 = self._make_arg("Value") + arg4 = self._make_arg("MoreKey") + arg5 = self._make_arg("MoreValue") + instance = Expression.map_set(arg1, arg2, arg3, arg4, arg5) + assert instance.name == "map_set" + assert instance.params == [arg1, arg2, arg3, arg4, arg5] + assert repr(instance) == "Map1.map_set(Key, Value, MoreKey, MoreValue)" + infix_instance = arg1.map_set(arg2, arg3, arg4, arg5) + assert infix_instance == instance + + def test_map_keys(self): + arg1 = self._make_arg("Map1") + instance = Expression.map_keys(arg1) + assert instance.name == "map_keys" + assert instance.params == [arg1] + assert repr(instance) == "Map1.map_keys()" + infix_instance = arg1.map_keys() + assert infix_instance == instance + + def test_map_values(self): + arg1 = self._make_arg("Map1") + instance = Expression.map_values(arg1) + assert instance.name == "map_values" + assert instance.params == [arg1] + assert repr(instance) == "Map1.map_values()" + infix_instance = arg1.map_values() + assert infix_instance == instance + + def test_map_entries(self): + arg1 = self._make_arg("Map1") + instance = Expression.map_entries(arg1) + assert instance.name == "map_entries" + assert instance.params == [arg1] + assert repr(instance) == "Map1.map_entries()" + infix_instance = arg1.map_entries() + assert infix_instance == instance + + def test_type(self): + arg1 = self._make_arg("Value") + instance = Expression.type(arg1) + assert instance.name == "type" + assert instance.params == [arg1] + assert repr(instance) == "Value.type()" + infix_instance = arg1.type() + assert infix_instance == instance + + def test_is_type(self): + arg1 = self._make_arg("Value") + arg2 = self._make_arg("TypeString") + instance = Expression.is_type(arg1, arg2) + assert instance.name == "is_type" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Value.is_type(TypeString)" + infix_instance = arg1.is_type(arg2) + assert infix_instance == instance From d241ada56dbcf61ad6c2f4ec94fff0480d2a01a3 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 23 Mar 2026 17:17:35 -0700 Subject: [PATCH 05/10] improved types --- .../cloud/firestore_v1/pipeline_expressions.py | 16 ++++++++-------- 1 file changed, 8 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 d695ebac1e38..187b82901837 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 @@ -2015,7 +2015,7 @@ def map_set( key: "Expression | CONSTANT_TYPE", value: "Expression | CONSTANT_TYPE", *more_key_values: "Expression | CONSTANT_TYPE", - ) -> "Expression": + ) -> "Map": """Creates an expression that returns a new Map with the specified entries added or updated. Example: @@ -2023,7 +2023,7 @@ def map_set( >>> Map({"city": "Los Angeles"}).map_set("city", "San Francisco") Returns: - A new `Expression` representing the map_set operation as a Map. + A new `Map` expression representing the map_set operation. """ args = [ self, @@ -2034,7 +2034,7 @@ def map_set( return FunctionExpression("map_set", args) @expose_as_static - def map_keys(self) -> "Expression": + def map_keys(self) -> "Array": """Creates an expression that returns the keys of a map as an Array. Example: @@ -2042,12 +2042,12 @@ def map_keys(self) -> "Expression": >>> Map({"city": "Los Angeles"}).map_keys() Returns: - A new `Expression` representing the map_keys operation as an Array. + A new `Array` expression representing the map_keys operation. """ return FunctionExpression("map_keys", [self]) @expose_as_static - def map_values(self) -> "Expression": + def map_values(self) -> "Array": """Creates an expression that returns the values of a map as an Array. Example: @@ -2055,12 +2055,12 @@ def map_values(self) -> "Expression": >>> Map({"city": "Los Angeles"}).map_values() Returns: - A new `Expression` representing the map_values operation as an Array. + A new `Array` expression representing the map_values operation. """ return FunctionExpression("map_values", [self]) @expose_as_static - def map_entries(self) -> "Expression": + def map_entries(self) -> "Array": """Creates an expression that returns the entries of a map as an Array of structured Maps. Example: @@ -2068,7 +2068,7 @@ def map_entries(self) -> "Expression": >>> Map({"city": "Los Angeles"}).map_entries() Returns: - A new `Expression` representing the map_entries operation as an Array of Maps (containing 'key' and 'value' fields). + A new `Array` expression representing the map_entries operation (containing 'key' and 'value' fields). """ return FunctionExpression("map_entries", [self]) From 8d48e28f790c058df521f43a8b64eaffbbca1a38 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Mon, 23 Mar 2026 17:18:02 -0700 Subject: [PATCH 06/10] ran format --- .../cloud/firestore_v1/pipeline_expressions.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 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 187b82901837..beb4e8fa24b0 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 @@ -1933,11 +1933,15 @@ def cmp(self, other: "Expression | CONSTANT_TYPE") -> "Expression": Returns: A new `Expression` representing the comparison operation. """ - return FunctionExpression("cmp", [self, self._cast_to_expr_or_convert_to_constant(other)]) + return FunctionExpression( + "cmp", [self, self._cast_to_expr_or_convert_to_constant(other)] + ) @expose_as_static def timestamp_trunc( - self, granularity: "Expression | str", timezone: "Expression | str | None" = None + self, + granularity: "Expression | str", + timezone: "Expression | str | None" = None, ) -> "Expression": """Creates an expression that truncates a timestamp to a specified granularity. @@ -2030,7 +2034,9 @@ def map_set( self._cast_to_expr_or_convert_to_constant(key), self._cast_to_expr_or_convert_to_constant(value), ] - args.extend([self._cast_to_expr_or_convert_to_constant(o) for o in more_key_values]) + args.extend( + [self._cast_to_expr_or_convert_to_constant(o) for o in more_key_values] + ) return FunctionExpression("map_set", args) @expose_as_static From 1b30d6f5bd38229e369bd1455b4673b86fc74585 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 25 Mar 2026 15:03:19 -0700 Subject: [PATCH 07/10] removed already implemened expressions --- .../firestore_v1/pipeline_expressions.py | 273 +----------- .../tests/system/pipeline_e2e/array.yaml | 413 ------------------ .../tests/system/pipeline_e2e/map.yaml | 54 --- .../unit/v1/test_pipeline_expressions.py | 162 +------ 4 files changed, 3 insertions(+), 899 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 beb4e8fa24b0..24d48addf186 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 @@ -781,211 +781,7 @@ def array_reverse(self) -> "Expression": """ return FunctionExpression("array_reverse", [self]) - @expose_as_static - def array_maximum(self) -> "Expression": - """Creates an expression that finds the maximum element in a numeric array. - - Example: - >>> # Get the maximum value in the 'scores' array - >>> Field.of("scores").array_maximum() - - Returns: - A new `Expression` representing the maximum element. - """ - return FunctionExpression( - "maximum", [self], infix_name_override="array_maximum" - ) - - @expose_as_static - def array_minimum(self) -> "Expression": - """Creates an expression that finds the minimum element in a numeric array. - - Example: - >>> # Get the minimum value in the 'scores' array - >>> Field.of("scores").array_minimum() - - Returns: - A new `Expression` representing the minimum element. - """ - return FunctionExpression( - "minimum", [self], infix_name_override="array_minimum" - ) - - @expose_as_static - def array_first(self) -> "Expression": - """Creates an expression that returns the first element of an array. - - Example: - >>> # Get the first value in the 'scores' array - >>> Field.of("scores").array_first() - - Returns: - A new `Expression` representing the first element. - """ - return FunctionExpression("array_first", [self]) - - @expose_as_static - def array_first_n(self, n: Expression | int) -> "Expression": - """Creates an expression that returns the first N elements of an array. - - Example: - >>> # Get the first 3 values in the 'scores' array - >>> Field.of("scores").array_first_n(3) - - Args: - n: The number of elements to return. Can be an integer or an Expression. - - Returns: - A new `Expression` representing the first N elements. - """ - return FunctionExpression( - "array_first_n", [self, self._cast_to_expr_or_convert_to_constant(n)] - ) - - @expose_as_static - def array_last(self) -> "Expression": - """Creates an expression that returns the last element of an array. - - Example: - >>> # Get the last value in the 'scores' array - >>> Field.of("scores").array_last() - - Returns: - A new `Expression` representing the last element. - """ - return FunctionExpression("array_last", [self]) - - @expose_as_static - def array_last_n(self, n: Expression | int) -> "Expression": - """Creates an expression that returns the last N elements of an array. - - Example: - >>> # Get the last 3 values in the 'scores' array - >>> Field.of("scores").array_last_n(3) - - Args: - n: The number of elements to return. Can be an integer or an Expression. - - Returns: - A new `Expression` representing the last N elements. - """ - return FunctionExpression( - "array_last_n", [self, self._cast_to_expr_or_convert_to_constant(n)] - ) - - @expose_as_static - def array_maximum_n(self, n: Expression | int) -> "Expression": - """Creates an expression that finds the N maximum elements in an array. - - Example: - >>> # Get the 3 highest scores - >>> Field.of("scores").array_maximum_n(3) - - Note: This does not use a stable sort, meaning the order of equivalent - elements is undefined. - - Args: - n: The number of elements to return. Can be an integer or an Expression. - - Returns: - A new `Expression` representing the maximum N elements. - """ - return FunctionExpression( - "maximum_n", - [self, self._cast_to_expr_or_convert_to_constant(n)], - infix_name_override="array_maximum_n", - ) - - @expose_as_static - def array_minimum_n(self, n: Expression | int) -> "Expression": - """Creates an expression that finds the N minimum elements in an array. - - Example: - >>> # Get the 3 lowest scores - >>> Field.of("scores").array_minimum_n(3) - - Note: This does not use a stable sort, meaning the order of equivalent - elements is undefined. - - Args: - n: The number of elements to return. Can be an integer or an Expression. - - Returns: - A new `Expression` representing the minimum N elements. - """ - return FunctionExpression( - "minimum_n", - [self, self._cast_to_expr_or_convert_to_constant(n)], - infix_name_override="array_minimum_n", - ) - - @expose_as_static - def array_index_of(self, search: Expression | CONSTANT_TYPE) -> "Expression": - """Creates an expression that returns the first index of a search value in an array. - - Returns the first index of the search value in the array, or -1 if not found. - - Example: - >>> # Get the first position of 'user_1' in an array - >>> Field.of("users").array_index_of("user_1") - - Args: - search: The value to search for. Can be an Expression or a constant. - - Returns: - A new `Expression` representing the index of the element. - """ - 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 array_index_of_all(self, search: Expression | CONSTANT_TYPE) -> "Expression": - """Creates an expression that returns the indices of all occurrences of a search value in an array. - - Example: - >>> # Get all positions of 'user_1' in an array - >>> Field.of("users").array_index_of_all("user_1") - - Args: - search: The value to search for. Can be an Expression or a constant. - - Returns: - 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_slice( - self, offset: Expression | int, length: Expression | int | None = None - ) -> "Expression": - """Creates an expression that returns a slice of an array starting from the specified offset. - - Example: - >>> # Get a slice of the array starting at index 1 and taking 2 elements - >>> Field.of("scores").array_slice(1, 2) - - Args: - offset: The 0-based index of the first element to include. - length: The number of elements to include. If omitted, takes all remaining. - - Returns: - A new `Expression` representing the sliced 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 + @expose_as_static def array_concat( self, *other_arrays: Array | list[Expression | CONSTANT_TYPE] | Expression ) -> "Expression": @@ -1148,7 +944,7 @@ def count(self) -> "Expression": Example: >>> # Count the total number of products - >>> Field.of("productId").count().as_("totalProducts") + >>> Field.of("productId").count().as_("totalProducts"ss Returns: A new `AggregateFunction` representing the 'count' aggregation. @@ -2013,71 +1809,6 @@ def if_null(self, *others: "Expression | CONSTANT_TYPE") -> "Expression": [self] + [self._cast_to_expr_or_convert_to_constant(o) for o in others], ) - @expose_as_static - def map_set( - self, - key: "Expression | CONSTANT_TYPE", - value: "Expression | CONSTANT_TYPE", - *more_key_values: "Expression | CONSTANT_TYPE", - ) -> "Map": - """Creates an expression that returns a new Map with the specified entries added or updated. - - Example: - >>> # Update the 'city' key in a map to "San Francisco" - >>> Map({"city": "Los Angeles"}).map_set("city", "San Francisco") - - Returns: - A new `Map` expression representing the map_set operation. - """ - args = [ - self, - self._cast_to_expr_or_convert_to_constant(key), - self._cast_to_expr_or_convert_to_constant(value), - ] - args.extend( - [self._cast_to_expr_or_convert_to_constant(o) for o in more_key_values] - ) - return FunctionExpression("map_set", args) - - @expose_as_static - def map_keys(self) -> "Array": - """Creates an expression that returns the keys of a map as an Array. - - Example: - >>> # Get the keys of a map - >>> Map({"city": "Los Angeles"}).map_keys() - - Returns: - A new `Array` expression representing the map_keys operation. - """ - return FunctionExpression("map_keys", [self]) - - @expose_as_static - def map_values(self) -> "Array": - """Creates an expression that returns the values of a map as an Array. - - Example: - >>> # Get the values from a map - >>> Map({"city": "Los Angeles"}).map_values() - - Returns: - A new `Array` expression representing the map_values operation. - """ - return FunctionExpression("map_values", [self]) - - @expose_as_static - def map_entries(self) -> "Array": - """Creates an expression that returns the entries of a map as an Array of structured Maps. - - Example: - >>> # Get the entries of a map - >>> Map({"city": "Los Angeles"}).map_entries() - - Returns: - A new `Array` expression representing the map_entries operation (containing 'key' and 'value' fields). - """ - return FunctionExpression("map_entries", [self]) - @expose_as_static def type(self) -> "Expression": """Creates an expression that returns the data type of this expression's result as a string. 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 159d8f1db8d6..f82f1cbc1564 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -461,417 +461,4 @@ tests: - fieldReferenceValue: tags - integerValue: '-1' name: array_get - 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" - 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: - maxTag: - functionValue: - args: - - fieldReferenceValue: tags - name: maximum - name: select - - 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" - 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: - minTag: - functionValue: - args: - - fieldReferenceValue: tags - name: minimum - 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 - - "val" - assert_results: - - val: "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: - val: - functionValue: - args: - - fieldReferenceValue: tags - name: array_first - 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 - - "val" - assert_results: - - val: ["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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - integerValue: '2' - name: array_first_n - 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 - - "val" - assert_results: - - val: "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: - val: - functionValue: - args: - - fieldReferenceValue: tags - name: array_last - 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 - - "val" - assert_results: - - val: ["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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - integerValue: '2' - name: array_last_n - name: select - - 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 - - "val" - assert_results: - - val: ["space", "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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - integerValue: '2' - name: maximum_n - name: select - - 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 - - "val" - assert_results: - - val: ["adventure", "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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - integerValue: '2' - name: minimum_n - name: select - - 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" - - "val" - assert_results: - - val: 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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - stringValue: "space" - - stringValue: "first" - name: array_index_of - name: select - - description: testArrayIndexOfAll - pipeline: - - Collection: books - - Where: - - FunctionExpression.equal: - - Field: title - - Constant: "The Hitchhiker's Guide to the Galaxy" - - Select: - - AliasedExpression: - - FunctionExpression.array_index_of_all: - - Field: tags - - Constant: "space" - - "val" - assert_results: - - val: [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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - stringValue: "space" - name: array_index_of_all - name: select - - description: testArraySlice - 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: 2 - - "val" - assert_results: - - val: ["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: - val: - functionValue: - args: - - fieldReferenceValue: tags - - integerValue: '1' - - integerValue: '2' - name: array_slice name: select \ No newline at end of file diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml index d9f5b20431d9..3e5e5de12e9d 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/map.yaml @@ -267,57 +267,3 @@ tests: a: "orig" b: "new" c: "new" - - description: testMapSet - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.map_set: - - Map: - elements: {"a": "orig"} - - Constant: "b" - - Constant: "new" - - "merged" - assert_results: - - merged: - a: "orig" - b: "new" - - description: testMapKeys - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.map_keys: - - Map: - elements: {"a": "1", "b": "2"} - - "keys" - assert_results: - - keys: ["a", "b"] - - description: testMapValues - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.map_values: - - Map: - elements: {"a": "1", "b": "2"} - - "values" - assert_results: - - values: ["1", "2"] - - description: testMapEntries - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.map_entries: - - Map: - elements: {"a": "1"} - - "entries" - assert_results: - - entries: - - k: "a" - v: "1" 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 b7e91d0ae7fa..4f8e8c75091e 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 @@ -1453,126 +1453,7 @@ def test_array_reverse(self): infix_instance = arg1.array_reverse() assert infix_instance == instance - def test_array_maximum(self): - arg1 = self._make_arg("Array") - instance = Expression.array_maximum(arg1) - assert instance.name == "maximum" - assert instance.params == [arg1] - assert repr(instance) == "Array.array_maximum()" - infix_instance = arg1.array_maximum() - assert infix_instance == instance - - def test_array_minimum(self): - arg1 = self._make_arg("Array") - instance = Expression.array_minimum(arg1) - assert instance.name == "minimum" - assert instance.params == [arg1] - assert repr(instance) == "Array.array_minimum()" - infix_instance = arg1.array_minimum() - assert infix_instance == instance - - def test_array_first(self): - arg1 = self._make_arg("Array") - instance = Expression.array_first(arg1) - assert instance.name == "array_first" - assert instance.params == [arg1] - assert repr(instance) == "Array.array_first()" - infix_instance = arg1.array_first() - assert infix_instance == instance - - def test_array_first_n(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("N") - instance = Expression.array_first_n(arg1, arg2) - assert instance.name == "array_first_n" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Array.array_first_n(N)" - infix_instance = arg1.array_first_n(arg2) - assert infix_instance == instance - - def test_array_last(self): - arg1 = self._make_arg("Array") - instance = Expression.array_last(arg1) - assert instance.name == "array_last" - assert instance.params == [arg1] - assert repr(instance) == "Array.array_last()" - infix_instance = arg1.array_last() - assert infix_instance == instance - - def test_array_last_n(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("N") - instance = Expression.array_last_n(arg1, arg2) - assert instance.name == "array_last_n" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Array.array_last_n(N)" - infix_instance = arg1.array_last_n(arg2) - assert infix_instance == instance - - def test_array_maximum_n(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("N") - instance = Expression.array_maximum_n(arg1, arg2) - assert instance.name == "maximum_n" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Array.array_maximum_n(N)" - infix_instance = arg1.array_maximum_n(arg2) - assert infix_instance == instance - - def test_array_minimum_n(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("N") - instance = Expression.array_minimum_n(arg1, arg2) - assert instance.name == "minimum_n" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Array.array_minimum_n(N)" - infix_instance = arg1.array_minimum_n(arg2) - assert infix_instance == instance - - def test_array_index_of(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("Search") - instance = Expression.array_index_of(arg1, arg2) - assert instance.name == "array_index_of" - assert len(instance.params) == 3 - assert instance.params[0] == arg1 - assert instance.params[1] == arg2 - assert instance.params[2] == "first" - infix_instance = arg1.array_index_of(arg2) - assert infix_instance == instance - - def test_array_index_of_all(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("Search") - instance = Expression.array_index_of_all(arg1, arg2) - assert instance.name == "array_index_of_all" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Array.array_index_of_all(Search)" - infix_instance = arg1.array_index_of_all(arg2) - assert infix_instance == instance - - def test_array_slice(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("Offset") - instance = Expression.array_slice(arg1, arg2) - assert instance.name == "array_slice" - assert instance.params == [arg1, arg2] - assert repr(instance) == "Array.array_slice(Offset)" - infix_instance = arg1.array_slice(arg2) - assert infix_instance == instance - - def test_array_slice_with_length(self): - arg1 = self._make_arg("Array") - arg2 = self._make_arg("Offset") - arg3 = self._make_arg("Length") - instance = Expression.array_slice(arg1, arg2, arg3) - assert instance.name == "array_slice" - assert instance.params == [arg1, arg2, arg3] - assert repr(instance) == "Array.array_slice(Offset, Length)" - infix_instance = arg1.array_slice(arg2, arg3) - assert infix_instance == instance - - def test_array_concat(self): + def test_array_concat(self): arg1 = self._make_arg("ArrayRef1") arg2 = self._make_arg("ArrayRef2") instance = Expression.array_concat(arg1, arg2) @@ -1829,47 +1710,6 @@ def test_if_null(self): assert repr(instance) == "Field1.if_null(Field2, Field3)" infix_instance = arg1.if_null(arg2, arg3) assert infix_instance == instance - - def test_map_set(self): - arg1 = self._make_arg("Map1") - arg2 = self._make_arg("Key") - arg3 = self._make_arg("Value") - arg4 = self._make_arg("MoreKey") - arg5 = self._make_arg("MoreValue") - instance = Expression.map_set(arg1, arg2, arg3, arg4, arg5) - assert instance.name == "map_set" - assert instance.params == [arg1, arg2, arg3, arg4, arg5] - assert repr(instance) == "Map1.map_set(Key, Value, MoreKey, MoreValue)" - infix_instance = arg1.map_set(arg2, arg3, arg4, arg5) - assert infix_instance == instance - - def test_map_keys(self): - arg1 = self._make_arg("Map1") - instance = Expression.map_keys(arg1) - assert instance.name == "map_keys" - assert instance.params == [arg1] - assert repr(instance) == "Map1.map_keys()" - infix_instance = arg1.map_keys() - assert infix_instance == instance - - def test_map_values(self): - arg1 = self._make_arg("Map1") - instance = Expression.map_values(arg1) - assert instance.name == "map_values" - assert instance.params == [arg1] - assert repr(instance) == "Map1.map_values()" - infix_instance = arg1.map_values() - assert infix_instance == instance - - def test_map_entries(self): - arg1 = self._make_arg("Map1") - instance = Expression.map_entries(arg1) - assert instance.name == "map_entries" - assert instance.params == [arg1] - assert repr(instance) == "Map1.map_entries()" - infix_instance = arg1.map_entries() - assert infix_instance == instance - def test_type(self): arg1 = self._make_arg("Value") instance = Expression.type(arg1) From bc3516b0ec43c9b0e9d6506588c5aadf04b13b97 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 25 Mar 2026 15:04:50 -0700 Subject: [PATCH 08/10] fixed typos --- .../google/cloud/firestore_v1/pipeline_expressions.py | 4 ++-- .../tests/unit/v1/test_pipeline_expressions.py | 2 +- 2 files changed, 3 insertions(+), 3 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 24d48addf186..dc94ddbba9bb 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 @@ -781,7 +781,7 @@ def array_reverse(self) -> "Expression": """ return FunctionExpression("array_reverse", [self]) - @expose_as_static + @expose_as_static def array_concat( self, *other_arrays: Array | list[Expression | CONSTANT_TYPE] | Expression ) -> "Expression": @@ -944,7 +944,7 @@ def count(self) -> "Expression": Example: >>> # Count the total number of products - >>> Field.of("productId").count().as_("totalProducts"ss + >>> Field.of("productId").count().as_("totalProducts") Returns: A new `AggregateFunction` representing the 'count' aggregation. 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 4f8e8c75091e..f2ea176b8a62 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 @@ -1453,7 +1453,7 @@ def test_array_reverse(self): infix_instance = arg1.array_reverse() assert infix_instance == instance - def test_array_concat(self): + def test_array_concat(self): arg1 = self._make_arg("ArrayRef1") arg2 = self._make_arg("ArrayRef2") instance = Expression.array_concat(arg1, arg2) From 5276408bdb9518a4c39532ba2ceea3245c64490a Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 25 Mar 2026 15:17:59 -0700 Subject: [PATCH 09/10] improved date/time tests --- .../system/pipeline_e2e/date_and_time.yaml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml index 1eaf25d37d23..afbbf72640af 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/date_and_time.yaml @@ -110,11 +110,13 @@ tests: - FunctionExpression.timestamp_trunc: - Field: time - Constant: "day" + - Constant: "America/Los_Angeles" - "trunc_day" - AliasedExpression: - FunctionExpression.timestamp_extract: - Field: time - Constant: "year" + - Constant: "America/Los_Angeles" - "extract_year" - AliasedExpression: - FunctionExpression.timestamp_diff: @@ -125,3 +127,46 @@ tests: - Field: time - Constant: "hour" - "diff_hours" + assert_results: + - trunc_day: "1993-04-28T07:00:00.000000+00:00" + extract_year: 1993 + diff_hours: 24 + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /timestamps + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trunc_day: + functionValue: + args: + - fieldReferenceValue: time + - stringValue: day + - stringValue: America/Los_Angeles + name: timestamp_trunc + extract_year: + functionValue: + args: + - fieldReferenceValue: time + - stringValue: year + - stringValue: America/Los_Angeles + name: timestamp_extract + diff_hours: + functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: time + - stringValue: day + - integerValue: '1' + name: timestamp_add + - fieldReferenceValue: time + - stringValue: hour + name: timestamp_diff + name: select From 47d9e0eabd5c14e54c41eb582b7e5c86ba8ebd05 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 25 Mar 2026 15:33:38 -0700 Subject: [PATCH 10/10] added Type enum --- .../firestore_v1/pipeline_expressions.py | 41 +++++++++++++++---- .../unit/v1/test_pipeline_expressions.py | 13 ++++++ 2 files changed, 47 insertions(+), 7 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 dc94ddbba9bb..40df6b30de46 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 @@ -90,6 +90,31 @@ def _to_pb(self) -> Value: ) +class Type(str, Enum): + """Enumeration of the different types generated by the Firestore backend.""" + + NULL = "null" + ARRAY = "array" + BOOLEAN = "boolean" + BYTES = "bytes" + TIMESTAMP = "timestamp" + GEO_POINT = "geo_point" + NUMBER = "number" + INT32 = "int32" + INT64 = "int64" + FLOAT64 = "float64" + DECIMAL128 = "decimal128" + MAP = "map" + REFERENCE = "reference" + STRING = "string" + VECTOR = "vector" + MAX_KEY = "max_key" + MIN_KEY = "min_key" + OBJECT_ID = "object_id" + REGEX = "regex" + REQUEST_TIMESTAMP = "request_timestamp" + + class Expression(ABC): """Represents an expression that can be evaluated to a value within the execution of a pipeline. @@ -120,6 +145,8 @@ def _cast_to_expr_or_convert_to_constant( """Convert arbitrary object to an Expression.""" if isinstance(o, Expression): return o + if isinstance(o, Enum): + o = o.value if isinstance(o, dict): return Map(o) if isinstance(o, list): @@ -1714,7 +1741,7 @@ def as_(self, alias: str) -> "AliasedExpression": return AliasedExpression(self, alias) @expose_as_static - def cmp(self, other: "Expression | CONSTANT_TYPE") -> "Expression": + def cmp(self, other: Expression | CONSTANT_TYPE) -> "Expression": """Creates an expression that compares this expression to another expression. Returns an integer: @@ -1736,8 +1763,8 @@ def cmp(self, other: "Expression | CONSTANT_TYPE") -> "Expression": @expose_as_static def timestamp_trunc( self, - granularity: "Expression | str", - timezone: "Expression | str | None" = None, + granularity: Expression | str, + timezone: Expression | str | None = None, ) -> "Expression": """Creates an expression that truncates a timestamp to a specified granularity. @@ -1755,7 +1782,7 @@ def timestamp_trunc( @expose_as_static def timestamp_extract( - self, part: "Expression | str", timezone: "Expression | str | None" = None + self, part: Expression | str, timezone: Expression | str | None = None ) -> "Expression": """Creates an expression that extracts a part of a timestamp. @@ -1773,7 +1800,7 @@ def timestamp_extract( @expose_as_static def timestamp_diff( - self, start: "Expression | CONSTANT_TYPE", unit: "Expression | str" + self, start: Expression | CONSTANT_TYPE, unit: Expression | str ) -> "Expression": """Creates an expression that computes the difference between two timestamps in the specified unit. @@ -1794,7 +1821,7 @@ def timestamp_diff( ) @expose_as_static - def if_null(self, *others: "Expression | CONSTANT_TYPE") -> "Expression": + def if_null(self, *others: Expression | CONSTANT_TYPE) -> "Expression": """Creates an expression that returns the first non-null expression from the provided arguments. Example: @@ -1823,7 +1850,7 @@ def type(self) -> "Expression": return FunctionExpression("type", [self]) @expose_as_static - def is_type(self, type_val: "Expression | str") -> "BooleanExpression": + def is_type(self, type_val: Type | str | Expression) -> "BooleanExpression": """Creates an expression that checks if the result is of the specified type. Example: 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 f2ea176b8a62..fe6136b38c79 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 @@ -1728,3 +1728,16 @@ def test_is_type(self): assert repr(instance) == "Value.is_type(TypeString)" infix_instance = arg1.is_type(arg2) assert infix_instance == instance + + def test_type_enum(self): + from google.cloud.firestore_v1.pipeline_expressions import Type + + arg1 = self._make_arg("Value") + instance = Expression.is_type(arg1, Type.STRING) + assert instance.name == "is_type" + assert instance.params[0] == arg1 + assert isinstance(instance.params[1], Constant) + assert instance.params[1].value == Type.STRING.value + assert repr(instance) == "Value.is_type(Constant.of('string'))" + infix_instance = arg1.is_type(Type.STRING) + assert infix_instance == instance