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..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 @@ -1307,6 +1307,224 @@ 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 split(self, delimiter: str | Constant[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 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 string_replace_all( + self, + 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. + + 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 string_replace_one( + self, + 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. + + 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 string_index_of( + self, + 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. + + 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 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 20a97ba60663..097ccab859df 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,599 @@ 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: '\b[A-Z][a-z]{4}\b' + - "found" + assert_results: + - found: "Guide" + 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: '\b[A-Z][a-z]{4}\b' + 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 + + - 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 + + - 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 + + - description: testStringReplaceAll + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.string_replace_all: + - 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_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 + - 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 + + - 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 + - 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 + + - 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 + + - 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 80738799f975..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 @@ -1279,6 +1279,118 @@ 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_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_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_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_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_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_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")