> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-revert-104359-revert-104251-parquet-single.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Funciones definidas por el usuario (UDF) de Python

> Cree UDF nativas de Python en chDB con argumentos tipados, manejo de NULL y control de excepciones.

chDB permite registrar funciones de Python como UDF invocables desde SQL. Se ejecutan de forma nativa en el mismo proceso, sin iniciar subprocesos ni añadir sobrecarga de serialización. Las funciones tienen tipado seguro, admiten la inferencia automática de tipos a partir de anotaciones de Python y permiten configurar el manejo de NULL y de excepciones.

<div id="quick-start">
  ## Inicio rápido
</div>

```python theme={null}
from chdb import query, func
from chdb.sqltypes import INT64

@func([INT64, INT64], INT64)
def add(a, b):
    return a + b

result = query("SELECT add(2, 3)")
print(result)  # 5
```

<Note>
  Los ejemplos de esta guía ejecutan `query()` con el formato de salida CSV predeterminado. Los comentarios en línea muestran los valores lógicos del resultado; la salida sin procesar representa `NULL` como `\N` y aplica comillas de CSV a los valores de cadena y fecha (por ejemplo, `"Hello, world!"`).
</Note>

<div id="registration-methods">
  ## Métodos de registro
</div>

<div id="func-decorator">
  ### Decorador `@func`
</div>

La forma más sencilla de registrar una UDF. El atributo `__name__` de la función se convierte en el nombre de la función SQL.

```python theme={null}
from chdb import func
from chdb.sqltypes import INT64, STRING

# Explicit types
@func([INT64, INT64], INT64)
def add(a, b):
    return a + b

# Types inferred from annotations
@func()
def multiply(a: int, b: int) -> int:
    return a * b

# Explicit return_type, arg_types inferred from annotations
@func(return_type=STRING)
def greet(name: str):
    return f"Hello, {name}!"
```

La función decorada sigue pudiendo invocarse como una función normal de Python:

```python theme={null}
add(2, 3)       # 5 (Python call)
query("SELECT add(2, 3)")  # 5 (SQL call)
```

<div id="create-function">
  ### `create_function`
</div>

Registre cualquier objeto invocable (lambda, función, método) con un nombre explícito:

```python theme={null}
from chdb import create_function, query
from chdb.sqltypes import INT64, STRING

create_function("strlen", len, arg_types=[STRING], return_type=INT64)
query("SELECT strlen('hello')")  # 5

create_function("double", lambda x: x * 2, arg_types=[INT64], return_type=INT64)
query("SELECT double(21)")  # 42
```

<div id="drop-function">
  ### `drop_function`
</div>

Elimina una UDF registrada. Eliminar un nombre que no está registrado no tiene ningún efecto, por lo que se puede llamar incondicionalmente de forma segura:

```python theme={null}
from chdb import drop_function

drop_function("strlen")
# query("SELECT strlen('hello')")  # Error: function not found
```

<Note>
  Registrar un nombre que ya está registrado genera un error; las UDF no se reemplazan de forma silenciosa. Primero, llame a `drop_function(name)` para volver a registrar una función, por ejemplo, al volver a ejecutar una celda de un notebook.
</Note>

<div id="type-system">
  ## Sistema de tipos
</div>

<div id="available-types">
  ### Tipos disponibles
</div>

Todos los tipos se pueden importar desde `chdb.sqltypes`:

```python theme={null}
from chdb.sqltypes import (
    # Boolean
    BOOL,
    # Signed integers
    INT8, INT16, INT32, INT64, INT128, INT256,
    # Unsigned integers
    UINT8, UINT16, UINT32, UINT64, UINT128, UINT256,
    # Floating point
    FLOAT32, FLOAT64,
    # String
    STRING,
    # Date and time
    DATE, DATE32, DATETIME, DATETIME64,
)
```

<div id="specifying-types">
  ### Especificar tipos
</div>

Los tipos se pueden proporcionar de cuatro formas:

| Método                    | Ejemplo                                | Descripción                                                                                                 |
| ------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| constante `ChdbType`      | `INT64`, `STRING`                      | Se importa desde `chdb.sqltypes`                                                                            |
| cadena de tipo ClickHouse | `"Int64"`, `"String"`                  | Nombres de tipo estándar de ClickHouse                                                                      |
| cadena parametrizada      | `"DateTime('UTC')"`, `"DateTime64(6)"` | Para tipos con parámetros                                                                                   |
| tipo de Python            | `int`, `str`, `float`                  | Se pasa directamente en `arg_types`/`return_type` o se usa como anotación de tipo en la firma de la función |

```python theme={null}
from chdb import create_function, func
from chdb.sqltypes import INT64

# All equivalent:
create_function("f1", lambda x: x * 2, arg_types=[INT64], return_type=INT64)
create_function("f2", lambda x: x * 2, arg_types=["Int64"], return_type="Int64")
create_function("f3", lambda x: x * 2, arg_types=[int], return_type=int)

@func()
def f4(x: int) -> int:
    return x * 2
```

<div id="automatic-type-inference">
  ### Inferencia automática de tipos
</div>

Cuando se omiten `arg_types` o `return_type`, chDB infiere los tipos a partir de las anotaciones de tipo de Python:

| Tipo de Python      | Tipo de ClickHouse |
| ------------------- | ------------------ |
| `bool`              | `Bool`             |
| `int`               | `Int64`            |
| `float`             | `Float64`          |
| `str`               | `String`           |
| `bytes`             | `String`           |
| `bytearray`         | `String`           |
| `datetime.date`     | `Date`             |
| `datetime.datetime` | `DateTime64(6)`    |

```python theme={null}
@func()
def process(name: str, age: int) -> str:
    return f"{name} is {age} years old"

# Equivalent to:
# @func([STRING, INT64], STRING)
```

<Note>
  Si se proporciona `arg_types` explícitamente, debe incluir **todos** los parámetros; no se admite combinar parcialmente tipos explícitos e inferidos. Esto se aplica tanto a `create_function` como al decorador `@func`: especifique los tipos de todos los parámetros u omítalos por completo y deje que chDB los infiera a partir de las anotaciones.
</Note>

Siempre se requiere un tipo de retorno: si se omite `return_type` y la función no tiene una anotación de retorno, se produce un error al registrarla. En cambio, los tipos de los argumentos son opcionales: un parámetro sin tipo explícito ni anotación acepta dinámicamente cualquier tipo de entrada compatible.

<div id="null-handling">
  ## Manejo de NULL
</div>

El parámetro `on_null` controla el comportamiento cuando alguno de los argumentos de entrada es NULL.

| Valor                     | Comportamiento                                                      |
| ------------------------- | ------------------------------------------------------------------- |
| `"skip"` (predeterminado) | Devuelve NULL inmediatamente sin llamar a la función                |
| `"pass"`                  | Convierte NULL en `None` de Python y llama a la función normalmente |

También puedes usar el enum: `chdb.NullHandling.SKIP` / `chdb.NullHandling.PASS`.

<div id="null-skip">
  ### Ejemplo: predeterminado (omitir)
</div>

```python theme={null}
@func(return_type="Int64")
def increment(x: int) -> int:
    return x + 1

query("SELECT increment(NULL)")  # NULL
query("SELECT increment(5)")     # 6
```

<div id="null-pass">
  ### Ejemplo: pasar NULL como `None`
</div>

```python theme={null}
@func(return_type="Int64", on_null="pass")
def null_to_zero(x):
    return 0 if x is None else x + 1

query("SELECT null_to_zero(NULL)")  # 0
query("SELECT null_to_zero(5)")     # 6
```

<div id="null-multiple-args">
  ### Ejemplo: varios argumentos
</div>

```python theme={null}
@func(arg_types=["Int64", "Int64"], return_type="Int64", on_null="pass")
def add_or_zero(a, b):
    return (a or 0) + (b or 0)

query("SELECT add_or_zero(NULL, 5)")    # 5
query("SELECT add_or_zero(NULL, NULL)") # 0
query("SELECT add_or_zero(3, 7)")       # 10
```

<div id="exception-handling">
  ## Manejo de excepciones
</div>

El parámetro `on_error` controla el comportamiento cuando la función de Python genera una excepción.

| Valor                          | Comportamiento                                     |
| ------------------------------ | -------------------------------------------------- |
| `"propagate"` (predeterminado) | Propaga la excepción como un error SQL             |
| `"ignore"`                     | Captura la excepción y devuelve NULL para esa fila |

También puede usar el enum: `chdb.ExceptionHandling.PROPAGATE` / `chdb.ExceptionHandling.IGNORE`.

<div id="exception-propagate">
  ### Ejemplo: predeterminado (propagar)
</div>

```python theme={null}
@func(arg_types=["Int64", "Int64"], return_type="Int64")
def divide(a, b):
    return a // b

query("SELECT divide(10, 2)")  # 5
query("SELECT divide(1, 0)")   # Error: ZeroDivisionError
```

<div id="exception-ignore">
  ### Ejemplo: ignorar errores
</div>

```python theme={null}
@func(arg_types=["Int64", "Int64"], return_type="Int64", on_error="ignore")
def safe_divide(a, b):
    return a // b

query("SELECT safe_divide(10, 2)")  # 5
query("SELECT safe_divide(1, 0)")   # NULL
```

<div id="combining-null-and-exception">
  ## Combinación de NULL y manejo de excepciones
</div>

Las opciones `on_null` y `on_error` se pueden combinar:

| on\_null | on\_error     | Entrada NULL     | Excepción       |
| -------- | ------------- | ---------------- | --------------- |
| `"skip"` | `"propagate"` | Devuelve NULL    | Genera un error |
| `"skip"` | `"ignore"`    | Devuelve NULL    | Devuelve NULL   |
| `"pass"` | `"propagate"` | Llama con `None` | Genera un error |
| `"pass"` | `"ignore"`    | Llama con `None` | Devuelve NULL   |

```python theme={null}
@func(
    arg_types=["Int64", "Int64"],
    return_type="Int64",
    on_null="pass",
    on_error="ignore",
)
def robust_divide(a, b):
    if a is None or b is None:
        return -1
    return a // b

query("SELECT robust_divide(10, 2)")     # 5
query("SELECT robust_divide(NULL, 2)")   # -1
query("SELECT robust_divide(1, 0)")      # NULL (exception caught)
```

<div id="datetime-and-timezone">
  ## Compatibilidad con DateTime y zonas horarias
</div>

Las UDF admiten plenamente tipos de fecha y hora compatibles con zonas horarias.

<div id="date-types">
  ### Tipos de Date
</div>

```python theme={null}
from datetime import date, timedelta

@func()
def next_day(d: date) -> date:
    return d + timedelta(days=1)

@func()
def get_year(d: date) -> int:
    return d.year

query("SELECT next_day(toDate('2024-06-15'))")  # 2024-06-16
query("SELECT get_year(toDate('2024-06-15'))")  # 2024
```

<div id="datetime-with-timezones">
  ### DateTime con zonas horarias
</div>

```python theme={null}
from datetime import timedelta

@func(arg_types=["DateTime('UTC')"], return_type="DateTime('UTC')")
def add_one_hour(dt):
    return dt + timedelta(hours=1)

query("SELECT add_one_hour(toDateTime('2024-01-01 12:00:00', 'UTC'))")  # 2024-01-01 13:00:00
```

<div id="datetime64">
  ### DateTime64 (alta precisión)
</div>

`DATETIME64` tiene una escala predeterminada de 6 (microsegundos):

```python theme={null}
from datetime import timedelta

@func(arg_types=["DateTime64(6, 'UTC')"], return_type="DateTime64(6, 'UTC')")
def add_microsecond(dt):
    return dt + timedelta(microseconds=1)

query("SELECT add_microsecond(toDateTime64('2024-01-01 12:00:00.000000', 6, 'UTC'))")  # 2024-01-01 12:00:00.000001
```

<Note>
  * Los valores de entrada `DateTime`/`DateTime64` incluyen información sobre la zona horaria de ClickHouse
  * Los objetos `datetime` de salida conservan la información sobre la zona horaria
  * La conversión de zona horaria se gestiona automáticamente
</Note>

<div id="using-udfs-with-sessions">
  ## Uso de UDFs con sesiones
</div>

Las UDFs se registran de forma global y están disponibles en todas las sesiones del mismo proceso:

```python theme={null}
from chdb import session as chs, func
from chdb.sqltypes import INT64

@func([INT64], INT64)
def double(x):
    return x * 2

sess = chs.Session()
sess.query("CREATE TABLE t (x Int64) ENGINE = Memory")
sess.query("INSERT INTO t VALUES (1), (2), (3)")
result = sess.query("SELECT double(x) FROM t ORDER BY x", "CSV")
print(result)
# 2
# 4
# 6
```
