Skip to content

Commit 5d1e9e6

Browse files
kesmit13claude
andcommitted
Add code generation for UDF handler function registration
Build complete @udf-decorated Python functions from signature metadata and raw function body instead of requiring full source code. This adds dtype-to-Python type mapping and constructs properly annotated functions at registration time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 49521d1 commit 5d1e9e6

File tree

1 file changed

+101
-7
lines changed

1 file changed

+101
-7
lines changed

singlestoredb/functions/ext/wasm/udf_handler.py

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

101120
class 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

Comments
 (0)