@@ -61,6 +61,86 @@ def _call_module_alias_function(value):
6161
6262
6363class DataFrameUDFDeclarationTests (unittest .TestCase ):
64+ def test_arrow_annotation_inference_and_overrides (self ):
65+ def arrow_identity (values : pa .Array ) -> pa .ChunkedArray :
66+ return pa .chunked_array ([values ])
67+
68+ def integer_array (values : pa .Int64Array ) -> pa .Int64Array :
69+ return values
70+
71+ def list_array (values : pa .ListArray ):
72+ return values .flatten ()
73+
74+ def struct_array (values : "pa.StructArray" ):
75+ return values .field ("value" )
76+
77+ def concrete_return (values ) -> pa .Int64Array :
78+ return values
79+
80+ def mixed (values : pd .Series ) -> pa .Int64Array :
81+ return pa .array (values )
82+
83+ def captured (context : pd .Series , values : pa .Array ) -> pa .Array :
84+ return values
85+
86+ class ArrowCallable :
87+ def __call__ (self , values : "pa.Array" ) -> "pa.Array" :
88+ return values
89+
90+ class ArrowScalar (ScalarFunction ):
91+ def eval (self , values : pa .Array ) -> pa .Array :
92+ return values
93+
94+ for func in (arrow_identity , integer_array , list_array , struct_array , concrete_return ,
95+ ArrowCallable , ArrowCallable (), ArrowScalar , ArrowScalar (),
96+ functools .partial (captured , pd .Series ([1 ]))):
97+ with self .subTest (func = func ):
98+ declaration = pf .udf (func , return_dtype = pf .DataType .int64 ())
99+ self .assertEqual (cast (Any , declaration )._func_type , "arrow" )
100+
101+ for mode in ("general" , "pandas" , "arrow" ):
102+ with self .subTest (mode = mode ):
103+ declaration = pf .udf (mixed , return_dtype = pf .DataType .int64 (), func_type = mode )
104+ self .assertEqual (cast (Any , declaration )._func_type , mode )
105+
106+ with self .assertRaisesRegex (ValueError , "pandas.*Arrow.*func_type" ):
107+ pf .udf (mixed , return_dtype = pf .DataType .int64 ())
108+ with self .assertRaisesRegex (TypeError , "return_dtype is required for arrow" ):
109+ pf .udf (arrow_identity )
110+
111+ def test_explicit_arrow_declarations (self ):
112+ from pyflink .table .udf import udf as table_udf
113+
114+ def identity (values ):
115+ return values
116+
117+ declaration = pf .udf (identity , return_dtype = pf .DataType .string (), func_type = "arrow" )
118+ self .assertEqual (_return_dtype (declaration ), pf .DataType .string ())
119+ table_udf (identity , result_type = TableDataTypes .STRING (), func_type = "arrow" )
120+
121+ with self .assertRaisesRegex (TypeError , "return_dtype is required for arrow" ):
122+ pf .udf (identity , func_type = "arrow" )
123+
124+ async def async_identity (values ):
125+ return values
126+
127+ class AsyncCallable :
128+ async def __call__ (self , values ):
129+ return values
130+
131+ for declare in (
132+ lambda : pf .udf (async_identity , return_dtype = pf .DataType .string (), func_type = "arrow" ),
133+ lambda : table_udf (async_identity , result_type = TableDataTypes .STRING (),
134+ func_type = "arrow" ),
135+ lambda : table_udf (AsyncCallable (), result_type = TableDataTypes .STRING (),
136+ func_type = "arrow" ),
137+ lambda : table_udf (functools .partial (AsyncCallable ()),
138+ result_type = TableDataTypes .STRING (), func_type = "arrow" ),
139+ ):
140+ with self .subTest (declare = declare ):
141+ with self .assertRaisesRegex (ValueError , "Async.*arrow" ):
142+ declare ()
143+
64144 def test_function_declarations_return_types_and_metadata (self ):
65145 class Details (TypedDict ):
66146 label : str
@@ -547,9 +627,9 @@ def __call__(self, context: pd.Series, value: int) -> int:
547627 False ,
548628 ),
549629 (
550- "pyarrow annotations remain general " ,
630+ "inferred arrow " ,
551631 lambda : pf .udf (arrow_add_one , return_dtype = pf .DataType .int64 ()),
552- "general " ,
632+ "arrow " ,
553633 False ,
554634 ),
555635 (
@@ -1178,11 +1258,11 @@ def eval(self, value):
11781258 "name must not be empty" ,
11791259 ),
11801260 (
1181- "arrow func type" ,
1261+ "unsupported func type" ,
11821262 lambda : pf .udf (
11831263 missing_return ,
11841264 return_dtype = pf .DataType .int64 (),
1185- func_type = "arrow " ,
1265+ func_type = "unsupported " ,
11861266 ),
11871267 ValueError ,
11881268 "func_type must be one of" ,
@@ -1480,6 +1560,21 @@ def close(self):
14801560
14811561
14821562class DataFrameUDFPlannerTests (PyFlinkDataFrameUTTestCase ):
1563+ def test_arrow_calls_require_a_column_argument (self ):
1564+ from pyflink .table import ExplainDetail
1565+
1566+ @pf .udf (return_dtype = pf .DataType .int64 (), func_type = "arrow" )
1567+ def identity (* values ):
1568+ return values [0 ]
1569+
1570+ dataframe = pf .from_records ([(1 ,)], schema = ["id" ])
1571+ for args in ((), (1 ,), (pf .lit (1 ),), (identity (),)):
1572+ with self .subTest (args = args ):
1573+ with self .assertRaisesRegex (Exception , "at least one column-valued argument" ):
1574+ result = dataframe .with_columns (
1575+ valid = identity (pf .col ("id" )), invalid = identity (* args ))
1576+ result .to_table ().explain (ExplainDetail .JSON_EXECUTION_PLAN )
1577+
14831578 def test_with_columns_binds_expressions_and_resolves_output_schema (self ):
14841579 @pf .udf (name = "render_value" )
14851580 def render (value : int , suffix : str ) -> str :
@@ -1521,6 +1616,25 @@ def describe(value):
15211616
15221617class DataFrameUDFITCase (PyFlinkStreamDataFrameTestCase ):
15231618 def test_supported_scalar_udfs_in_one_job (self ):
1619+ import pyarrow .compute as pc
1620+
1621+ self .env .set_parallelism (1 )
1622+ self .t_env .get_config ().set ("python.fn-execution.bundle.size" , "3" )
1623+ self .t_env .get_config ().set ("python.fn-execution.arrow.batch.size" , "2" )
1624+
1625+ @pf .udf (return_dtype = pf .DataType .string ())
1626+ def normalize_name (names : pa .Array ) -> pa .Array :
1627+ return pc .utf8_upper (names )
1628+
1629+ @pf .udf (return_dtype = pf .DataType .struct ({"value" : pf .DataType .int64 ().not_null ()}))
1630+ def describe (values : pa .Array ) -> pa .ChunkedArray :
1631+ result = pa .StructArray .from_arrays ([pc .multiply (values , 2 )], names = ["value" ])
1632+ return pa .chunked_array ([result .slice (0 , 1 ), result .slice (1 )])
1633+
1634+ @pf .udf (return_dtype = pf .DataType .int64 ())
1635+ def struct_value (values : pa .Array ) -> pa .Array :
1636+ return pc .struct_field (values , "value" )
1637+
15241638 @dataclass
15251639 class Details :
15261640 doubled : int
@@ -1561,19 +1675,27 @@ def eval(self, *values: int) -> int:
15611675 opened_scalar_class = pf .udf (OpenedScalarFunction )
15621676
15631677 result = (
1564- pf .from_records ([(1 ,) ], schema = ["id" ])
1678+ pf .from_records ([(1 , "alice" ), ( 2 , None ), ( 3 , "Bob" ) ], schema = ["id" , "name " ])
15651679 .with_columns (async_value = add_two (pf .col ("id" )))
15661680 .with_columns (
15671681 pandas_value = add_three (pf .col ("id" )),
15681682 details = details (pf .col ("id" )),
15691683 deferred_value = deferred (pf .col ("id" )),
15701684 scalar_value = opened_scalar_class (pf .col ("id" )),
1685+ normalized_name = normalize_name (pf .col ("name" )),
1686+ arrow_details = describe (pf .col ("id" )),
1687+ arrow_after_pandas = struct_value (describe (add_three (pf .col ("id" )))),
1688+ pandas_after_arrow = add_three (struct_value (describe (pf .col ("id" )))),
15711689 )
15721690 )
15731691
15741692 self .assertEqual (
1575- result .collect (),
1576- [Row (1 , 3 , 4 , Row (2 , ["1" ]), 5 , 6 )],
1693+ sorted (result .collect (), key = lambda row : row [0 ]),
1694+ [
1695+ Row (1 , "alice" , 3 , 4 , Row (2 , ["1" ]), 5 , 6 , "ALICE" , Row (2 ), 8 , 5 ),
1696+ Row (2 , None , 4 , 5 , Row (4 , ["2" ]), 6 , 7 , None , Row (4 ), 10 , 7 ),
1697+ Row (3 , "Bob" , 5 , 6 , Row (6 , ["3" ]), 7 , 8 , "BOB" , Row (6 ), 12 , 9 ),
1698+ ],
15771699 )
15781700
15791701
0 commit comments