> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fonctions Python définies par l’utilisateur (UDF)

> Créez des UDF Python natives dans chDB avec des arguments typés, la gestion de NULL et le contrôle des exceptions.

chDB vous permet d’enregistrer des fonctions Python en tant qu’UDF appelables depuis SQL. Elles s’exécutent nativement dans le processus, sans lancer de sous-processus ni générer de surcharge de sérialisation. Ces fonctions sont sûres du point de vue des types, prennent en charge l’inférence automatique des types à partir des annotations Python et permettent de configurer la gestion de NULL et des exceptions.

<div id="quick-start">
  ## Quick start
</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>
  Les exemples de ce guide exécutent `query()` avec le format de sortie CSV par défaut. Les commentaires intégrés indiquent les valeurs de résultat logiques ; la sortie brute affiche `NULL` sous la forme `\N` et applique les règles d’échappement CSV aux valeurs de type chaîne et date (par exemple `"Hello, world!"`).
</Note>

<div id="registration-methods">
  ## Méthodes d’inscription
</div>

<div id="func-decorator">
  ### Décorateur `@func`
</div>

La façon la plus simple d’enregistrer une UDF. L’attribut `__name__` de la fonction devient le nom de la fonction 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 fonction décorée reste appelable comme une fonction Python normale :

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

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

Enregistrez tout objet appelable (lambda, fonction, méthode) sous un nom explicite :

```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>

Supprime une UDF enregistrée. La suppression d’un nom non enregistré n’a aucun effet ; cette fonction peut donc être appelée sans condition :

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

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

<Note>
  L’enregistrement d’un nom déjà utilisé génère une erreur : les UDFs ne sont pas remplacées silencieusement. Appelez d’abord `drop_function(name)` pour réenregistrer une fonction, par exemple lors de la réexécution d’une cellule de notebook.
</Note>

<div id="type-system">
  ## Système de types
</div>

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

Tous les types peuvent être importés depuis `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">
  ### Spécifier les types
</div>

Les types peuvent être fournis de quatre manières :

| Méthode                   | Exemple                                | Description                                                                                                              |
| ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| constante `ChdbType`      | `INT64`, `STRING`                      | Importée depuis `chdb.sqltypes`                                                                                          |
| chaîne de type ClickHouse | `"Int64"`, `"String"`                  | Noms de types ClickHouse standard                                                                                        |
| chaîne paramétrée         | `"DateTime('UTC')"`, `"DateTime64(6)"` | Pour les types comportant des paramètres                                                                                 |
| type Python               | `int`, `str`, `float`                  | Transmis directement dans `arg_types`/`return_type` ou utilisé comme annotation de type dans la signature de la fonction |

```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">
  ### Inférence automatique des types
</div>

Lorsque `arg_types` ou `return_type` est omis, chDB infère les types à partir des annotations de type Python :

| Type Python         | Type 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 `arg_types` est fourni explicitement, il doit couvrir **tous** les paramètres — il n'est pas possible de combiner des types explicitement fournis et des types inférés. Cela s'applique à `create_function` comme au décorateur `@func` : spécifiez les types de tous les paramètres ou omettez-les entièrement et laissez chDB les déduire des annotations.
</Note>

Un type de retour est toujours requis : si `return_type` est omis et que la fonction ne possède aucune annotation de retour, l'enregistrement échoue. En revanche, les types d'arguments sont facultatifs : un paramètre sans type explicite ni annotation accepte dynamiquement tout type d'entrée pris en charge.

<div id="null-handling">
  ## Gestion des NULL
</div>

Le paramètre `on_null` contrôle le comportement lorsqu’un argument d’entrée est NULL.

| Valeur                | Comportement                                                          |
| --------------------- | --------------------------------------------------------------------- |
| `"skip"` (par défaut) | Renvoie immédiatement NULL sans appeler la fonction                   |
| `"pass"`              | Convertit NULL en `None` de Python et appelle la fonction normalement |

Vous pouvez également utiliser l’enum : `chdb.NullHandling.SKIP` / `chdb.NullHandling.PASS`.

<div id="null-skip">
  ### Exemple : default (ignorer)
</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">
  ### Exemple : transmettre NULL avec `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">
  ### Exemple : plusieurs arguments
</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">
  ## Gestion des exceptions
</div>

Le paramètre `on_error` définit le comportement à adopter lorsqu’une fonction Python lève une exception.

| Valeur                     | Comportement                                            |
| -------------------------- | ------------------------------------------------------- |
| `"propagate"` (par défaut) | Propage l’exception en tant qu’erreur SQL               |
| `"ignore"`                 | Intercepte l’exception et renvoie NULL pour cette ligne |

Vous pouvez également utiliser l’enum : `chdb.ExceptionHandling.PROPAGATE` / `chdb.ExceptionHandling.IGNORE`.

<div id="exception-propagate">
  ### Exemple : default (propager)
</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">
  ### Exemple : ignorer les erreurs
</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">
  ## Combinaison de la gestion de NULL et des exceptions
</div>

Les options `on_null` et `on_error` peuvent être combinées :

| on\_null | on\_error     | Entrée NULL         | Exception       |
| -------- | ------------- | ------------------- | --------------- |
| `"skip"` | `"propagate"` | Retourne NULL       | Lève une erreur |
| `"skip"` | `"ignore"`    | Retourne NULL       | Retourne NULL   |
| `"pass"` | `"propagate"` | Appelle avec `None` | Lève une erreur |
| `"pass"` | `"ignore"`    | Appelle avec `None` | Retourne 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">
  ## Prise en charge de DateTime et des fuseaux horaires
</div>

Les UDFs prennent pleinement en charge les types de date et d’heure tenant compte des fuseaux horaires.

<div id="date-types">
  ### Types 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 avec fuseaux horaires
</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 (haute précision)
</div>

`DATETIME64` utilise par défaut une précision de 6 (microsecondes) :

```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>
  * Les valeurs `DateTime`/`DateTime64` d’entrée incluent les informations de fuseau horaire provenant de ClickHouse
  * Les objets `datetime` de sortie conservent les informations de fuseau horaire
  * La conversion de fuseau horaire est gérée automatiquement
</Note>

<div id="using-udfs-with-sessions">
  ## Utilisation des UDF avec les sessions
</div>

Les UDF sont enregistrées de manière globale et disponibles dans toutes les sessions d’un même processus :

```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
```
