dianfu commented on code in PR #29146:
URL: https://github.com/apache/flink/pull/29146#discussion_r3976445004


##########
flink-python/pyflink/dataframe/sql.py:
##########
@@ -257,6 +315,97 @@ def _register_bindings(
             _warn_skipped(name, f"registration failed: {e}")
             continue
         registered.append(name)
+    return registered
+
+
+def _register_functions(
+    t_env: TableEnvironment,
+    explicit: Dict[str, _DataFrameUDFWrapper],
+    auto: Dict[str, _DataFrameUDFWrapper],
+) -> List[str]:
+    """
+    Register explicit and auto-collected UDFs as temporary system functions 
and return
+    the registered names. Registration is all-or-nothing: if an explicit 
binding is
+    rejected, the functions registered before it are dropped again before 
raising.
+
+    System functions are looked up by bare name independently of the current 
catalog
+    and database, which matches how a Python name is referenced in the query. 
Function
+    names are case-insensitive: the catalog normalizes them to lower case, so
+    collisions are checked case-insensitively. Mirroring views, explicit 
bindings may
+    shadow built-in and permanent catalog functions but never an existing 
temporary
+    function; auto-bind shadows nothing.
+    """
+    if not explicit and not auto:
+        return []
+    # list_user_defined_functions() covers temporary and permanent functions 
alike;
+    # the permanent ones are those the current catalog lists for the current 
database.
+    user_defined = {f.lower() for f in t_env.list_user_defined_functions()}
+    temporary_functions = user_defined - _permanent_functions(t_env)

Review Comment:
   The temporary functions cannot be derived by subtracting these two name 
sets. `list_user_defined_functions()` merges temporary system, temporary 
catalog, and permanent catalog functions, so the source information has already 
been lost.
   
   For example, if both a permanent function `f` and a temporary catalog 
function `f` exist:
   
   - `user_defined == {"f"}`
   - `_permanent_functions(t_env) == {"f"}`
   - `temporary_functions == set()`
   
   An explicit `f=my_udf` binding is then allowed and registered as a temporary 
system function, which shadows the existing temporary function instead of 
raising the documented `ValueError`.



##########
flink-python/pyflink/dataframe/sql.py:
##########
@@ -257,6 +315,97 @@ def _register_bindings(
             _warn_skipped(name, f"registration failed: {e}")
             continue
         registered.append(name)
+    return registered
+
+
+def _register_functions(
+    t_env: TableEnvironment,
+    explicit: Dict[str, _DataFrameUDFWrapper],
+    auto: Dict[str, _DataFrameUDFWrapper],
+) -> List[str]:
+    """
+    Register explicit and auto-collected UDFs as temporary system functions 
and return
+    the registered names. Registration is all-or-nothing: if an explicit 
binding is
+    rejected, the functions registered before it are dropped again before 
raising.
+
+    System functions are looked up by bare name independently of the current 
catalog
+    and database, which matches how a Python name is referenced in the query. 
Function
+    names are case-insensitive: the catalog normalizes them to lower case, so
+    collisions are checked case-insensitively. Mirroring views, explicit 
bindings may
+    shadow built-in and permanent catalog functions but never an existing 
temporary
+    function; auto-bind shadows nothing.
+    """
+    if not explicit and not auto:
+        return []
+    # list_user_defined_functions() covers temporary and permanent functions 
alike;
+    # the permanent ones are those the current catalog lists for the current 
database.
+    user_defined = {f.lower() for f in t_env.list_user_defined_functions()}
+    temporary_functions = user_defined - _permanent_functions(t_env)
+    # Explicit bindings take precedence on name collisions, so only the 
remaining
+    # auto-bound candidates need the built-in function names. list_functions() 
covers
+    # those as well, but is comparatively expensive, so skip it when nothing 
needs it.
+    auto_candidates = {name: value for name, value in auto.items() if name not 
in explicit}

Review Comment:
   Should we normalize the explicit names first and filter auto candidates 
using the normalized name set?
   
   For the following example:
   
       add_one = pf.udf(...)
       pf.sql("SELECT ADD_ONE(a)", ADD_ONE=override)
   
   `add_one` is not removed from `auto_candidates` because this comparison uses 
the original Python keys. The explicit function is registered first, and the 
auto-bound `add_one` then produces a misleading "function already exists" 
warning. 



##########
flink-python/pyflink/dataframe/sql.py:
##########
@@ -90,48 +103,84 @@ def sql(query: str, *, auto_bind: bool = True, **bindings: 
DataFrame) -> DataFra
         ...     auto_bind=False,
         ...     src=df1,
         ... )
+        >>> # UDFs are bound the same way, under their variable or keyword name
+        >>> @pf.udf
+        ... def add_one(value: int) -> int:
+        ...     return value + 1
+        >>> pf.sql("SELECT add_one(a) AS a1 FROM df1")
+        >>> pf.sql("SELECT inc(a) FROM src", auto_bind=False, src=df1, 
inc=add_one)
         >>> # Mix SQL and the DataFrame API
         >>> pf.sql("SELECT a, b FROM df1").filter(pf.col("a") > 1).to_pandas()
 
     .. versionadded:: 2.4.0
     """
     if not isinstance(query, str):
         raise TypeError("query must be a string")
-    auto_bindings: Dict[str, DataFrame] = {}
+
+    # Collect auto bindings first.
+    variables = {}
+    # Gather the variables in the namespace
     if auto_bind:
-        frame = inspect.currentframe()
-        caller = frame.f_back if frame is not None else None
-        try:
-            if caller is not None:
-                # Locals take precedence over globals.
-                namespace = {**caller.f_globals, **caller.f_locals}
-                auto_bindings = {
-                    name: value
-                    for name, value in namespace.items()
-                    if isinstance(value, DataFrame)
-                }
-        finally:
-            del frame, caller
-    t_env = _resolve_table_environment(bindings, auto_bindings)
-    registered: List[str] = []
+        if frame := inspect.currentframe():
+            if outer_frame := frame.f_back:
+                variables = {**outer_frame.f_globals, **outer_frame.f_locals}
+            # Suggested by python docs
+            del outer_frame
+            del frame
+    auto_frames = _get_dataframes(variables)
+    auto_udfs = _get_udfs(variables)
+
+    # Check that explicit bindings are the correct type first
+    for name, value in bindings.items():
+        if not isinstance(value, _BINDABLE_TYPES):
+            raise TypeError(
+                f"sql() binding '{name}' must be a DataFrame or a UDF created 
with "
+                f"pyflink.dataframe.udf, got {type(value).__name__}"
+            )
+    explicit_frames = _get_dataframes(bindings)
+    explicit_udfs = _get_udfs(bindings)
+
+    t_env = _resolve_table_environment(explicit_frames, auto_frames)
+    # Each registration step is all-or-nothing: it rolls back its own partial 
work on
+    # failure and only returns names on success, so a step that raises leaves 
nothing
+    # of its own behind and the finally block only drops what earlier steps 
returned.
+    views: List[str] = []
+    functions: List[str] = []
     try:
-        _register_bindings(t_env, bindings, auto_bindings, registered)
+        views = _register_views(t_env, explicit_frames, auto_frames)

Review Comment:
   The registered names are assigned to `views` / `functions` only after each 
helper returns successfully. If an exception occurs after a partial auto-bind, 
the outer `finally` cannot see or clean up the objects that were already 
registered.



##########
flink-python/pyflink/dataframe/sql.py:
##########
@@ -33,49 +34,61 @@
 
 _LOG = logging.getLogger(__name__)
 
+_Binding = Union[DataFrame, _DataFrameUDFWrapper]

Review Comment:
   The public type contract here does not match the return type of `pf.udf()`.
   
   `sql()` accepts `_DataFrameUDFWrapper`, while the `pf.udf()` overloads are 
declared to return `Callable[..., Expression]`. 
   
   The documented usage works at runtime, but it will be rejected by static 
type checkers:
   
       df = pf.from_dict({"a": [1]})
   
       @pf.udf
       def inc(value: int) -> int:
           return value + 1
   
       result = pf.sql(
           "SELECT inc(a) FROM df",
           auto_bind=False,
           df=df,
           inc=inc,
       )
   
   At runtime, `inc` is an `_DataFrameUDFWrapper`. However, the `pf.udf()` 
overload declares it as `Callable[..., Expression]`, which is incompatible with 
the `_Binding` annotation expected by `pf.sql()`.
   
   Do you think it makes sense to update the return type of py.udf from 
`Callable[..., Expression]` to `_DataFrameUDFWrapper` though it's not directly 
introduced by this PR?
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to