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) # 5Mé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)") # 42drop_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 foundSystè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 * 2Infé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)") # 6Exemple : 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)") # 6Exemple : 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)") # 10Gestion 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: ZeroDivisionErrorExemple : 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)") # NULLCombinaison 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'))") # 2024DateTime 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:00DateTime64 (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.000001Utilisation 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