@@ -97,6 +97,25 @@ def format(self, record: logging.LogRecord) -> str:
9797 'bytes' : - ft .STRING ,
9898}
9999
100+ # Map dtype strings to Python type annotation strings for code generation.
101+ _dtype_to_python : Dict [str , str ] = {
102+ 'bool' : 'bool' ,
103+ 'int8' : 'int' ,
104+ 'int16' : 'int' ,
105+ 'int32' : 'int' ,
106+ 'int64' : 'int' ,
107+ 'int' : 'int' ,
108+ 'uint8' : 'int' ,
109+ 'uint16' : 'int' ,
110+ 'uint32' : 'int' ,
111+ 'uint64' : 'int' ,
112+ 'float32' : 'float' ,
113+ 'float64' : 'float' ,
114+ 'float' : 'float' ,
115+ 'str' : 'str' ,
116+ 'bytes' : 'bytes' ,
117+ }
118+
100119
101120class FunctionRegistry :
102121 """Registry of discovered UDF functions."""
@@ -256,27 +275,98 @@ def _build_json_descriptions(
256275 })
257276 return descriptions
258277
278+ @staticmethod
279+ def _python_type_annotation (dtype : str ) -> str :
280+ """Convert a dtype string to a Python type annotation.
281+
282+ Handles nullable types (trailing '?') by wrapping in Optional.
283+ """
284+ nullable = dtype .endswith ('?' )
285+ base = dtype .rstrip ('?' )
286+ py_type = _dtype_to_python .get (base )
287+ if py_type is None :
288+ raise ValueError (f'Unsupported dtype: { dtype !r} ' )
289+ if nullable :
290+ return f'Optional[{ py_type } ]'
291+ return py_type
292+
293+ @staticmethod
294+ def _build_python_code (
295+ sig : Dict [str , Any ],
296+ body : str ,
297+ ) -> str :
298+ """Build a complete @udf-decorated Python function from signature and body.
299+
300+ Args:
301+ sig: Parsed signature dict with 'name', 'args', 'returns'.
302+ body: The function body (e.g. "return x * 3").
303+
304+ Returns:
305+ Complete Python source with imports and a @udf-decorated function.
306+ """
307+ func_name = sig ['name' ]
308+ args = sig .get ('args' , [])
309+ returns = sig .get ('returns' , [])
310+
311+ # Build parameter list with type annotations
312+ params = []
313+ for arg in args :
314+ ann = FunctionRegistry ._python_type_annotation (arg ['dtype' ])
315+ params .append (f'{ arg ["name" ]} : { ann } ' )
316+ params_str = ', ' .join (params )
317+
318+ # Build return type annotation
319+ if len (returns ) == 0 :
320+ ret_ann = 'None'
321+ elif len (returns ) == 1 :
322+ ret_ann = FunctionRegistry ._python_type_annotation (
323+ returns [0 ]['dtype' ],
324+ )
325+ else :
326+ parts = [
327+ FunctionRegistry ._python_type_annotation (r ['dtype' ])
328+ for r in returns
329+ ]
330+ ret_ann = f'Tuple[{ ", " .join (parts )} ]'
331+
332+ # Indent body lines
333+ indented_body = '\n ' .join (
334+ f' { line } ' for line in body .splitlines ()
335+ )
336+
337+ return (
338+ 'from singlestoredb.functions import udf\n '
339+ 'from typing import Optional, Tuple\n '
340+ '\n '
341+ '@udf\n '
342+ f'def { func_name } ({ params_str } ) -> { ret_ann } :\n '
343+ f'{ indented_body } \n '
344+ )
345+
259346 def create_function (
260347 self ,
261348 signature_json : str ,
262349 code : str ,
263350 replace : bool ,
264351 ) -> List [str ]:
265- """Register a function from its signature and Python source code.
352+ """Register a function from its signature and function body.
353+
354+ Constructs a complete @udf-decorated Python function from the
355+ signature metadata and the raw function body, then compiles
356+ and executes it.
266357
267358 Args:
268359 signature_json: JSON object matching the describe-functions
269360 element schema (must contain a 'name' field)
270- code: Python source code containing the @udf-decorated function
361+ code: Function body (e.g. "return x * 3"), not full source
271362 replace: If False, raise an error if the function already exists
272363
273364 Returns:
274365 List of newly registered function names
275366
276367 Raises:
277- SyntaxError: If the code has syntax errors
278- ValueError: If no @udf-decorated functions are found or
279- function already exists and replace is False
368+ SyntaxError: If the generated code has syntax errors
369+ ValueError: If the function already exists and replace is False
280370 """
281371 sig = json .loads (signature_json )
282372 func_name = sig .get ('name' )
@@ -297,11 +387,14 @@ def create_function(
297387 if replace and func_name in self .functions :
298388 del self .functions [func_name ]
299389
390+ # Build a complete @udf-decorated function from signature + body
391+ full_code = self ._build_python_code (sig , code )
392+
300393 # Use __main__ as the module name for dynamically submitted functions
301394 name = '__main__'
302395
303396 # Validate syntax
304- compiled = compile (code , f'<{ name } >' , 'exec' )
397+ compiled = compile (full_code , f'<{ name } >' , 'exec' )
305398
306399 # Reuse existing module to avoid corrupting the componentize-py
307400 # runtime state (replacing sys.modules['__main__'] traps WASM).
@@ -320,7 +413,8 @@ def create_function(
320413
321414 if not new_names :
322415 raise ValueError (
323- 'No @udf-decorated functions found in submitted code' ,
416+ f'Function "{ func_name } " was not registered. '
417+ f'Check that the signature dtypes are supported.' ,
324418 )
325419
326420 logger .info (
0 commit comments