diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 969ddf2794a5..d2d6637f013a 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -188,7 +188,7 @@ class Expression(ABC): """Represents an expression that can be evaluated to a value within the execution of a pipeline. - Expressionessions are the building blocks for creating complex queries and + Expressions are the building blocks for creating complex queries and transformations in Firestore pipelines. They can represent: - **Field references:** Access values from document fields. @@ -761,6 +761,9 @@ def array_get(self, offset: Expression | int) -> "FunctionExpression": Creates an expression that indexes into an array from the beginning or end and returns the element. A negative offset starts from the end. + If the expression is evaluated against a non-array type, it evaluates to an error. See `offset` + for an alternative that evaluates to unset instead. + Example: >>> Array([1,2,3]).array_get(0) @@ -774,6 +777,26 @@ def array_get(self, offset: Expression | int) -> "FunctionExpression": "array_get", [self, self._cast_to_expr_or_convert_to_constant(offset)] ) + @expose_as_static + def offset(self, offset: Expression | int) -> "FunctionExpression": + """ + Creates an expression that indexes into an array from the beginning or end and returns the + element. A negative offset starts from the end. + If the expression is evaluated against a non-array type, it evaluates to unset. + + Example: + >>> Array([1,2,3]).offset(0) + + Args: + offset: the index of the element to return + + Returns: + A new `Expression` representing the `offset` operation. + """ + return FunctionExpression( + "offset", [self, self._cast_to_expr_or_convert_to_constant(offset)] + ) + @expose_as_static def array_contains( self, element: Expression | CONSTANT_TYPE @@ -877,6 +900,70 @@ def array_reverse(self) -> "Expression": """ return FunctionExpression("array_reverse", [self]) + @expose_as_static + def array_filter( + self, + filter_expr: "BooleanExpression", + element_alias: str | Constant[str], + index_alias: str | Constant[str] | None = None, + ) -> "Expression": + """Filters an array based on a predicate. + + Example: + >>> # Filter the 'tags' array to only include the tag "comedy" + >>> Field.of("tags").array_filter(Variable("tag").equal("comedy"), "tag") + >>> # Filter the 'tags' array to only include elements after the first element (index > 0) + >>> Field.of("tags").array_filter(Variable("i").greater_than(0), element_alias="tag", index_alias="i") + + Args: + filter_expr: The predicate boolean expression used to filter the elements. + element_alias: A string or string constant used to refer to the current array + element as a variable within the filter expression. + index_alias: An optional string or string constant used to refer to the index + of the current array element as a variable within the filter expression. + + Returns: + A new `Expression` representing the filtered array. + """ + args = [self, self._cast_to_expr_or_convert_to_constant(element_alias)] + if index_alias is not None: + args.append(self._cast_to_expr_or_convert_to_constant(index_alias)) + args.append(filter_expr) + + return FunctionExpression("array_filter", args) + + @expose_as_static + def array_transform( + self, + transform_expr: "Expression", + element_alias: str | Constant[str], + index_alias: str | Constant[str] | None = None, + ) -> "Expression": + """Creates an expression that applies a provided transformation to each element in an array. + + Example: + >>> # Convert each tag in the 'tags' array to uppercase + >>> Field.of("tags").array_transform(Variable("tag").to_upper(), "tag") + >>> # Append the index to each tag in the 'tags' array + >>> Field.of("tags").array_transform(Variable("tag").string_concat(Variable("i")), element_alias="tag", index_alias="i") + + Args: + transform_expr: The expression used to transform the elements. + element_alias: A string or string constant used to refer to the current array + element as a variable within the transform expression. + index_alias: An optional string or string constant used to refer to the index + of the current array element as a variable within the transform expression. + + Returns: + A new `Expression` representing the transformed array. + """ + args = [self, self._cast_to_expr_or_convert_to_constant(element_alias)] + if index_alias is not None: + args.append(self._cast_to_expr_or_convert_to_constant(index_alias)) + args.append(transform_expr) + + return FunctionExpression("array_transform", args) + @expose_as_static def array_concat( self, *other_arrays: Array | list[Expression | CONSTANT_TYPE] | Expression @@ -938,7 +1025,7 @@ def is_absent(self) -> "BooleanExpression": >>> Field.of("email").is_absent() Returns: - A new `BooleanExpressionession` representing the isAbsent operation. + A new `BooleanExpression` representing the isAbsent operation. """ return BooleanExpression("is_absent", [self]) @@ -1006,6 +1093,76 @@ def exists(self) -> "BooleanExpression": """ return BooleanExpression("exists", [self]) + @expose_as_static + def coalesce(self, *others: Expression | CONSTANT_TYPE) -> "Expression": + """Creates an expression that evaluates to the first non-null, non-error value. + + Example: + >>> # Return the "preferredName" field if it exists. + >>> # Otherwise, check the "fullName" field. + >>> # Otherwise, return the literal string "Anonymous". + >>> Field.of("preferredName").coalesce(Field.of("fullName"), "Anonymous") + + >>> # Equivalent static call: + >>> Expression.coalesce(Field.of("preferredName"), Field.of("fullName"), "Anonymous") + + Args: + *others: Additional expressions or constants to evaluate if the current + expression evaluates to null or error. + + Returns: + An Expression representing the coalesce operation. + """ + return FunctionExpression( + "coalesce", + [self] + + [Expression._cast_to_expr_or_convert_to_constant(x) for x in others], + ) + + @expose_as_static + def switch_on( + self, result: Expression | CONSTANT_TYPE, *others: Expression | CONSTANT_TYPE + ) -> "Expression": + """Creates an expression that evaluates to the result corresponding to the first true condition. + + This function behaves like a `switch` statement. It accepts an alternating sequence of + conditions and their corresponding results. If an odd number of arguments is provided, the + final argument serves as a default fallback result. If no default is provided and no condition + evaluates to true, it throws an error. + + Example: + >>> # Return "Pending" if status is 1, "Active" if status is 2, otherwise "Unknown" + >>> Field.of("status").equal(1).switch_on( + ... "Pending", Field.of("status").equal(2), "Active", "Unknown" + ... ) + + Args: + result: The result to return if this condition is true. + *others: Additional alternating conditions and results, optionally followed by a default value. + + Returns: + An Expression representing the "switch_on" operation. + """ + return FunctionExpression( + "switch_on", + [self, Expression._cast_to_expr_or_convert_to_constant(result)] + + [Expression._cast_to_expr_or_convert_to_constant(x) for x in others], + ) + + @expose_as_static + def storage_size(self) -> "Expression": + """Calculates the Firestore storage size of a given value. + + Mirrors the sizing rules detailed in Firebase/Firestore documentation. + + Example: + >>> Field.of("content").storage_size() + + Returns: + A new `Expression` representing the storage size. + """ + return FunctionExpression("storage_size", [self]) + @expose_as_static def sum(self) -> "Expression": """Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. @@ -1366,6 +1523,7 @@ def join(self, delimeter: Expression | str) -> "Expression": @expose_as_static def map_get(self, key: str | Constant[str]) -> "Expression": """Accesses a value from the map produced by evaluating this expression. + If the expression is evaluated against a non-map type, it evaluates to an error. Example: >>> Map({"city": "London"}).map_get("city") @@ -2808,6 +2966,22 @@ def __init__(self, *conditions: "BooleanExpression"): super().__init__("or", conditions, use_infix_repr=False) +class Nor(BooleanExpression): + """ + Represents an expression that performs a logical 'NOR' operation on multiple filter conditions. + + Example: + >>> # Check if neither the 'age' field is greater than 18 nor the 'city' field is "London" + >>> Nor(Field.of("age").greater_than(18), Field.of("city").equal("London")) + + Args: + *conditions: The filter conditions to 'NOR' together. + """ + + def __init__(self, *conditions: "BooleanExpression"): + super().__init__("nor", conditions, use_infix_repr=False) + + class Xor(BooleanExpression): """ Represents an expression that performs a logical 'XOR' (exclusive OR) operation on multiple filter conditions. diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml index e29ef0d6c2ed..5b1a7f1aa05a 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/array.yaml @@ -424,6 +424,60 @@ tests: - integerValue: '0' name: array_get name: select + - description: testArrayGet_NonArray + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.is_error: + - FunctionExpression.array_get: + - Field: title + - Constant: 0 + - "isError" + assert_results: + - isError: true + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + isError: + functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: title + - integerValue: '0' + name: array_get + name: is_error + name: select + - description: testOffset_NonArray + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Where: + - FunctionExpression.offset: + - Field: title + - Constant: 0 + assert_count: 0 - description: testArrayGet_NegativeOffset pipeline: - Collection: books @@ -462,6 +516,44 @@ tests: - integerValue: '-1' name: array_get name: select + - description: testOffset + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.offset: + - Field: tags + - Constant: -1 + - "lastTag" + assert_results: + - lastTag: "adventure" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + lastTag: + functionValue: + args: + - fieldReferenceValue: tags + - integerValue: '-1' + name: offset + name: select - description: testArrayFirst pipeline: - Collection: books @@ -800,3 +892,135 @@ tests: - stringValue: "Science Fiction" name: array_index_of_all name: select + - description: testArrayFilter + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_filter: + - Field: tags + - FunctionExpression.equal: + - Variable: tag + - Constant: comedy + - "tag" + - "comedyTag" + assert_results: + - comedyTag: ["comedy"] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + comedyTag: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "tag" + - functionValue: + args: + - fieldReferenceValue: tag + - stringValue: "comedy" + name: equal + name: array_filter + name: select + - description: testArrayTransform + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_transform: + - Field: tags + - FunctionExpression.to_upper: + - Variable: tag + - "tag" + - "upperTags" + assert_results: + - upperTags: ["COMEDY", "SPACE", "ADVENTURE"] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + upperTags: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "tag" + - functionValue: + args: + - fieldReferenceValue: tag + name: to_upper + name: array_transform + name: select + - description: testArrayTransformWithIndex + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.array_transform: + - Field: tags + - Variable: "i" + - "tag" + - "i" + - "indexedTags" + assert_results: + - indexedTags: [0, 1, 2] + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + indexedTags: + functionValue: + args: + - fieldReferenceValue: tags + - stringValue: "tag" + - stringValue: "i" + - fieldReferenceValue: i + name: array_transform + name: select diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index b14e636d1ebc..15ad164fa575 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -295,53 +295,6 @@ tests: - Pipeline: - Collection: books assert_count: 20 # Results will be duplicated - - description: testDocumentId - pipeline: - - Collection: books - - Where: - - FunctionExpression.equal: - - Field: title - - Constant: "The Hitchhiker's Guide to the Galaxy" - - Select: - - AliasedExpression: - - FunctionExpression.document_id: - - Field: __name__ - - "doc_id" - assert_results: - - doc_id: "book1" - assert_proto: - pipeline: - stages: - - args: - - referenceValue: /books - name: collection - - args: - - functionValue: - args: - - fieldReferenceValue: title - - stringValue: "The Hitchhiker's Guide to the Galaxy" - name: equal - name: where - - args: - - mapValue: - fields: - doc_id: - functionValue: - name: document_id - args: - - fieldReferenceValue: __name__ - name: select - - description: testCollectionId - pipeline: - - Collection: books - - Limit: 1 - - Select: - - AliasedExpression: - - FunctionExpression.collection_id: - - Field: __name__ - - "collectionName" - assert_results: - - collectionName: "books" - description: testCollectionGroup pipeline: - CollectionGroup: books @@ -765,3 +718,61 @@ tests: res: fieldReferenceValue: res name: select + - description: testCoalesce + pipeline: + - Literals: + - res: + FunctionExpression.coalesce: + - Constant: null + - Constant: "B" + - Select: + - res + assert_results: + - res: "B" + - description: testSwitchOn + pipeline: + - Literals: + - res: + FunctionExpression.switch_on: + - FunctionExpression.equal: + - Constant: 1 + - Constant: 2 + - Constant: "A" + - FunctionExpression.equal: + - Constant: 1 + - Constant: 1 + - Constant: "B" + - Constant: "C" + - Select: + - res + assert_results: + - res: "B" + - description: testStorageSize + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.storage_size: + - Field: __name__ + - res + assert_results: + - res: 29 + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - integerValue: '1' + name: limit + - args: + - mapValue: + fields: + res: + functionValue: + args: + - fieldReferenceValue: __name__ + name: storage_size + name: select diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml index a3de3a4a07fe..775c7c78ea80 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/logical.yaml @@ -759,3 +759,74 @@ tests: - "value_or_default" assert_results: - value_or_default: "1984" + - description: whereByNorCondition + pipeline: + - Collection: books + - Where: + - Nor: + - FunctionExpression.equal: + - Field: genre + - Constant: Romance + - FunctionExpression.equal: + - Field: genre + - Constant: Dystopian + - FunctionExpression.equal: + - Field: genre + - Constant: Fantasy + - FunctionExpression.greater_than: + - Field: published + - Constant: 1949 + - Select: + - title + - Sort: + - Ordering: + - Field: title + - ASCENDING + assert_results: + - title: "Crime and Punishment" + - title: "The Great Gatsby" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - functionValue: + args: + - fieldReferenceValue: genre + - stringValue: Romance + name: equal + - functionValue: + args: + - fieldReferenceValue: genre + - stringValue: Dystopian + name: equal + - functionValue: + args: + - fieldReferenceValue: genre + - stringValue: Fantasy + name: equal + - functionValue: + args: + - fieldReferenceValue: published + - integerValue: '1949' + name: greater_than + name: nor + name: where + - args: + - mapValue: + fields: + title: + fieldReferenceValue: title + name: select + - args: + - mapValue: + fields: + direction: + stringValue: ascending + expression: + fieldReferenceValue: title + name: sort diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml new file mode 100644 index 000000000000..ed29330f811d --- /dev/null +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/references.yaml @@ -0,0 +1,49 @@ +tests: + - description: testDocumentId + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: title + - Constant: "The Hitchhiker's Guide to the Galaxy" + - Select: + - AliasedExpression: + - FunctionExpression.document_id: + - Field: __name__ + - "doc_id" + assert_results: + - doc_id: "book1" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: title + - stringValue: "The Hitchhiker's Guide to the Galaxy" + name: equal + name: where + - args: + - mapValue: + fields: + doc_id: + functionValue: + name: document_id + args: + - fieldReferenceValue: __name__ + name: select + - description: testCollectionId + pipeline: + - Collection: books + - Limit: 1 + - Select: + - AliasedExpression: + - FunctionExpression.collection_id: + - Field: __name__ + - "collectionName" + assert_results: + - collectionName: "books" + diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index 98db3c3a8f17..51a510d2b78f 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -735,6 +735,14 @@ def test_or(self): assert instance.params == [arg1, arg2] assert repr(instance) == "Or(Arg1, Arg2)" + def test_nor(self): + arg1 = self._make_arg("Arg1") + arg2 = self._make_arg("Arg2") + instance = expr.Nor(arg1, arg2) + assert instance.name == "nor" + assert instance.params == [arg1, arg2] + assert repr(instance) == "Nor(Arg1, Arg2)" + def test_array_get(self): arg1 = self._make_arg("ArrayField") arg2 = self._make_arg("Offset") @@ -742,8 +750,18 @@ def test_array_get(self): assert instance.name == "array_get" assert instance.params == [arg1, arg2] assert repr(instance) == "ArrayField.array_get(Offset)" - infix_istance = arg1.array_get(arg2) - assert infix_istance == instance + infix_instance = arg1.array_get(arg2) + assert infix_instance == instance + + def test_offset(self): + arg1 = self._make_arg("ArrayField") + arg2 = self._make_arg("Offset") + instance = Expression.offset(arg1, arg2) + assert instance.name == "offset" + assert instance.params == [arg1, arg2] + assert repr(instance) == "ArrayField.offset(Offset)" + infix_instance = arg1.offset(arg2) + assert infix_instance == instance def test_array_contains(self): arg1 = self._make_arg("ArrayField") @@ -904,6 +922,42 @@ def test_if_error(self): infix_instance = arg1.if_error(arg2) assert infix_instance == instance + def test_coalesce(self): + arg1 = self._make_arg("Arg1") + arg2 = self._make_arg("Arg2") + arg3 = self._make_arg("Arg3") + instance = Expression.coalesce(arg1, arg2, arg3) + assert instance.name == "coalesce" + assert instance.params == [arg1, arg2, arg3] + assert repr(instance) == "Arg1.coalesce(Arg2, Arg3)" + infix_instance = arg1.coalesce(arg2, arg3) + assert infix_instance == instance + + def test_switch_on(self): + arg1 = self._make_arg("Condition1") + arg2 = self._make_arg("Result1") + arg3 = self._make_arg("Condition2") + arg4 = self._make_arg("Result2") + arg5 = self._make_arg("Default") + instance = Expression.switch_on(arg1, arg2, arg3, arg4, arg5) + assert instance.name == "switch_on" + assert instance.params == [arg1, arg2, arg3, arg4, arg5] + assert ( + repr(instance) + == "Condition1.switch_on(Result1, Condition2, Result2, Default)" + ) + infix_instance = arg1.switch_on(arg2, arg3, arg4, arg5) + assert infix_instance == instance + + def test_storage_size(self): + arg1 = self._make_arg("Input") + instance = Expression.storage_size(arg1) + assert instance.name == "storage_size" + assert instance.params == [arg1] + assert repr(instance) == "Input.storage_size()" + infix_instance = arg1.storage_size() + assert infix_instance == instance + def test_not(self): arg1 = self._make_arg("Condition") instance = expr.Not(arg1) @@ -1595,14 +1649,74 @@ def test_array_length(self): assert infix_instance == instance def test_array_reverse(self): - arg1 = self._make_arg("Array") + arg1 = self._make_arg("ArrayField") instance = Expression.array_reverse(arg1) assert instance.name == "array_reverse" assert instance.params == [arg1] - assert repr(instance) == "Array.array_reverse()" + assert repr(instance) == "ArrayField.array_reverse()" infix_instance = arg1.array_reverse() assert infix_instance == instance + def test_array_filter(self): + arg1 = self._make_arg("ArrayField") + arg2 = "element_alias" + arg3 = self._make_arg("FilterExpr", expr_type=BooleanExpression) + instance = Expression.array_filter(arg1, arg3, arg2) + assert instance.name == "array_filter" + assert instance.params == [arg1, Constant.of(arg2), arg3] + assert ( + repr(instance) + == "ArrayField.array_filter(Constant.of('element_alias'), FilterExpr)" + ) + infix_instance = arg1.array_filter(arg3, arg2) + assert infix_instance == instance + + arg4 = "index_alias" + instance_with_idx = Expression.array_filter(arg1, arg3, arg2, arg4) + assert instance_with_idx.name == "array_filter" + assert instance_with_idx.params == [ + arg1, + Constant.of(arg2), + Constant.of(arg4), + arg3, + ] + assert ( + repr(instance_with_idx) + == "ArrayField.array_filter(Constant.of('element_alias'), FilterExpr, Constant.of('index_alias'))" + ) + infix_instance_with_idx = arg1.array_filter(arg3, arg2, arg4) + assert infix_instance_with_idx == instance_with_idx + + def test_array_transform(self): + arg1 = self._make_arg("ArrayField") + arg2 = "element_alias" + arg3 = self._make_arg("TransformExpr") + instance = Expression.array_transform(arg1, arg3, arg2) + assert instance.name == "array_transform" + assert instance.params == [arg1, Constant.of(arg2), arg3] + assert ( + repr(instance) + == "ArrayField.array_transform(Constant.of('element_alias'), TransformExpr)" + ) + infix_instance = arg1.array_transform(arg3, arg2) + assert infix_instance == instance + + arg4 = "index_alias" + instance_with_idx = Expression.array_transform(arg1, arg3, arg2, arg4) + assert instance_with_idx.name == "array_transform" + assert instance_with_idx.params == [ + arg1, + Constant.of(arg2), + Constant.of(arg4), + arg3, + ] + assert ( + repr(instance_with_idx) + == "ArrayField.array_transform(TransformExpr, Constant.of('element_alias'), Constant.of('index_alias'))" + ) + infix_instance_with_idx = arg1.array_transform(arg3, arg2, arg4) + assert infix_instance_with_idx == instance_with_idx + def test_array_concat(self): arg1 = self._make_arg("ArrayRef1") arg2 = self._make_arg("ArrayRef2")