From 4e7cbf3a0027a9f884e3900f7ce24080bb34af5c Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 01:53:02 +0000 Subject: [PATCH 01/14] feat: support regex_find and regex_find_all --- .../firestore_v1/pipeline_expressions.py | 42 ++++++++++ .../tests/system/pipeline_e2e/string.yaml | 83 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 20 +++++ 3 files changed, 145 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..e7c6039a383c 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 @@ -1307,6 +1307,48 @@ def map_merge( [self] + [self._cast_to_expr_or_convert_to_constant(m) for m in other_maps], ) + @expose_as_static + def regex_find(self, pattern: str | Constant[str] | "Expression") -> "Expression": + """Creates an expression that returns the first substring of a string expression that + matches a specified regular expression. + + This expression uses the RE2 regular expression syntax. See https://github.com/google/re2/wiki/Syntax. + + Example: + >>> Field.of("email").regex_find("@[A-Za-z0-9.-]+") + >>> Field.of("email").regex_find(Field.of("pattern")) + + Args: + pattern: The regular expression to search for. + + Returns: + A new `Expression` representing the regular expression find function. + """ + return FunctionExpression( + "regex_find", [self, self._cast_to_expr_or_convert_to_constant(pattern)] + ) + + @expose_as_static + def regex_find_all(self, pattern: str | Constant[str] | "Expression") -> "Expression": + """Creates an expression that evaluates to an array of all substrings in a string expression + that match a specified regular expression. + + This expression uses the RE2 regular expression syntax. See https://github.com/google/re2/wiki/Syntax. + + Example: + >>> Field.of("comment").regex_find_all("@[A-Za-z0-9_]+") + >>> Field.of("comment").regex_find_all(Field.of("pattern")) + + Args: + pattern: The regular expression to search for. + + Returns: + A new `Expression` representing the regular expression find function. + """ + return FunctionExpression( + "regex_find_all", [self, self._cast_to_expr_or_convert_to_constant(pattern)] + ) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index 20a97ba60663..b96ab506a10d 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -652,3 +652,86 @@ tests: - stringValue: ", " name: join name: select + - description: testRegexFind + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: author + - Constant: "Douglas Adams" + - Select: + - AliasedExpression: + - FunctionExpression.regex_find: + - Field: title + - Constant: "Hitchhiker's" + - "found" + assert_results: + - found: "Hitchhiker's" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: "Douglas Adams" + name: equal + name: where + - args: + - mapValue: + fields: + found: + functionValue: + args: + - fieldReferenceValue: title + - stringValue: "Hitchhiker's" + name: regex_find + name: select + - description: testRegexFindAll + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: author + - Constant: "Douglas Adams" + - Select: + - AliasedExpression: + - FunctionExpression.regex_find_all: + - Field: title + - Constant: "(?i)[a-z]+" + - "found_all" + assert_results: + - found_all: + - "The" + - "Hitchhiker" + - "s" + - "Guide" + - "to" + - "the" + - "Galaxy" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: "Douglas Adams" + name: equal + name: where + - args: + - mapValue: + fields: + found_all: + functionValue: + args: + - fieldReferenceValue: title + - stringValue: "(?i)[a-z]+" + name: regex_find_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 80738799f975..b5a36505c6a1 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 @@ -1279,6 +1279,26 @@ def test_cosine_distance(self): infix_instance = arg1.cosine_distance(arg2) assert infix_instance == instance + def test_regex_find(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("pattern") + instance = Expression.regex_find(arg1, arg2) + assert instance.name == "regex_find" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.regex_find(pattern)" + infix_instance = arg1.regex_find(arg2) + assert infix_instance == instance + + def test_regex_find_all(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("pattern") + instance = Expression.regex_find_all(arg1, arg2) + assert instance.name == "regex_find_all" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.regex_find_all(pattern)" + infix_instance = arg1.regex_find_all(arg2) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From 2bc617048b45daa3a8c888576916fe9e24600ff7 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 02:15:58 +0000 Subject: [PATCH 02/14] support split --- .../firestore_v1/pipeline_expressions.py | 18 ++++++++ .../tests/system/pipeline_e2e/string.yaml | 45 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 10 +++++ 3 files changed, 73 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 e7c6039a383c..47a5ed311cb2 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 @@ -1349,6 +1349,24 @@ def regex_find_all(self, pattern: str | Constant[str] | "Expression") -> "Expres "regex_find_all", [self, self._cast_to_expr_or_convert_to_constant(pattern)] ) + @expose_as_static + def split(self, delimiter: str | "Expression") -> "Expression": + """Creates an expression that splits the value of a field on the provided delimiter. + + Example: + >>> Field.of("date").split("date", "-") + + Args: + field_name: Split the value in this field. + delimiter: The delimiter string to split on. + + Returns: + A new `Expression` representing the split function. + """ + return FunctionExpression( + "split", [self, self._cast_to_expr_or_convert_to_constant(delimiter)] + ) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index b96ab506a10d..93bf65b15d00 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -735,3 +735,48 @@ tests: - stringValue: "(?i)[a-z]+" name: regex_find_all name: select + + - description: testSplit + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: author + - Constant: "Douglas Adams" + - Select: + - AliasedExpression: + - FunctionExpression.split: + - Field: title + - Constant: " " + - "title_words" + assert_results: + - title_words: + - "The" + - "Hitchhiker's" + - "Guide" + - "to" + - "the" + - "Galaxy" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: "Douglas Adams" + name: equal + name: where + - args: + - mapValue: + fields: + title_words: + functionValue: + args: + - fieldReferenceValue: title + - stringValue: " " + name: split + 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 b5a36505c6a1..3b8da3368bba 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 @@ -1299,6 +1299,16 @@ def test_regex_find_all(self): infix_instance = arg1.regex_find_all(arg2) assert infix_instance == instance + def test_split(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("-") + instance = Expression.split(arg1, arg2) + assert instance.name == "split" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.split(-)" + infix_instance = arg1.split(arg2) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From 9a5c829a9efc38fcf5637cf693b7d7513670c6e2 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 02:32:40 +0000 Subject: [PATCH 03/14] support string_repeat() --- .../firestore_v1/pipeline_expressions.py | 21 +++++++++ .../tests/system/pipeline_e2e/string.yaml | 44 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 10 +++++ 3 files changed, 75 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 47a5ed311cb2..b0c2f3406776 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 @@ -1367,6 +1367,27 @@ def split(self, delimiter: str | "Expression") -> "Expression": "split", [self, self._cast_to_expr_or_convert_to_constant(delimiter)] ) + @expose_as_static + def string_repeat(self, repetitions: int | "Expression") -> "Expression": + """Creates an expression that repeats a string or byte array a specified number + of times. + + Example: + >>> # Called on an existing field expression: + >>> Field.of("name").string_repeat(3) + >>> # Called statically using the field name: + >>> Expression.string_repeat("name", 3) + + Args: + repetitions: The number of times to repeat the string or byte array. + + Returns: + A new `Expression` representing the repeated string or byte array. + """ + return FunctionExpression( + "string_repeat", [self, self._cast_to_expr_or_convert_to_constant(repetitions)] + ) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index 93bf65b15d00..5ca92cc0de03 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -780,3 +780,47 @@ tests: - stringValue: " " name: split name: select + + - description: testStringRepeat + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: author + - Constant: "Douglas Adams" + - Select: + - AliasedExpression: + - FunctionExpression.string_repeat: + - FunctionExpression.to_upper: + - Field: author + - Constant: 2 + - "repeated" + assert_results: + - repeated: "DOUGLAS ADAMSDOUGLAS ADAMS" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: "Douglas Adams" + name: equal + name: where + - args: + - mapValue: + fields: + repeated: + functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: author + name: to_upper + - integerValue: "2" + name: string_repeat + 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 3b8da3368bba..7df15dfbd1a1 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 @@ -1309,6 +1309,16 @@ def test_split(self): infix_instance = arg1.split(arg2) assert infix_instance == instance + def test_string_repeat(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("3") + instance = Expression.string_repeat(arg1, arg2) + assert instance.name == "string_repeat" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.string_repeat(3)" + infix_instance = arg1.string_repeat(arg2) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From c5b259d8685a99cb41509d2fb9c18c1af5d2cea1 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 02:34:00 +0000 Subject: [PATCH 04/14] type annotation --- .../google/cloud/firestore_v1/pipeline_expressions.py | 8 ++++---- 1 file changed, 4 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 b0c2f3406776..0f4f591cf4f9 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 @@ -1308,7 +1308,7 @@ def map_merge( ) @expose_as_static - def regex_find(self, pattern: str | Constant[str] | "Expression") -> "Expression": + def regex_find(self, pattern: str | Constant[str] | Expression) -> "Expression": """Creates an expression that returns the first substring of a string expression that matches a specified regular expression. @@ -1329,7 +1329,7 @@ def regex_find(self, pattern: str | Constant[str] | "Expression") -> "Expression ) @expose_as_static - def regex_find_all(self, pattern: str | Constant[str] | "Expression") -> "Expression": + def regex_find_all(self, pattern: str | Constant[str] | Expression) -> "Expression": """Creates an expression that evaluates to an array of all substrings in a string expression that match a specified regular expression. @@ -1350,7 +1350,7 @@ def regex_find_all(self, pattern: str | Constant[str] | "Expression") -> "Expres ) @expose_as_static - def split(self, delimiter: str | "Expression") -> "Expression": + def split(self, delimiter: str | Constant[str] | Expression) -> "Expression": """Creates an expression that splits the value of a field on the provided delimiter. Example: @@ -1368,7 +1368,7 @@ def split(self, delimiter: str | "Expression") -> "Expression": ) @expose_as_static - def string_repeat(self, repetitions: int | "Expression") -> "Expression": + def string_repeat(self, repetitions: int | Expression) -> "Expression": """Creates an expression that repeats a string or byte array a specified number of times. From 39de405f54bcb909a866dbce979d0008cef19384 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 02:43:43 +0000 Subject: [PATCH 05/14] support string_replace_all() --- .../firestore_v1/pipeline_expressions.py | 29 +++++++++++++ .../tests/system/pipeline_e2e/string.yaml | 41 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 11 +++++ 3 files changed, 81 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 0f4f591cf4f9..79947ed7bbae 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 @@ -1388,6 +1388,35 @@ def string_repeat(self, repetitions: int | Expression) -> "Expression": "string_repeat", [self, self._cast_to_expr_or_convert_to_constant(repetitions)] ) + @expose_as_static + def string_replace_all( + self, + find: str | bytes | Constant | Expression, + replacement: str | bytes | Constant | Expression, + ) -> "Expression": + """Creates an expression that replaces all occurrences of a substring or byte + sequence with a replacement. + + Example: + >>> # Called on an existing field expression: + >>> Field.of("text").string_replace_all("foo", "bar") + + Args: + find: The substring or byte sequence to search for. + replacement: The replacement string or byte sequence. + + Returns: + A new `Expression` representing the string or byte array with replacements. + """ + return FunctionExpression( + "string_replace_all", + [ + self, + self._cast_to_expr_or_convert_to_constant(find), + self._cast_to_expr_or_convert_to_constant(replacement), + ], + ) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index 5ca92cc0de03..f0c9c7ba67a8 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -824,3 +824,44 @@ tests: name: string_repeat name: select + - description: testStringReplaceAll + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: author + - Constant: "Douglas Adams" + - Select: + - AliasedExpression: + - FunctionExpression.string_replace_all: + - Field: author + - Constant: "as" + - Constant: "AS" + - "replaced" + assert_results: + - replaced: "DouglAS Adams" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: "Douglas Adams" + name: equal + name: where + - args: + - mapValue: + fields: + replaced: + functionValue: + args: + - fieldReferenceValue: author + - stringValue: "as" + - stringValue: "AS" + name: string_replace_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 7df15dfbd1a1..de5f4a0e8a5d 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 @@ -1319,6 +1319,17 @@ def test_string_repeat(self): infix_instance = arg1.string_repeat(arg2) assert infix_instance == instance + def test_string_replace_all(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("find") + arg3 = self._make_arg("replacement") + instance = Expression.string_replace_all(arg1, arg2, arg3) + assert instance.name == "string_replace_all" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "String.string_replace_all(find, replacement)" + infix_instance = arg1.string_replace_all(arg2, arg3) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From 34acf715fa1fdf9d0dd39b092077f8e196967a0e Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 02:48:25 +0000 Subject: [PATCH 06/14] improve system test --- .../tests/system/pipeline_e2e/string.yaml | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index f0c9c7ba67a8..5976ba1b442e 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -829,17 +829,20 @@ tests: - Collection: books - Where: - FunctionExpression.equal: - - Field: author - - Constant: "Douglas Adams" + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" - Select: - AliasedExpression: - FunctionExpression.string_replace_all: - - Field: author - - Constant: "as" - - Constant: "AS" + - FunctionExpression.concat: + - Field: author + - Constant: ": " + - Field: title + - Constant: "la" + - Constant: "LA" - "replaced" assert_results: - - replaced: "DouglAS Adams" + - replaced: "DougLAs Adams: The Hitchhiker's Guide to the GaLAxy" assert_proto: pipeline: stages: @@ -849,8 +852,8 @@ tests: - args: - functionValue: args: - - fieldReferenceValue: author - - stringValue: "Douglas Adams" + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" name: equal name: where - args: @@ -859,9 +862,14 @@ tests: replaced: functionValue: args: - - fieldReferenceValue: author - - stringValue: "as" - - stringValue: "AS" + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: ": " + - fieldReferenceValue: title + name: concat + - stringValue: "la" + - stringValue: "LA" name: string_replace_all name: select From 22fd6adf8437e66231660d2c0217a16f3b6bd700 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 02:52:48 +0000 Subject: [PATCH 07/14] support string_replace_one() --- .../firestore_v1/pipeline_expressions.py | 29 +++++++++++ .../tests/system/pipeline_e2e/string.yaml | 49 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 11 +++++ 3 files changed, 89 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 79947ed7bbae..07710b6f2411 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 @@ -1417,6 +1417,35 @@ def string_replace_all( ], ) + @expose_as_static + def string_replace_one( + self, + find: str | bytes | Constant | Expression, + replacement: str | bytes | Constant | Expression, + ) -> "Expression": + """Creates an expression that replaces the first occurrence of a substring or byte + sequence with a replacement. + + Example: + >>> # Called on an existing field expression: + >>> Field.of("text").string_replace_one("foo", "bar") + + Args: + find: The substring or byte sequence to search for. + replacement: The replacement string or byte sequence. + + Returns: + A new `Expression` representing the string or byte array with the replacement. + """ + return FunctionExpression( + "string_replace_one", + [ + self, + self._cast_to_expr_or_convert_to_constant(find), + self._cast_to_expr_or_convert_to_constant(replacement), + ], + ) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index 5976ba1b442e..60dbfb7e98fc 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -873,3 +873,52 @@ tests: name: string_replace_all name: select + - description: testStringReplaceOne + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.string_replace_one: + - FunctionExpression.concat: + - Field: author + - Constant: ": " + - Field: title + - Constant: "la" + - Constant: "LA" + - "replaced" + assert_results: + - replaced: "DougLAs Adams: The Hitchhiker's Guide to the Galaxy" + 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: + replaced: + functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: author + - stringValue: ": " + - fieldReferenceValue: title + name: concat + - stringValue: "la" + - stringValue: "LA" + name: string_replace_one + 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 de5f4a0e8a5d..0d33b3129ceb 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 @@ -1330,6 +1330,17 @@ def test_string_replace_all(self): infix_instance = arg1.string_replace_all(arg2, arg3) assert infix_instance == instance + def test_string_replace_one(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("find") + arg3 = self._make_arg("replacement") + instance = Expression.string_replace_one(arg1, arg2, arg3) + assert instance.name == "string_replace_one" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "String.string_replace_one(find, replacement)" + infix_instance = arg1.string_replace_one(arg2, arg3) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From e810622e492a71adcb2569f333786a8edfc40dca Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 03:00:19 +0000 Subject: [PATCH 08/14] support string_index_of() --- .../firestore_v1/pipeline_expressions.py | 26 +++++++++++++ .../tests/system/pipeline_e2e/string.yaml | 39 +++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 10 +++++ 3 files changed, 75 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 07710b6f2411..fd8c7e6dc4e0 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 @@ -1446,6 +1446,32 @@ def string_replace_one( ], ) + @expose_as_static + def string_index_of( + self, + search: str | bytes | Constant | Expression, + ) -> "Expression": + """Creates an expression that finds the index of the first occurrence of a substring or + byte sequence. + + Example: + >>> # Called on an existing field expression: + >>> Field.of("text").string_index_of("foo") + + Args: + search: The substring or byte sequence to search for. + + Returns: + A new `Expression` representing the index of the first occurrence. + """ + return FunctionExpression( + "string_index_of", + [ + self, + self._cast_to_expr_or_convert_to_constant(search), + ], + ) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index 60dbfb7e98fc..f585d37439d7 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -922,3 +922,42 @@ tests: name: string_replace_one name: select + - description: testStringIndexOf + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.string_index_of: + - Field: title + - Constant: "Hitchhiker" + - "index" + assert_results: + - index: 4 + 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: + index: + functionValue: + args: + - fieldReferenceValue: title + - stringValue: "Hitchhiker" + name: string_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 0d33b3129ceb..0219127defb5 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 @@ -1341,6 +1341,16 @@ def test_string_replace_one(self): infix_instance = arg1.string_replace_one(arg2, arg3) assert infix_instance == instance + def test_string_index_of(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("search") + instance = Expression.string_index_of(arg1, arg2) + assert instance.name == "string_index_of" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.string_index_of(search)" + infix_instance = arg1.string_index_of(arg2) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From 6870cd1907b319094dfa36427402761035f11131 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 03:01:57 +0000 Subject: [PATCH 09/14] improve type hint --- .../google/cloud/firestore_v1/pipeline_expressions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index fd8c7e6dc4e0..85d6fc56c90a 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 @@ -1391,8 +1391,8 @@ def string_repeat(self, repetitions: int | Expression) -> "Expression": @expose_as_static def string_replace_all( self, - find: str | bytes | Constant | Expression, - replacement: str | bytes | Constant | Expression, + find: str | bytes | Constant[str] | Constant[bytes] | Expression, + replacement: str | bytes | Constant[str] | Constant[bytes] | Expression, ) -> "Expression": """Creates an expression that replaces all occurrences of a substring or byte sequence with a replacement. From b91062ab72a49338bc465951d7749aa1d93e2888 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 03:11:58 +0000 Subject: [PATCH 10/14] type hints --- .../google/cloud/firestore_v1/pipeline_expressions.py | 6 +++--- 1 file 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 85d6fc56c90a..593fe0a68ad2 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 @@ -1420,8 +1420,8 @@ def string_replace_all( @expose_as_static def string_replace_one( self, - find: str | bytes | Constant | Expression, - replacement: str | bytes | Constant | Expression, + find: str | bytes | Constant[str] | Constant[bytes] | Expression, + replacement: str | bytes | Constant[str] | Constant[bytes] | Expression, ) -> "Expression": """Creates an expression that replaces the first occurrence of a substring or byte sequence with a replacement. @@ -1449,7 +1449,7 @@ def string_replace_one( @expose_as_static def string_index_of( self, - search: str | bytes | Constant | Expression, + search: str | bytes | Constant[str] | Constant[bytes] | Expression, ) -> "Expression": """Creates an expression that finds the index of the first occurrence of a substring or byte sequence. From 2ed994e105830b4dcc94fbead2b8f000732dcce5 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 03:19:39 +0000 Subject: [PATCH 11/14] add system tests for bytes --- .../tests/system/pipeline_e2e/string.yaml | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index f585d37439d7..05313348ce61 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -873,6 +873,40 @@ tests: name: string_replace_all name: select + - description: testStringReplaceAllWithBytes + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.string_replace_all: + - Constant: !!binary Zm9vYmF6Zm9v + - Constant: !!binary Zm9v + - Constant: !!binary YmFy + - "replacedBytes" + assert_results: + - replacedBytes: !!binary YmFyYmF6YmFy + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + replacedBytes: + functionValue: + args: + - bytesValue: Zm9vYmF6Zm9v + - bytesValue: Zm9v + - bytesValue: YmFy + name: string_replace_all + name: select + - description: testStringReplaceOne pipeline: - Collection: books @@ -922,6 +956,40 @@ tests: name: string_replace_one name: select + - description: testStringReplaceOneWithBytes + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.string_replace_one: + - Constant: !!binary Zm9vYmF6Zm9v + - Constant: !!binary Zm9v + - Constant: !!binary YmFy + - "replacedBytes" + assert_results: + - replacedBytes: !!binary YmFyYmF6Zm9v + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + replacedBytes: + functionValue: + args: + - bytesValue: Zm9vYmF6Zm9v + - bytesValue: Zm9v + - bytesValue: YmFy + name: string_replace_one + name: select + - description: testStringIndexOf pipeline: - Collection: books @@ -961,3 +1029,34 @@ tests: name: string_index_of name: select + - description: testStringIndexOfWithBytes + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.string_index_of: + - Constant: !!binary Zm9vYmF6 + - Constant: !!binary YmF6 + - "index" + assert_results: + - index: 3 + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + index: + functionValue: + args: + - bytesValue: Zm9vYmF6 + - bytesValue: YmF6 + name: string_index_of + name: select From c05adc58d39b20bcf3aa3d24f4753e8983fdae2e Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 03:37:22 +0000 Subject: [PATCH 12/14] support ltrim() and rtrim() --- .../firestore_v1/pipeline_expressions.py | 52 +++++ .../tests/system/pipeline_e2e/string.yaml | 188 ++++++++++++++++++ .../unit/v1/test_pipeline_expressions.py | 40 ++++ 3 files changed, 280 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 593fe0a68ad2..e28c2a30c1e5 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 @@ -1472,6 +1472,58 @@ def string_index_of( ], ) + @expose_as_static + def ltrim( + self, + chars: str | bytes | Constant[str] | Constant[bytes] | Expression | None = None, + ) -> "Expression": + """Creates an expression that trims leading whitespace or a specified sequence + of characters/bytes from a string or byte sequence. + + Example: + >>> # Called on an existing field expression: + >>> Field.of("text").ltrim() + >>> Field.of("text").ltrim(" ") + + Args: + chars: The substring or byte sequence to trim. If not provided, + whitespace will be trimmed. + + Returns: + A new `Expression` representing the trimmed value. + """ + args = [self] + if chars is not None: + args.append(self._cast_to_expr_or_convert_to_constant(chars)) + + return FunctionExpression("ltrim", args) + + @expose_as_static + def rtrim( + self, + chars: str | bytes | Constant[str] | Constant[bytes] | Expression | None = None, + ) -> "Expression": + """Creates an expression that trims trailing whitespace or a specified sequence + of characters/bytes from a string or byte sequence. + + Example: + >>> # Called on an existing field expression: + >>> Field.of("text").rtrim() + >>> Field.of("text").rtrim(" ") + + Args: + chars: The substring or byte sequence to trim. If not provided, + whitespace will be trimmed. + + Returns: + A new `Expression` representing the trimmed value. + """ + args = [self] + if chars is not None: + args.append(self._cast_to_expr_or_convert_to_constant(chars)) + + return FunctionExpression("rtrim", args) + @expose_as_static def cosine_distance(self, other: Expression | list[float] | Vector) -> "Expression": """Calculates the cosine distance between two vectors. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index 05313348ce61..acb55c58c413 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -1060,3 +1060,191 @@ tests: - bytesValue: YmF6 name: string_index_of name: select + + - description: testLtrim + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.ltrim: + - Constant: " foo" + - "trimmed" + assert_results: + - trimmed: "foo" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trimmed: + functionValue: + args: + - stringValue: " foo" + name: ltrim + name: select + + - description: testLtrimWithChars + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.ltrim: + - Constant: "foobar" + - Constant: "fo" + - "trimmed" + assert_results: + - trimmed: "bar" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trimmed: + functionValue: + args: + - stringValue: "foobar" + - stringValue: "fo" + name: ltrim + name: select + + - description: testLtrimWithBytes + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.ltrim: + - Constant: !!binary Zm9vYmFy + - Constant: !!binary Zm8= + - "trimmed" + assert_results: + - trimmed: !!binary YmFy + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trimmed: + functionValue: + args: + - bytesValue: Zm9vYmFy + - bytesValue: Zm8= + name: ltrim + name: select + + - description: testRtrim + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.rtrim: + - Constant: "foo " + - "trimmed" + assert_results: + - trimmed: "foo" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trimmed: + functionValue: + args: + - stringValue: "foo " + name: rtrim + name: select + + - description: testRtrimWithChars + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.rtrim: + - Constant: "foobar" + - Constant: "ar" + - "trimmed" + assert_results: + - trimmed: "foob" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trimmed: + functionValue: + args: + - stringValue: "foobar" + - stringValue: "ar" + name: rtrim + name: select + + - description: testRtrimWithBytes + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.rtrim: + - Constant: !!binary Zm9vYmFy + - Constant: !!binary YXI= + - "trimmed" + assert_results: + - trimmed: !!binary Zm9vYg== + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + trimmed: + functionValue: + args: + - bytesValue: Zm9vYmFy + - bytesValue: YXI= + name: rtrim + 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 0219127defb5..ca4ddd782a73 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 @@ -1351,6 +1351,46 @@ def test_string_index_of(self): infix_instance = arg1.string_index_of(arg2) assert infix_instance == instance + def test_ltrim(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("chars") + + # Without args + instance = Expression.ltrim(arg1) + assert instance.name == "ltrim" + assert instance.params == [arg1] + assert repr(instance) == "String.ltrim()" + infix_instance = arg1.ltrim() + assert infix_instance == instance + + # With args + instance = Expression.ltrim(arg1, arg2) + assert instance.name == "ltrim" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.ltrim(chars)" + infix_instance = arg1.ltrim(arg2) + assert infix_instance == instance + + def test_rtrim(self): + arg1 = self._make_arg("String") + arg2 = self._make_arg("chars") + + # Without args + instance = Expression.rtrim(arg1) + assert instance.name == "rtrim" + assert instance.params == [arg1] + assert repr(instance) == "String.rtrim()" + infix_instance = arg1.rtrim() + assert infix_instance == instance + + # With args + instance = Expression.rtrim(arg1, arg2) + assert instance.name == "rtrim" + assert instance.params == [arg1, arg2] + assert repr(instance) == "String.rtrim(chars)" + infix_instance = arg1.rtrim(arg2) + assert infix_instance == instance + def test_dot_product(self): arg1 = self._make_arg("Vector1") arg2 = self._make_arg("Vector2") From 4097985b17b5360656fb6c80f8ba93543791a765 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 03:47:28 +0000 Subject: [PATCH 13/14] lint --- .../google/cloud/firestore_v1/pipeline_expressions.py | 3 ++- 1 file changed, 2 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 e28c2a30c1e5..4e0a154c2b58 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 @@ -1385,7 +1385,8 @@ def string_repeat(self, repetitions: int | Expression) -> "Expression": A new `Expression` representing the repeated string or byte array. """ return FunctionExpression( - "string_repeat", [self, self._cast_to_expr_or_convert_to_constant(repetitions)] + "string_repeat", + [self, self._cast_to_expr_or_convert_to_constant(repetitions)], ) @expose_as_static From 02f0c2cb381159e592dfa77c30f85ad576b210d7 Mon Sep 17 00:00:00 2001 From: Linchin Date: Tue, 24 Mar 2026 23:18:13 +0000 Subject: [PATCH 14/14] improve regex test case --- .../tests/system/pipeline_e2e/string.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml index acb55c58c413..097ccab859df 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/string.yaml @@ -663,10 +663,10 @@ tests: - AliasedExpression: - FunctionExpression.regex_find: - Field: title - - Constant: "Hitchhiker's" + - Constant: '\b[A-Z][a-z]{4}\b' - "found" assert_results: - - found: "Hitchhiker's" + - found: "Guide" assert_proto: pipeline: stages: @@ -687,7 +687,7 @@ tests: functionValue: args: - fieldReferenceValue: title - - stringValue: "Hitchhiker's" + - stringValue: '\b[A-Z][a-z]{4}\b' name: regex_find name: select - description: testRegexFindAll