编写扩展函数
扩展函数是用 C++ 编写的,并向 VEF 注册。包含一个标头以访问 VDF、类型和扩展生成器 API:异常安全
控制异常是扩展的责任。SDK 不会将您的实现包装在try/catch 中,服务器也不会,因此从入口点逃逸的异常会跨越 C ABI 边界展开,在其之上没有任何处理程序,服务器进程会终止。普通的 SQL 输入就会触发这一点:std::stoi() 在遇到非数字字符串时会抛出异常,而 nlohmann::json::dump() 在遇到非有效 UTF-8 的文本时会抛出异常。
请包装您注册的每一个入口点的主体:VDF、聚合函数、prerun 和 postrun 钩子,以及自定义类型的各项操作。
const std::exception & 之外,还要捕获 ...:依赖库可能会抛出一个不派生自 std::exception 的类型,而那种异常仍然会终止服务器。在 warning() 和 error() 之间的选择依据与其他任何失败相同——warning() 用于语句应当继续执行的错误输入,error() 用于继续操作不安全的情况。
参数和结果类型
VDF 参数和结果作为类型安全的参数和结果类型传递。框架会从您的函数签名中检测它们并自动进行调整——make_func 注册语法保持不变。
参数类型: IntArg、RealArg、StringArg、CustomArg——每个都提供 is_null() 和 value()。对于参数化的自定义类型,CustomArgWith<P> 添加了一个 params() 访问器,该访问器返回缓存的解析参数结构(请参阅 参数化类型)。
结果类型: IntResult、RealResult、StringResult、CustomResult——每个都提供 set_null()、warning(msg) 和 error(msg)。标量结果还提供 set(value)。缓冲区结果提供 buffer() 和 set_length(len)。StringResult 此外还提供 set(std::string_view),它从视图中复制最多 buffer().size() 个字节,并在一次调用中设置长度。对于参数化的自定义类型,CustomResultWith<P> 添加了一个 params() 访问器。
Span 类型: 面向字节的参数和结果类型上的 value() 和 buffer() 返回一个 vsql::Span<T>——一个非所有权视图,用于连续的 T 序列,具有 data()、size()、empty()、begin()/end() 和 operator[]。在 C++20 中,它是 std::span<T> 的别名;在 C++17 中,此 SDK 提供了一个最小的兼容实现,因此相同的代码可以在任何标准中编译。它通过 <villagesql/vsql.h> 提供。
warning(msg) 会为该行返回 SQL NULL,并附加一个 SQL 警告。从服务器的角度来看,该 NULL 只是一个普通值,因此它仅会像任何普通 NULL 一样变成语句错误:在 STRICT_TRANS_TABLES 下将其写入 NOT NULL 列会失败,这与 INSERT INTO t VALUES (NULL) 相同。将其写入可空列则会成功,并附带警告。请将 warning(msg) 用于可恢复的错误输入,例如编码函数中无法解析的字符串。对于已损坏的存储数据或任何继续操作不安全的情况,请使用 error(msg)。两种情况下的消息都会被截断以适应服务器的内部错误缓冲区(如果需要)。
标量示例——将两个整数相加:
StringResult 和 CustomResult,写入 buffer(),然后调用 set_length(),其中包含写入的字节数。buffer().size() 是最大容量。
对于返回自定义类型的 VDF (.returns(MYTYPE)),服务器会自动调整结果缓冲区的大小以匹配解析的返回类型的 persisted_length——扩展作者在此情况下不需要在函数生成器上声明 .buffer_size(...)。如果 prerun 进一步增大缓冲区,则会保留更大的大小。这使得,例如,SVECTOR::from_string('[…1024 floats…]') 可以在不使结果缓冲区耗尽空间的情况下编码一个大型向量。
您可以在同一扩展中的不同函数中使用不同的样式——每个函数的样式由其自身的签名决定。
聚合 VDF
聚合 VDF 在每个GROUP BY 组内的行中累积状态,并为每个组返回单个结果,类似于 SQL SUM 或 COUNT。使用 make_aggregate_func<State, &result_fn>("name") 注册一个。State 类型是每个组的累积缓冲区;prerun 和 postrun 是自动生成的,用于分配和删除它。
结果函数必须具有签名 void(const State&, ResultType),其中 ResultType 是 IntResult、RealResult、StringResult、CustomResult 或 CustomResultWith<P> 中的一个。调用 out.set(value) 以返回一个值,或者调用 out.set_null() 以返回 SQL NULL。
.clear<>() 和 .accumulate<>() 都是必需的。生成器在编译时(通过 build())强制执行这一点,并且服务器在 INSTALL EXTENSION 时再次验证它——clear 重置状态,accumulate 折叠行,结果函数读取最终状态。
make_aggregate_func<State, &result_fn>()自动生成prerun和postrun(值初始化并删除State)。.clear<&fn>()注册您的void(State&)重置函数。.accumulate<&fn>()注册您的void(State&, TypedArgs...)折叠函数。TypedArgs从函数签名中推断出来(IntArg、StringArg等)。请在所有.param(TYPE)调用之后再调用.accumulate<>():生成器会在编译时将类型化参数的数量与声明的 SQL 参数数量进行核对,因此提前调用会导致构建失败。- 结果类型 (
IntResult、RealResult等)从结果函数签名中推断出来。
StringResult 聚合 VDF 返回文本:结果报告 utf8mb4_bin 字符集和排序规则,因此客户端将其显示为字符而不是十六进制——与标量 VDF STRING 路径相同。它还以相同的方式支持 .max_result_length(n)——这需要选择加入的开发 ABI 标头 (-DVSQL_USE_DEV_ABI=ON) 和协议 4——调整物化聚合结果(GROUP BY/DISTINCT 临时表、CREATE TABLE ... SELECT 或 UNION)的大小,使其不会在参数宽度处被截断。有关调整大小的规则和上限,请参阅 自定义缓冲区大小。
每个语句的状态(Prerun 和 Postrun)
某些 VDF 需要跨单个查询触及的每一行都存在的状态——调用计数器、缓存结果、打开的资源。在 prerun 钩子中分配它,在 VDF 主体中访问它,并在 postrun 钩子中释放它。这两个钩子都为每个语句运行一次;VDF 主体为每一行运行一次。 使用.prerun<&Hook>() 和 .postrun<&Hook>() 注册它们。所需的签名是:
使用
PrerunResult::set_user_data(void*) 来存储状态;使用 PostrunArgs::delete_state<T>() 来释放它。如果 prerun 调用 set_user_data(new T{}),则 postrun 必须调用 delete_state<T>()——SDK 不会自动释放。
PrerunArgs::type_at(i) 公开了在读取任何行之前声明的每个参数的 SQL 类型;返回的 PrerunArgType 上的谓词 is_int()、is_real()、is_str()、is_custom() 镜像列类型。在 prerun 中使用它来验证参数类型或调用 PrerunResult::request_buffer_size(n) 以调整结果缓冲区的大小。
可变参数 VDF
varargs VDF 接受任意数量的任何 SQL 类型的参数。使用.varargs() 在函数生成器上声明一个,这与 .no_params() 和 .param(TYPE) 互斥。主体接收一个 vsql::VarArgs 参数,而不是通常的固定参数个数的参数类型。
框架无法验证 varargs VDF 的参数计数或类型。将每个 varargs 注册与 prerun 钩子配对,该钩子在输入无效时调用 PrerunResult::error(),或者调用 PrerunResult::request_buffer_size(n) 以调整结果缓冲区的大小。
使用 range-for 循环遍历参数。每个 AnyArg 元素都需要在读取其值之前进行类型检查:
在任何访问器之前检查
is_null()——所有四个访问器在 null 参数上都是未定义的。
PrerunArgType::custom_type() 以 std::string_view 形式返回参数的自定义类型名称——对于非自定义参数则为空。单独使用 is_custom() 会接受服务器上注册的每一个自定义类型;将它与 custom_type() 比较搭配使用,可以将可变参数调用收窄到某一个特定类型:
在解析时决定函数的类型参数
服务器的内置规则通常能够根据参数声明的类型推算出返回类型的参数。但有时它们做不到。例如,tvector_concat(TVECTOR(M), TVECTOR(N)) 返回一个 TVECTOR(M+N)。内置规则会拒绝两个维度不同的 TVECTOR 参数,并且它们可以将某个参数的参数复制到返回类型,但永远不会把它们相加。对于这类情况,请在函数生成器上使用 .bind_and_check_types<&Hook>() 注册一个绑定并检查类型钩子,其签名为:
.bind_and_check_types() 和 .varargs() 在同一个函数上互斥——可变参数 VDF 没有固定的参数列表供该钩子推理。在生成器链中先声明 .varargs() 再声明 .bind_and_check_types<&Hook>() 是编译时 static_assert 失败。以相反的顺序声明它们可以编译,但服务器加载该扩展时会拒绝它:注册过程会遍历每个函数最终构建出的状态,并拒绝第一个既是可变参数又带有绑定钩子的函数,错误为:
vsql_tvector 示例扩展在 tvector_concat 上注册了这样一个钩子:
vsql_tvector;发行版压缩包则不包含。
扩展加载和卸载钩子
扩展有时需要在每次加载时执行一些工作,而不是在每次调用时执行——例如选择针对特定 CPU 的实现、一次性读取自己的配置,或者构建一个供 VDF 随后读取的查找表。使用扩展生成器上的.on_init<&Fn>() 注册该工作,并使用 .on_deinit<&Fn>() 注册与之匹配的清理工作。两者都接受一个签名为 void() 的函数,并且必须一起注册——只声明其中一个而不声明另一个会导致编译失败。以其他方式带着不匹配的一对到达服务器的扩展则会在加载时被拒绝:
on_init 在每次加载时运行一次——在 INSTALL EXTENSION 时,以及在每次服务器启动时——时机是在服务器验证该扩展并填充其所需功能之后,因此已注册的系统变量此时已经持有其配置值。特别是在启动时,on_init 会推迟到持久化的系统变量值应用完毕之后,因此它绝不会看到编译时默认值取代了通过 SET PERSIST 设置的值。对于被服务器拒绝的扩展,它绝不会运行。on_deinit 在卸载时运行——UNINSTALL EXTENSION 或服务器关闭——时机是在服务器清空这些功能之前,因此它看到的实时状态与 on_init 看到的相同。
INSTALL EXTENSION my_extension 之后,SELECT fast_sqrt(16) 返回 4——只有在 build_table 已经运行的情况下才能得到这个结果。
VEF_GENERATE_REGISTRATION
VEF_GENERATE_REGISTRATION 创建一个内部 _vef_do_register() 辅助函数,该函数执行扩展注册,但不定义 extern "C" 入口点。当您需要自定义 vef_register 行为时使用它——例如,在测试构建中注册后修补描述符。对于常规扩展,请改用 VEF_GENERATE_ENTRY_POINTS。
自定义类型操作
有关完整的类型操作生成器参考——编码、解码、比较、哈希、内建默认值和参数化类型——请参阅 类型操作。预览功能
以下 VEF 功能作为选择加入的预览标头提供。ABI 和 API 仍在积极开发中;有关完整参考,请参阅 预览功能。- 扩展系统变量——预览功能 → 系统变量
- 扩展状态变量——预览功能 → 状态变量
- 密钥环访问——预览功能 → 密钥环访问
- 列存储——预览功能 → 列存储
检查扩展注册元数据
INFORMATION_SCHEMA.EXTENSION_REGISTRATION 将每个已加载扩展的内存 VEF 注册结构作为 JSON 文档公开。使用它来验证服务器在 INSTALL EXTENSION 之后是否已正确解析扩展的函数和类型。
另请参阅
- 使用 C++ 创建扩展 — 端到端构建步骤、CMake 设置和安装
- C++ 测试 — 本地开发服务器、MTR 以及调试失败
- 类型操作 — 编码、解码、比较、哈希、参数化类型
- C++ API 参考 — VDF 合约、空值处理和缓冲区大小
- 扩展架构 — 生命周期、Victionary 缓存、性能模式和安全模型

