Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

Funções definidas pelo usuário (UDFs) em Python

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)  # 5

Mé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)")  # 42

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

Sistema 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 * 2

Inferê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)")     # 6

Exemplo: 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)")     # 6

Exemplo: 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)")       # 10

Tratamento 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: ZeroDivisionError

Exemplo: 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)")   # NULL

Combinando 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'))")  # 2024

DateTime 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:00

DateTime64 (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.000001

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