Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

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

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.

Quick start

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

Méthodes d’inscription

Décorateur @func

La façon la plus simple d’enregistrer une UDF. L’attribut __name__ de la fonction devient le nom de la fonction SQL.

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 :

add(2, 3)       # 5 (Python call)
query("SELECT add(2, 3)")  # 5 (SQL call)

create_function

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

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

drop_function

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 :

from chdb import drop_function

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

Système de types

Types disponibles

Tous les types peuvent être importés depuis chdb.sqltypes :

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,
)

Spécifier les types

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

Inférence automatique des types

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)
@func()
def process(name: str, age: int) -> str:
    return f"{name} is {age} years old"

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

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.

Gestion des NULL

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.

Exemple : default (ignorer)

@func(return_type="Int64")
def increment(x: int) -> int:
    return x + 1

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

Exemple : transmettre NULL avec None

@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

Exemple : plusieurs arguments

@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

Gestion des exceptions

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.

Exemple : default (propager)

@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

Exemple : ignorer les erreurs

@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

Combinaison de la gestion de NULL et des exceptions

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
@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)

Prise en charge de DateTime et des fuseaux horaires

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

Types Date

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

DateTime avec fuseaux horaires

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

DateTime64 (haute précision)

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

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

Utilisation des UDF avec les sessions

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

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
Navigation