O chDB permite registrar funções Python como UDFs que podem ser chamadas em SQL. Elas são executadas nativamente no processo — sem criar subprocessos nem incorrer em sobrecarga de serialização. As funções têm segurança de tipos, oferecem inferência automática de tipos com base em anotações Python e permitem configurar o tratamento de NULL e exceções.
Início rápido
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étodos de registro
Decorador @func
A maneira mais simples de registrar uma UDF. O __name__ da função se torna o nome da função 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}!"A função decorada continua podendo ser chamada normalmente em Python:
add(2, 3) # 5 (Python call)
query("SELECT add(2, 3)") # 5 (SQL call)create_function
Registre qualquer função chamável (lambda, função, método) com um nome explícito:
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
Remove uma UDF registrada. Remover um nome que não está registrado não tem efeito; portanto, é seguro chamar a função incondicionalmente:
from chdb import drop_function
drop_function("strlen")
# query("SELECT strlen('hello')") # Error: function not foundSistema de tipos
Tipos disponíveis
Todos os tipos podem ser importados de 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,
)Especificando tipos
Os tipos podem ser especificados de quatro maneiras:
| Método | Exemplo | Descrição |
|---|---|---|
constante ChdbType |
INT64, STRING |
Importada de chdb.sqltypes |
| string de tipo do ClickHouse | "Int64", "String" |
Nomes de tipos padrão do ClickHouse |
| string parametrizada | "DateTime('UTC')", "DateTime64(6)" |
Para tipos com parâmetros |
| tipo Python | int, str, float |
Passado diretamente em arg_types/return_type ou usado como anotação de tipo na assinatura da função |
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 * 2Inferência automática de tipos
Quando arg_types ou return_type é omitido, o chDB infere os tipos com base nas anotações de tipo do Python:
| Tipo Python | Tipo 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)Um tipo de retorno é sempre obrigatório: se return_type for omitido e a função não tiver uma anotação de retorno, o registro falhará. Em contrapartida, os tipos dos argumentos são opcionais — um parâmetro sem tipo explícito nem anotação aceita dinamicamente qualquer tipo de entrada compatível.
Tratamento de NULL
O parâmetro on_null controla o comportamento quando qualquer argumento de entrada é NULL.
| Valor | Comportamento |
|---|---|
"skip" (padrão) |
Retorna NULL imediatamente, sem chamar a função |
"pass" |
Converte NULL em None do Python e chama a função normalmente |
Você também pode usar o enum: chdb.NullHandling.SKIP / chdb.NullHandling.PASS.
Exemplo: default (pular)
@func(return_type="Int64")
def increment(x: int) -> int:
return x + 1
query("SELECT increment(NULL)") # NULL
query("SELECT increment(5)") # 6Exemplo: passar NULL como 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)") # 6Exemplo: vários argumentos
@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)") # 10Tratamento de exceções
O parâmetro on_error controla o comportamento quando a função Python lança uma exceção.
| Valor | Comportamento |
|---|---|
"propagate" (padrão) |
Propaga a exceção como um erro SQL |
"ignore" |
Captura a exceção e retorna NULL para essa linha |
Você também pode usar o enum: chdb.ExceptionHandling.PROPAGATE / chdb.ExceptionHandling.IGNORE.
Exemplo: default (propagar)
@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: ZeroDivisionErrorExemplo: ignorar erros
@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)") # NULLCombinando o tratamento de NULL e exceções
As opções on_null e on_error podem ser combinadas:
| on_null | on_error | Entrada NULL | Exceção |
|---|---|---|---|
"skip" |
"propagate" |
Retorna NULL | Gera um erro |
"skip" |
"ignore" |
Retorna NULL | Retorna NULL |
"pass" |
"propagate" |
Chamada com None |
Gera um erro |
"pass" |
"ignore" |
Chamada com None |
Retorna 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)Suporte a DateTime e fuso horário
As UDFs oferecem suporte completo a tipos de data e hora com suporte a fuso horário.
Tipo 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 com fusos horários
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 (alta precisão)
DATETIME64 tem escala 6 (microssegundos) por padrão:
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.000001Usando UDFs com sessões
As UDFs são registradas globalmente e ficam disponíveis em todas as sessões do mesmo processo:
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