> ## 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, session_var, thread_worker, sql_query, column_store, storage 및 statement_event 기능, 그리고 등록 패턴을 다룹니다.

미리보기 기능은 API가 최종화되기 전에 확장 프로그램에 노출되는 서버 제공 기능입니다. 미리보기 기능을 선언하는 확장 프로그램은 설치 시 `vsql_allow_preview_extensions = ON`이 필요합니다 (참조: [미리보기 계층 활성화](#enabling-the-preview-tier)) — 미리보기 기능을 사용하지 않는 확장 프로그램은 이 설정과 무관하게 일반적으로 설치됩니다.

> [!WARNING]
>
> 미리보기 기능 API는 안정적이지 않습니다. 미리보기 기능을 기반으로 빌드된 확장 프로그램은 서버 업데이트 후 로드에 실패할 수 있습니다. 기능이 안정화되면 헤더가 버전화된 안정적 C++ SDK 경로로 이동합니다.

## 미리보기 계층 활성화

미리보기 기능을 사용하는 확장 프로그램을 설치하기 전에 `vsql_allow_preview_extensions = ON`을 `SET PERSIST`로 설정합니다:

```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::index_profile` | `<villagesql/preview/index_builder.h>` | 미리보기 — dev ABI 전용 (`-DVSQL_USE_DEV_ABI=ON`); [사용자 정의 인덱스](https://villagesql.com/docs/ko/extensions/build/custom-indexes.md) 참조 |
| `vsql::preview::index_type` | `<villagesql/preview/index_builder.h>` | 미리보기 — dev ABI 전용 (`-DVSQL_USE_DEV_ABI=ON`); [사용자 정의 인덱스](https://villagesql.com/docs/ko/extensions/build/custom-indexes.md) 참조 |
| `vsql::preview::keyring` | `<villagesql/preview/keyring.h>` | 미리보기 |
| `vsql::preview::mysql_services` | `<villagesql/preview/mysql_services.h>` | 미리보기 — dev ABI 전용 (`-DVSQL_USE_DEV_ABI=ON`) |
| `vsql::session_var` | `<villagesql/preview/session_var.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`
>
> 전체 오류는 다음과 같이 표시됩니다: `Failed to load VEF extension '<name>': vef_register returned an error: <message above>`.

## Keyring 접근

keyring 기능(`vsql::preview::keyring`)은 MySQL keyring 구성 요소에 저장된 비밀을 읽고 쓸 수 있게 해줍니다. 확장 프로그램은 API 키, 암호화 키 또는 SQL 테이블에 저장되지 않아야 하는 기타 비밀을 처리하는 데 사용합니다.

기능 이름 `VEF_PREVIEW_KEYRING_NAME`은 `"vsql::preview::keyring"`입니다.

keyring 구성 요소(예: `component_keyring_file`)는 [Keyring 구성 요소 설치](https://dev.mysql.com/doc/refman/8.4/en/keyring-component-installation.html)에 설명된 대로 구성 요소 매니페스트와 그 구성 파일을 통해 서버 시작 시 로드되어야 합니다. `INSTALL COMPONENT`로는 사용할 수 없습니다. 없으면 작업은 `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` 아래에 저장합니다. `component_keyring_file`에서는 기존 키에 쓰면 `Status::ERROR`가 반환됩니다.

### 완전한 예제

이것은 서버의 `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();
  if (value.size() <= buf.size())
    memcpy(buf.data(), value.data(), value.size());
  out.set_length(value.size());
}

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

## MySQL 서비스

mysql\_services 기능(`vsql::preview::mysql_services`)은 확장 프로그램이 MySQL 레지스트리 서비스를 사용할 수 있게 해줍니다 — MySQL 구성 요소가 사용하는 것과 동일한 서비스로, 설치된 구성 요소 또는 서버 코어가 제공합니다. 확장 프로그램은 필요한 모든 서비스를 한곳에서 선언하고, 서버는 확장 프로그램이 로드될 때 각 서비스를 획득하고 확장 프로그램이 언로드될 때 해제합니다.

기능 이름 `VEF_PREVIEW_MYSQL_SERVICES_NAME`은 `"vsql::preview::mysql_services"`입니다.

서버 설비에 자체 VEF 기능이 없을 때 이것을 사용하세요. 세션 속성과 keyring 자체의 구성 요소 서비스는 모두 이 방식으로 접근할 수 있습니다. 사용만 지원됩니다: 확장 프로그램 자체의 구현을 레지스트리에 등록하는 것은 향후 계획된 작업이며 이 기능에 포함되지 않습니다.

### 기능 선언

파일 범위에서 하나의 `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`이어야 하며, 직접 선언하는 모든 참조도 마찬가지입니다: 서버가 로드 시 이들을 통해 값을 기록하므로, 확장 프로그램보다 오래 유지되어야 합니다.

### 특정 구현 고정

`VSQL_REQUIRE_SERVICE`는 `name`을 두 번 사용합니다 — C++ `SERVICE_TYPE(name)`으로, 그리고 서버가 레지스트리에서 조회하는 문자열로. 이 단순 이름 아래에서 서버는 해당 서비스의 기본 구현을 획득합니다.

대신 하나의 구현을 지정하려면, 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);
```

정규화된 이름은 단순 이름과 동일한 방식으로 획득되므로, 일반적인 규칙이 그대로 적용됩니다: 해당 구현이 정확히 등록되어 있지 않으면 확장 프로그램은 다른 구현으로 대체되지 않고 설치에 실패합니다.

### MySQL 헤더에 대해 빌드

서비스 정의는 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`로 전달합니다. 아웃오브트리 빌드는 자체 포함 경로를 설정합니다.

두 가지 빌드 실패는 원인이 된 줄이 아닌 다른 곳에서 나타납니다.

서비스의 MySQL 헤더를 생략하면 `VSQL_REQUIRE_SERVICE`에 아무것도 해석되지 않는 이름이 남으므로, 오류는 누락된 include가 아니라 매크로에서 나타납니다(clang 17):

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

일부 서비스 정의는 `<cstddef>`를 포함하지 않고 `size_t`를 사용하므로, 그러한 헤더 중 하나를 모든 villagesql 헤더보다 앞에 두면 MySQL 자체 헤더 내부에서 실패합니다:

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

이 페이지의 예제처럼 `<cstddef>`를 먼저 포함하세요.

### 서비스 호출

서비스 참조는 자체적으로 `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의 관례를 포함하여, 해당 헤더가 명시하는 그대로 호출하세요.

### 획득 실패

선언된 모든 서비스는 확장 프로그램이 로드될 때, 즉 어떤 함수도 호출되기 전에 획득되므로, 등록되지 않은 서비스는 나중에 드러나지 않고 로드를 실패시킵니다. `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인 서버에 설치하는 경우입니다. 둘 다 [등록 패턴](#registration-pattern)에서 다룹니다.

### 완전한 예제

서버의 `villagesql/test-extensions/` 트리에 있는 `vsql_mysql_services_session_test`의 간단한 버전입니다. 이는 두 서비스를 조합하여 호출 세션에서 실행 중인 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 my_ext;
SELECT my_ext.session_sql_command() AS sql_command;
```

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

## 상태 변수

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 *`를 요구합니다. 이 두 유형만 지원됩니다.

### 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`를 통해 노출된 근사 호출 카운터에 대해 허용됩니다.

## 시스템 변수

sys\_var 기능(`vsql::sys_var`)은 확장 프로그램이 소유한 저장소를 기반으로 하는 MySQL 시스템 변수를 등록할 수 있게 해줍니다. 네 가지 유형이 지원됩니다: `BOOL`(`bool *`), `INT`(`long long *`), `STR`(`char **`), 그리고 dev ABI 전용으로 `ENUM`(`unsigned long *`, 고정된 순서의 이름 목록에 대한 0 기반 인덱스를 저장). `INT` 디스크립터는 `min_val` 및 `max_val` 경계도 포함하며, 모든 디스크립터는 기본값과 주석을 포함합니다.

`vsql::preview_sys_var::make_capability()`으로 기능을 구축하고, 대응하는 팩토리 함수인 `make_bool`, `make_int`, `make_str`, 그리고 `make_enum`(dev ABI 전용)을 사용합니다. 기능 객체는 확장 코드에서의 프로그래밍 접근을 위해 `get()` 및 `set()`을 노출합니다. 둘 다 성공 시 `false`를 반환합니다.

값 변경에 반응하려면 디스크립터에 `.on_change<&fn>()`을 연결합니다. 콜백은 `var_name()` 및 타입 접근자(`as_int()`, `as_str()`, 그리고 dev ABI 전용으로 `as_enum()`)를 포함하는 `sv::SysVarChange`를 수신합니다.

서버는 전역 시스템 변수 잠금을 보유한 상태에서 그 콜백을 호출합니다. 그곳에서 이 확장 프로그램의 다른 변수를 저장소 포인터를 통해 읽거나 쓰는 것은 안전하며, 서버가 동일한 잠금 아래에서 그 변수들을 읽기 때문에 다른 세션은 새 값을 즉시 봅니다.

> [!WARNING]
>
> 기능의 `get()` 또는 `set()`을 호출하거나, SQL을 실행하거나, 둘 중 하나를 수행하는 스레드를 기다리면 그 잠금에서 교착 상태가 발생합니다. 콜백을 짧고 논블로킹으로 유지하고, SQL이 필요한 작업은 [스레드 워커](#thread-worker)에 넘기거나, `sql/sys_vars.cc`의 `event_scheduler_update()`가 하는 것처럼 블로킹 부분 주위에서 `LOCK_global_system_variables`를 해제했다가 반환하기 전에 다시 획득하세요.

기능 객체는 정적 저장 기간을 가져야 합니다. MySQL은 사용자가 변수를 설정할 때 저장소 포인터에 직접 씁니다.

| 팩토리 | 저장소 유형 | 추가 매개변수 |
| - | - | - |
| `sv::make_bool` | `bool *` | `def_val` |
| `sv::make_int` | `long long *` | `def_val`, `min_val`, `max_val` |
| `sv::make_str` | `char **` | `def_val` |
| `sv::make_enum` | `unsigned long *` | `names`, `name_count`, `def_val` |

열거형 변수는 이름(`SET GLOBAL my_ext.mode = 'sync'`)으로 또는 이름의 0 기반 인덱스로 설정합니다. 어느 쪽이든 서버는 값을 수락하기 전에 `names`와 대조하여 검증하며, 인식되지 않는 이름이나 범위를 벗어난 인덱스는 저장하는 대신 거부합니다:

```
ERROR 1231 (42000): Variable 'my_ext.mode' can't be set to the value of 'bogus'
```

이 검증이 자유 형식의 `make_str`과 구별되는 점입니다: 값 공간이 닫혀 있고 `SET` 시점에 강제됩니다. 편의 오버로드는 `static const char *const NAMES[] = {...}` 배열에서 `name_count`를 추론하므로, 개수 인수를 생략할 수 있습니다.

### 완전한 예제

이 예제는 `make_enum`을 사용하므로, dev ABI(`-DVSQL_USE_DEV_ABI=ON`)로 빌드하세요.

```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 unsigned long g_mode  = 0;  // index into MODE_NAMES; default "off"

static const char *const MODE_NAMES[] = {"off", "on", "sync"};

static void on_threshold_change(sv::SysVarChange c) {
  // c.var_name() identifies the variable; c.as_int().value() 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"),
    sv::make_enum("mode",         "Operating mode",  &g_mode,      MODE_NAMES, 0)});

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';
SET GLOBAL my_ext.mode = 'sync';
```

### 확장 코드에서 읽기 및 쓰기

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`에만 쓰기; 다음 재시작 시 적용. |

## 세션 변수

session\_var 기능(`vsql::session_var`)은 확장 프로그램이 세션별(THD 로컬) 시스템 변수를 등록할 수 있게 해줍니다 — 플러그인의 `MYSQL_THDVAR_*`에 기능적으로 상응하는 것입니다. 이는 [`vsql::sys_var`](#system-variables)의 세션 범위 형제입니다: sys\_var는 모든 연결이 공유하는 하나의 값을 갖는 반면, session\_var는 각 연결에 `SET SESSION`으로 설정하는 자체 값을 부여합니다. 새 세션의 시작점은 디스크립터의 기본값 또는 `SET GLOBAL`로 마지막으로 저장된 값입니다. 변수의 범위는 등록 시 고정됩니다 — 세션별 변수에는 이 기능을, 전역 변수에는 `vsql::sys_var`를 사용하세요.

두 가지 유형만 지원됩니다: `INT`(`long long`)와 `STR`(`std::string`). `BOOL` 또는 `DOUBLE` 세션 변수는 없으며, `on_change` 콜백 메커니즘도 없습니다 — `vsql::sys_var`와 달리, 확장 프로그램은 세션 변수의 값을 변경 시 알림을 받는 것이 아니라 다시 읽어서만 관찰합니다.

### 기능 선언

`vsql::preview_session_var::make_capability()`으로 기능을 구축하고, `make_int(name, comment)` 또는 `make_str(name, comment)`에서 제공하는 디스크립터 목록을 중괄호로 감싸 전달합니다. 각 디스크립터는 플루언트 `.default_()`, `.min()`, `.max()` 설정자로 구성합니다(`min`/`max`는 `INT`에만 적용됩니다):

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

namespace sv = vsql::preview_session_var;

static auto SESSION_VARS = sv::make_capability({
    sv::make_int("session_int_var", "Search width")
        .default_(20)
        .min(1)
        .max(4096),
    sv::make_str("label", "Per-session label")
        .default_("default"),
});

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .with(SESSION_VARS))
```

`make_int`는 제약 없이 시작합니다(기본값 `0`, 전체 `long long` 범위). `make_str`은 빈 문자열 기본값으로 시작합니다. 기능 객체는 이 페이지의 다른 모든 미리보기 기능과 마찬가지로 정적 저장 기간이 필요합니다.

### 세션 값 읽기

두 가지 읽기 경로가 있으며, 둘 다 *호출하는* 연결의 스레드에 대해 해석됩니다 — 따라서 둘 다 **연결 스레드(연결에서 실행되는 VDF 또는 콜백)에서 호출해야 하며, 백그라운드 스레드 워커에서는 절대 호출하면 안 됩니다**:

* `SESSION_VARS.get_session_int(name, out)` / `get_session_str(name, out)` — 이름 기반이며, 호출마다 변수를 다시 해석합니다. 가끔 읽는 경우에 사용하세요. `STR` 변수의 경우 이것이 유일한 경로입니다: 잠금 없는 문자열 빠른 경로는 없는데, `STR` 슬롯은 별도로 할당된 버퍼에 대한 포인터를 보유하며 동시에 실행되는 `SET SESSION` 또는 `UNINSTALL`이 그 버퍼를 해제할 수 있기 때문입니다.
* `SESSION_VARS.int_var(name)`은 첫 `read()` 시 지연 해석하고 결과를 캐시하는 `IntVar`를 반환하므로, 기능이 로드되기 전에 파일/정적 범위에서 선언할 수 있습니다. 이후의 각 `read()`는 잠금 없는 스레드별 오프셋 로드입니다 — 이름 조회 없음, 전역 잠금 없음 — 플러그인의 `THDVAR(thd, var)`에 상응합니다. 이 빠른 경로는 `INT` 전용입니다.

```cpp theme={null}
static auto SESSION_INT = SESSION_VARS.int_var("session_int_var");

void read_session_int_var_impl(IntResult out) {
  long long val = 0;
  if (SESSION_INT.read(val)) { out.set_null(); return; }
  out.set(val);
}
```

### SQL에서 접근

`INSTALL EXTENSION my_ext` 후, 각 변수는 확장 프로그램 이름을 구성 접두사로 사용하며 `@@session.`과 `@@global.` 둘 다로 읽고 설정할 수 있습니다:

```sql theme={null}
SELECT @@global.my_ext.session_int_var;
SELECT @@session.my_ext.session_int_var;
SET SESSION my_ext.session_int_var = 100;
```

두 번째 연결은 첫 번째 연결의 `SET SESSION`에 영향을 받지 않으며, 자체 값을 설정하기 전까지는 컴파일된 기본값을 봅니다. `SET GLOBAL`은 새 세션이나 재설정된 세션이 가져오는 기본값을 변경하며, 이미 설정된 어떤 세션의 값도 변경하지 않습니다.

범위를 벗어난 `INT` 대입은 경계값으로 잘리고 경고가 발생하며, 잘못된 유형의 대입은 거부됩니다. 일반 MySQL 시스템 변수와 정확히 동일합니다:

```sql theme={null}
SET SESSION my_ext.session_int_var = 999999;  -- max is 4096
```

```text theme={null}
Warning	1292	Truncated incorrect my_ext.session_int_var value: '999999'
```

```sql theme={null}
SET SESSION my_ext.session_int_var = "hello";
```

```text theme={null}
ERROR 1232 (42000): Incorrect argument type to variable 'my_ext.session_int_var'
```

### 재시작 동작

`SET PERSIST`와 `SET PERSIST_ONLY`는 다른 MySQL 시스템 변수와 마찬가지로 세션 변수의 전역 값에 대해 작동합니다: 값은 서버 재시작 후에도 유지되고, 각 새 세션은 그 값에서 시작합니다. `SET SESSION` 값은 연결별 상태이며 재시작 후 유지되지 않습니다. `SET PERSIST`가 없으면, 확장 프로그램이 여전히 설치된 상태로 재시작한 후 각 변수는 컴파일된 기본값으로 다시 나타납니다.

## 스레드 워커

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>`)로 제공되므로, 다음 서명을 가진 함수여야 합니다. 첫 번째 생성자 인수는 스레드 이름 접미사이며, 선택적 두 번째 인수는 제어 시스템 변수 이름을 오버라이드합니다.

### 작업 함수 서명

```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`는 디스크립터에 등록된 불투명(opaque) 포인터이며, 변경 없이 전달됩니다.

### 웨이크업 라이프사이클

서버는 다음 네 가지 이유 중 하나로 작업 함수를 호출합니다:

| 이유 | 의미 |
| - | - |
| `VEF_WAKEUP_ENABLE` | 워커가 방금 활성화되었습니다(제어 시스템 변수가 ON으로 전환됨). 반환 값은 초기 `poll_fd` 및 `sleep_ms`를 설정합니다. |
| `VEF_WAKEUP_PERIODIC` | 주기적 타이머가 발동되었습니다(`sleep_ms` 경과). |
| `VEF_WAKEUP_POLL_FD` | 감시 중인 파일 디스크립터가 읽을 수 있게 되었습니다. |
| `VEF_WAKEUP_DISABLE` | 워커 비활성화(제어 시스템 변수 OFF) 또는 서버 종료. 반환 값은 무시됩니다. |

`reason`이 `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`를 반환하여 다음 웨이크업 구성 설정을 업데이트합니다. 각 필드의 0 값은 "현재 설정 유지"를 의미합니다 — 변경하지 않으려면 구조체를 기본값으로 초기화하여 반환(`return {};`)하세요.

새로운 poll 파일 디스크립터를 설정하려면 해당 값을 반환합니다(0보다 커야 함). 기존 poll 파일 디스크립터를 제거하려면 `poll_fd`에 `-1`을 반환합니다.

`reason`이 `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 문을 실행할 수 있게 해줍니다. 쿼리는 확장 프로그램이 MySQL 클라이언트 라이브러리에 링크하지 않고 기능 vtable을 통해 서버 내부에서 실행됩니다.

기능 이름 `VEF_PREVIEW_SQL_QUERY_NAME`은 `"vsql::preview::sql_query"`입니다.

> [!WARNING]
>
> SQL 세션은 스레드 워커 콜백에서 해당 콜백의 `vef_thread_handle_t *`를 사용하여 열어야 합니다. `open()`은 VDF 또는 임의의 확장 프로그램 생성 스레드에서 유효하지 않습니다 — 워커 세션 컨텍스트가 필요합니다.

### 기능 선언

헤더를 포함하고, 파일 범위에서 `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`를 반환합니다. `Result`가 null이 아닌 경우 문이 성공했다는 의미가 아닙니다 — `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()`은 `next()` 호출 또는 `Result` 소멸 전까지 유효한 `string_view`를 반환합니다. 더 긴 수명이 필요한 경우 복사하세요. `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`를 통해 진단을 표시합니다. 진단은 하나의 `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`보다 오래 유지되어야 할 경우 복사하세요.

### 완전한 예제

각 틱마다 버퍼링 쿼리와 스트리밍 쿼리를 실행하고, 두 쿼리에서 진단을 로깅하는 워커:

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

## 컬럼 저장

컬럼 저장은 확장 프로그램이 InnoDB에 직접 커스텀 유형의 이진 디스크 레이아웃을 등록할 수 있게 해줍니다. 이는 VARBINARY 페이로드를 통해 유형 바이트를 라우팅하는 대신, 커스텀 유형의 디스크 모양을 VARBINARY가 표현할 수 없는 경우(예: 전용 페이지에 저장되어야 하는 밀집된 부동 소수점 배열)에 사용됩니다. 이는 새로운 기능을 제공하는 특성입니다: 기존 레이아웃에 대한 튜닝 장치가 아니라 새로운 저장 레이아웃을 활성화합니다.

> [!WARNING]
>
> 컬럼 저장은 미리보기 ABI입니다 — 개발 중이며 릴리스 간에 변경될 수 있습니다. 현재는 행 수준 지속성만 지원합니다. 커스텀 저장 컬럼은 [사용자 정의 인덱스](https://villagesql.com/docs/ko/extensions/build/custom-indexes.md)만 허용하며, 일반 인덱스는 `ERROR 3219 (HY000): InnoDB: Indexing for types with column storage is supported only with custom index.`로 거부됩니다.

### 기능 선언

두 개의 미리보기 기능이 함께 작동합니다:

* `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)`은 구현을 동일한 확장 프로그램에서 등록된 하나의 커스텀 유형과 연결합니다. 모든 7개의 슬롯은 `build()` 시점에 필수적입니다. 각 슬롯은 InnoDB가 정상 운영 중에 도달하는 고유한 컬럼 라이프사이클 단계를 나타내기 때문입니다.

### 일곱 개의 저장 함수

모든 함수는 `storage::Column::StorageCtx<MyCtx>*`를 받습니다. 이 `user()` 접근자는 확장 프로그램의 컬럼별 상태를 반환하고, `arena()`는 보조 개체에 대한 서버 관리 할당을 제공합니다. 모든 함수는 성공 시 `false`를 반환하고, 오류 시 `true`를 반환하며, `error_msg`(용량 `error_msg_len`)에 메시지를 작성합니다. 서버는 그 메시지를 오류 로그에 기록합니다. 실패한 `create` 또는 `insert`는 `ERROR 3219 (HY000): InnoDB: Custom type operation failed. See server error log for details.`로 문을 실패시킵니다.

```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가 퍼지가 실행될 때까지 삭제된 행을 오래된 스냅샷에서 읽을 수 있도록 유지해야 하기 때문에 구분됩니다.

### 컬럼별 컨텍스트 및 아레나

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`이 반환된 후 자동으로 아레나를 파괴하고 `~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}
// Read: a shared latch
storage::Page page;
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);
```

```cpp theme={null}
// Write: an exclusive latch. Do not load a page for writing
// when the same mini-transaction already holds it with a shared latch.
storage::Page page;
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을 하드코딩하는 대신 사용하세요. |

페이지 접근자는 헤더 또는 트레일러 영역 내부의 오프셋을 거부합니다: 릴리스 빌드는 읽기 또는 쓰기를 건너뛰고, `NDEBUG`가 없는 빌드는 `assert`에 실패합니다. 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()` | 기본 스키마, 또는 선택된 것이 없으면 `NULL`. |
| `status()` | 성공 시 `0`, 그렇지 않으면 MySQL 오류 코드. |
| `warning_count()` | 문이 실행되는 동안 발생한 조건(노트, 경고, 오류)의 수. |
| `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()`가 높으면 전체 테이블 스캔을 의미합니다. |
| `client_pid()`, `client_name()`, `program_name()` | 핸드셰이크 시 한 번 전송되는 클라이언트 제공 연결 속성(`_pid`, `_client_name`, `program_name`): 클라이언트의 OS 프로세스 id, 커넥터/드라이버 라이브러리(예: `libmysql`, `mysql-connector-j`), 애플리케이션 이름(예: `mysql`, `mysqldump`). 클라이언트가 해당 속성을 보내지 않은 경우 각각 `NULL`입니다. |

> [!NOTE]
>
> **0.0.8-dev에서 변경됨.** 문 자체가 실패한 경우, `warning_count()`는 조건을 하나 적게 셉니다.

`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에서 활성화

미리보기 계층이 활성화된 상태에서(참조: [미리보기 계층 활성화](#enabling-the-preview-tier)), 확장 프로그램을 `-DVSQL_USE_DEV_ABI=ON`으로 빌드하고, 그 `.veb` 파일을 `veb_dir`에 복사하고, 설치한 다음, 시스템 변수를 통해 구성합니다:

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

## 인증 방식

auth 기능(`vsql::preview::auth`)은 확장 프로그램이 서버 인증 방식을 제공할 수 있게 해줍니다. 계정은 `CREATE USER ... IDENTIFIED WITH <method-name>`으로 이를 선택합니다. 연결 시점에 그 이름이 로드된 MySQL 인증 플러그인이 아니면, 서버는 VEF 인증 레지스트리를 조회하고 핸드셰이크 전반에 걸쳐 확장 프로그램의 핸들러를 호출합니다. MySQL 인증 플러그인을 작성하지 않고도 서버가 알지 못하는 자격 증명 소스 — 베어러 토큰, 외부 신원 공급자, 또는 사용자 정의 챌린지 — 를 기준으로 계정을 인증하는 데 사용하세요.

기능 이름 `VEF_PREVIEW_AUTH_NAME`은 `"vsql::preview::auth"`입니다.

핸들러는 `AuthContext`를 받는 타입이 지정된 함수입니다: 서버가 소유한 이 컨텍스트를 통해 핸드셰이크 패킷을 읽고 쓰면서 클라이언트와 통신하며, MySQL의 내부 인증 구조체는 전혀 보지 않습니다.

> [!WARNING]
>
> 인증 결과는 실패 시 차단(fail-closed)됩니다. 서버는 `AuthResult::kOk`이 아닌 모든 것을 거부된 연결로 처리합니다 — "아마도"나 실패 시 허용(fail-open) 결과는 의도적으로 존재하지 않습니다. `AuthResult::kReject`를 반환하거나, `AuthResult::kError`를 반환하거나, 유효 계정을 설정하지 않는 핸들러는 로그인을 거부합니다.

### 기능 선언

헤더를 포함하고, 타입이 지정된 핸들러를 작성하고, 플루언트 `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))
```

빌더는 여섯 부분으로 구성됩니다:

| 요소 | 의미 |
| - | - |
| `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) 참조). |
| `.roles_mode(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 없음, 블로킹 없음, 부작용 없음.

### 핸들러 계약

핸들러는 `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`로 매핑하는 것처럼 — 그것은 프록시이며, MySQL 플러그인 인증 경로에서와 똑같이 `GRANT PROXY`가 필요합니다.

### 활성 역할 스테이징

`c.set_active_roles(roles, n_roles)`는 세션에서 활성화되어야 하는 역할을 스테이징하며, 이 로그인에 대해 계정의 기본 역할 활성화를 대체합니다. `roles`는 NUL로 끝나는 이름 `n_roles`개의 배열입니다. 문자열은 복사되므로 호출자가 유지할 필요가 없습니다. 서버는 계정 확인 *후에* `SET ROLE`과 동일한 부여 검사 활성화를 사용하여 이를 적용합니다: 인증된 계정에 실제로 부여된 역할만 활성화되고, 부여되지 않은 이름은 조용히 건너뜁니다 — 따라서 방식이 `.roles_mode()`를 설정하지 않는 한, 토큰은 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))
```

### 계정 바인딩 및 연결

미리보기 계층이 활성화된 상태에서(참조: [미리보기 계층 활성화](#enabling-the-preview-tier)), 확장 프로그램을 `-DVSQL_USE_DEV_ABI=ON`으로 빌드하고, 그 `.veb` 파일을 `veb_dir`에 복사하고, 설치한 다음, 계정을 방식에 바인딩합니다. 핸들러가 두 번째 계정으로 매핑하므로, 그 계정도 생성하고 연결하는 계정이 그 신원을 취할 수 있도록 `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"
```

```text theme={null}
+-----------------------+-----------------+
| CURRENT_USER()        | @@external_user |
+-----------------------+-----------------+
| vsql_auth_test_user@% | auth_user       |
+-----------------------+-----------------+
```

확장 프로그램을 제거하면 방식이 사라지며, 여기에 바인딩된 계정은 더 이상 인증할 수 없습니다:

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

### 계정 자동 생성

방식은 아직 존재하지 않는 계정의 로그인도 처리하고, 로그인이 성공하는 과정의 일부로 서버가 계정을 생성하도록 할 수 있습니다. 이것이 없으면 알 수 없는 계정은 어떤 방식이 실행되기도 전에 거부됩니다.

`.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` 서버에서 — 계정 없이 진행하는 대신 로그인이 실패합니다.

> [!NOTE]
>
> **0.0.8-dev에서 변경됨.** 서버는 자동 `CREATE USER`와 그 `GRANT`, 그리고 역할 조정을 위한 `GRANT`와 `REVOKE`를 바이너리 로그에 기록하지 않습니다. 복제 토폴로지의 각 서버는 계정을 스스로 생성하고 조정합니다.

역할은 [활성 역할 스테이징](#staging-active-roles)에서와 동일하게 동작합니다: DBA가 이를 소유합니다. 각 이름은 부여 가능한 역할로 이미 존재해야 하며, 부여할 수 없는 이름은 로그인을 실패시키지 않고 로깅된 후 건너뜁니다. 따라서 토큰은 역할을 지정할 수는 있어도 결코 생성하거나 상승시킬 수 없습니다. 계정 이름은 클라이언트에서 오므로 서버는 이를 식별자로 인용합니다 — 조작된 이름은 이상한 이름의 계정 하나가 될 뿐, 결코 두 번째 문이 되지 않습니다.

`vsql_auth_test` 확장 프로그램은 연결하는 사용자에게 `vsql_role_granted` 역할을 프로비저닝하며, 옵트인을 `OFF`로 시작하는 `vsql_auth_test.auto_create` 뒤에 둡니다. 이를 켜고 역할을 먼저 생성한 다음, 존재하지 않는 계정으로 연결하세요:

```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: NO)
```

> [!WARNING]
>
> 옵트인하면 알 수 없는 계정과 기존 계정의 차이가 관찰 가능해지는데, 이는 표준 알 수 없는 계정 거부가 의도적으로 숨기는 것입니다: 알 수 없는 계정에 대한 거부된 로그인은 `(using password: NO)`를 보고하는 반면, 일반 비밀번호를 가진 계정에 대한 거부된 로그인은 `(using password: YES)`를 보고하며, 유효한 자격 증명은 계정을 생성합니다. 이것이 이 기능이 감수하는 대가입니다. 자격 증명이 널리 보유된 방식에서 옵트인을 활성화하기 전에 이를 저울질하세요.

### 역할 자동 부여

기본적으로 토큰이 지정한 역할은 계정이 이미 그것을 보유한 경우에만 적용됩니다. 보유하지 않은 역할은 건너뛰며, 서버는 그 이유를 로깅합니다(`VEF auth: role '<role>'@'<host>' requested for account '<user>'@'<host>' is not granted; skipping`). `.roles_mode(&callback)`은 이를 바꿉니다: 콜백은 `vef_auth_roles_mode_t` — `VEF_AUTH_ROLES_ACTIVATE`(기본값), `VEF_AUTH_ROLES_GRANT`, 또는 `VEF_AUTH_ROLES_SYNC` — 를 반환하고, 서버는 로그인마다 그에 따라 스테이징된 역할을 계정의 부여된 역할과 조정합니다.

콜백은 `.auto_create()`와 형태가 같습니다 — 인수가 없습니다 — 하지만 `bool` 대신 모드 열거형을 반환하며, 서버가 로그인마다 호출하여 그 뒤에 있는 값을 실시간으로 읽습니다:

```cpp theme={null}
unsigned long g_auto_grant = 0;  // 0=ACTIVATE, 1=GRANT, 2=SYNC
vef_auth_roles_mode_t roles_mode_cb() {
  return static_cast<vef_auth_roles_mode_t>(g_auto_grant);
}

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

두 옵트인은 로그인의 서로 다른 시점에 실행됩니다. `.auto_create()`는 아직 존재하지 않는 계정의 로그인이 계정을 프로비저닝할 수 있는지를 결정하고, `.roles_mode()`는 그 후에 로그인이 확인한 계정 — 방금 프로비저닝된 계정이든 이미 존재하던 계정이든 — 에 대해 실행되므로, 어느 경우든 동일한 조정이 적용됩니다. `VEF_AUTH_ROLES_ACTIVATE` 또는 콜백 없음은 활성화 전용 기본 동작을 유지합니다.

`VEF_AUTH_ROLES_GRANT`는 추가적입니다: 계정에 없는 스테이징된 역할을 부여하고, 취소 단계는 전혀 실행하지 않습니다. `VEF_AUTH_ROLES_SYNC`는 그 단계를 실행합니다: 토큰이 요구하지 않은 부여된 역할을 모두 취소하므로, 토큰에서 빠진 역할은 다음 로그인 시 계정에서도 빠집니다 — 단, 운영자의 `ALTER USER`로 계정의 `DEFAULT ROLE`로 설정된 역할은 예외이며, SYNC는 이를 결코 취소하지 않습니다. 어느 모드에서든, DB 역할로 존재하지 않는 스테이징된 역할은 `GRANT`에 실패하며 로그인을 실패시키지 않고 로깅된 후 건너뜁니다.

`vsql_auth_test`는 이를 `vsql_auth_test.auto_grant`로 노출하며, 이는 `OFF`/`ON`/`SYNC` 열거형(이 또한 `OFF`로 시작)으로 그 인덱스 순서는 위의 모드 순서와 일치합니다. 그 `-token-roles` 토큰은 `vsql_role_granted`와 `vsql_role_denied`를 스테이징하며, 아래 계정은 둘 다 보유하지 않습니다. 설정이 꺼져 있으면 로그인은 계정의 역할을 그대로 둡니다:

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

위와 동일한 방식으로 비밀번호 `vsql-auth-test-token-roles`를 사용해 `auth_user`로 연결하고, `SELECT CURRENT_ROLE()`로 무엇이 활성 상태인지 물으세요:

```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]
>
> `vsql_auth_test.auto_grant`가 `ON` 또는 `SYNC`로 설정되어 있으면 유효한 토큰만으로 그것이 지정하는 어떤 역할이든 얻을 수 있습니다. DB 역할로 아직 존재하지 않는 역할은 `GRANT`에 실패하고 건너뛰므로 토큰이 생성된 적 없는 역할을 만들어낼 수는 여전히 없지만, 존재하는 역할 중 어느 것에 계정이 도달할 수 있는지를 더 이상 DBA가 결정하지 않고 인증 방식이 결정합니다. `SYNC`는 더 나아갑니다: DBA가 별도로 부여했지만 토큰이 요구하지 않는 역할도 계정이 다음에 로그인할 때 취소합니다 — 운영자는 역할을 계정의 `DEFAULT ROLE`로 설정하여 여전히 하나의 역할을 고정할 수 있으며, SYNC는 이를 그대로 둡니다. `vsql_auth_test_user`에서 취소된 역할은 이미 그것을 활성화한 세션에서 사라지지 않습니다: 취소 후 동일한 세션에서 `CURRENT_ROLE()`을 다시 조회하면 여전히 그 역할이 나열됩니다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.