跳到主要内容

Python Function API

你可以基于 Python 函数创建 Goose 用户自定义函数(UDF),并在 SQL 查询中使用它。 与常规函数类似,它也需要名称、返回类型和参数类型。

下面是一个调用第三方库的 Python 函数示例。

import goose
from goose.sqltypes import VARCHAR
from faker import Faker

def generate_random_name():
fake = Faker()
return fake.name()

goose.create_function("random_name", generate_random_name, [], VARCHAR)
res = goose.sql("SELECT random_name()").fetchall()
print(res)
[('Gerald Ashley',)]

创建函数

要注册 Python UDF,请使用 Goose 连接上的 create_function 方法。语法如下:

import goose
con = goose.connect()
con.create_function(name, function, parameters, return_type)

create_function 方法接受以下参数:

  1. name:字符串,表示该 UDF 在当前连接目录中的唯一名称。
  2. function:你要注册为 UDF 的 Python 函数。
  3. parameters:标量函数可以作用于一列或多列。该参数为输入列类型列表。
  4. return_type:标量函数每行返回一个元素。该参数指定函数返回类型。
  5. type(可选):Goose 同时支持原生 Python 类型和 PyArrow 数组。默认 type = 'native',你也可以指定 type = 'arrow' 来使用 PyArrow 数组。通常 Arrow UDF 比 native 更高效,因为可以按批处理。
  6. null_handling(可选):默认会自动使用 NULL-in NULL-out 处理 NULL 值。你可以设置 null_handling = 'special' 以指定 NULL 值的特殊处理行为。
  7. exception_handling(可选):默认情况下,Python 函数抛出的异常会在 Python 端重新抛出。你可以将此参数设置为 'return_null',改为返回 NULL
  8. side_effects(可选):默认认为函数在相同输入下产生相同输出。如果函数结果受随机性等因素影响,必须将 side_effects 设为 True

要注销 UDF,可以使用 UDF 名称调用 remove_function

con.remove_function(name)

使用 Partial 函数

Goose UDF 也可以通过 Python partial functions 创建。

在下面的示例中,自定义 logger 会返回拼接字符串:先是 ISO 格式的执行时间, 再依次拼接创建 UDF 时传入的参数和函数调用时的输入参数:

from datetime import datetime
import goose
import functools


def get_datetime_iso_format() -> str:
return datetime.now().isoformat()


def logger_udf(func, arg1: str, arg2: int) -> str:
return ' '.join([func(), arg1, str(arg2)])


with goose.connect() as con:
con.sql("select * from range(10) tbl(id)").to_table("example_table")

con.create_function(
'custom_logger',
functools.partial(logger_udf, get_datetime_iso_format, 'logging data')
)
rel = con.sql("SELECT custom_logger(id) from example_table;")
rel.show()

con.create_function(
'another_custom_logger',
functools.partial(logger_udf, get_datetime_iso_format, ':')
)
rel = con.sql("SELECT another_custom_logger(id) from example_table;")
rel.show()
┌───────────────────────────────────────────┐
│ custom_logger(id) │
│ varchar │
├───────────────────────────────────────────┤
│ 2025-03-27T12:07:56.811251 logging data 0 │
│ 2025-03-27T12:07:56.811264 logging data 1 │
│ 2025-03-27T12:07:56.811266 logging data 2 │
│ 2025-03-27T12:07:56.811268 logging data 3 │
│ 2025-03-27T12:07:56.811269 logging data 4 │
│ 2025-03-27T12:07:56.811270 logging data 5 │
│ 2025-03-27T12:07:56.811271 logging data 6 │
│ 2025-03-27T12:07:56.811272 logging data 7 │
│ 2025-03-27T12:07:56.811274 logging data 8 │
│ 2025-03-27T12:07:56.811275 logging data 9 │
├───────────────────────────────────────────┤
│ 10 rows │
└───────────────────────────────────────────┘

┌────────────────────────────────┐
│ another_custom_logger(id) │
│ varchar │
├────────────────────────────────┤
│ 2025-03-27T12:07:56.812106 : 0 │
│ 2025-03-27T12:07:56.812116 : 1 │
│ 2025-03-27T12:07:56.812118 : 2 │
│ 2025-03-27T12:07:56.812119 : 3 │
│ 2025-03-27T12:07:56.812121 : 4 │
│ 2025-03-27T12:07:56.812122 : 5 │
│ 2025-03-27T12:07:56.812123 : 6 │
│ 2025-03-27T12:07:56.812124 : 7 │
│ 2025-03-27T12:07:56.812126 : 8 │
│ 2025-03-27T12:07:56.812127 : 9 │
├────────────────────────────────┤
│ 10 rows │
└────────────────────────────────┘

类型注解

当函数带有类型注解时,通常可以省略全部可选参数。 通过 GoosePyType,我们可以将许多已知类型隐式转换为 Goose 类型系统。 例如:

import goose

def my_function(x: int) -> str:
return x

goose.create_function("my_func", my_function)
print(goose.sql("SELECT my_func(42)"))
┌─────────────┐
│ my_func(42) │
│ varchar │
├─────────────┤
│ 42 │
└─────────────┘

如果只能推断参数列表类型,你需要将 parameters 传为 None

NULL 处理

默认情况下,函数接收到 NULL 值时会立即返回 NULL,这是默认 NULL 处理行为的一部分。 如果不希望这样,需要显式将该参数设置为 "special"

import goose
from goose.sqltypes import BIGINT

def dont_intercept_null(x):
return 5

goose.create_function("dont_intercept", dont_intercept_null, [BIGINT], BIGINT)
res = goose.sql("SELECT dont_intercept(NULL)").fetchall()
print(res)
[(None,)]

使用 null_handling="special"

import goose
from goose.sqltypes import BIGINT

def dont_intercept_null(x):
return 5

goose.create_function("dont_intercept", dont_intercept_null, [BIGINT], BIGINT, null_handling="special")
res = goose.sql("SELECT dont_intercept(NULL)").fetchall()
print(res)
[(5,)]

当函数可能返回 NULL 时,请始终使用 null_handling="special"

import goose
from goose.sqltypes import VARCHAR


def return_str_or_none(x: str) -> str | None:
if not x:
return None

return x

goose.create_function(
"return_str_or_none",
return_str_or_none,
[VARCHAR],
VARCHAR,
null_handling="special"
)
res = goose.sql("SELECT return_str_or_none('')").fetchall()
print(res)
[(None,)]

异常处理

默认情况下,当 Python 函数抛出异常时,我们会透传(重新抛出)该异常。 如果你想禁用此行为并改为返回 NULL,需要将该参数设置为 "return_null"

import goose
from goose.sqltypes import BIGINT

def will_throw():
raise ValueError("ERROR")

goose.create_function("throws", will_throw, [], BIGINT)
try:
res = goose.sql("SELECT throws()").fetchall()
except goose.InvalidInputException as e:
print(e)

goose.create_function("doesnt_throw", will_throw, [], BIGINT, exception_handling="return_null")
res = goose.sql("SELECT doesnt_throw()").fetchall()
print(res)
Invalid Input Error: Python exception occurred while executing the UDF: ValueError: ERROR

At:
...(5): will_throw
...(9): <module>
[(None,)]

副作用

默认情况下,Goose 会假设创建的函数是纯函数,即相同输入产生相同输出。 如果你的函数不满足这个规则(例如使用了随机性),就需要将该函数标记为具有 side_effects

例如,下面这个函数每次调用都会产生一个新的计数值。

def count() -> int:
old = count.counter;
count.counter += 1
return old

count.counter = 0

如果创建该函数时不标记副作用,结果如下:

con = goose.connect()
con.create_function("my_counter", count, side_effects=False)
res = con.sql("SELECT my_counter() FROM range(10)").fetchall()
print(res)
[(0,), (0,), (0,), (0,), (0,), (0,), (0,), (0,), (0,), (0,)]

这显然不是期望结果。添加 side_effects=True 后,结果就会符合预期:

con.remove_function("my_counter")
count.counter = 0
con.create_function("my_counter", count, side_effects=True)
res = con.sql("SELECT my_counter() FROM range(10)").fetchall()
print(res)
[(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,), (8,), (9,)]

Python 函数类型

当前支持两种函数类型:native(默认)和 arrow

Arrow

如果函数期望接收 Arrow 数组,请将 type 参数设置为 'arrow'

这会通知系统向函数提供最多包含 STANDARD_VECTOR_SIZE 个元组的 Arrow 数组, 并期望函数返回包含相同数量元组的数组。

通常,Arrow UDF 会比 native 更高效,因为它可以按批处理。

import goose
import pyarrow as pa
from goose.sqltypes import VARCHAR
from pyarrow import compute as pc


def mirror(strings: pa.Array, sep: pa.Array) -> pa.Array:
assert isinstance(strings, pa.ChunkedArray)
assert isinstance(sep, pa.ChunkedArray)
return pc.binary_join_element_wise(strings, pc.ascii_reverse(strings), sep)


goose.create_function(
"mirror",
mirror,
[VARCHAR, VARCHAR],
return_type=VARCHAR,
type="arrow",
)

goose.sql(
"CREATE OR REPLACE TABLE strings AS SELECT 'hello' AS str UNION ALL SELECT 'world' AS str;"
)
print(goose.sql("SELECT mirror(str, '|') FROM strings;").fetchall())
[('hello|olleh',), ('world|dlrow',)]

Native

当函数类型设置为 native 时,函数每次只会收到一个元组,并仅返回一个值。 这对无法基于 Arrow 运行的 Python 库很有用,例如 faker

import goose

from goose.sqltypes import DATE
from faker import Faker

def random_date():
fake = Faker()
return fake.date_between()

goose.create_function(
"random_date",
random_date,
parameters=[],
return_type=DATE,
type="native",
)
res = goose.sql("SELECT random_date()").fetchall()
print(res)
[(datetime.date(2019, 5, 15),)]