> ## 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.

# 预览功能

> 预览功能使扩展程序能够访问仍在稳定中的服务器功能。本页涵盖启用预览层、auth、keyring、mysql_services、status_var、sys_var、thread_worker、sql_query 和 statement_event 功能，以及注册模式。

预览功能是服务器提供的功能，在最终确定其 API 之前，会将其暴露给扩展程序。声明了预览功能的扩展程序需要 `vsql_allow_preview_extensions = ON` 才能安装（请参阅[启用预览层](#启用预览层)）——不使用预览功能的扩展程序无论此设置如何都会正常安装。

<Warning>
  预览功能 API 并不稳定。基于预览功能构建的扩展程序在服务器更新后可能无法加载。当某个功能稳定后，其头文件将移动到版本化的稳定 C++ SDK 路径。
</Warning>

## 启用预览层

在安装任何使用预览功能的扩展程序之前，使用 `SET PERSIST` 设置 `vsql_allow_preview_extensions = ON`：

```sql theme={null}
SET PERSIST vsql_allow_preview_extensions = ON;
```

对于此变量，`SET GLOBAL` 会被拒绝——服务器需要 `SET PERSIST`，以便设置在重启后仍然有效。具有预览功能的扩展程序在启动时加载，因此在服务器启动时，该变量必须为 ON。

如果您直接启动 mysqld（例如，从首次启动服务器的安装脚本中启动），请在命令行中传递该标志，而不是使用 `mysqld-auto.cnf`（因为此时 `mysqld-auto.cnf` 尚未存在，无法保存持久化值）：

```bash theme={null}
mysqld --vsql_allow_preview_extensions=ON
```

要禁用：

```sql theme={null}
SET PERSIST vsql_allow_preview_extensions = OFF;
```

如果当前安装了任何使用预览功能的扩展程序，则此操作将失败。首先卸载这些扩展程序，然后关闭该设置。

## 功能索引

| 功能                               | 头文件                                      | 状态                                    |
| -------------------------------- | ---------------------------------------- | ------------------------------------- |
| `vsql::preview::auth`            | `<villagesql/preview/auth.h>`            | 预览（仅 dev ABI，`-DVSQL_USE_DEV_ABI=ON`） |
| `vsql::preview::column_store`    | `<villagesql/preview/storage_builder.h>` | 预览                                    |
| `vsql::preview::keyring`         | `<villagesql/preview/keyring.h>`         | 预览                                    |
| `vsql::preview::mysql_services`  | `<villagesql/preview/mysql_services.h>`  | 预览（仅 dev ABI，`-DVSQL_USE_DEV_ABI=ON`） |
| `vsql::preview::sql_query`       | `<villagesql/preview/sql_query.h>`       | 预览                                    |
| `vsql::preview::statement_event` | `<villagesql/preview/statement_event.h>` | 预览（仅 dev ABI，`-DVSQL_USE_DEV_ABI=ON`） |
| `vsql::status_var`               | `<villagesql/preview/status_var.h>`      | 预览                                    |
| `vsql::preview::storage`         | `<villagesql/preview/storage_builder.h>` | 预览                                    |
| `vsql::sys_var`                  | `<villagesql/preview/sys_var.h>`         | 预览                                    |
| `vsql::preview::thread_worker`   | `<villagesql/preview/thread_worker.h>`   | 预览                                    |

## 注册模式

要使用预览功能，请在文件范围内声明一个功能对象，并将其按引用传递给 `make_extension()` 中的 `.with()`。服务器将在注册期间填充对象的 `abi` 指针：

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

using KeyringCapability = vsql::preview_keyring::KeyringCapability;

static KeyringCapability g_keyring;

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .func(/* ... */)
        .with(g_keyring))
```

`.with(capability)` 告诉服务器扩展程序需要哪些功能。如果扩展程序在安装时 `vsql_allow_preview_extensions` 为 OFF，则服务器会拒绝安装，并显示一个指明该扩展程序名称的错误：`ERROR 3219 (HY000): Failed to load VEF extension 'name': extension requires preview capabilities but vsql_allow_preview_extensions is OFF`。该消息不会说明是哪项功能导致的。

<Warning>
  扩展程序中声明的每个功能对象都必须精确地传递给 `.with()` 一次。在加载时，服务器会交叉检查每个声明的功能实例与 `.with()` 接收到的内容，如果违反了该规则，则 `INSTALL EXTENSION` 将失败：

  * **已声明但从未传递给 `.with()`：**
    `capability '<Type>' was declared but never passed to .with(); every CapabilityBase-derived static must be registered via .with(cap) in the extension builder`
  * **同一实例多次传递给 `.with()`：**
    `capability '<Type>' passed to .with() more than once`
  * **传递给 `.with()` 的对象不是功能：**
    `.with() received an object that does not inherit vsql::detail::CapabilityBase; not a registered capability`

  完整的错误消息为：`Failed to load VEF extension '<name>': vef_register returned an error: <message above>`。
</Warning>

<h2 id="keyring-access">
  密钥环访问
</h2>

密钥环功能 (`vsql::preview::keyring`) 允许扩展程序读取和写入存储在 MySQL 密钥环组件中的密钥。扩展程序可将其用于 API 密钥、加密密钥或其他不应存储在 SQL 表中的密钥。

功能名称 `VEF_PREVIEW_KEYRING_NAME` 为 `"vsql::preview::keyring"`。

为了使读取和写入成功，必须在 MySQL 服务器上安装密钥环组件。如果没有，则操作将返回 `KeyringCapability::Status::UNAVAILABLE`。

### 状态值

`KeyringCapability::Status` 是一个作用域枚举，由 `read()`（在 `ReadResult` 中）和 `write()` 返回：

| 状态                    | 含义           |
| --------------------- | ------------ |
| `Status::OK`          | 操作成功。        |
| `Status::NOT_FOUND`   | 密钥不存在（仅限读取）。 |
| `Status::UNAVAILABLE` | 没有安装密钥环组件。   |
| `Status::ERROR`       | 其他错误。        |

### 声明功能

包含头文件，在文件范围内声明一个功能对象，并将其传递给 `.with()`：

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

using KeyringCapability = vsql::preview_keyring::KeyringCapability;

static KeyringCapability g_keyring;

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_keyring))
```

服务器将在加载时填充 `g_keyring` 对象。如果未安装密钥环组件，则 `read()` 和 `write()` 方法在运行时将返回 `Status::UNAVAILABLE`——请在每次调用时检查该状态，而不是基于单独的可用性探测。

### 读取和写入

```cpp theme={null}
struct KeyringCapability::ReadResult {
  KeyringCapability::Status status;
  std::string value;
};

[[nodiscard]] KeyringCapability::ReadResult
KeyringCapability::read(std::string_view data_id,
                        std::string_view auth_id = {}) const;

[[nodiscard]] KeyringCapability::Status
KeyringCapability::write(std::string_view data_id,
                         std::string_view auth_id,
                         std::string_view data) const;
```

`data_id` 是密钥标识符。`auth_id` 是所有者用户——传递一个空字符串（或在 `read` 上省略它，`read` 默认使用 `{}`）以读取或写入与特定用户无关的内部密钥。

`read` 按值返回一个 `ReadResult`。使用结构化绑定将其绑定：

```cpp theme={null}
auto [status, value] = g_keyring.read("my_secret");
if (status == KeyringCapability::Status::OK) {
  // value contains the secret bytes
}
```

对于任何状态（`Status::OK` 除外），`value` 都是空的。

`write` 直接返回 `Status`，并将 `data` 存储在 `data_id` / `auth_id` 下。

### 完整示例

这是服务器 `villagesql/test-extensions/` 目录中的 `vsql_keyring_reader` 测试扩展程序的简化版本。它注册了 2 个 VDF：`keyring_read` 和 `keyring_store`。

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

using namespace vsql;
using KeyringCapability = vsql::preview_keyring::KeyringCapability;

static KeyringCapability g_keyring;

void keyring_read(StringArg data_id, StringArg auth_id, StringResult out) {
  if (data_id.is_null()) { out.set_null(); return; }

  const auto [status, value] =
      g_keyring.read(data_id.value(), auth_id.is_null() ? "" : auth_id.value());
  if (status == KeyringCapability::Status::UNAVAILABLE) {
    out.error("No keyring component is installed");
    return;
  }
  if (status != KeyringCapability::Status::OK) { out.set_null(); return; }

  auto buf = out.buffer();
  size_t len = std::min(value.size(), buf.size());
  memcpy(buf.data(), value.data(), len);
  out.set_length(len);
}

void keyring_store(StringArg data_id, StringArg auth_id, StringArg value,
                   IntResult out) {
  if (data_id.is_null() || value.is_null()) { out.set(1); return; }

  KeyringCapability::Status status = g_keyring.write(
      data_id.value(), auth_id.is_null() ? "" : auth_id.value(), value.value());
  if (status == KeyringCapability::Status::UNAVAILABLE) {
    out.error("No keyring component is installed");
    return;
  }
  out.set(status == KeyringCapability::Status::OK ? 0 : 1);
}

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .func(make_func<&keyring_read>("keyring_read")
                  .returns(STRING).param(STRING).param(STRING).build())
        .func(make_func<&keyring_store>("keyring_store")
                  .returns(INT).param(STRING).param(STRING).param(STRING).build())
        .with(g_keyring))
```

<h2 id="mysql-services">
  MySQL 服务
</h2>

mysql\_services 功能 (`vsql::preview::mysql_services`) 允许扩展程序使用 MySQL 注册表服务——MySQL 组件所使用的正是这些服务，它们由已安装的组件或服务器核心提供。扩展程序在一处声明它需要的每一项服务；服务器在扩展程序加载时获取每一项服务，并在扩展程序卸载时释放它们。

功能名称 `VEF_PREVIEW_MYSQL_SERVICES_NAME` 为 `"vsql::preview::mysql_services"`。

当某项服务器设施没有自己的 VEF 功能时，请使用它。会话属性和密钥环自身的组件服务都可以通过这种方式访问。仅支持使用服务：将扩展程序自己的实现注册到注册表中是计划中的后续工作，不属于此功能的范围。

### 声明功能

在文件范围内声明一个 `MysqlServices` 对象，用 `VSQL_REQUIRE_SERVICE` 命名您使用的每一项服务，并将该对象传递给 `.with()`。请为每一项服务包含 MySQL 自己的头文件——服务的类型和方法都在该头文件中声明：

```cpp theme={null}
#include <cstddef>

#include <mysql/components/services/mysql_current_thread_reader.h>
#include <villagesql/preview/mysql_services.h>
#include <villagesql/vsql.h>

using namespace vsql;

static preview_mysql_services::MysqlServices services;
VSQL_REQUIRE_SERVICE(services, mysql_current_thread_reader, thd_reader);

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .func(/* ... */)
        .with(services))
```

`VSQL_REQUIRE_SERVICE(services, name, var)` 声明 `var`（服务器将获取到的服务写入其中的引用），并在 `services` 上注册 `name`。它会为您将 `var` 声明为 `static`。`MysqlServices` 对象也需要是 `static` 的，您手动声明的任何引用同样如此：服务器在加载时通过它们写入，它们必须比扩展程序存活得更久。

<h3 id="pinning-a-specific-implementation">
  固定特定实现
</h3>

`VSQL_REQUIRE_SERVICE` 两次使用 `name`——既作为 C++ 的 `SERVICE_TYPE(name)`，也作为服务器在注册表中查找的字符串。在该裸名称下，服务器获取该服务的默认实现。

要改为指定某一个实现，请使用其限定的注册表名称——`service.component`，即 MySQL 的 `PROVIDES_SERVICE(component, service)` 生成的形式。下面请求的是 `component_keyring_file` 组件提供的密钥环读取器，而不是默认实现：

```cpp theme={null}
static preview_mysql_services::ServiceRef<SERVICE_TYPE(keyring_reader_with_status)>
    reader;
static const int reader_req =
    (services.require<SERVICE_TYPE(keyring_reader_with_status)>(
         "keyring_reader_with_status.component_keyring_file", reader),
     0);
```

限定名称的获取方式与裸名称相同，因此通常的规则同样适用：如果该确切实现未注册，则扩展程序将安装失败，而不会回退到另一个实现。

<h3 id="building-against-mysqls-headers">
  基于 MySQL 的头文件构建
</h3>

服务定义属于 MySQL 的组件框架，而不属于 VEF，并且服务器不会安装它们。因此，`mysql/components/services/*.h` 不在扩展 SDK 中，也不在 `make install` 构建的任何内容中，其中包括发行版压缩包和 Docker 镜像。使用服务的扩展程序需要基于 VillageSQL 服务器源代码树构建：

| 包含路径               | 提供内容                                 |
| ------------------ | ------------------------------------ |
| `<source>/include` | 服务定义，`mysql/components/services/*.h` |
| `<build>/include`  | 构建时生成的头文件，例如 `mysqld_error.h`        |

树内测试扩展程序从 `vsql_add_test_extension()` 上的 `MYSQL_HEADERS` 标志获得这两者，该标志将它们作为 `MYSQL_INCLUDE_DIR` 和 `MYSQL_GENERATED_INCLUDE_DIR` 传递。树外构建则设置自己的包含路径。

有两种构建失败出现的位置与导致它们的那一行不同。

遗漏某项服务的 MySQL 头文件会使 `VSQL_REQUIRE_SERVICE` 得到一个无法解析的名称，因此错误出现在该宏上，而不是出现在缺失的 include 上（clang 17）：

```text theme={null}
error: unknown type name 'mysql_service_mysql_current_thread_reader_t'
```

某些服务定义使用了 `size_t` 却没有包含 `<cstddef>`，因此将其中某个头文件放在所有 villagesql 头文件之前，会在 MySQL 自己的头文件内部失败：

```text theme={null}
error: unknown type name 'size_t'
```

请像本页示例那样，首先包含 `<cstddef>`。

<h3 id="calling-a-service">
  调用服务
</h3>

服务引用自身提供 `valid()`，而 `->` 转发到服务。对引用使用 `.`，对服务使用 `->`：

```cpp theme={null}
if (!thd_reader.valid()) { out.error("service unavailable"); return; }
MYSQL_THD thd = nullptr;
if (thd_reader->get(&thd) || thd == nullptr) { out.set_null(); return; }
```

在每次 `->` 调用之前检查 `valid()`。`->` 返回获取到的指针，当服务未被获取时该指针为空。

获取失败的服务会导致安装失败，因此在一个正在运行的函数内部，所需的服务是有效的。这项检查仍然重要，因为手动声明且从未传递给 `require()` 的 `ServiceRef` 永远不会被写入：它可以编译，扩展程序可以安装，而 `valid()` 在扩展程序的整个生命周期内都为 false。

服务*是什么*——它的方法、这些方法的参数以及返回值——由 MySQL 记录，而不是在此处记录。对于名为 `NAME` 的服务，请阅读服务器源代码树中的 `include/mysql/components/services/NAME.h`：其 `BEGIN_SERVICE_DEFINITION(NAME)` 块声明了每个方法，并附有各自的文档。请完全按照该头文件的规定调用它们，包括 MySQL 的约定：`bool` 返回 `false` 表示成功，返回 `true` 表示失败。

<h3 id="acquisition-failure">
  获取失败
</h3>

每一项声明的服务都在扩展程序加载时获取，早于其任何函数被调用，因此未注册的服务会使加载失败，而不是稍后才浮现。`INSTALL EXTENSION` 会失败并指出该服务。

下面的 `vsql_mysql_services_missing_test` 是一个树内测试扩展程序，它要求一项注册表中不存在的服务。它不是您可以安装的东西——它是此失败被捕获的方式，也是当您自己的扩展程序要求此服务器不提供的服务时所产生的结果：

```text theme={null}
ERROR 3219 (HY000): Failed to load VEF extension 'vsql_mysql_services_missing_test': failed to acquire MySQL service 'vsql_intentionally_missing'
```

另有两种安装失败从此功能之外到达同一处：将 `MysqlServices` 对象排除在 `.with()` 之外，以及在 `vsql_allow_preview_extensions` 为 OFF 的服务器上安装。两者都在[注册模式](#注册模式)中介绍。

### 完整示例

`vsql_mysql_services_session_test` 的简化版本，位于服务器的 `villagesql/test-extensions/` 树中。它通过组合两项服务，读取调用会话上正在运行的 SQL 命令：一项返回当前的 `THD`，另一项从中读取指定的属性。两者都是每台服务器上都注册的服务器核心服务，因此无需先安装任何东西：

```cpp theme={null}
#include <cstddef>

#include <mysql/components/services/defs/mysql_string_defs.h>
#include <mysql/components/services/mysql_current_thread_reader.h>
#include <mysql/components/services/mysql_thd_attributes.h>
#include <villagesql/preview/mysql_services.h>
#include <villagesql/vsql.h>

using namespace vsql;

static preview_mysql_services::MysqlServices services;
VSQL_REQUIRE_SERVICE(services, mysql_current_thread_reader, thd_reader);
VSQL_REQUIRE_SERVICE(services, mysql_thd_attributes, attrs);

// session_sql_command() -> STRING: the name of the SQL command running on the
// calling session, or NULL when the THD or the attribute cannot be read.
void session_sql_command(StringResult out) {
  if (!thd_reader.valid() || !attrs.valid()) {
    out.error("MySQL session services are not available");
    return;
  }

  MYSQL_THD thd = nullptr;
  // MySQL convention: a false return means success.
  if (thd_reader->get(&thd) || thd == nullptr) {
    out.set_null();
    return;
  }

  mysql_cstring_with_length value{nullptr, 0};
  if (attrs->get(thd, "sql_command", &value) || value.str == nullptr) {
    out.set_null();
    return;
  }

  out.set(std::string_view(value.str, value.length));
}

VEF_GENERATE_ENTRY_POINTS(make_extension().with(services).func(
    make_func<&session_sql_command>("session_sql_command")
        .returns(STRING)
        .no_params()
        .build()))
```

安装它并调用该函数：

```sql theme={null}
INSTALL EXTENSION vsql_mysql_services_session_test;
SELECT vsql_mysql_services_session_test.session_sql_command() AS sql_command;
```

```text theme={null}
+-------------+
| sql_command |
+-------------+
| select      |
+-------------+
```

<h2 id="status-variables">
  状态变量
</h2>

`status_var` 功能 (`vsql::status_var`) 允许扩展程序将 `long long` 和 `double` 计数器作为 MySQL 状态变量公开。扩展程序拥有存储空间并写入其中；服务器每次查询状态变量时，都会通过指针读取。

使用 `vsql::preview_status_var::make_capability()` 构建功能，并传递来自 `make_int(name, value_ptr)` 或 `make_double(name, value_ptr)` 的大括号列表。模板从大括号列表中推断计数，因此不需要显式大小。

### 完整示例

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

namespace sv = vsql::preview_status_var;

static long long g_hits   = 0;
static long long g_misses = 0;

static auto STATUS_VARS = sv::make_capability({
    sv::make_int("ext_hits",   &g_hits),
    sv::make_int("ext_misses", &g_misses)});

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(STATUS_VARS))
```

`make_int` 需要一个 `long long *`；`make_double` 需要一个 `double *`。这是支持的两种类型。

### 从 SQL 访问

在 `INSTALL EXTENSION my_ext` 之后，该变量将以扩展程序名称作为前缀显示：

```sql theme={null}
SHOW GLOBAL STATUS LIKE 'my_ext%';
```

```
Variable_name       Value
my_ext.ext_hits     0
my_ext.ext_misses   0
```

来自多个查询线程的并发递增（使用非原子 `++`）可能会偶尔丢失；这对于通过 `SHOW STATUS` 公开的近似调用计数器是可以接受的。

<h2 id="system-variables">
  系统变量
</h2>

`sys_var` 功能 (`vsql::sys_var`) 允许扩展程序注册由扩展程序拥有的存储空间支持的 MySQL 系统变量。支持四种类型：`BOOL` (`bool *`)、`INT` (`long long *`)、`DOUBLE` (`double *`) 和 `STR` (`char **`)。`INT` 和 `DOUBLE` 描述符还携带 `min_val` 和 `max_val` 边界；所有描述符都携带默认值和注释。

使用 `vsql::preview_sys_var::make_capability()` 和相应的工厂函数 `make_bool`、`make_int`、`make_double` 和 `make_str` 构建功能。功能对象还公开 `get()` 和 `set()`，以便从扩展程序代码中进行编程访问。两者都返回 `false` 表示成功。

要响应值更改，请在描述符上链接 `.on_change<&fn>()`。回调将接收一个 `sv::SysVarChange`，其中包含 `var_name()` 和类型化的访问器 (`as_int()`、`as_real()`、`as_str()`)。

服务器在持有其全局系统变量锁的同时调用该回调。在那里通过存储指针读取或写入本扩展程序的另一个变量是安全的，并且其他会话会立即看到新值，因为服务器也在同一把锁下读取这些变量。

<Warning>
  调用该功能的 `get()` 或 `set()`、运行 SQL，或者等待执行上述任一操作的线程，都会在该锁上死锁。请让回调保持简短且非阻塞，将需要 SQL 的工作交给[线程工作器](#线程工作器)，或者在阻塞部分前后释放并重新获取 `LOCK_global_system_variables`，就像 `sql/sys_vars.cc` 中的 `event_scheduler_update()` 那样。
</Warning>

功能对象必须具有静态存储期。当用户设置变量时，MySQL 会直接写入存储指针。

| 工厂                | 存储类型          | 附加参数                          |
| ----------------- | ------------- | ----------------------------- |
| `sv::make_bool`   | `bool *`      | `def_val`                     |
| `sv::make_int`    | `long long *` | `def_val`、`min_val`、`max_val` |
| `sv::make_double` | `double *`    | `def_val`、`min_val`、`max_val` |
| `sv::make_str`    | `char **`     | `def_val`                     |

### 完整示例

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

namespace sv = vsql::preview_sys_var;

static bool      g_enabled   = true;
static long long g_threshold = 1000;
static char     *g_log_file  = nullptr;

static void on_threshold_change(sv::SysVarChange c) {
  // c.var_name() identifies the variable; c.as_int() returns the new value
}

static auto SYS_VARS = sv::make_capability({
    sv::make_bool("enabled",      "Enable feature",  &g_enabled,   true),
    sv::make_int ("threshold_ms", "Threshold in ms", &g_threshold, 1000, 0, 3600000)
        .on_change<&on_threshold_change>(),
    sv::make_str ("log_file",     "Log file path",   &g_log_file,  "/tmp/myext.log")});

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(SYS_VARS))
```

### 从 SQL 访问

安装 `INSTALL EXTENSION my_ext` 后，可以使用扩展名作为组件前缀来访问变量：

```sql theme={null}
SELECT @@global.my_ext.threshold_ms;
SET GLOBAL my_ext.threshold_ms = 500;
SET GLOBAL my_ext.log_file = '/var/log/myext.log';
```

### 从扩展代码读取和写入

对于 INT 和 BOOL 变量，直接读取全局存储指针——MySQL 以原子方式更新这些变量。要通过 MySQL 更新变量（以便服务器处理锁定、范围验证和持久性），请调用
`SYS_VARS.set(extension_name, var_name, scope, value)`。`set` 和 `get` 都成功时返回 `false`。两者都不能从 `on_change` 回调中调用：它们都会在系统变量锁上死锁。

```cpp theme={null}
bool err = SYS_VARS.set("my_ext", "threshold_ms", nullptr, value);
```

`scope` 参数控制持久性：

| 范围               | 行为                              |
| ---------------- | ------------------------------- |
| `nullptr`        | 仅更新运行时值，不持久化。                   |
| `"PERSIST"`      | 更新运行时值并写入 `mysqld-auto.cnf`。    |
| `"PERSIST_ONLY"` | 仅写入 `mysqld-auto.cnf`；在下次重启时生效。 |

## 线程工作器

线程工作器功能 (`vsql::preview::thread_worker`) 允许扩展在服务器驱动的后台线程中运行。线程通过控制系统变量进行启动和停止，该变量由服务器在扩展加载时注册；服务器在定期计时器上、在文件描述符准备就绪时，或响应启用/禁用事件时调用扩展的工作函数。

功能名称 `VEF_PREVIEW_THREAD_WORKER_NAME` 是
`"vsql::preview::thread_worker"`。

### 声明功能

包含头文件，在文件范围内声明一个实例化了工作函数的 `ThreadWorkerCapability`，并将其传递给 `.with()`：

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

static vef_next_wakeup_t my_work(vef_wakeup_reason_t reason,
                                 struct vef_thread_handle_t *thread,
                                 void *arg) {
  // ...
  return {};
}

static vsql::preview_thread_worker::ThreadWorkerCapability<&my_work>
    g_worker{"suffix"};

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_worker))
```

工作函数作为非类型模板参数提供 (`ThreadWorkerCapability<&my_work>`)，因此它必须是一个具有以下签名的函数。第一个构造函数参数是线程名称后缀；可选的第二个参数覆盖控制系统变量名称。

### 工作函数签名

```c theme={null}
typedef vef_next_wakeup_t (*vef_work_fn_t)(vef_wakeup_reason_t reason,
                                           struct vef_thread_handle_t *thread,
                                           void *arg);
```

`reason` 指示服务器调用该函数的原因。`thread` 是服务器拥有的此工作器的句柄（在初始 `VEF_WAKEUP_ENABLE` 调用时为 NULL——参见下文）。`arg` 是在描述符上注册的不透明指针；它会保持不变地传递。

### 唤醒生命周期

服务器使用以下四个原因之一调用工作函数：

| 原因                    | 含义                                                    |
| --------------------- | ----------------------------------------------------- |
| `VEF_WAKEUP_ENABLE`   | 工作器刚刚启用（控制系统变量切换为 ON）。返回值设置初始 `poll_fd` 和 `sleep_ms`。 |
| `VEF_WAKEUP_PERIODIC` | 定期计时器触发（`sleep_ms` 过去）。                               |
| `VEF_WAKEUP_POLL_FD`  | 监视的文件描述符变为可读。                                         |
| `VEF_WAKEUP_DISABLE`  | 工作器禁用（控制系统变量 OFF）或服务器正在关闭。返回值将被忽略。                    |

当原因是 `VEF_WAKEUP_ENABLE` 时，`thread` 参数为 NULL，因为此时线程句柄尚不存在。对于其他三个原因，`thread` 不为 NULL。

### 唤醒返回值

```c theme={null}
typedef struct {
  unsigned int sleep_ms;
  int poll_fd;
} vef_next_wakeup_t;
```

工作函数返回一个 `vef_next_wakeup_t`，以更新下一个唤醒配置。任何字段中的零值都表示“保持当前设置”——返回一个值初始化的结构体 (`return {};`) 以保持两者不变。

要设置新的轮询文件描述符，请返回其值（必须大于零）。要清除现有的轮询文件描述符，请在 `poll_fd` 中返回 `-1`。

当原因是 `VEF_WAKEUP_DISABLE` 时，返回值将被忽略。

### 线程名称和控制变量

描述符上的两个字段控制命名：

* `suffix`——线程名称后缀。服务器将其与扩展名称连接起来，生成线程名称，如 `my_ext/monitor`。
* `var_name`——可选。如果非 NULL，服务器将注册此确切名称作为控制系统变量。如果为 NULL，服务器将使用默认模式 `{suffix}_enabled`。

控制变量是服务器注册的系统变量，因此它以扩展名称作为组件前缀。对于后缀为 `monitor` 的扩展 `my_ext`，该变量为 `my_ext.monitor_enabled`。

将其设置为 `ON` 会启动工作器：服务器先以 `VEF_WAKEUP_ENABLE` 调用工作函数，然后创建线程，因此该语句要等到首次调用完成后才返回。在工作器已经运行时再次将其设置为 `ON` 不会有任何效果。将其设置为 `OFF` 会在线程退出后才返回。服务器在这两种操作前后都会释放其全局系统变量锁，因此工作函数可以读取系统变量并运行 SQL。

### 完整示例

一个带有单个周期性工作器的最小扩展，该工作器在每个计时器周期中递增一个心跳计数器。

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

#include <atomic>

static std::atomic<unsigned long long> g_heartbeat{0};

static vef_next_wakeup_t heartbeat_work(vef_wakeup_reason_t reason,
                                        struct vef_thread_handle_t *thread,
                                        void *arg) {
  switch (reason) {
    case VEF_WAKEUP_ENABLE:
      return {1000, 0};  // tick every 1000 ms, no poll fd
    case VEF_WAKEUP_PERIODIC:
      g_heartbeat.fetch_add(1, std::memory_order_relaxed);
      return {};  // keep current sleep_ms and poll_fd
    case VEF_WAKEUP_POLL_FD:
      return {};  // not used in this example
    case VEF_WAKEUP_DISABLE:
      return {};  // ignored
  }
  return {};
}

static vsql::preview_thread_worker::ThreadWorkerCapability<&heartbeat_work>
    g_worker{"heartbeat"};

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_worker))
```

安装此扩展（并且 `vsql_allow_preview_extensions = ON`）后，服务器将在扩展名称下注册一个 `heartbeat_enabled` 系统变量。对于名为 `my_ext` 的扩展，使用以下命令启用工作器：

```sql theme={null}
SET GLOBAL my_ext.heartbeat_enabled = ON;
```

## SQL 查询

sql\_query 功能 (`vsql::preview::sql_query`) 允许扩展从后台线程执行 SQL 语句。查询在服务器内部通过功能 vtable 运行——扩展不链接到任何 MySQL 客户端库。

功能名称 `VEF_PREVIEW_SQL_QUERY_NAME` 是
`"vsql::preview::sql_query"`。

<Warning>
  必须从线程工作器回调中使用该回调的 `vef_thread_handle_t *` 打开 SQL 会话。从 VDF 或从任意扩展创建的线程中调用 `open()` 是无效的——它需要工作器会话上下文。
</Warning>

### 声明功能

包含头文件，在文件范围内声明一个 `SqlQueryCapability`，并将其传递给 `.with()`。它通常与 `ThreadWorkerCapability` 一起注册，因为会话是从工作器回调中打开的：

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

static vsql::preview_sql_query::SqlQueryCapability g_sql;

static vef_next_wakeup_t my_work(vef_wakeup_reason_t reason,
                                 struct vef_thread_handle_t *thread,
                                 void *arg) {
  auto session = g_sql.open(thread);
  if (!session) return {};
  // ...
  return {};
}

static vsql::preview_thread_worker::ThreadWorkerCapability<&my_work>
    g_worker{"sql_demo"};

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_worker)
        .with(g_sql))
```

`g_sql.open(handle)` 返回一个 `Session`。在使用之前，使用 `operator bool` 检查它；无效的 `Session` 表示功能 vtable 未绑定或服务器无法分配会话。`Session` 是只移动的，并在销毁时自行关闭。

### 执行查询

`Session` 通过 `session.sql(sv)` 生成一个 `SqlQuery`。查询可以以两种模式运行：

* `execute()`——运行该语句并将完整的结果集缓冲到 `Result` 中。通过按调用方自己的速度调用 `next()` 来迭代行。
* `for_each(fn)`——运行该语句，并为生成的每一行调用一次 `fn`，而不进行缓冲。返回的 `Result` 仅包含诊断信息（不包含行）。

两者都返回一个 `Result`。非 NULL 的 `Result` 并不意味着该语句已成功——调用 `has_error()` 以确定。

缓冲 (`execute`)：

```cpp theme={null}
auto result = session.sql("SELECT id, name FROM t").execute();
if (result.has_error()) {
  // result.error().message holds the server error string.
  return {};
}
while (result.next()) {
  long long id          = result.column_int(0);
  std::string_view name = result.column_str(1);
  // ...
}
```

`column_str()` 返回一个 `string_view`，它仅在下一次 `next()` 调用或 `Result` 销毁之前有效。如果需要更长的生命周期，请复制它。`string_view` 具有 `data() == nullptr` 表示 SQL NULL。

流式 (`for_each`)：

```cpp theme={null}
auto status = session.sql("SELECT 1").for_each(
    [](const auto &row) {
      // row.column_int(0), row.column_str(1), etc.
    });
if (status.has_error()) {
  // status.error().message
}
```

传递给回调的 `Row` 仅在调用期间有效——不要存储对它的引用，以便跨行使用。`for_each` 返回的 `Result` 不包含缓冲的行；在其上调用 `next()` 不会产生数据。仅将其用于 `has_error()`、`error()`、`warning_count()` 和 `warning(i)`。

### 诊断

`execute()` 和 `for_each()` 都通过返回的 `Result` 提供诊断信息。一个诊断信息是一个 `Diag`：

```cpp theme={null}
struct Diag {
  uint32_t errno_;
  vef_sql_diag_severity_t severity;   // NOTE | WARNING | ERROR
  std::string_view sqlstate;          // 5-char SQLSTATE
  std::string_view message;           // may be empty
};
```

| 字段         | 含义                                                                 |
| ---------- | ------------------------------------------------------------------ |
| `errno_`   | MySQL 错误编号。默认构造的 `Diag` 在没有错误时返回 `0`。                              |
| `severity` | `VEF_SQL_DIAG_NOTE`、`VEF_SQL_DIAG_WARNING` 或 `VEF_SQL_DIAG_ERROR`。 |
| `sqlstate` | 5 个字符的 SQLSTATE。                                                   |
| `message`  | 服务器提供的诊断消息；可能为空。                                                   |

`Result` 暴露：

```cpp theme={null}
bool         Result::has_error() const;
Diag         Result::error() const;
unsigned int Result::warning_count() const;
Diag         Result::warning(unsigned int i) const;
```

`error()` 在语句成功时返回一个默认构造的 `Diag` (`errno_ == 0`)。`warning(i)` 在 `i >= warning_count()` 时返回一个默认构造的 `Diag`。

`sqlstate` 和 `message` 视图指向由 `Result` 拥有的存储，并在 `Result` 销毁时失效——如果需要超出其生命周期，请复制它们。

### 完整示例

一个工作器，在每个周期执行一个带缓冲的查询和一个流式查询，并记录来自两者的诊断信息：

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

static vsql::preview_sql_query::SqlQueryCapability g_sql;

static vef_next_wakeup_t sql_demo_work(vef_wakeup_reason_t reason,
                                       struct vef_thread_handle_t *thread,
                                       void *arg) {
  if (reason == VEF_WAKEUP_ENABLE) return {5000, 0};
  if (reason != VEF_WAKEUP_PERIODIC) return {};

  auto session = g_sql.open(thread);
  if (!session) return {};

  // Buffered: read a small result set.
  auto rs = session.sql("SELECT id, name FROM mydb.t LIMIT 10").execute();
  if (rs.has_error()) {
    auto e = rs.error();
    // Log e.errno_, e.sqlstate, e.message somewhere extension-owned.
  } else {
    while (rs.next()) {
      long long id          = rs.column_int(0);
      std::string_view name = rs.column_str(1);
      (void)id; (void)name;
    }
  }

  // Streaming: process rows without buffering.
  auto status = session.sql("SELECT v FROM mydb.t").for_each(
      [](const auto &row) {
        long long v = row.column_int(0);
        (void)v;
      });
  for (unsigned i = 0; i < status.warning_count(); ++i) {
    auto w = status.warning(i);
    (void)w;
  }
  return {};
}

static vsql::preview_thread_worker::ThreadWorkerCapability<&sql_demo_work>
    g_worker{"sql_demo"};

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_worker)
        .with(g_sql))
```

<h2 id="column-storage">
  列存储
</h2>

列存储允许扩展直接向 InnoDB 注册自定义的磁盘布局，用于其自定义类型之一，而不是将该类型的数据通过行的 VARBINARY 负载进行路由。当您的类型需要一种 VARBINARY 无法表达的磁盘布局时，请使用它——例如，必须存储在专用页中的打包浮点数组。这是一个功能特性：它启用了新的存储布局，而不是用于现有布局的调整旋钮。

<Warning>
  列存储是一个预览版 ABI——正在积极开发中，并且可能在不同版本之间发生变化。它目前仅涵盖行级持久性；对自定义存储列进行索引尚未可用。
</Warning>

### 声明功能

两个预览功能协同工作：

* `vsql::preview::storage` — 开放对 InnoDB 存储基础设施的访问（微事务、段、页）。在文件范围内声明一个 `StorageCapability`。
* `vsql::preview::column_store` — 将每个类型的存储实现绑定到扩展的自定义类型之一。在文件范围内使用 `make_column_store<Ctx>(TYPE).…build()` 声明一个 `ColumnStoreCapability`。

两者都必须传递给 `make_extension()` 上的 `.with()`：

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

namespace storage = vsql::preview_storage;
using vsql::preview_storage_builder::ColumnStoreCapability;
using vsql::preview_storage_builder::make_column_store;
using vsql::preview_storage_builder::StorageCapability;

struct MyCtx {
  storage::Space::Ref space = 0;
  storage::Segment::PageRef root_page = storage::Page::INVALID_REF;
};

static auto STORAGE = StorageCapability{};

static constexpr auto kMyStorage =
    make_column_store<MyCtx>(MY_TYPE)
        .create<&MyStorage::create>()
        .drop<&MyStorage::drop>()
        .load<&MyStorage::load>()
        .insert<&MyStorage::insert>()
        .select<&MyStorage::select>()
        .mark_delete<&MyStorage::mark_delete>()
        .purge<&MyStorage::purge>()
        .build();

static auto COLUMN_STORE = ColumnStoreCapability().column_store(kMyStorage);

using namespace vsql;

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(STORAGE)
        .with(COLUMN_STORE)
        .type(MY_TYPE))
```

`make_column_store<MyCtx>(MY_TYPE)` 将实现绑定到在同一扩展上注册的一个自定义类型。所有七个槽位都必须在 `build()` 时提供，因为每个槽位都映射到列生命周期中的一个不同点，InnoDB 在正常操作期间会到达该点。

### 七个存储函数

每个函数都接受 `storage::Column::StorageCtx<MyCtx>*`，其 `user()` 访问器返回扩展的每个列状态，并且其 `arena()` 提供服务器管理的辅助对象分配。每个函数在成功时返回 `false`，在出错时返回 `true`，并将消息写入 `error_msg`（容量 `error_msg_len`），以便将故障传递到 SQL 客户端。

```cpp theme={null}
// CREATE TABLE / ALTER TABLE ADD COLUMN.
// col_len is the type's persisted length. Reserve segments here and store
// space + root_page in ctx->user() so DML functions can reach them.
bool create(storage::Column::StorageCtx<MyCtx>*, storage::Space::Ref,
            storage::Segment::TrxRef, uint32_t col_len,
            char* error_msg, uint32_t error_msg_len);

// DROP TABLE / ALTER TABLE DROP COLUMN.
// Release any segments reserved in create(). Arena memory is freed by the
// server after this call returns.
bool drop(storage::Column::StorageCtx<MyCtx>*, storage::Segment::TrxRef,
          char* error_msg, uint32_t error_msg_len);

// Called when the server reattaches to existing storage (e.g. after restart).
// Recover space and root_page from the StorageRef set in create().
bool load(storage::Column::StorageCtx<MyCtx>*, storage::Column::StorageRef,
          char* error_msg, uint32_t error_msg_len);

// INSERT. col_data is the encoded value; rowid_prefix identifies the owning
// row. Write into your storage layout and return a Column::Ref the server
// stores in the row payload in place of the value bytes.
bool insert(storage::Column::StorageCtx<MyCtx>*, storage::MtrCtx::Ref,
            storage::Segment::TrxRef, storage::Column::Data col_data,
            storage::Column::Data rowid_prefix, storage::Column::Ref* col_ref,
            char* error_msg, uint32_t error_msg_len);

// SELECT. Given the Column::Ref produced by insert, populate col_data and
// rowid_prefix, and report the writing transaction and delete-mark status.
bool select(storage::Column::StorageCtx<MyCtx>*, storage::MtrCtx::Ref,
            storage::Column::Ref, storage::Column::Data* col_data,
            storage::Column::Data* rowid_prefix, storage::Segment::TrxRef*,
            bool* delete_marked, char* error_msg, uint32_t error_msg_len);

// DELETE (in-transaction). Set or clear the delete-mark flag. The actual
// bytes must remain readable until purge() runs.
bool mark_delete(storage::Column::StorageCtx<MyCtx>*, storage::MtrCtx::Ref,
                 storage::Segment::TrxRef, storage::Column::Ref,
                 bool delete_mark, char* error_msg, uint32_t error_msg_len);

// InnoDB purge. Reclaim storage for entries whose deleting transaction is
// no longer visible to any active snapshot.
bool purge(storage::Column::StorageCtx<MyCtx>*, storage::MtrCtx::Ref,
           storage::Segment::TrxRef, storage::Column::Ref,
           char* error_msg, uint32_t error_msg_len);
```

`mark_delete` 和 `purge` 是不同的，因为 InnoDB MVCC 要求已删除的行在 purge 运行之前对旧的快照保持可读。

### 每个列的上下文和 Arena

C++ SDK 在调用 `create` 或 `load` 之前，默认构造 `MyCtx`——在进入您的函数时，`ctx->user()` 已经填充。`MyCtx` 必须是默认可构造的；C++ SDK 使用不带参数的 `T()` 调用。

直接使用 `ctx->user()` 来初始化状态。不要调用 `ctx->arena().construct<MyCtx>()`——这将分配第二个、未使用的实例，并且 `ctx->user()` 不指向它。

```cpp theme={null}
bool MyStorage::create(storage::Column::StorageCtx<MyCtx>* ctx,
                       storage::Space::Ref space, storage::Segment::TrxRef trx,
                       uint32_t col_len,
                       char* error_msg, uint32_t error_msg_len) {
  storage::Segment::PageRef root;
  if (storage::Segment::create(space, 1, trx, root) != storage::Error::SUCCESS) {
    snprintf(error_msg, error_msg_len, "%s", storage::last_error().data());
    return true;
  }

  ctx->user()->space = space;
  ctx->user()->root_page = root;
  // Encode space and root into StorageRef so load() can recover both.
  ctx->set_ref((static_cast<storage::Column::StorageRef>(space) << 32) |
               static_cast<storage::Column::StorageRef>(root));
  return false;
}
```

`load` 遵循相同的模式——`ctx->user()` 预先填充，并且 `storage_ref` 携带由 `ctx->set_ref()` 在 `create` 中存储的打包值：

```cpp theme={null}
bool MyStorage::load(storage::Column::StorageCtx<MyCtx>* ctx,
                     storage::Column::StorageRef storage_ref,
                     char* error_msg, uint32_t error_msg_len) {
  ctx->user()->space =
      static_cast<storage::Space::Ref>(storage_ref >> 32);
  ctx->user()->root_page =
      static_cast<storage::Segment::PageRef>(storage_ref & 0xFFFFFFFF);
  ctx->set_ref(storage_ref);
  return false;
}
```

仅使用 `ctx->arena()` 来分配辅助对象，这些对象太大或太动态，无法直接嵌入到 `MyCtx` 中。C++ SDK 在 `drop` 返回后，无论 `drop` 是否成功，都会自动销毁 arena 并调用 `~MyCtx()`。

### InnoDB 访问实用程序

包含 `<villagesql/preview/storage_api.h>` 以获取 InnoDB 原语。所有页面的读取和写入都必须在微事务中进行：

```cpp theme={null}
storage::MtrCtx mtr;
storage::MtrCtx::Ref mtr_ref = mtr.start();
if (mtr_ref == nullptr) { /* OOM — handle error */ return true; }
// ... page operations ...
mtr.commit();
```

提交微事务会释放页面锁，并写入重做日志记录，以使更改持久化。

**段** 在 `create` 时保留——有关完整的设置模式，请参见上面“每个列的上下文”中的 `create` 和 `load` 示例。在 DML 操作期间，从根页面获取段引用以分配新页面：

```cpp theme={null}
storage::Page root;
root.load(ctx->user()->space, ctx->user()->root_page,
          storage::Page::Latch::EXCLUSIVE, mtr_ref);
storage::Segment::Ref seg = storage::Segment::get_header(root, 0);
storage::Page data_page;
data_page.load_new(seg, mtr_ref);  // allocates a fresh page
```

**页面** 使用共享锁读取，使用独占锁写入。将 `mtr_ref` 传递给写入调用，以便 InnoDB 记录更改：

```cpp theme={null}
storage::Page page;

// Read
page.load(ctx->user()->space, page_num, storage::Page::Latch::SHARED, mtr_ref);
uint32_t v = page.read_integer_4(storage::Page::HEADER_SIZE + offset);

// Write
page.load(ctx->user()->space, page_num, storage::Page::Latch::EXCLUSIVE, mtr_ref);
page.write_integer_4(storage::Page::HEADER_SIZE + offset, v, mtr_ref);
```

页面布局常量：

| 常量                               | 值    | 备注                                  |
| -------------------------------- | ---- | ----------------------------------- |
| `storage::Page::HEADER_SIZE`     | `38` | 扩展数据从此偏移量开始。                        |
| `storage::Page::TRAILER_SIZE`    | `8`  | 不要写入 `page_size - TRAILER_SIZE` 之后。 |
| `storage::Page::get_size(space)` | 运行时  | 使用它来代替硬编码的 16384。                   |

在标头或尾部区域内读取或写入会损坏页面——InnoDB 使用这些字节范围用于其自身的簿记和校验和。

## 语句事件

语句事件功能 (`vsql::preview::statement_event`) 在每个查询完成执行后运行一个扩展提供的处理程序。服务器在查询自己的线程上同步调用该处理程序，并传递执行元数据——查询文本、计时、行计数、连接标识以及优化器质量指标。可将其用于慢查询日志记录、审计或指标收集。

功能名称 `VEF_PREVIEW_STATEMENT_EVENT_NAME` 是
`"vsql::preview::statement_event"`。

### 声明功能

在文件范围内声明一个以触发阶段和处理程序函数实例化的 `StatementEventCapability`，并将其传递给 `.with()`：

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

namespace se = vsql::preview_statement_event;

static void on_statement(const se::StatementEventArgs &args,
                         se::StatementEventResult &result) {
  // inspect args; optionally write an advisory message via result
}

static se::StatementEventCapability<VEF_STATEMENT_EVENT_POSTEXECUTE,
                                    &on_statement>
    g_statement_event;

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_statement_event))
```

第一个模板参数是触发阶段，一个 `vef_statement_event_phase_t` 值。`VEF_STATEMENT_EVENT_POSTEXECUTE` 在查询完成执行后触发（无论成功还是失败），并且是此版本中唯一实现的阶段。其他 `vef_statement_event_phase_t` 值是保留的；为其中之一声明处理程序会导致服务器拒绝 `INSTALL EXTENSION`。

### 处理程序参数

`StatementEventArgs` 是已完成查询的只读视图；在 POSTEXECUTE 阶段，每个字段都已填充。部分访问器：

| 访问器                                                                                                  | 含义                                                                                                                    |
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `query()`                                                                                            | 查询文本，作为 `string_view`。当服务器有该语句的重写形式时，这就是该形式——即与常规日志、慢查询日志和二进制日志所记录的相同的脱敏文本。                                           |
| `query_time_secs()`                                                                                  | 挂钟执行时间，以秒为单位。                                                                                                         |
| `lock_time_secs()`                                                                                   | 等待锁所花费的时间，以秒为单位。                                                                                                      |
| `rows_sent()`、`rows_examined()`、`rows_affected()`                                                    | 行计数器。                                                                                                                 |
| `user()`、`client_ip()`、`connection_id()`                                                             | 连接标识。                                                                                                                 |
| `schema()`                                                                                           | 默认数据库（schema），如果未选择则为 `NULL`。                                                                                         |
| `status()`                                                                                           | 成功时为 `0`，否则为 MySQL 错误代码。                                                                                              |
| `digest_text()`                                                                                      | 规范化的查询形式，用于对相似查询进行分组。                                                                                                 |
| `no_index_used()`                                                                                    | 当查询在没有可用索引的情况下运行时为 `true`。                                                                                            |
| `digest_hash()`                                                                                      | 语句摘要，形式为 64 个小写十六进制字符——即 `performance_schema` 以 `DIGEST` 公开的值。它是对相同语句进行分组的紧凑键；只要 `digest_text()` 为 `NULL`，它也为 `NULL`。 |
| `read_first()`、`read_last()`、`read_key()`、`read_next()`、`read_prev()`、`read_rnd()`、`read_rnd_next()` | 每语句的处理程序行访问计数器（慢日志的 `Read_*` 字段）。它们量化了 `no_index_used()` 仅作标记的访问方法——例如，`read_rnd_next()` 数值偏高表示全表扫描。                  |

由于 `query()` 在存在重写形式时返回服务器的重写形式，因此携带凭据的语句在送达处理程序时，其中的机密信息（如密码）已被混淆，而不是以明文形式呈现，这与常规日志、慢查询日志和二进制日志已经对它们进行脱敏的方式相匹配：`SET PASSWORD`、`CREATE`/`ALTER USER ... IDENTIFIED BY`、`CHANGE REPLICATION SOURCE ... SOURCE_PASSWORD` 以及 `CREATE SERVER ... OPTIONS(PASSWORD ...)`。没有重写规则的语句会逐字传递。

诸如 `query()`、`sqlstate()` 和 `error_message()` 之类的字符串访问器指向仅在处理程序调用期间有效的存储——如果您在处理程序返回后需要它们，请复制这些字节。

`StatementEventResult::error_msg(fmt, ...)` 写入一条 printf 格式化的消息。在 POSTEXECUTE 阶段，该消息是建议性的：服务器会记录它，但不会将其传播到客户端。

### 完整示例

[`vsql_slow_query_log`](https://github.com/villagesql/villagesql-server/tree/main/villagesql/test-extensions/vsql-slow-query-log) 测试扩展的精简形式。它记录每个执行时间超过阈值的查询，将语句事件功能与[系统变量](#system-variables)结合以进行运行时配置：

```cpp theme={null}
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <mutex>

#include <villagesql/preview/statement_event.h>
#include <villagesql/preview/sys_var.h>
#include <villagesql/vsql.h>

using namespace vsql;
namespace sv = vsql::preview_sys_var;
namespace se = vsql::preview_statement_event;

static bool g_enabled;
static long long g_threshold_ms;
static char *g_log_filename;
static std::mutex g_log_mutex;

static void slow_query_hook(const se::StatementEventArgs &args,
                            se::StatementEventResult &result) {
  if (!g_enabled) return;
  if (args.query_time_secs() * 1000.0 < static_cast<double>(g_threshold_ms))
    return;

  time_t now = static_cast<time_t>(args.query_start_utime() / 1000000);
  char ts[32];
  struct tm tm_utc;
  gmtime_r(&now, &tm_utc);
  strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%SZ", &tm_utc);

  std::lock_guard<std::mutex> lock(g_log_mutex);
  FILE *f = fopen(g_log_filename, "a");
  if (f == nullptr) {
    result.error_msg("failed to open '%s': %s", g_log_filename,
                     strerror(errno));
    return;
  }

  fprintf(f, "# Time: %s\n", ts);
  fprintf(f, "# User@Host: %s @ %s  Id: %lu\n", args.user() ? args.user() : "",
          args.client_ip() ? args.client_ip() : "", args.connection_id());
  fprintf(f,
          "# Schema: %s  Query_time: %.6f  Lock_time: %.6f"
          "  Rows_sent: %llu  Rows_examined: %llu\n",
          args.schema() ? args.schema() : "", args.query_time_secs(),
          args.lock_time_secs(), (unsigned long long)args.rows_sent(),
          (unsigned long long)args.rows_examined());
  fprintf(f, "SET timestamp=%llu;\n", (unsigned long long)now);
  auto q = args.query();
  fprintf(f, "%.*s;\n", (int)q.size(), q.data());
  fclose(f);
}

static auto SYS_VARS = sv::make_capability({
    sv::make_bool("enabled", "Enable the slow query log", &g_enabled, false),
    sv::make_int("threshold_ms", "Minimum execution time to log, in ms",
                 &g_threshold_ms, 1000, 0, 3600000),
    sv::make_str("log_file", "Path to the slow query log file",
                 &g_log_filename, "/tmp/vsql_slow_query.log")});

static se::StatementEventCapability<VEF_STATEMENT_EVENT_POSTEXECUTE,
                                    &slow_query_hook>
    STATEMENT_EVENT;

VEF_GENERATE_ENTRY_POINTS(
    make_extension().with(SYS_VARS).with(STATEMENT_EVENT))
```

#### 从 SQL 启用

启用预览层后（请参阅[启用预览层](#启用预览层)），安装该扩展并通过其系统变量进行配置：

```sql theme={null}
INSTALL EXTENSION vsql_slow_query_log;
SET GLOBAL vsql_slow_query_log.enabled = ON;
SET GLOBAL vsql_slow_query_log.threshold_ms = 500;
```

每个比阈值慢的查询都会追加到配置的日志文件中：

```
# Time: 2026-06-22T22:53:44Z
# User@Host: root @   Id: 27
# Schema:   Query_time: 0.605084  Lock_time: 0.000000  Rows_sent: 1  Rows_examined: 1
SET timestamp=1782168824;
SELECT SLEEP(0.6);
```

<h2 id="authentication-methods">
  身份验证方法
</h2>

auth 功能 (`vsql::preview::auth`) 允许扩展程序提供一种服务器身份验证方法。账户通过 `CREATE USER ... IDENTIFIED WITH <method-name>` 选择使用它；在连接时，如果该名称不是已加载的 MySQL 认证插件，服务器会查询 VEF 认证注册表，并在握手过程中调用扩展程序的处理程序。当您需要针对服务器不了解的凭据来源（持有者令牌、外部身份提供方或自定义质询）对账户进行身份验证，而又不想编写 MySQL 认证插件时，请使用它。

功能名称 `VEF_PREVIEW_AUTH_NAME` 为 `"vsql::preview::auth"`。

该处理程序是一个类型化函数，它接收一个 `AuthContext`：它通过这个由服务器拥有的上下文读写握手数据包来与客户端通信，并且永远不会看到 MySQL 的内部认证结构。

<Warning>
  认证结果是故障关闭的。服务器会将 `AuthResult::kOk` 以外的任何结果都视为拒绝连接——这里刻意没有“可能”或故障开放的结果。返回 `AuthResult::kReject`、返回 `AuthResult::kError` 或者从未设置有效账户的处理程序都会拒绝此次登录。
</Warning>

### 声明功能

包含头文件，编写一个类型化处理程序，使用流式的 `make_auth<>` 构建器构建一个描述符，并将该描述符交给一个 `AuthCapability` 令牌，再把该令牌传递给 `.with()`。预览功能头文件不属于 `<villagesql/vsql.h>` 总括头文件，因此请显式包含 `<villagesql/preview/auth.h>`：

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

using namespace vsql;
using vsql::preview_auth::AuthContext;
using vsql::preview_auth::AuthResult;

AuthResult authenticate(AuthContext &c) {
  // ... validate the client and set the effective account ...
  return AuthResult::kOk;
}

constexpr auto MY_AUTH =
    vsql::preview_auth::make_auth<&authenticate>("my_auth")
        .client_plugin("mysql_clear_password")
        .build();

static vsql::preview_auth::AuthCapability g_auth{MY_AUTH};

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(g_auth))
```

该构建器有六个组成部分：

| 元素                                 | 含义                                                                                                                                       |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `make_auth<&handler>("name")`      | 启动构建器。处理程序是一个编译期模板参数，因此空处理程序或签名错误的处理程序是编译错误，而不是运行时失败。`"name"` 是账户绑定的认证方法名称 (`IDENTIFIED WITH <name>`)，最长为 `VEF_AUTH_MAX_NAME_LEN`（64）字节。 |
| `.client_plugin(name)`             | 可选。覆盖服务器在握手期间通告的客户端认证插件。                                                                                                                 |
| `.accepts_client_plugin(callback)` | 可选。接收 `bool (*)(const char *offered)`。返回 `true` 会保留客户端提供的插件；返回 `false` 会将客户端切换到 `.client_plugin()`。                                      |
| `.auto_create(callback)`           | 可选。让该方法选择接受针对不存在账户的登录（请参阅[自动创建账户](#auto-creating-accounts)）。                                                                             |
| `.auto_grant(callback)`            | 可选。让服务器授予处理程序暂存的角色，而不仅仅是激活账户已持有的角色（请参阅[自动授予角色](#auto-granting-roles)）。                                                                   |
| `.build()`                         | 产生您交给 `AuthCapability` 的描述符。                                                                                                             |

`AuthCapability g_auth{descriptor}` 是由 `.with()` 使用的自注册令牌。请将其声明为 `static`，以便它比注册过程存活得更久。

`client_plugin` 是可选的。`make_auth` 将通告的插件默认设置为 `"mysql_clear_password"`——每个 MySQL 客户端都附带的最低共同标准——因此从不调用 `.client_plugin()` 的方法仍然可以安装，简单的客户端仍然可以连接。调用 `.client_plugin(name)` 可以请求不同的插件；`mysql_clear_password` 会在密码槽位中原样接收持有者令牌。

如果客户端提供的插件不是该方法请求的插件，则它会被切换到所请求的插件并原样重发其凭据，这会耗费一次往返，并且需要客户端愿意进行该切换。`.accepts_client_plugin(&callback)` 让该方法可以改为保留客户端提供的插件：服务器将每个提供的名称传递给该回调，包括所请求的插件本身——无论回调返回什么，它都会被接受。未设置回调的方法不接受任何其他提议，因此其他每一个提议都会切换到所请求的插件。接受是最终的——服务器此后不会再切换回所请求的插件——因此只接受处理程序确实能解析其帧格式的插件。服务器在握手协商期间、在处理程序的首次读取之前查询该回调，因此它必须是一个纯谓词：没有数据包 I/O，不阻塞，没有副作用。

<h3 id="the-handler-contract">
  处理程序约定
</h3>

处理程序符合 `AuthHandler` 类型——它接收一个 `AuthContext &` 并返回一个 `AuthResult`：

```cpp theme={null}
AuthResult authenticate(AuthContext &c);
```

它在握手期间于连接线程上同步调用。`AuthContext` 封装了服务器拥有的每次尝试的上下文；只在调用期间持有它，不要保留它。请调用它的方法，而不要通过函数表传递上下文指针。基于令牌的处理程序会使用的方法：

| 方法                                             | 用途                                                                                                                                                                     |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `c.read_packet()`                              | 读取客户端发送的下一个数据包。以 `Span<const unsigned char>` 形式返回字节，在下一次读取之前有效（协议或连接错误时为空）。与 `mysql_clear_password` 搭配时，一次读取即可得到持有者令牌。                                                 |
| `c.write_packet(data)`                         | 向客户端发送一个数据包（例如一个质询）。接收 `Span<const unsigned char>`，失败时返回 `true`。                                                                                                       |
| `c.user_name()`                                | 客户端连接时使用的账户名。                                                                                                                                                          |
| `c.auth_string()`                              | `IDENTIFIED WITH <m> AS '...'` 中的 `AS '...'` 子句，若无则为空。                                                                                                                 |
| `c.host_or_ip()`                               | 客户端主机或 IP。                                                                                                                                                             |
| `c.client_auth_plugin()`                       | 客户端在其握手回复中通告的客户端认证插件（例如 `"mysql_clear_password"`）。当该方法接受了这个提议时，它也是为处理程序读取的凭据构造帧的插件，因此处理程序可以按名称解析，而不必嗅探字节。强制切换到 `.client_plugin()` 不会更新它，因此在那条路径上它仍然报告客户端最初提供的插件。未知时为空。 |
| `c.authenticate_as(account)`                   | 设置会话运行时的有效账户（由 `CURRENT_USER()` 显示）。在返回 `AuthResult::kOk` 之前必须设置。                                                                                                      |
| `c.set_external_user(identity)`                | 为审计跟踪设置原始外部身份 (`@@external_user`)。                                                                                                                                     |
| `c.set_active_roles(roles, n_roles)`           | 暂存会话的活动角色（请参阅[暂存活动角色](#staging-active-roles)）。                                                                                                                         |
| `c.account_unknown()`                          | 当正在进行身份验证的账户不存在，且此次登录因该方法的 `.auto_create()` 选择而被路由到该方法时，为 `true`。针对已存在账户的登录则为 `false`。                                                                                 |
| `c.request_provision(account, roles, n_roles)` | 请求服务器创建 `account` 并向其授予 `roles`（请参阅[自动创建账户](#auto-creating-accounts)）。                                                                                                 |

处理程序返回三种结果之一：

| 结果                    | 含义                                                                   |
| --------------------- | -------------------------------------------------------------------- |
| `AuthResult::kOk`     | 身份验证成功。处理程序必须已调用 `authenticate_as()`；会话以该账户身份运行。                     |
| `AuthResult::kReject` | 身份验证失败——凭据错误或策略拒绝。                                                   |
| `AuthResult::kError`  | 内部错误导致无法作出判定（例如，密钥来源不可用）。服务器对它的处理与拒绝完全相同；它存在的意义只是在日志中区分“被拒绝”和“无法判定”。 |

`AuthResult::kReject` 和 `AuthResult::kError` 都会拒绝连接。只有 `AuthResult::kOk` 才会成功。

当处理程序将连接账户映射到另一个有效账户时——就像下面的示例将连接账户映射到 `vsql_auth_test_user` 那样——这就是代理，它需要 `GRANT PROXY`，与 MySQL 插件认证路径上的做法完全一样。

<h3 id="staging-active-roles">
  暂存活动角色
</h3>

`c.set_active_roles(roles, n_roles)` 暂存应在会话上激活的角色，替代此次登录中账户的默认角色激活。`roles` 是一个包含 `n_roles` 个以 NUL 结尾名称的数组；这些字符串会被复制，因此调用方无需保留它们。服务器在账户解析*之后*应用它们，使用与 `SET ROLE` 相同的、经过授权检查的激活方式：只有确实授予了已认证账户的角色才会激活，未被授予的名称会被静默跳过——因此令牌永远无法授予或提升超出 DBA 所配置范围的权限。传递 `n_roles == 0` 则不激活任何角色（等同于 `SET ROLE NONE`）。

### 完整示例

一个最小的认证器，浓缩自服务器源代码树中 `villagesql/test-extensions/vsql-auth-test/` 处的 `vsql_auth_test` 扩展程序，任何发行版都不包含它。它接受一个固定令牌，将连接映射到 `vsql_auth_test_user`，并请求 `mysql_clear_password`，以便令牌原样到达密码槽位。（树内扩展程序还添加了额外的令牌路径、一个 `.accepts_client_plugin()` 回调，以及下文介绍的两个选择项，用于驱动其测试套件。）

```cpp theme={null}
#include <cstring>

#include <villagesql/preview/auth.h>
#include <villagesql/vsql.h>

using namespace vsql;
using vsql::preview_auth::AuthContext;
using vsql::preview_auth::AuthResult;

namespace {

constexpr char kToken[] = "vsql-auth-test-token";
constexpr char kMappedAccount[] = "vsql_auth_test_user";

AuthResult authenticate(AuthContext &c) {
  auto pkt = c.read_packet();
  if (pkt.empty()) return AuthResult::kError;

  // mysql_clear_password sends a NUL-terminated string; drop the trailing NUL.
  size_t len = pkt.size();
  if (len && pkt[len - 1] == '\0') --len;

  if (len != std::strlen(kToken) ||
      std::memcmp(pkt.data(), kToken, len) != 0) {
    return AuthResult::kReject;
  }

  c.authenticate_as(kMappedAccount);
  // @@external_user records the connecting identity, not the mapped account.
  c.set_external_user(c.user_name());
  return AuthResult::kOk;
}

constexpr auto AUTH_METHOD =
    vsql::preview_auth::make_auth<&authenticate>("vsql_auth_test")
        .client_plugin("mysql_clear_password")
        .build();
vsql::preview_auth::AuthCapability g_auth{AUTH_METHOD};

}  // namespace

VEF_GENERATE_ENTRY_POINTS(make_extension().with(g_auth))
```

<h3 id="binding-an-account-and-connecting">
  绑定账户并连接
</h3>

在启用预览层后（请参阅[启用预览层](#启用预览层)），安装该扩展程序并将一个账户绑定到该方法。由于处理程序会映射到第二个账户，因此也要创建该账户，并授予它 `PROXY` 权限，使连接账户可以取得其身份：

```sql theme={null}
INSTALL EXTENSION vsql_auth_test;
CREATE USER auth_user IDENTIFIED WITH vsql_auth_test;
CREATE USER vsql_auth_test_user;
GRANT SELECT ON *.* TO vsql_auth_test_user;
GRANT PROXY ON vsql_auth_test_user TO auth_user;
```

`CREATE USER ... IDENTIFIED WITH vsql_auth_test` 之所以被接受，是因为 `vsql_auth_test` 是一个已注册的 VEF 认证方法——与接受已安装插件名称的方式相同。

只有 `IDENTIFIED WITH <method>` 形式被接受，并可选地带 `AS '...'`。添加 `BY '...'` 是要求该方法将密码转换为存储的凭据——这是 MySQL 插件通过 `generate_authentication_string()` 完成的工作——而如今没有任何 VEF 认证方法声明该钩子，因此服务器会拒绝它：

```sql theme={null}
CREATE USER auth_user IDENTIFIED WITH vsql_auth_test BY 'secret';
```

```text theme={null}
ERROR 1827 (HY000): The password hash doesn't have the expected format.
```

绑定的方法名称会写入账户的 `plugin` 列，而不是表的默认值，该账户下次登录时读取的正是这一列：

```sql theme={null}
SELECT plugin FROM mysql.user WHERE user = 'auth_user';
```

```text theme={null}
+----------------+
| plugin         |
+----------------+
| vsql_auth_test |
+----------------+
```

该方法请求 `mysql_clear_password`，因此客户端必须传递 `--enable-cleartext-plugin` 才能以明文发送令牌。在令牌正确时，会话以映射后的账户身份运行，并通过 `@@external_user` 公开连接账户：

```bash theme={null}
mysql --enable-cleartext-plugin --user=auth_user \
      --password=vsql-auth-test-token \
      -e "SELECT CURRENT_USER(), @@external_user"
```

```
CURRENT_USER()         @@external_user
vsql_auth_test_user@%  auth_user
```

卸载该扩展程序会移除该方法；绑定到它的账户将无法再进行身份验证：

```sql theme={null}
UNINSTALL EXTENSION vsql_auth_test;
```

<h3 id="auto-creating-accounts">
  自动创建账户
</h3>

一种方法也可以处理针对尚不存在账户的登录，并让服务器在登录成功时顺带创建该账户。如果没有这项功能，未知账户会在任何方法运行之前就被拒绝。

使用 `.auto_create(&callback)` 选择启用。该回调不接受参数并返回 `bool`；服务器在每次未知账户登录时调用它，而不是在注册时读取一次，因此该方法可以遵循它自己的运行时设置，而不必在扩展程序加载时就固定这一选择：

```cpp theme={null}
bool auto_create_enabled() { return true; }

constexpr auto AUTH_METHOD =
    vsql::preview_auth::make_auth<&authenticate>("vsql_auth_test")
        .client_plugin("mysql_clear_password")
        .auto_create(&auto_create_enabled)
        .build();
```

不使用 `.auto_create()`，或从回调返回 `false`，都会保持标准行为：未知账户被拒绝。同一时间只能有一个已安装的方法选择启用它——如果有两个返回 `true`，服务器不会去猜测，而是向错误日志记录一条警告，并像没有任何方法选择启用一样拒绝未知账户。

在处理程序中，`c.account_unknown()` 用于区分这两种情况。请先验证凭据，然后描述要创建什么并以其身份进行身份验证：

```cpp theme={null}
if (c.account_unknown()) {
  const char *roles[] = {"vsql_role_granted"};
  c.request_provision(c.user_name(), roles, 1);
  c.authenticate_as(c.user_name());
  c.set_external_user(c.user_name());
  return AuthResult::kOk;
}
```

`request_provision(account, roles, n_roles)` 记录意图，不返回任何内容。服务器会在处理程序返回 `AuthResult::kOk` 之后自行运行 DDL，并且仅针对作为未知账户被路由进来的登录——因此处理程序随后拒绝的登录不会创建任何东西，而指定一个已存在账户的请求会被忽略。服务器运行的是 `CREATE USER IF NOT EXISTS <account>@'%' IDENTIFIED WITH <method>`，随后为每个指定的角色执行一条 `GRANT`：该账户始终为主机 `%` 创建并绑定到对其进行身份验证的方法，而且 `account` 不必是连接的用户名。如果无法完成创建——例如在 `super_read_only` 服务器上——则登录会失败，而不会在没有账户的情况下继续。

角色的行为与[暂存活动角色](#staging-active-roles)中一致：角色归 DBA 所有。每个名称都必须已经作为可授予的角色存在，无法授予的角色会被记录并跳过，而不会使登录失败，因此令牌可以指定角色，但永远无法创建或提升角色。账户名来自客户端，因此服务器会将其作为标识符加引号——精心构造的名称只会变成一个名字古怪的账户，而绝不会变成第二条语句。

`vsql_auth_test` 扩展程序为连接用户配置角色 `vsql_role_granted`，并将这一选择项置于 `vsql_auth_test.auto_create` 之后，该变量初始为 `OFF`。先将它打开并创建该角色，然后以一个不存在的账户进行连接：

```sql theme={null}
INSTALL EXTENSION vsql_auth_test;
SET GLOBAL vsql_auth_test.auto_create = ON;
CREATE ROLE vsql_role_granted;
GRANT SELECT ON *.* TO vsql_role_granted;
```

```bash theme={null}
mysql --enable-cleartext-plugin --user=auto_created_user \
      --password=vsql-auth-test-token \
      -e "SELECT CURRENT_USER() AS who, @@external_user AS ext"
```

```text theme={null}
+---------------------+-------------------+
| who                 | ext               |
+---------------------+-------------------+
| auto_created_user@% | auto_created_user |
+---------------------+-------------------+
```

该账户现在已经存在，绑定到该方法，并持有已授予的角色：

```sql theme={null}
SELECT user, host, plugin FROM mysql.user WHERE user = 'auto_created_user';
```

```text theme={null}
+-------------------+------+----------------+
| user              | host | plugin         |
+-------------------+------+----------------+
| auto_created_user | %    | vsql_auth_test |
+-------------------+------+----------------+
```

```sql theme={null}
SHOW GRANTS FOR 'auto_created_user'@'%';
```

```text theme={null}
+----------------------------------------------------------+
| Grants for auto_created_user@%                           |
+----------------------------------------------------------+
| GRANT USAGE ON *.* TO `auto_created_user`@`%`            |
| GRANT `vsql_role_granted`@`%` TO `auto_created_user`@`%` |
+----------------------------------------------------------+
```

错误的令牌仍然会故障关闭，并且不会配置任何东西：

```bash theme={null}
mysql --enable-cleartext-plugin --user=never_created --password=wrong-token \
      -e "SELECT 1"
```

```text theme={null}
ERROR 1045 (28000): Access denied for user 'never_created'@'localhost' (using password: YES)
```

<Warning>
  选择启用会使未知账户与已存在账户之间的差别对任何持有有效凭据的人变得可观察，而标准的未知账户拒绝行为刻意隐藏了这一点。这是此功能所作的取舍；在为一个凭据被广泛持有的方法启用该选择项之前，请权衡这一点。
</Warning>

<h3 id="auto-granting-roles">
  自动授予角色
</h3>

默认情况下，令牌指定的角色只有在账户已经持有它时才会生效，而账户未持有的角色会被记录并跳过。`.auto_grant(&callback)` 改变了这一点：服务器会将暂存的角色授予该账户，因此由令牌决定会话获得哪些角色，而不仅仅是决定打开账户现有角色中的哪些。

该回调在形态上与 `.auto_create()` 一致——不接受参数，返回 `bool`，并且服务器在每次登录时调用它，因此它可以遵循一项运行时设置：

```cpp theme={null}
bool auto_grant_enabled() { return true; }

constexpr auto AUTH_METHOD =
    vsql::preview_auth::make_auth<&authenticate>("vsql_auth_test")
        .client_plugin("mysql_clear_password")
        .auto_grant(&auto_grant_enabled)
        .build();
```

这两个选择项是相互独立的。`.auto_create()` 管辖针对不存在账户的登录；`.auto_grant()` 管辖向登录所解析到的账户授予角色，无论该账户是否是刚刚创建的。不使用 `.auto_grant()`，或返回 `false`，都会保持仅激活的默认行为。

该授予是持久的——它是一次普通的 `GRANT`，而不是仅限会话的激活——并且是累加的：服务器永远不会撤销令牌不再指定的角色。

`vsql_auth_test` 将其公开为 `vsql_auth_test.auto_grant`，同样初始为 `OFF`。它的 `-token-roles` 令牌暂存 `vsql_role_granted` 和 `vsql_role_denied`，而下面的账户两者都不持有。在该设置关闭时，登录不会改动该账户的角色：

```sql theme={null}
CREATE USER auth_user IDENTIFIED WITH vsql_auth_test;
CREATE USER vsql_auth_test_user;
GRANT SELECT ON *.* TO vsql_auth_test_user;
GRANT PROXY ON vsql_auth_test_user TO auth_user;
CREATE ROLE vsql_role_granted, vsql_role_denied;
```

以与上面相同的方式，用该令牌以 `auth_user` 身份连接，并查询哪些角色处于活动状态：

```text theme={null}
+----------------+
| CURRENT_ROLE() |
+----------------+
| NONE           |
+----------------+
```

打开该设置并重复同一次登录：

```sql theme={null}
SET GLOBAL vsql_auth_test.auto_grant = ON;
```

```text theme={null}
+------------------------------------------------+
| CURRENT_ROLE()                                 |
+------------------------------------------------+
| `vsql_role_denied`@`%`,`vsql_role_granted`@`%` |
+------------------------------------------------+
```

现在两个角色都处于活动状态，并且 `SHOW GRANTS` 会显示服务器添加的授予：

```sql theme={null}
SHOW GRANTS FOR vsql_auth_test_user;
```

```text theme={null}
+-----------------------------------------------------------------------------------+
| Grants for vsql_auth_test_user@%                                                  |
+-----------------------------------------------------------------------------------+
| GRANT SELECT ON *.* TO `vsql_auth_test_user`@`%`                                  |
| GRANT `vsql_role_denied`@`%`,`vsql_role_granted`@`%` TO `vsql_auth_test_user`@`%` |
+-----------------------------------------------------------------------------------+
```

<Warning>
  在 `.auto_grant()` 打开时，一个有效的令牌就足以获得它所指定的任何角色。该角色必须已经存在，因此令牌仍然无法凭空创造权限，但决定账户可以取得哪些现有角色的不再是 DBA，而是该方法。
</Warning>
