> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 开发

> 编写扩展函数并为您的 VillageSQL 扩展运行测试。

本指南介绍了如何编写 VDF 实现以及为 VillageSQL 扩展运行回归测试。它是 [创建扩展](/docs/zh/mysql-8.4/0.0.4/create) 的补充，后者涵盖了端到端的构建步骤。

<Warning>
  VEF 协议 3 自 v0.0.4 版本起已稳定。协议 4 正在开发中，并且仅通过选择加入的开发 ABI 标头提供 (`-DVSQL_USE_DEV_ABI=ON`)。使用旧协议 2 构建的扩展会被服务器拒绝，必须重新构建。
</Warning>

<Note>
  如果您正在为 VillageSQL 服务器本身做贡献（而不是构建扩展），请参阅 [从源代码构建](/docs/zh/mysql-8.4/0.0.4/source)，其中涵盖了完整的服务器开发人员工作流程，包括使用 `mysql-test-run.pl` 直接运行测试。
</Note>

## 设置您的环境

要开发和测试扩展，您需要一个已构建的 VillageSQL 服务器。请按照 [从源代码克隆和构建](/docs/zh/mysql-8.4/0.0.4/source) 指南来编译服务器二进制文件。

构建完成后，使用 `villagesql` CLI 管理本地开发服务器实例。从安装 VillageSQL 的目录运行所有命令。

### 启动本地开发服务器

初始化并启动服务器实例：

```bash theme={null}
./villagesql init    # initialize database and seed bundled extensions
./villagesql start   # start the server (default port 3307)
./villagesql status  # check the server is running
./villagesql connect # open a mysql shell
./villagesql stop    # stop the server
```

要在初始化时设置 root 密码：

```bash theme={null}
./villagesql init --password
./villagesql start
```

在任何命令之前传递 `--dir <path>` 以管理多个独立的实例，或者使用 `--here` 在当前工作目录中创建一个服务器目录：

```bash theme={null}
./villagesql --here init
./villagesql --here start
```

### 管理扩展文件

在通过 SQL 安装扩展之前，其 `.veb` 文件必须存在于服务器上。CLI 管理服务器的 `lib/veb/` 目录：

```bash theme={null}
./villagesql veb add /path/to/my_extension.veb  # copy a .veb to the server
./villagesql veb ls                              # list available .veb files
./villagesql veb rm my_extension                # remove a .veb file
```

在 `init` 之前放置在 `lib/veb/` 中的 `.veb` 文件会自动预置。添加文件后，通过 SQL 安装扩展：

```sql theme={null}
INSTALL EXTENSION my_extension;
```

## 编写扩展函数

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

```cpp theme={null}
#include <villagesql/vsql.h>
```

<h3 id="typed-wrappers-recommended">
  类型化包装器（推荐）
</h3>

类型化包装器为 VDF 参数和结果提供类型安全接口。
框架检测函数签名中的包装器类型并自动进行调整——`make_func` 注册语法保持不变。

**输入包装器：** `IntArg`、`RealArg`、`StringArg`、`CustomArg`——每个包装器都提供 `is_null()` 和 `value()`。对于参数化的自定义类型，`CustomArgWith<P>` 添加了一个 `params()` 访问器，该访问器返回缓存的解析参数结构（请参阅 [参数化类型](#parameterized-types)）。

**结果包装器：** `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 警告。在严格模式 (`STRICT_TRANS_TABLES`) 下，MySQL 会将其升级为 INSERT/UPDATE 上的语句错误，因此它在严格上下文中表现得像 `error(msg)`。将其用于可恢复的错误输入，例如编码函数中的无法解析的字符串。对于已损坏的存储数据或任何继续操作不安全的情况，请使用 `error(msg)`。两种情况下的消息都会被截断以适应服务器的内部错误缓冲区（如果需要）。

**标量示例**——将两个整数相加：

```cpp theme={null}
using namespace vsql;

void add_impl(IntArg a, IntArg b, IntResult out) {
  if (a.is_null() || b.is_null()) { out.set_null(); return; }
  out.set(a.value() + b.value());
}

// Registration is unchanged:
make_func<&add_impl>("add").returns(INT).param(INT).param(INT).build();
```

**二进制示例**——就地转换自定义类型缓冲区：

```cpp theme={null}
using namespace vsql;

void rot13_impl(CustomArg in, CustomResult out) {
  if (in.is_null()) { out.set_null(); return; }
  auto src = in.value();   // vsql::Span<const unsigned char>
  auto dst = out.buffer(); // vsql::Span<unsigned char>
  for (size_t i = 0; i < src.size(); i++) { dst[i] = transform(src[i]); }
  out.set_length(src.size());
}
```

对于 `StringResult` 和 `CustomResult`，写入 `buffer()`，然后调用 `set_length()`，其中包含写入的字节数。`buffer().size()` 是最大容量。

对于返回自定义类型的 VDF (`returns(CUSTOM(MYTYPE))`)，服务器会自动调整结果缓冲区的大小以匹配解析的返回类型的 `persisted_length`——扩展作者不需要在此情况下在函数生成器上声明 `.buffer_size(...)`。如果 `prerun` 进一步增大缓冲区，则会保留更大的大小。这使得，例如，`SVECTOR::from_string('[…1024 floats…]')` 可以在不使结果包装器耗尽空间的情况下编码一个大型向量。

您可以在同一扩展中的不同函数中使用不同的样式——每个函数的样式由其自身的签名决定。

<h3 id="aggregate-vdfs">
  聚合 VDF
</h3>

聚合 VDF 在每个 `GROUP BY` 组内的行中累积状态，并为每个组返回单个结果，类似于 SQL `SUM` 或 `COUNT`。使用 `make_aggregate_func<State, &result_fn>("name")` 注册一个。`State` 类型是每个组的累积缓冲区；`prerun` 和 `postrun` 是自动生成的，用于分配和删除它。

结果函数必须具有签名 `void(const State&, ResultWrapper)`，其中 `ResultWrapper` 是 `IntResult`、`RealResult`、`StringResult`、`CustomResult` 或 `CustomResultWith<P>` 中的一个。调用 `out.set(value)` 以返回一个值，或者调用 `out.set_null()` 以返回 SQL NULL。

`clear<>()` 和 `accumulate<>()` 都是必需的。生成器在编译时（通过 `build()`）强制执行这一点，并且服务器在 `INSTALL EXTENSION` 时再次验证它——`clear` 重置状态，`accumulate` 折叠行，结果函数读取最终状态。

```cpp theme={null}
#include <villagesql/vsql.h>
#include <optional>

using namespace vsql;

// State type: nullopt means no non-NULL rows seen yet.
using SumState = std::optional<long long>;

void my_clear(SumState &s) { s = std::nullopt; }
void my_acc(SumState &s, IntArg v) {
  if (!v.is_null()) s = s.value_or(0) + v.value();
}
void my_result(const SumState &s, IntResult out) {
  if (!s.has_value()) { out.set_null(); return; }
  out.set(s.value());
}

// Registration:
// make_aggregate_func<SumState, &my_result>("my_sum")
//     .returns(INT)
//     .param(INT)
//     .clear<&my_clear>()
//     .accumulate<&my_acc>()
//     .build()
```

生成器方法的工作方式：

* `make_aggregate_func<State, &result_fn>()` 自动生成 `prerun` 和 `postrun`（值初始化并删除 `State`）。
* `.clear<&fn>()` 包装 `void(State&)` → `vef_vdf_clear_func_t`
* `.accumulate<&fn>()` 包装 `void(State&, TypedArgs...)` → `vef_vdf_accumulate_func_t`。`TypedArgs` 从函数签名中推断出来（`IntArg`、`StringArg` 等）。
* 结果类型 (`IntResult`、`RealResult` 等)从结果函数签名中推断出来。

对于永不返回 NULL 的计数器，请使用纯状态类型：

```cpp theme={null}
using CountState = long long;
void count_clear(CountState &s) { s = 0; }
void count_acc(CountState &s, IntArg v) { if (!v.is_null()) s++; }
void count_result(const CountState &s, IntResult out) { out.set(s); }
```

<h3 id="per-statement-state-prerun-and-postrun">
  每个语句的状态（Prerun 和 Postrun）
</h3>

某些 VDF 需要跨单个查询触及的每一行都存在的状态——调用计数器、缓存结果、打开的资源。在 **prerun** 钩子中分配它，在 VDF 主体中访问它，并在 **postrun** 钩子中释放它。这两个钩子都为每个语句运行一次；VDF 主体为每一行运行一次。

使用 `.prerun<&Hook>()` 和 `.postrun<&Hook>()` 注册它们。所需的签名是：

| 钩子      | 所需签名                                         |
| ------- | -------------------------------------------- |
| Prerun  | `void(vsql::PrerunArgs, vsql::PrerunResult)` |
| Postrun | `void(vsql::PostrunArgs)`                    |

原始 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)` 以调整结果缓冲区的大小。

```cpp theme={null}
#include <villagesql/vsql.h>
using namespace vsql;

struct CallCounter { long long n = 0; };

void ba_call_index_prerun(PrerunArgs, PrerunResult out) {
  out.set_user_data(new CallCounter{});
}

void ba_call_index(CallCounter &state, IntResult out) {
  state.n++;
  out.set(state.n);
}

void ba_call_index_postrun(PostrunArgs args) {
  args.delete_state<CallCounter>();
}

// Registration:
// make_func<&ba_call_index>("ba_call_index")
//     .returns(INT).no_params()
//     .prerun<&ba_call_index_prerun>()
//     .postrun<&ba_call_index_postrun>()
//     .build()
```

### Varargs VDF

**varargs** VDF 接受任意数量的任何 SQL 类型的参数。使用 `.varargs()` 在函数生成器上声明一个，这与 `.no_params()` 和 `.param(TYPE)` 互斥。主体接收一个 `vsql::VarArgs` 参数，而不是通常的固定参数包装器。

<Warning>
  Varargs 注册需要 VEF 协议 3。旧服务器会在安装时拒绝该扩展。
</Warning>

框架无法验证 varargs VDF 的参数计数或类型。将每个 varargs 注册与 prerun 钩子配对，该钩子在输入无效时调用 `PrerunResult::error()`，或者调用 `PrerunResult::request_buffer_size(n)` 以调整结果缓冲区的大小。

使用 range-for 循环遍历参数。每个 `AnyArg` 元素都需要在读取其值之前进行类型检查：

| 谓词            | 访问器           | 返回类型                              |
| ------------- | ------------- | --------------------------------- |
| `is_int()`    | `as_int()`    | `long long`                       |
| `is_real()`   | `as_real()`   | `double`                          |
| `is_str()`    | `as_str()`    | `std::string_view`                |
| `is_custom()` | `as_custom()` | `vsql::Span<const unsigned char>` |

在任何访问器之前检查 `is_null()`——所有四个访问器在 null 参数上都是未定义的。

```cpp theme={null}
#include <villagesql/vsql.h>
#include <cstring>
using namespace vsql;

constexpr size_t kBytearrayLen = 4;

void ba_concat_all_prerun(PrerunArgs args, PrerunResult out) {
  if (args.size() == 0) {
    out.error("ba_concat_all requires at least one argument");
    return;
  }
  for (size_t i = 0; i < args.size(); i++) {
    auto t = args.type_at(i);
    if (!t.is_custom() && !t.is_str()) {
      out.error("ba_concat_all: argument " + std::to_string(i) +
                " must be BYTEARRAY");
      return;
    }
  }
  out.request_buffer_size(args.size() * kBytearrayLen);
}

void ba_concat_all(VarArgs args, StringResult out) {
  auto dst = out.buffer();
  size_t off = 0;
  for (auto a : args) {
    if (a.is_null() || !a.is_custom()) { out.set_null(); return; }
    auto bytes = a.as_custom();
    std::memcpy(dst.data() + off, bytes.data(), bytes.size());
    off += bytes.size();
  }
  out.set_length(off);
}

// Registration:
// make_func<&ba_concat_all>("ba_concat_all")
//     .returns(STRING).varargs()
//     .prerun<&ba_concat_all_prerun>()
//     .build()
```

### VEF\_GENERATE\_REGISTRATION

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

```cpp theme={null}
VEF_GENERATE_REGISTRATION(
    make_extension()
        .func(make_func<&my_impl>("my_func").returns(INT).build()))

// Then define your own extern "C" vef_register/vef_unregister that call
// _vef_do_register() and optionally modify the result.
```

<h3 id="type-operation-builders">
  类型操作生成器
</h3>

*仅当您的扩展定义自定义列类型时才需要。如果您只是编写函数，请跳过到 [运行回归测试](#running-regression-tests)。*

自定义类型需要引擎内部调用的三个操作：
编码（字符串到二进制）、解码（二进制到字符串）和比较。
哈希是可选的。使用以下 C++ 签名实现它们（所有签名都通过 `<villagesql/vsql.h>` 提供）：

#### 固定长度类型

```cpp theme={null}
// Encode: string -> binary. Write the encoded bytes via out.buffer() and
// out.set_length(n); call out.set_null() for SQL NULL, out.warning(msg) for
// recoverable bad input, or out.error(msg) to abort the statement. Returning
// without calling any of these surfaces a default warning.
using TypeEncodeFunc = void (*)(std::string_view from, vsql::CustomResult out);

// Decode: binary -> string. Report the outcome by calling
// out.set_length(n), out.set(sv), out.set_null(), out.warning(msg), or
// out.error(msg). If none is called the wrapper falls back to a default
// "failed to decode value" ERROR.
using TypeDecodeFunc = void (*)(vsql::CustomArg in, vsql::StringResult out);

// Compare: returns -1, 0, or 1 (used for ORDER BY and indexes).
using TypeCompareFunc = int (*)(vsql::CustomArg a, vsql::CustomArg b);

// Hash: returns hash code (used for hash joins).
using TypeHashFunc = size_t (*)(vsql::CustomArg in);
```

使用 `vsql::make_type<kTypeName>()` 注册这些操作。类型名称
作为非类型模板参数 (NTTP) 传递——一个 `static constexpr const char[]`
数组。构建器会根据此 NTTP 自动生成 `TYPE::method` 格式的 VDF 名称
（例如，`"MYTYPE::from_string"`），因此无需手动进行字符串匹配。将构建的类型对象传递给扩展构建器中的 `.type()`；
无需为类型操作单独调用 `.func()`。

<Warning>
  类型名称必须是 `static constexpr const char[]` 变量——不能将字符串字面量用作非类型模板参数。直接传递 `"MYTYPE"` 会导致编译器错误，例如：

  ```
  error: '"MYTYPE"' is not a valid template argument for type 'const char*'
  ```

  如以下所示，将名称声明为命名数组。
</Warning>

```cpp theme={null}
#include <villagesql/vsql.h>

using namespace vsql;

static constexpr const char kMyTypeName[] = "MYTYPE";

constexpr auto MYTYPE =
    vsql::make_type<kMyTypeName>()
        .persisted_length(8)
        .max_decode_buffer_length(64)
        .from_string<&my_encode>()   // auto: "MYTYPE::from_string"
        .to_string<&my_decode>()     // auto: "MYTYPE::to_string"
        .compare<&my_compare>()      // auto: "MYTYPE::compare"
        .hash<&my_hash>()            // optional, auto: "MYTYPE::hash"
        .intrinsic_default_str("0")  // string-literal intrinsic default
        .build();

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .type(MYTYPE))
```

如果缺少 `from_string`、`to_string` 或 `compare`，`build()` 会在编译时失败。每个模板方法都会通过
`static_assert` 检查函数指针签名。

#### 内建默认值

当 `NOT NULL` 自定义类型列在 `IGNORE` 模式下接收 `NULL` 时
（例如，`INSERT IGNORE` 或 `UPDATE IGNORE`），服务器会调用默认值
以生成回退值，而不是引发错误。默认值
提供字符串表示形式；服务器使用类型的 `from_string` 函数将其转换为二进制形式。

<注意>
  如果同时省略 `.intrinsic_default_str()` 和 `.intrinsic_default_vdf()`，
  服务器将调用 `from_string("")` 作为回退。当类型
  **首次使用**（在创建表时），而不是在 `INSTALL EXTENSION` 时，会发生这种情况。如果您的
  编码函数拒绝空字符串——或者将其编码为错误的字节数——类型初始化将以 SQL 客户端中可见的错误失败：

  ```
  Type 'MYTYPE' failed to initialize: from_string VDF encoded intrinsic
  default input '' to N bytes, expected persisted_length=M
  ```

  对于固定长度类型，默认字符串必须编码为正好
  `persisted_length` 个字节。为任何空字符串不是有效输入的类型设置显式默认值。
</注意>

**字符串字面量：`.intrinsic_default_str()`**

对于常量默认值，直接在类型构建器中传递字符串（如上述固定长度示例中的 `.intrinsic_default_str("0")`）。

**基于 VDF：`.intrinsic_default_vdf()` + `make_intrinsic_default`**

当默认值取决于类型参数时，针对以下签名之一实现一个函数（通过 `<villagesql/vsql.h>` 提供）：

<警告>
  **重大变更**：`IntrinsicDefaultFunc` 和
  `IntrinsicDefaultWithParamsFunc` 返回 `std::string` 而不是 `const char*`。
  更新任何现有的默认值实现，以直接返回 `std::string`。
</警告>

```cpp theme={null}
// Fixed (no type parameters):
using IntrinsicDefaultFunc = std::string (*)(char *error_msg);

// Parameterized (receives cached parsed params):
template <typename P>
using IntrinsicDefaultWithParamsFunc = std::string (*)(const P &,
                                                       char *error_msg);
```

返回默认值的 `std::string` 表示形式。如果发生错误，
将消息写入 `error_msg` 并返回任何值（SDK 检查 `error_msg[0] != '\0'` 以检测错误）。使用
`make_intrinsic_default<&fn>("vdf_name")` 注册（一个参数：VDF 名称），并在类型构建器中使用 `.intrinsic_default_vdf()` 引用该名称。
以下参数化类型示例显示了完整的注册模式。

```cpp theme={null}
std::string mytype_default(const MyTypeParams &p, char * /*error_msg*/) {
  return /* build string representation based on p */;
}
```

<h4 id="parameterized-types">
  参数化类型
</h4>

可变长度类型需要在编码、解码、比较和哈希时使用列声明的参数来确定分配大小和布局。
定义一个带有解析函数和反向 `to_strings` 函数的参数结构，并在类型构建器中使用
`.params<P, &ParseFunc, &ToStringsFunc>()` 注册两者，并将 `const P&` 作为类型操作函数的第一个
参数。SDK 会缓存每个唯一参数组合的解析结果，因此解析函数最多运行一次
每个类型实例化。`to_strings` 函数是 `parse` 的反函数：
它将类型化的 `P` 重新写入规范的键/值字符串形式，以便服务器可以以 `parse` 消耗的相同形式发布推断的参数。

```cpp theme={null}
struct MyTypeParams {
  int64_t dimension;
  static MyTypeParams parse(const std::map<std::string, std::string> &p) {
    return {.dimension = stoll(p.at("dimension"))};
  }
  static void to_strings(const MyTypeParams &p,
                         std::map<std::string, std::string> &out) {
    out["dimension"] = std::to_string(p.dimension);
  }
};

void mytype_encode(vsql::MaybeParams<MyTypeParams> &params,
                   std::string_view from, vsql::CustomResult out) {
  const MyTypeParams &p = params.value();  // is_known() is always true at runtime
  size_t bytes = (size_t)p.dimension * 4;
  auto buf = out.buffer();
  if (buf.size() < bytes) { out.error("MYTYPE: buffer too small"); return; }
  // ... parse from, write to buf ...
  out.set_length(bytes);
}

void mytype_decode(vsql::CustomArgWith<MyTypeParams> in,
                   vsql::StringResult out) {
  const MyTypeParams &p = in.params();
  // ... read p.dimension floats from in.value(), write to out.buffer() ...
  out.set_length(bytes_written);
}

int mytype_compare(vsql::CustomArgWith<MyTypeParams> a,
                   vsql::CustomArgWith<MyTypeParams> b) {
  // Returns -1, 0, or 1.
}

size_t mytype_hash(vsql::CustomArgWith<MyTypeParams> in) {
  // Returns hash code.
}

// Converts MYTYPE(N) integer syntax to a parameter map.
// Signature: IntToTypeParamsFunc from <villagesql/vsql.h>.
bool mytype_int_to_params_fn(int64_t value,
                             std::map<std::string, std::string> &params,
                             char *error_msg) {
  if (value <= 0) {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: dimension must be a positive integer");
    return true;
  }
  params["dimension"] = std::to_string(value);
  return false;  // success
}

// Validates parameters and computes storage sizes.
// Signature: ResolveTypeParamsFunc from <villagesql/vsql.h>.
bool mytype_resolve_params_fn(const std::map<std::string, std::string> &params,
                              vsql::ResolvedTypeParams *result,
                              char *error_msg) {
  int64_t dim = std::stoll(params.at("dimension"));
  result->persisted_length = dim * 4;
  result->max_decode_buffer_length = 64;
  return false;  // success
}
```

在类型构建器上注册 `.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。

```cpp theme={null}
static constexpr const char kMyTypeName[] = "MYTYPE";

// Maximum valid dimension for MYTYPE.
constexpr int64_t kMyTypeMaxDimension = 1024;  // your max valid dimension
// Upper bound on MYTYPE's persisted byte size across all valid params.
constexpr int64_t kMyTypeMaxPersistedLength = kMyTypeMaxDimension * 4;

constexpr auto MYTYPE =
    vsql::make_type<kMyTypeName>()
        .persisted_length(-1)
        .max_decode_buffer_length(16)
        .max_persisted_length(kMyTypeMaxPersistedLength)
        .params<MyTypeParams, &MyTypeParams::parse, &MyTypeParams::to_strings>()
        .int_to_params<&mytype_int_to_params_fn>()
        .resolve_params<&mytype_resolve_params_fn>()
        .from_string<&mytype_encode>()
        .to_string<&mytype_decode>()
        .compare<&mytype_compare>()
        .intrinsic_default_vdf("mytype_intrinsic_default")
        .build();

using namespace vsql;

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .type(MYTYPE)
        .func(make_intrinsic_default<&mytype_default>(
            "mytype_intrinsic_default")))
```

参数化变体——`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` 变量声明。服务器在例程执行时使用已安装扩展的类型元数据来解析自定义类型。

```sql theme={null}
DELIMITER //
CREATE PROCEDURE insert_complex(IN val COMPLEX)
BEGIN
  DECLARE tmp COMPLEX;
  SET tmp = val;
  INSERT INTO t1 VALUES (tmp);
END //
DELIMITER ;
```

### 扩展系统变量

扩展系统变量是一个预览功能——请参阅
[预览功能](/docs/zh/mysql-8.4/0.0.4/preview-capabilities#system-variables)
以获取完整的 API 参考、工厂函数、SQL 访问和完整示例。

### 扩展状态变量

扩展状态变量是一个预览功能——请参阅
[预览功能](/docs/zh/mysql-8.4/0.0.4/preview-capabilities#status-variables)
以获取完整的 API 参考、工厂函数、SQL 访问和完整示例。

### 密钥环访问

密钥环访问是一个预览功能——请参阅
[预览功能](/docs/zh/mysql-8.4/0.0.4/preview-capabilities#keyring-access)
以获取完整的 API 参考、结果代码和完整示例。

### 列存储

列存储是一个预览功能——请参阅
[预览功能](/docs/zh/mysql-8.4/0.0.4/preview-capabilities#column-storage)
以获取完整的 API 参考和完整示例。

### 检查扩展注册元数据

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

```sql theme={null}
SELECT EXTENSION_NAME, NEGOTIATED_PROTOCOL, REGISTRATION_JSON
FROM INFORMATION_SCHEMA.EXTENSION_REGISTRATION
WHERE EXTENSION_NAME = 'my_ext';
```

| 列                     | 类型                | 描述                                                         |
| --------------------- | ----------------- | ---------------------------------------------------------- |
| `EXTENSION_NAME`      | `VARCHAR(64)`     | 已安装扩展的名称。                                                  |
| `NEGOTIATED_PROTOCOL` | `BIGINT UNSIGNED` | 扩展和服务器之间协商的 VEF 协议版本。                                      |
| `REGISTRATION_JSON`   | `TEXT`            | `vef_registration_t` 结构的 JSON 序列化，包括 `funcs` 和 `types` 数组。 |

<h2 id="running-regression-tests">
  运行回归测试
</h2>

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

### 运行完整套件

要运行扩展的所有测试：

```bash theme={null}
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test --parallel=auto
```

### 运行单个测试

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

```bash theme={null}
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test my_test_name
```

## 创建新测试

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

### 测试位置

扩展测试位于扩展自己的存储库中的 `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 并卸载：

```sql theme={null}
# Description of the test

INSTALL EXTENSION my_extension;

# ... Your Test Code Here ...
CREATE TABLE t1 (val MYTYPE);
INSERT INTO t1 VALUES ('some_value');
SELECT * FROM t1;
DROP TABLE t1;

UNINSTALL EXTENSION my_extension;
```

当您的测试输出包含来自测试运行器的临时目录的路径时，请在 `.test` 文件中添加以下指令以对其进行规范化——否则，记录的结果将包含在其他机器上会出错的绝对路径：

```sql theme={null}
--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR
```

### 添加测试的步骤

1. 在扩展的 `test/t/` 目录中创建 `.test` 文件。
2. 在扩展的 `test/r/` 目录中创建空的 `.result` 文件。
3. 使用 `--record` 运行测试，以生成预期的输出：
   ```bash theme={null}
   cd $BUILD_HOME
   ./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test --record my_new_test
   ```
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` 文件之间的差异。

要使用额外的调试信息运行测试：

```bash theme={null}
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --verbose --suite=/path/to/your/extension/test my_new_test

# To surface LogVSQL() messages in the error log:
./mysql-test/mysql-test-run.pl --mysqld=--log-error-verbosity=3 \
    --suite=/path/to/your/extension/test my_new_test
```

## 另请参阅

* [创建扩展](/docs/zh/mysql-8.4/0.0.4/create) — 端到端构建步骤、CMake 设置和安装
* [扩展 API 参考](/docs/zh/mysql-8.4/0.0.4/extension-api-reference) — VDF 合约、空值处理和缓冲区大小
* [扩展架构](/docs/zh/mysql-8.4/0.0.4/architecture) — 生命周期、Victionary 缓存、性能模式和安全模型
