Skip to main content
本指南介绍了如何编写 VDF 实现以及为 VillageSQL 扩展运行回归测试。它是 创建扩展 的补充,后者涵盖了端到端的构建步骤。
VEF 协议 3 自 v0.0.4 版本起已稳定。协议 4 正在开发中,并且仅通过选择加入的开发 ABI 标头提供 (-DVSQL_USE_DEV_ABI=ON)。使用旧协议 2 构建的扩展会被服务器拒绝,必须重新构建。
如果您正在为 VillageSQL 服务器本身做贡献(而不是构建扩展),请参阅 从源代码构建,其中涵盖了完整的服务器开发人员工作流程,包括使用 mysql-test-run.pl 直接运行测试。

设置您的环境

要开发和测试扩展,您需要一个已构建的 VillageSQL 服务器。请按照 从源代码克隆和构建 指南来编译服务器二进制文件。 构建完成后,使用 villagesql CLI 管理本地开发服务器实例。从安装 VillageSQL 的目录运行所有命令。

启动本地开发服务器

初始化并启动服务器实例:
要在初始化时设置 root 密码:
在任何命令之前传递 --dir <path> 以管理多个独立的实例,或者使用 --here 在当前工作目录中创建一个服务器目录:

管理扩展文件

在通过 SQL 安装扩展之前,其 .veb 文件必须存在于服务器上。CLI 管理服务器的 lib/veb/ 目录:
init 之前放置在 lib/veb/ 中的 .veb 文件会自动预置。添加文件后,通过 SQL 安装扩展:

编写扩展函数

扩展函数是用 C++ 编写的,并向 VEF 注册。包含一个标头以访问完整的 SDK:
类型化包装器为 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&, ResultWrapper),其中 ResultWrapperIntResultRealResultStringResultCustomResultCustomResultWith<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&)vef_vdf_clear_func_t
  • .accumulate<&fn>() 包装 void(State&, TypedArgs...)vef_vdf_accumulate_func_tTypedArgs 从函数签名中推断出来(IntArgStringArg 等)。
  • 结果类型 (IntResultRealResult 等)从结果函数签名中推断出来。
对于永不返回 NULL 的计数器,请使用纯状态类型:

每个语句的状态(Prerun 和 Postrun)

某些 VDF 需要跨单个查询触及的每一行都存在的状态——调用计数器、缓存结果、打开的资源。在 prerun 钩子中分配它,在 VDF 主体中访问它,并在 postrun 钩子中释放它。这两个钩子都为每个语句运行一次;VDF 主体为每一行运行一次。 使用 .prerun<&Hook>().postrun<&Hook>() 注册它们。所需的签名是: 原始 ABI 签名在编译时会被拒绝。使用 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) 以调整结果缓冲区的大小。

Varargs 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 参数上都是未定义的。

VEF_GENERATE_REGISTRATION

VEF_GENERATE_REGISTRATION 创建一个内部 _vef_do_register() 辅助函数,该函数执行扩展注册,但不定义 extern "C" 入口点。当您需要自定义 vef_register 行为时使用它——例如,在测试构建中修补描述符。对于常规扩展,请使用 VEF_GENERATE_ENTRY_POINTS

类型操作生成器

仅当您的扩展定义自定义列类型时才需要。如果您只是编写函数,请跳过到 运行回归测试 自定义类型需要引擎内部调用的三个操作: 编码(字符串到二进制)、解码(二进制到字符串)和比较。 哈希是可选的。使用以下 C++ 签名实现它们(所有签名都通过 <villagesql/vsql.h> 提供):

固定长度类型

使用 vsql::make_type<kTypeName>() 注册这些操作。类型名称 作为非类型模板参数 (NTTP) 传递——一个 static constexpr const char[] 数组。构建器会根据此 NTTP 自动生成 TYPE::method 格式的 VDF 名称 (例如,"MYTYPE::from_string"),因此无需手动进行字符串匹配。将构建的类型对象传递给扩展构建器中的 .type(); 无需为类型操作单独调用 .func()
类型名称必须是 static constexpr const char[] 变量——不能将字符串字面量用作非类型模板参数。直接传递 "MYTYPE" 会导致编译器错误,例如:
如以下所示,将名称声明为命名数组。
如果缺少 from_stringto_stringcomparebuild() 会在编译时失败。每个模板方法都会通过 static_assert 检查函数指针签名。

内建默认值

NOT NULL 自定义类型列在 IGNORE 模式下接收 NULL 时 (例如,INSERT IGNOREUPDATE IGNORE),服务器会调用默认值 以生成回退值,而不是引发错误。默认值 提供字符串表示形式;服务器使用类型的 from_string 函数将其转换为二进制形式。 字符串字面量:.intrinsic_default_str() 对于常量默认值,直接在类型构建器中传递字符串(如上述固定长度示例中的 .intrinsic_default_str("0"))。 基于 VDF:.intrinsic_default_vdf() + make_intrinsic_default 当默认值取决于类型参数时,针对以下签名之一实现一个函数(通过 <villagesql/vsql.h> 提供):
返回默认值的 std::string 表示形式。如果发生错误, 将消息写入 error_msg 并返回任何值(SDK 检查 error_msg[0] != '\0' 以检测错误)。使用 make_intrinsic_default<&fn>("vdf_name") 注册(一个参数:VDF 名称),并在类型构建器中使用 .intrinsic_default_vdf() 引用该名称。 以下参数化类型示例显示了完整的注册模式。

参数化类型

可变长度类型需要在编码、解码、比较和哈希时使用列声明的参数来确定分配大小和布局。 定义一个带有解析函数和反向 to_strings 函数的参数结构,并在类型构建器中使用 .params<P, &ParseFunc, &ToStringsFunc>() 注册两者,并将 const P& 作为类型操作函数的第一个 参数。SDK 会缓存每个唯一参数组合的解析结果,因此解析函数最多运行一次 每个类型实例化。to_strings 函数是 parse 的反函数: 它将类型化的 P 重新写入规范的键/值字符串形式,以便服务器可以以 parse 消耗的相同形式发布推断的参数。
在类型构建器上注册 .params<>()。使用 .int_to_params<&mytype_int_to_params_fn>() 来处理 MYTYPE(N) 整数语法,并使用 .resolve_params<&mytype_resolve_params_fn>() 来 验证参数并计算存储大小。使用 .max_persisted_length(N) 调用,其中 N 是 所有有效参数化中持久化字节大小的上限;服务器仅在类型参数推断路径上使用它,此时它尚未推断 参数,因此无法查阅 resolve_params 来确定编码缓冲区的大小。 对于基于 VDF 的默认值,使用 .intrinsic_default_vdf() 和 VDF 名称,并 通过 make_intrinsic_default<&mytype_default>() 单独注册 VDF。
参数化变体——TypeEncodeWithParamsFunc<P>TypeDecodeWithParamsFunc<P>TypeCompareWithParamsFunc<MyTypeParams>TypeHashWithParamsFunc<MyTypeParams>,以及 ParamsToStringsFunc<MyTypeParams> (void fn(const P&, std::map<std::string,std::string>&)) 可通过 <villagesql/vsql.h> 获得。 vsql::make_type 模板方法检测参数,并自动通过参数缓存。编码函数将 vsql::MaybeParams<MyTypeParams> & 作为第一个参数;is_known() 在运行时始终为 true,并且 value() 返回 const P&。解码、比较和哈希 变体采用 vsql::CustomArgWith<MyTypeParams>,其 params() 访问器返回 const P&

存储过程中的自定义类型

自定义扩展类型可以用作存储过程参数类型和 DECLARE 变量声明。服务器在例程执行时使用已安装扩展的类型元数据来解析自定义类型。

扩展系统变量

扩展系统变量是一个预览功能——请参阅 预览功能 以获取完整的 API 参考、工厂函数、SQL 访问和完整示例。

扩展状态变量

扩展状态变量是一个预览功能——请参阅 预览功能 以获取完整的 API 参考、工厂函数、SQL 访问和完整示例。

密钥环访问

密钥环访问是一个预览功能——请参阅 预览功能 以获取完整的 API 参考、结果代码和完整示例。

列存储

列存储是一个预览功能——请参阅 预览功能 以获取完整的 API 参考和完整示例。

检查扩展注册元数据

INFORMATION_SCHEMA.EXTENSION_REGISTRATION 将每个已加载扩展的内存 VEF 注册结构作为 JSON 文档公开。使用它来验证服务器是否已正确解析扩展的函数、类型和系统 变量,然后再 INSTALL EXTENSION

运行回归测试

使用 VillageSQL 构建目录中的 MySQL 测试运行器来运行扩展回归测试。

运行完整套件

要运行扩展的所有测试:

运行单个测试

要运行单个测试用例,请指定套件路径和测试名称:

创建新测试

在添加新功能或修复错误时,您应该添加相应的回归测试。

测试位置

扩展测试位于扩展自己的存储库中的 test/ 目录中——而不是在 VillageSQL 服务器的 mysql-test/suite/ 树中。
  • 测试文件以 .test 结尾,并位于 test/t/ 中。
  • 预期结果文件以 .result 结尾,并位于 test/r/ 中。
例如,对于名为 my_extension 的扩展:
  • test/t/my_new_test.test
  • test/r/my_new_test.result

测试文件约定

典型的扩展测试安装扩展、运行 SQL 并卸载:
当您的测试输出包含来自测试运行器的临时目录的路径时,请在 .test 文件中添加以下指令以对其进行规范化——否则,记录的结果将包含在其他机器上会出错的绝对路径:

添加测试的步骤

  1. 在扩展的 test/t/ 目录中创建 .test 文件。
  2. 在扩展的 test/r/ 目录中创建空的 .result 文件。
  3. 使用 --record 运行测试,以生成预期的输出:
  4. 验证生成的 .result 文件中的输出,以确保它与您的期望相符。

调试测试

如果测试失败,测试框架会提供详细的日志。
  • 测试输出: 检查 mysql-test/var/log/mysqltest.log(合并日志)或 mysql-test/var/log/<test_name>/(每个测试的目录)。
  • 服务器错误日志: 检查 mysql-test/var/log/mysqld.1.err。 VillageSQL 特定的日志消息(通过 LogVSQL() 发出)仅在服务器使用 --log-error-verbosity=3 运行时才会出现。
  • 差异: 框架会输出实际输出与预期的 .result 文件之间的差异。
要使用额外的调试信息运行测试:

另请参阅

  • 创建扩展 — 端到端构建步骤、CMake 设置和安装
  • 扩展 API 参考 — VDF 合约、空值处理和缓冲区大小
  • 扩展架构 — 生命周期、Victionary 缓存、性能模式和安全模型