Skip to main content
本指南是编写 C++ VDF 实现的深度参考。它是 使用 C++ 创建扩展 的补充,后者涵盖端到端的构建步骤;也是 C++ 测试 的补充,后者涵盖测试与迭代循环。
VEF 协议 3 自 v0.0.4 版本起已稳定。协议 4 正在开发中,并且仅通过选择加入的开发 ABI 标头提供 (-DVSQL_USE_DEV_ABI=ON)。使用旧协议 2 构建的扩展会被服务器拒绝,必须重新构建。

编写扩展函数

扩展函数是用 C++ 编写的,并向 VEF 注册。包含一个标头以访问完整的 SDK:

异常安全

控制异常是扩展的责任。SDK 不会将您的实现包装在 try/catch 中,服务器也不会,因此从入口点逃逸的异常会跨越 C ABI 边界展开,在其之上没有任何处理程序,服务器进程会终止。普通的 SQL 输入就会触发这一点:std::stoi() 在遇到非数字字符串时会抛出异常,而 nlohmann::json::dump() 在遇到非有效 UTF-8 的文本时会抛出异常。 请包装您注册的每一个入口点的主体:VDF、聚合结果函数,以及自定义类型的 from_string/to_string 操作。
除了 const std::exception & 之外,还要捕获 ...:依赖库可能会抛出一个不派生自 std::exception 的类型,而那种异常仍然会终止服务器。在 warning()error() 之间的选择依据与其他任何失败相同——warning() 用于语句应当继续执行的错误输入,error() 用于继续操作不安全的情况。

参数和结果类型

VDF 参数和结果作为类型安全的参数和结果类型传递。框架会从您的函数签名中检测它们并自动进行调整——make_func 注册语法保持不变。 参数类型: IntArgRealArgStringArgCustomArg——每个都提供 is_null()value()。对于参数化的自定义类型,CustomArgWith<P> 添加了一个 params() 访问器,该访问器返回缓存的解析参数结构(请参阅 参数化类型)。 结果类型: IntResultRealResultStringResultCustomResult——每个都提供 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 警告。在严格模式 (STRICT_TRANS_TABLES) 下,MySQL 会将其升级为 INSERT/UPDATE 上的语句错误,因此它在严格上下文中表现得像 error(msg)。将其用于可恢复的错误输入,例如编码函数中的无法解析的字符串。对于已损坏的存储数据或任何继续操作不安全的情况,请使用 error(msg)。两种情况下的消息都会被截断以适应服务器的内部错误缓冲区(如果需要)。 标量示例——将两个整数相加:
二进制示例——就地转换自定义类型缓冲区:
对于 StringResultCustomResult,写入 buffer(),然后调用 set_length(),其中包含写入的字节数。buffer().size() 是最大容量。 对于返回自定义类型的 VDF (returns(CUSTOM(MYTYPE))),服务器会自动调整结果缓冲区的大小以匹配解析的返回类型的 persisted_length——扩展作者在此情况下不需要在函数生成器上声明 .buffer_size(...)。如果 prerun 进一步增大缓冲区,则会保留更大的大小。这使得,例如,SVECTOR::from_string('[…1024 floats…]') 可以在不使结果缓冲区耗尽空间的情况下编码一个大型向量。 您可以在同一扩展中的不同函数中使用不同的样式——每个函数的样式由其自身的签名决定。

聚合 VDF

聚合 VDF 在每个 GROUP BY 组内的行中累积状态,并为每个组返回单个结果,类似于 SQL SUMCOUNT。使用 make_aggregate_func<State, &result_fn>("name") 注册一个。State 类型是每个组的累积缓冲区;prerunpostrun 是自动生成的,用于分配和删除它。 结果函数必须具有签名 void(const State&, ResultType),其中 ResultTypeIntResultRealResultStringResultCustomResultCustomResultWith<P> 中的一个。调用 out.set(value) 以返回一个值,或者调用 out.set_null() 以返回 SQL NULL。 .clear<>().accumulate<>() 都是必需的。生成器在编译时(通过 build())强制执行这一点,并且服务器在 INSTALL EXTENSION 时再次验证它——clear 重置状态,accumulate 折叠行,结果函数读取最终状态。
生成器方法的工作方式:
  • make_aggregate_func<State, &result_fn>() 自动生成 prerunpostrun(值初始化并删除 State)。
  • .clear<&fn>() 注册您的 void(State&) 重置函数。
  • .accumulate<&fn>() 注册您的 void(State&, TypedArgs...) 折叠函数。TypedArgs 从函数签名中推断出来(IntArgStringArg 等)。
  • 结果类型 (IntResultRealResult 等)从结果函数签名中推断出来。
对于永不返回 NULL 的计数器,请使用纯状态类型:
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 注册需要 VEF 协议 3。旧服务器会在安装时拒绝该扩展。
框架无法验证 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() 比较搭配使用,可以将可变参数调用收窄到某一个特定类型:

扩展加载和卸载钩子

扩展有时需要在每次加载时执行一些工作,而不是在每次调用时执行——例如选择针对特定 CPU 的实现,或者构建一个供 VDF 随后读取的查找表。使用扩展生成器上的 .on_init<&Fn>() 注册该工作,并使用 .on_deinit<&Fn>() 注册与之匹配的清理工作。两者都接受一个签名为 void() 的函数。
.on_init<&Fn>().on_deinit<&Fn>() 仅在选择加入的开发 ABI 标头中声明 (-DVSQL_USE_DEV_ABI=ON)。默认的稳定 ABI 构建没有这两个方法,因此注册钩子会导致编译失败。
on_init 在每次扩展加载时运行:在 INSTALL EXTENSION 时,以及在每次服务器启动时——都是在服务器验证并接受该扩展之后。对于被服务器拒绝的扩展,它绝不会运行。on_deinit 在卸载时运行——UNINSTALL EXTENSION 或服务器关闭。 这两个钩子都在扩展进程内运行,且无法访问服务器:它们不能运行 SQL、读取系统变量或访问服务器状态。因此,对于必须与服务器通信的初始化工作,它们不是合适的位置——请改用预览功能的 populate 步骤。
INSTALL EXTENSION my_extension 之后,SELECT fast_sqrt(16) 返回 4——只有在 build_table 已经运行的情况下才能得到这个结果。
on_deinit 是从 VEF_GENERATE_ENTRY_POINTS 生成的 vef_unregister 入口点调用的。VEF_GENERATE_REGISTRATION 不定义该入口点,因此使用它的扩展必须从其自行编写的 vef_unregister 中调用自己的清理函数。

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 之后是否已正确解析扩展的函数、类型和系统变量。

另请参阅