> ## 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` が設定されている場合にのみインストールできます（[プレビュー層の有効化](#enabling-the-preview-tier)を参照）—プレビュー機能を使用しない拡張機能は、この設定に関係なく通常通りインストールされます。

<Warning>
  プレビュー機能APIは安定していません。プレビュー機能に依存して構築された拡張機能は、サーバーのアップデート後にロードに失敗する可能性があります。機能が安定化すると、そのヘッダーはバージョン化された安定版 C++ SDK パスに移動されます。
</Warning>

<h2 id="enabling-the-preview-tier">
  プレビュー層の有効化
</h2>

プレビュー機能を使用する拡張機能をインストールする前に、`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` がまだ存在しないため、永続化された値を保持できません：

```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>`   | プレビュー                                     |

<h2 id="registration-pattern">
  登録パターン
</h2>

プレビュー機能を使用するには、ファイルスコープで機能オブジェクトを値として宣言し、`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()` に正確に1回渡す必要があります。ロード時にサーバーは宣言されたすべての機能インスタンスを `.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">
  Keyring アクセス
</h2>

keyring 機能（`vsql::preview::keyring`）は、MySQL keyring コンポーネントに格納されたシークレットを拡張機能が読み書きできるようにします。APIキー、暗号化キー、またはSQLテーブルに保存したくないその他のシークレットに使用します。

機能名 `VEF_PREVIEW_KEYRING_NAME` は `"vsql::preview::keyring"` です。

読み書きが成功するには、MySQLサーバーに keyring コンポーネントがインストールされている必要があります。インストールされていない場合、操作は `KeyringCapability::Status::UNAVAILABLE` を返します。

### ステータス値

`KeyringCapability::Status` は `read()`（`ReadResult` 内）および `write()` から返されるスコープ付き列挙型です：

| ステータス                 | 意味                             |
| --------------------- | ------------------------------ |
| `Status::OK`          | 操作が成功しました。                     |
| `Status::NOT_FOUND`   | キーが存在しません（読み取りのみ）。             |
| `Status::UNAVAILABLE` | keyring コンポーネントがインストールされていません。 |
| `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` オブジェクトはロード時にサーバーによって初期化されます。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` は値で `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 コンポーネントが利用するのと同じサービスで、インストールされたコンポーネントまたはサーバーコアによって提供されます。拡張機能は必要なすべてのサービスを1か所で宣言します。サーバーは拡張機能のロード時に各サービスを取得し、拡張機能のアンロード時に解放します。

機能名 `VEF_PREVIEW_MYSQL_SERVICES_NAME` は `"vsql::preview::mysql_services"` です。

サーバーの機構に専用の VEF 機能がない場合にこれを使用します。セッション属性と keyring 自身のコンポーネントサービスは、どちらもこの方法で利用できます。サポートされるのは利用のみです。拡張機能自身の実装をレジストリに登録することは今後の作業として計画されており、この機能には含まれません。

### 機能の宣言

ファイルスコープで `MysqlServices` オブジェクトを1つ宣言し、利用する各サービスを `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` を宣言し、`name` を `services` に登録します。`var` は自動的に `static` として宣言されます。`MysqlServices` オブジェクトも `static` である必要があり、手動で宣言する参照も同様です。サーバーはロード時にそれらを介して書き込むため、それらは拡張機能より長く存続する必要があります。

<h3 id="pinning-a-specific-implementation">
  特定の実装の指定
</h3>

`VSQL_REQUIRE_SERVICE` は `name` を2回使用します — C++ の `SERVICE_TYPE(name)` として、およびサーバーがレジストリで検索する文字列として。この修飾なしの名前では、サーバーはサービスのデフォルト実装を取得します。

代わりに1つの実装を指定するには、修飾されたレジストリ名 — MySQL の `PROVIDES_SERVICE(component, service)` が生成する `service.component` の形式 — を使用します。以下は、デフォルトではなく `component_keyring_file` コンポーネントの keyring リーダーを要求します：

```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>

サービス定義は VEF ではなく MySQL のコンポーネントフレームワークに属しており、サーバーはそれらをインストールしません。そのため `mysql/components/services/*.h` は、拡張機能 SDK にも、`make install` が構築するもの（リリース tarball や 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` として渡します。ツリー外のビルドは独自のインクルードパスを設定します。

2つのビルド失敗は、原因となった行とは別の場所で報告されます。

サービスの MySQL ヘッダーを省略すると、`VSQL_REQUIRE_SERVICE` に何も解決しない名前が残るため、エラーは不足しているインクルードではなくマクロの位置に表示されます（clang 17）：

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

一部のサービス定義は `<cstddef>` を含めずに `size_t` を使用するため、それらのヘッダーの1つをすべての 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()` を確認してください。`->` は取得されたポインタを返しますが、サービスが取得されなかった場合そのポインタは NULL です。

取得に失敗したサービスはインストールを失敗させるため、実行中の関数の内部では、必要なサービスは有効です。それでも確認は重要です。手動で宣言され `require()` に渡されなかった `ServiceRef` には何も書き込まれないためです。コンパイルは通り、拡張機能はインストールされ、`valid()` は拡張機能の生存期間を通じて false のままになります。

サービスが*何であるか* — そのメソッド、パラメータ、戻り値 — は、ここではなく MySQL によって文書化されています。`NAME` という名前のサービスについては、サーバーツリーの `include/mysql/components/services/NAME.h` を読んでください。その `BEGIN_SERVICE_DEFINITION(NAME)` ブロックが、各メソッドを独自のドキュメントとともに宣言しています。`bool` の戻り値が `false` の場合は成功、`true` の場合は失敗を意味するという MySQL の慣習を含め、そのヘッダーが指定するとおりに呼び出してください。

<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'
```

他に2つのインストール失敗が、この機能の外側から同じ機能に到達します：`MysqlServices` オブジェクトを `.with()` に渡さない場合と、`vsql_allow_preview_extensions` が OFF のサーバーにインストールする場合です。どちらも[登録パターン](#registration-pattern)で説明しています。

### 完全な例

サーバーの `villagesql/test-extensions/` ツリーにある `vsql_mysql_services_session_test` の簡略化されたバージョンです。2つのサービスを組み合わせて、呼び出し元のセッションで実行中の 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`）は、拡張機能が MySQL ステータス変数として `long long` および `double` カウンターを公開できるようにします。拡張機能はストレージを所有し、値を書き込みます。サーバーはステータス変数がクエリされるたびにポインタ経由で読み取ります。

`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 *` が必要です。これらがサポートされる唯一の2つの型です。

### 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 システム変数を登録できるようにします。4つの型がサポートされます：`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>()` をチェーンします。コールバックは `var_name()` および型付きアクセサ（`as_int()`、`as_real()`、`as_str()`）を備えた `sv::SysVarChange` を受け取ります。

サーバーは、グローバルなシステム変数ロックを保持したままそのコールバックを呼び出します。その中でこの拡張機能の別の変数をストレージポインタ経由で読み書きすることは安全です。また、サーバーが同じロックの下でそれらの変数を読み取るため、他のセッションはすぐに新しい値を参照できます。

<Warning>
  機能の `get()` または `set()` を呼び出すこと、SQL を実行すること、あるいはそのいずれかを行うスレッドを待機することは、そのロックでデッドロックします。コールバックは短くブロックしないように保ち、SQL を必要とする処理は[スレッドワーカー](#thread-worker)に渡すか、`sql/sys_vars.cc` の `event_scheduler_update()` が行うように、ブロックする部分の前後で `LOCK_global_system_variables` を解放し、戻る前に再取得してください。
</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` にのみ書き込み、次回起動時に有効になります。 |

<h2 id="thread-worker">
  スレッドワーカー
</h2>

thread worker 機能（`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>`）として渡されるため、以下のシグネチャを持つ関数でなければなりません。最初のコンストラクタ引数はスレッド名サフィックスです。オプションの2番目の引数は制御 sys var 名をオーバーライドします。

### 作業関数シグネチャ

```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` は記述子に登録された不透明ポインタで、変更されずに渡されます。

### Wakeup ライフサイクル

サーバーは4つの理由のいずれかで作業関数を呼び出します：

| 理由                    | 意味                                                                             |
| --------------------- | ------------------------------------------------------------------------------ |
| `VEF_WAKEUP_ENABLE`   | ワーカーが有効化された（制御 sys var が ON に切り替えられた）。戻り値は初期の `poll_fd` および `sleep_ms` を設定します。 |
| `VEF_WAKEUP_PERIODIC` | 定期タイマーが発火（`sleep_ms` 経過）。                                                      |
| `VEF_WAKEUP_POLL_FD`  | 監視中のファイルディスクリプタが読み取り可能になりました。                                                  |
| `VEF_WAKEUP_DISABLE`  | ワーカーが無効化（制御 sys var OFF）またはサーバーがシャットダウン中。戻り値は無視されます。                           |

理由が `VEF_WAKEUP_ENABLE` の場合、`thread` パラメータは NULL です。この時点でスレッドハンドルはまだ存在しないためです。他の3つの理由では `thread` は非 NULL です。

### Wakeup 戻り値

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

作業関数は `vef_next_wakeup_t` を返し、次回のワーカー起動設定を更新します。各フィールドのゼロ値は「現在の設定を維持」を意味します — 両方を変更しない場合は値初期化構造体を返します（`return {};`）。

新しい poll ファイルディスクリプタを設定するには、その値を返します（0より大きい必要があります）。既存の poll ファイルディスクリプタをクリアするには、`poll_fd` に `-1` を返します。

理由が `VEF_WAKEUP_DISABLE` の場合、戻り値は無視されます。

### スレッド名と制御変数

記述子の2つのフィールドが命名を制御します：

* `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>
  SQL セッションは、スレッドワーカーのコールバックからそのコールバックの `vef_thread_handle_t *` を使用して開く必要があります。`open()` は VDF または任意の拡張機能作成スレッドから有効ではありません — これはワーカーセッションコンテキストを必要とします。
</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` を生成します。クエリは2つのモードで実行できます：

* `execute()` — 文を実行し、完全な結果セットを `Result` にバッファリングします。`next()` を呼び出して行をイテレートします。
* `for_each(fn)` — 文を実行し、行が生成されるたびに `fn` を1回呼び出します。バッファリングしません。返される `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` 破棄まで有効です。より長い寿命が必要な場合はコピーしてください。`data() == nullptr` の `string_view` は 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` を介して診断情報を返します。診断は1つの `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` が破棄されると無効になります — `Result` の生存期間を超えて保持する必要がある場合はコピーしてください。

### 完全な例

タイマー刻みごとに1つのバッファリングクエリと1つのストリーミングクエリを実行し、両方の診断をログ出力するワーカー：

```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>

### 機能の宣言

2つのプレビュー機能が連携して動作します：

* `vsql::preview::storage` — InnoDB ストレージインフラストラクチャ（ミニトランザクション、セグメント、ページ）へのアクセスを提供します。ファイルスコープで `StorageCapability` を宣言します。
* `vsql::preview::column_store` — 型ごとのストレージ実装を、拡張機能のカスタム型の1つにバインドします。`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)` は実装を同じ拡張機能で登録された1つのカスタム型に紐付けます。`build()` 時に7つのスロットが必要です。各スロットは InnoDB が通常動作中に到達するカラムライフサイクルの異なるポイントに対応します。

### 7つのストレージ関数

すべての関数は `storage::Column::StorageCtx<MyCtx>*` を受け取ります。`user()` アクセサは拡張機能のカラムごとの状態を返し、`arena()` は補助オブジェクトのサーバー管理割り当てを提供します。すべての関数は成功時に `false` を返し、エラー時に `true` を返し、失敗が SQL クライアントに伝わるように `error_msg`（容量 `error_msg_len`）にメッセージを書き込みます。

```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 が実行されるまで古いスナップショットで読み取り可能でなければなりません。

### カラムごとのコンテキストとアリーナ

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` は `create` で `ctx->set_ref()` が格納したパックされた値を保持します：

```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` から戻った後にアリーナを自動的に破棄し（`~MyCtx()` を呼び出し）ます。

### InnoDB アクセスユーティリティ

InnoDB プリミティブには `<villagesql/preview/storage_api.h>` を含めます。すべてのページ読み書きはミニトランザクション内で行われます：

```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();
```

ミニトランザクションのコミットはページラッチを解放し、変更を永続化する redo ログレコードを書き込みます。

**セグメント** は `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
```

**ページ** は共有ラッチで読み取り、排他ラッチで書き込みます。InnoDB が変更をログに記録するため、`mtr_ref` を書き込み呼び出しに渡します：

```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)` | runtime | 16384 をハードコードする代わりに使用してください。                |

ヘッダーまたはトレーラー領域内での読み取りまたは書き込みはページを破損させます — InnoDB はこれらのバイト範囲を自身の管理およびチェックサムに使用しています。

## ステートメントイベント

statement event 機能（`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()`                                                                                                 | デフォルトスキーマ、選択されていない場合は `NULL`。                                                                                                                 |
| `status()`                                                                                                 | 成功時は `0`、それ以外は MySQL エラーコード。                                                                                                                  |
| `digest_text()`                                                                                            | 類似クエリをグループ化するための正規化されたクエリ形式。                                                                                                                  |
| `no_index_used()`                                                                                          | クエリが使用可能なインデックスなしで実行された場合 `true`。                                                                                                             |
| `digest_hash()`                                                                                            | 64文字の小文字16進数によるステートメントダイジェスト — `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) テスト拡張機能の簡略化された形式です。実行時間がしきい値を超える各クエリをログに記録し、statement event 機能を[システム変数](#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 からの有効化

プレビュー層を有効化した上で（[プレビュー層の有効化](#enabling-the-preview-tier)を参照）、拡張機能をインストールし、そのシステム変数を通じて設定します：

```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 認証プラグインを書かずに、サーバーが知らない認証情報ソース — ベアラートークン、外部 ID プロバイダー、カスタムチャレンジなど — に対してアカウントを認証する場合に使用します。

機能名 `VEF_PREVIEW_AUTH_NAME` は `"vsql::preview::auth"` です。

ハンドラは `AuthContext` を受け取る型付き関数です。サーバー所有のそのコンテキストを介してハンドシェイクパケットを読み書きすることでクライアントと通信し、MySQL の内部認証構造体を参照することはありません。

<Warning>
  認証結果はフェイルクローズです。サーバーは `AuthResult::kOk` 以外のすべてを接続拒否として扱います — 「たぶん」やフェイルオープンの結果は意図的に存在しません。`AuthResult::kReject` を返すハンドラ、`AuthResult::kError` を返すハンドラ、または実効アカウントを設定しないハンドラは、ログインを拒否します。
</Warning>

### 機能の宣言

ヘッダーを含め、型付きハンドラを記述し、`make_auth<>` ビルダーのチェーン呼び出しで記述子を構築し、その記述子を `.with()` に渡す `AuthCapability` トークンに渡します。プレビュー機能のヘッダーは `<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))
```

ビルダーは6つの要素で構成されます：

| 要素                                 | 意味                                                                                                                                                                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `make_auth<&handler>("name")`      | ビルダーを開始します。ハンドラはコンパイル時のテンプレート引数であるため、NULL または誤ったシグネチャのハンドラは実行時の失敗ではなくコンパイルエラーになります。`"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` はパスワードスロットでベアラートークンをそのまま受け取ります。

方式が要求するものとは別のプラグインを提示するクライアントは、要求されたプラグインに切り替えられ、認証情報をそのまま再送します。これには1往復のコストがかかり、その切り替えに応じるクライアントが必要です。`.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` と組み合わせると、1回の読み取りでベアラートークンが得られます。                                                                   |
| `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)`                | 監査証跡のために元の外部 ID を設定します（`@@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)を参照）。                                                                                                                                                 |

ハンドラは3つの結果のいずれかを返します：

| 結果                    | 意味                                                                                                       |
| --------------------- | -------------------------------------------------------------------------------------------------------- |
| `AuthResult::kOk`     | 認証に成功しました。ハンドラは `authenticate_as()` を呼び出している必要があります。セッションはそのアカウントとして実行されます。                              |
| `AuthResult::kReject` | 認証に失敗しました — 認証情報が不正、またはポリシーによる拒否。                                                                        |
| `AuthResult::kError`  | 内部エラーにより判断できませんでした（例：キーソースが利用できなかった）。サーバーは拒否とまったく同じように扱います。これは、ログ記録において「拒否した」と「判断できなかった」を区別するためだけに存在します。 |

`AuthResult::kReject` と `AuthResult::kError` はどちらも接続を拒否します。成功するのは `AuthResult::kOk` のみです。

ハンドラが接続アカウントを別の実効アカウントにマッピングする場合 — 以下の例が接続アカウントを `vsql_auth_test_user` にマッピングするように — それはプロキシであり、MySQL のプラグイン認証の経路とまったく同じように `GRANT PROXY` が必要です。

<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` 拡張機能を凝縮した、最小限の認証機能です。この拡張機能はどのリリースにも含まれていません。固定のトークンを1つ受け入れ、接続を `vsql_auth_test_user` にマッピングし、トークンがパスワードスロットにそのまま届くように `mysql_clear_password` を要求します。（ツリー内の拡張機能は、テストスイートを動かすために追加のトークン経路、`.accepts_client_plugin()` コールバック、および以下で説明する2つのオプトインを備えています。）

```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>

プレビュー層を有効化した上で（[プレビュー層の有効化](#enabling-the-preview-tier)を参照）、拡張機能をインストールし、アカウントをこの方式にバインドします。ハンドラは2つ目のアカウントにマッピングするため、そのアカウントも作成し、接続アカウントがその ID を引き継げるようにする `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;
```

`vsql_auth_test` は登録済みの VEF 認証方式であるため、`CREATE USER ... IDENTIFIED WITH vsql_auth_test` は受け入れられます — インストール済みのプラグイン名が受け入れられるのと同じ仕組みです。

受け入れられるのは `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` を返す場合、標準の動作が維持されます：未知のアカウントは拒否されます。同時にオプトインできるインストール済みの方式は1つだけです — 2つが `true` を返した場合、サーバーは推測を行わず、エラーログに警告を記録し、どの方式もオプトインしていない場合と同じように未知のアカウントを拒否します。

ハンドラ内では、`c.account_unknown()` が2つのケースを区別します。まず認証情報を検証し、次に何を作成するかを指定して、そのアカウントとして認証します：

```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>` で、続いて指定されたロールごとに1つの `GRANT` を実行します。アカウントは常にホスト `%` に対して作成され、認証を行った方式にバインドされます。また `account` は接続ユーザー名である必要はありません。作成ができない場合 — 例えば `super_read_only` のサーバーでは — アカウントなしで処理を続行するのではなく、ログインが失敗します。

ロールの扱いは[アクティブロールのステージング](#staging-active-roles)と同じです：ロールは DBA が管理します。各名前は付与可能なロールとしてすでに存在している必要があり、付与できないものはログインを失敗させるのではなくログに記録されてスキップされます。そのためトークンはロールを指定できますが、ロールを作成したり昇格させたりすることはできません。アカウント名はクライアントから渡されるため、サーバーはそれを識別子として引用符で囲みます — 細工された名前は奇妙な名前のアカウント1つになるだけで、2つ目の文になることはありません。

`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();
```

2つのオプトインは独立しています。`.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>
