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

# 개발

> VillageSQL 확장 작성 및 테스트 실행

이 가이드는 VillageSQL 확장에 대한 VDF 구현 작성 및 회귀 테스트 실행을 다룹니다. [확장 만들기](/docs/ko/mysql-8.4/0.0.4/create) 가이드와 함께 사용되며, 종단간(end-to-end) 빌드 단계를 다룹니다.

<Warning>
  VEF 프로토콜 3은 v0.0.4부터 안정적입니다. 프로토콜 4는 개발 중이며, 선택적으로 활성화하는 개발 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)를 통해 사용할 수 있습니다. 이전 프로토콜 2로 빌드된 확장은 서버에서 거부되며 재빌드가 필요합니다.
</Warning>

<Note>
  VillageSQL 서버 자체에 기여하는 경우(확장 빌드가 아닌 경우), [소스에서 빌드하기](/docs/ko/mysql-8.4/0.0.4/source)를 참조하세요. 이는 `mysql-test-run.pl`을 직접 사용하여 테스트를 실행하는 전체 서버 개발 워크플로우를 다룹니다.
</Note>

## 환경 설정

확장 개발 및 테스트를 위해 빌드된 VillageSQL 서버가 필요합니다. 서버 바이너리를 컴파일하기 위해 [소스에서 복제 및 빌드하기](/docs/ko/mysql-8.4/0.0.4/source) 가이드를 따르세요.

빌드가 완료되면 `villagesql` CLI를 사용하여 로컬 개발 서버 인스턴스를 관리합니다. 모든 명령어는 VillageSQL이 설치된 디렉터리에서 실행해야 합니다.

### 로컬 개발 서버 시작

서버 인스턴스를 초기화하고 시작합니다:

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

초기화 시 루트 암호를 설정하려면:

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

다중 독립 인스턴스를 관리하려면 명령어 앞에 `--dir <path>`를 지정하거나, 현재 작업 디렉터리에 서버 디렉터리를 생성하려면 `--here`를 사용합니다:

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

### 확장 파일 관리

SQL을 통해 확장을 설치하기 전에 `.veb` 파일이 서버에 있어야 합니다. CLI는 서버의 `lib/veb/` 디렉터리를 관리합니다:

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

`init` 이전에 `lib/veb/`에 배치된 `.veb` 파일은 자동으로 시드됩니다. 파일을 추가한 후 SQL을 통해 확장을 설치합니다:

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

## 확장 함수 작성

확장 함수는 C++로 작성되며 VEF에 등록됩니다. SDK 전체에 액세스하려면 단일 헤더를 포함합니다:

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

<h3 id="typed-wrappers-recommended">
  타입 안전 래퍼 (권장)
</h3>

타입 안전 래퍼는 VDF 매개변수 및 결과에 대한 타입 안전 인터페이스를 제공합니다. 프레임워크는 함수 시그니처에서 래퍼 타입을 감지하고 자동으로 적응합니다 — `make_func` 등록 구문은 변경되지 않습니다.

**입력 래퍼:** `IntArg`, `RealArg`, `StringArg`, `CustomArg` — 각각 `is_null()` 및 `value()`를 제공합니다. 매개변수화된 사용자 정의 타입의 경우, `CustomArgWith<P>`는 `params()` 액세서를 추가하여 캐시된 구문 분석된 매개변수 구조체를 반환합니다(참조: [매개변수화된 타입](#parameterized-types)).

**결과 래퍼:** `IntResult`, `RealResult`, `StringResult`, `CustomResult` — 각각 `set_null()`, `warning(msg)`, `error(msg)`를 제공합니다. 스칼라 결과는 `set(value)`를 제공합니다. 버퍼 결과는 `buffer()` 및 `set_length(len)`을 제공합니다. `StringResult`는 추가로 `set(std::string_view)`를 제공하여 뷰에서 최대 `buffer().size()` 바이트를 복사하고 길이를 한 번에 설정합니다. 매개변수화된 사용자 정의 타입의 경우, `CustomResultWith<P>`는 `params()` 액세서를 추가합니다.

**스팬 타입:** `value()` 및 `buffer()`는 바이트 기반 래퍼에서 `vsql::Span<T>`를 반환합니다 — `data()`, `size()`, `empty()`, `begin()`/`end()`, `operator[]`를 갖는 연속된 `T` 구간에 대한 소유하지 않는 뷰입니다. C++20에서는 `std::span<T>`의 별칭이며, C++17에서는 SDK가 최소한의 호환 구현을 제공하므로 동일한 코드가 두 표준에서 컴파일됩니다. `<villagesql/vsql.h>`를 통해 사용할 수 있습니다.

`warning(msg)`는 행에 SQL NULL을 반환하고 SQL 경고를 추가합니다. 엄격 모드(`STRICT_TRANS_TABLES`)에서는 MySQL이 INSERT/UPDATE에서 문장 오류로 승격시켜 `error(msg)`와 동일한 동작을 보입니다. 이는 구문 분석 불가능한 문자열이 인코딩 함수에서 발생하는 등 복구 가능한 잘못된 입력에 사용됩니다. 손상된 저장 데이터 또는 계속 실행이 안전하지 않은 조건에서는 `error(msg)`를 사용합니다. 두 메시지는 서버 내부 오류 버퍼에 맞게 필요한 경우 자동으로 잘립니다.

**스칼라 예제** — 두 정수를 더합니다:

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

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

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

**이진 예제** — 사용자 정의 타입 버퍼를 제자리에서 변환합니다:

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

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

`StringResult` 및 `CustomResult`의 경우, `buffer()`에 쓴 후 `set_length()`로 작성된 바이트 수를 호출합니다. `buffer().size()`는 최대 용량입니다.

VDF가 사용자 정의 타입을 반환하는 경우(`returns(CUSTOM(MYTYPE))`), 서버는 결과 버퍼를 해석된 반환 타입의 `persisted_length`에 맞춰 자동으로 크기 조정합니다 — 확장 작성자는 이 경우 함수 빌더에 `.buffer_size(...)`를 선언할 필요가 없습니다. `prerun`이 버퍼를 더 크게 늘리면 그 크기가 유지됩니다. 예를 들어, `SVECTOR::from_string('[…1024 float…]')`는 결과 래퍼가 공간 부족을 일으키지 않도록 넓은 벡터를 인코딩할 수 있습니다.

동일한 확장 내에서 함수 간에 다른 스타일을 사용할 수 있습니다 — 각 함수의 스타일은 자체 시그니처에 따라 결정됩니다.

<h3 id="aggregate-vdfs">
  집계 VDF
</h3>

집계 VDF는 각 `GROUP BY` 그룹 내에서 행을 통해 상태를 누적하고 그룹당 단일 결과를 반환합니다. SQL `SUM` 또는 `COUNT`와 유사합니다. `make_aggregate_func<State, &result_fn>("name")`을 사용하여 등록합니다. `State` 타입은 그룹별 누적 버퍼입니다. `prerun` 및 `postrun`은 자동으로 생성되어 할당 및 삭제합니다.

결과 함수는 `void(const State&, ResultWrapper)` 시그니처를 가져야 하며, `ResultWrapper`는 `IntResult`, `RealResult`, `StringResult`, `CustomResult`, 또는 `CustomResultWith<P>` 중 하나입니다. `out.set(value)`를 호출하여 값을 반환하거나 `out.set_null()`으로 SQL NULL을 반환합니다.

`.clear<>()` 및 `.accumulate<>()`가 모두 필요합니다. 빌더는 컴파일 타임에 강제하고(메서드 `build()`를 통해), 서버는 `INSTALL EXTENSION` 시점에 다시 검증합니다 — `clear`은 상태를 재설정하고, `accumulate`는 행을 결합하며, 결과 함수는 최종 상태를 읽습니다.

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

using namespace vsql;

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

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

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

빌더 메서드 작동 방식:

* `make_aggregate_func<State, &result_fn>()`은 `prerun` 및 `postrun`을 자동 생성합니다(값 초기화 및 삭제).
* `.clear<&fn>()`은 `void(State&)` → `vef_vdf_clear_func_t`로 래핑합니다.
* `.accumulate<&fn>()`은 `void(State&, TypedArgs...)` → `vef_vdf_accumulate_func_t`로 래핑합니다. `TypedArgs`는 함수 시그니처에서 추론됩니다(`IntArg`, `StringArg` 등).
* `ResultWrapper` 타입(`IntResult`, `RealResult` 등)은 결과 함수 시그니처에서 추론됩니다.

NULL을 반환하지 않는 카운터의 경우, 단순한 상태 타입을 사용합니다:

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

<h3 id="per-statement-state-prerun-and-postrun">
  문장별 상태 (Prerun 및 Postrun)
</h3>

일부 VDF는 단일 쿼리가 처리하는 모든 행을 통해 상태를 유지해야 합니다 — 호출 카운터, 캐시된 결과, 열린 리소스. **Prerun** 훅에서 할당하고, VDF 본문에서 액세스하고, **Postrun** 훅에서 해제합니다. 두 훅은 문장당 한 번 실행되며, VDF 본문은 행당 한 번 실행됩니다.

`.prerun<&Hook>()` 및 `.postrun<&Hook>()`으로 등록합니다. 필수 시그니처는 다음과 같습니다:

| 훅       | 필수 시그니처                                      |
| ------- | -------------------------------------------- |
| Prerun  | `void(vsql::PrerunArgs, vsql::PrerunResult)` |
| Postrun | `void(vsql::PostrunArgs)`                    |

원시 ABI 시그니처는 컴파일 타임에 거부됩니다. `PrerunResult::set_user_data(void*)`로 상태를 저장하고, `PostrunArgs::delete_state<T>()`로 해제합니다. `prerun`에서 `set_user_data(new T{})`를 호출하면 `postrun`은 **반드시** `delete_state<T>()`를 호출해야 합니다 — SDK는 자동 해제하지 않습니다.

`PrerunArgs::type_at(i)`는 행을 읽기 전에 각 매개변수의 선언된 SQL 타입을 노출합니다. `PrerunArgType`의 `is_int()`, `is_real()`, `is_str()`, `is_custom()` 조건식는 열 타입을 반영합니다. 이는 `prerun`에서 매개변수 타입을 검증하거나 `PrerunResult::request_buffer_size(n)`을 호출하여 결과 버퍼 크기를 지정하는 데 사용됩니다.

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

struct CallCounter { long long n = 0; };

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

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

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

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

### 가변 인자 VDF

**가변 인자** VDF는 SQL 타입의 임의 수의 인자를 수용합니다. `.varargs()`를 사용하여 선언하며, 이는 `.no_params()` 및 `.param(TYPE)`과 상호 배타적입니다. 본문은 일반적인 고정 인자 래퍼 대신 `vsql::VarArgs` 매개변수를 받습니다.

<Warning>
  가변 인자 등록은 VEF 프로토콜 3을 필요로 합니다. 이전 서버는 설치 시 확장을 거부합니다.
</Warning>

프레임워크는 가변 인자 VDF의 인자 개수 및 타입을 검증할 수 없습니다. 가변 인자 등록과 함께 `prerun` 훅을 사용하여 잘못된 입력에 `PrerunResult::error()`를 호출하거나 결과 버퍼 크기를 지정하기 위해 `PrerunResult::request_buffer_size(n)`을 호출해야 합니다.

범위-기반 for 루프로 인자를 반복합니다. 각 `AnyArg` 요소는 값 읽기 전에 타입 검사를 요구합니다:

| 조건식           | 액세서           | 반환 타입                             |
| ------------- | ------------- | --------------------------------- |
| `is_int()`    | `as_int()`    | `long long`                       |
| `is_real()`   | `as_real()`   | `double`                          |
| `is_str()`    | `as_str()`    | `std::string_view`                |
| `is_custom()` | `as_custom()` | `vsql::Span<const unsigned char>` |

액세서 사용 전에 `is_null()`을 확인해야 합니다 — 모든 네 가지는 NULL 인자에서 정의되지 않습니다.

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

constexpr size_t kBytearrayLen = 4;

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

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

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

### VEF\_GENERATE\_REGISTRATION

`VEF_GENERATE_REGISTRATION`은 내부 `_vef_do_register()` 헬퍼를 생성하여 확장 등록을 수행하지만, `extern "C"` 엔트리 포인트를 정의하지 않습니다. 테스트 빌드에서 등록 후 디스크립터를 수정해야 할 때 사용합니다. 일반 확장의 경우 대신 `VEF_GENERATE_ENTRY_POINTS`를 사용합니다.

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

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

<h3 id="type-operation-builders">
  타입 연산 빌더
</h3>

*확장이 사용자 정의 컬럼 타입을 정의하는 경우에만 필요합니다. 함수만 작성하는 경우 [회귀 테스트 실행](#running-regression-tests)으로 건너뜁니다.*

사용자 정의 타입은 엔진이 내부적으로 호출하는 세 가지 연산이 필요합니다: 인코딩(문자열 → 이진), 디코딩(이진 → 문자열), 비교. 해시는 선택적입니다. 이 연산들을 다음 C++ 시그니처에 맞게 구현하세요(`<villagesql/vsql.h>`를 통해 사용 가능합니다):

#### 고정 길이 타입

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

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

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

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

이 연산을 `vsql::make_type<kTypeName>()`으로 등록합니다. 타입 이름은 비타입 템플릿 매개변수(NTTP)로 전달됩니다 — `static constexpr const char[]` 배열입니다. 빌더는 NTTP에서 `TYPE::method` 형식의 VDF 이름을 자동 생성하므로 수동 문자열 매칭이 필요하지 않습니다. 빌드된 타입 객체를 확장 빌더의 `.type()`에 전달하고, 타입 연산을 위한 별도의 `.func()` 호출은 필요하지 않습니다.

<Warning>
  타입 이름은 `static constexpr const char[]` 변수여야 합니다 — 문자열 리터럴은 비타입 템플릿 매개변수로 사용할 수 없습니다. `"MYTYPE"`을 직접 전달하면 컴파일러 오류가 발생합니다:

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

  아래와 같이 이름을 명명된 배열로 선언하세요.
</Warning>

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

using namespace vsql;

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

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

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

`build()`는 `from_string`, `to_string`, 또는 `compare`가 누락된 경우 컴파일 타임에 실패합니다. 각 템플릿 메서드는 `static_assert`를 통해 함수 포인터 시그니처를 검사합니다.

#### 내장 기본값

`NOT NULL` 사용자 정의 타입 컬럼이 `IGNORE` 모드(예: `INSERT IGNORE` 또는 `UPDATE IGNORE`)에서 `NULL`을 받을 때, 서버는 오류를 발생시키지 않고 내장 기본값을 호출하여 대체 값을 생성합니다. 내장 기본값은 문자열 표현을 제공하며, 서버는 타입의 `from_string` 함수를 사용하여 이진으로 변환합니다.

<Note>
  `.intrinsic_default_str()` 및 `.intrinsic_default_vdf()`를 모두 생략하면, 서버는 `from_string("")`을 대체로 호출합니다. 이는 타입이 **처음 사용될 때**(테이블 생성 시) 발생하며, `INSTALL EXTENSION` 시가 아닙니다. 인코딩 함수가 빈 문자열을 거부하거나 잘못된 바이트 수로 인코딩하는 경우, 타입 초기화가 오류로 실패하여 SQL 클라이언트에서 오류가 표시됩니다:

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

  고정 길이 타입의 경우, 기본 문자열은 정확히 `persisted_length` 바이트로 인코딩되어야 합니다. 빈 문자열이 유효한 입력이 아닌 타입의 경우 명시적 기본값을 설정하세요.
</Note>

**문자열 리터럴: `.intrinsic_default_str()`**

상수 기본값을 전달하려면 타입 빌더에 직접 문자열을 전달합니다(고정 길이 예제에서 보인 대로 `.intrinsic_default_str("0")`).

**VDF 기반: `.intrinsic_default_vdf()` + `make_intrinsic_default`**

기본값이 타입 매개변수에 따라 달라질 때, 다음 시그니처 중 하나에 따라 함수를 구현합니다(사용 가능: `<villagesql/vsql.h>`):

<Warning>
  **중요 변경**: `IntrinsicDefaultFunc` 및 `IntrinsicDefaultWithParamsFunc`은 `const char*` 대신 `std::string`을 반환합니다. 기존 내장 기본값 구현을 `std::string` 직접 반환하도록 업데이트하세요.
</Warning>

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

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

`std::string` 형식의 기본값 표현을 반환합니다. 오류 시 `error_msg`에 메시지를 쓰고 임의 값(SDK는 `error_msg[0] != '\0'`을 확인하여 오류 감지)을 반환합니다. `make_intrinsic_default<&fn>("vdf_name")`(VDF 이름을 한 개의 인자로)으로 등록하고, 타입 빌더에서 `.intrinsic_default_vdf()`로 참조합니다. 매개변수화된 타입 예제는 전체 등록 패턴을 보여줍니다.

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

<h4 id="parameterized-types">
  매개변수화된 타입
</h4>

변동 길이 타입은 인코딩, 디코딩, 비교, 해시 시 열의 선언된 매개변수를 필요로 하여 할당 크기 및 레이아웃을 결정합니다. 매개변수 구조체를 정의하고, 구문 분석 함수와 역 `to_strings` 함수를 구현합니다. 타입 빌더에 `.params<P, &ParseFunc, &ToStringsFunc>()`로 등록하고, 타입 연산 함수의 첫 번째 인자로 `const P&`를 사용합니다. SDK는 고유한 매개변수 조합당 한 번만 구문 분석을 실행하도록 캐시합니다. `to_strings` 함수는 `parse`의 역함수로, 타입 `P`를 표준 키/값 문자열 형식으로 쓰여서 서버가 `parse`가 소비하는 형식과 동일한 구조로 추론된 매개변수를 게시합니다.

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

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

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

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

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

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

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

타입 빌더에 `.params<>()`를 등록합니다. `MYTYPE(N)` 정수 구문을 처리하려면 `.int_to_params<&mytype_int_to_params_fn>()`을 사용하고, 매개변수를 검증하고 저장 크기를 계산하려면 `.resolve_params<&mytype_resolve_params_fn>()`을 사용합니다. 모든 유효한 매개변수화에서의 최대 `persisted_length`를 지정하기 위해 `.max_persisted_length(N)`을 호출합니다. 서버는 이 값을 타입 매개변수 추론 경로에서만 사용하며, 아직 매개변수를 추론하지 않았으므로 `resolve_params`를 참조하여 인코딩 버퍼 크기를 결정할 수 없습니다. VDF 기반 내장 기본값의 경우, `.intrinsic_default_vdf()`와 VDF 이름을 사용하고, `make_intrinsic_default<&mytype_default>()`으로 별도로 VDF를 등록합니다.

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

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

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

using namespace vsql;

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

매개변수화된 변형 — `TypeEncodeWithParamsFunc<P>`, `TypeDecodeWithParamsFunc<P>`, `TypeCompareWithParamsFunc<P>`, `TypeHashWithParamsFunc<P>` 및 `ParamsToStringsFunc<P>`(`void fn(const P&, std::map<std::string,std::string>&)`)은 `<villagesql/vsql.h>`를 통해 사용 가능합니다. `vsql::make_type` 템플릿 메서드는 매개변수 인자를 감지하고 자동으로 매개변수 캐시를 통해 라우팅합니다. 인코딩 함수는 `vsql::MaybeParams<P> &`를 첫 번째 인자로 받습니다. 런타임에서 `is_known()`은 항상 참이며, `value()`는 `const P&`를 반환합니다. 디코딩, 비교, 해시 변형은 `vsql::CustomArgWith<P>`를 받으며, `params()` 액세서는 `const P&`를 반환합니다.

#### 저장 프로시저에서의 사용자 정의 타입

사용자 정의 확장 타입은 저장 프로시저 매개변수 타입 및 `DECLARE` 변수 선언에서 사용할 수 있습니다. 서버는 실행 시 설치된 확장의 타입 메타데이터를 사용하여 사용자 정의 타입을 해결합니다.

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

### 확장 시스템 변수

확장 시스템 변수는 미리보기 기능입니다 — 전체 API 참조, 팩토리 함수, SQL 액세스 및 완전한 예제는 [미리보기 기능](/docs/ko/mysql-8.4/0.0.4/preview-capabilities#system-variables)을 참조하세요.

### 확장 상태 변수

확장 상태 변수는 미리보기 기능입니다 — 전체 API 참조, 팩토리 함수, SQL 액세스 및 완전한 예제는 [미리보기 기능](/docs/ko/mysql-8.4/0.0.4/preview-capabilities#status-variables)을 참조하세요.

### 키링크 액세스

키링크 액세스는 미리보기 기능입니다 — 전체 API 참조, 결과 코드 및 완전한 예제는 [미리보기 기능](/docs/ko/mysql-8.4/0.0.4/preview-capabilities#keyring-access)을 참조하세요.

### 컬럼 저장

컬럼 저장은 미리보기 기능입니다 — 전체 API 참조 및 완전한 예제는 [미리보기 기능](/docs/ko/mysql-8.4/0.0.4/preview-capabilities#column-storage)을 참조하세요.

### 확장 등록 메타데이터 검사

`INFORMATION_SCHEMA.EXTENSION_REGISTRATION`은 로드된 확장의 모든 VEF 등록 구조체를 JSON 문서로 노출합니다. `INSTALL EXTENSION` 후 서버가 확장의 함수, 타입, 시스템 변수를 올바르게 해석했는지 확인하는 데 사용합니다.

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

| 열                     | 타입                | 설명                                                               |
| --------------------- | ----------------- | ---------------------------------------------------------------- |
| `EXTENSION_NAME`      | `VARCHAR(64)`     | 설치된 확장의 이름입니다.                                                   |
| `NEGOTIATED_PROTOCOL` | `BIGINT UNSIGNED` | 확장과 서버 간에 협상된 VEF 프로토콜 버전입니다.                                    |
| `REGISTRATION_JSON`   | `TEXT`            | `vef_registration_t` 구조체의 JSON 직렬화, `funcs` 및 `types` 배열을 포함합니다. |

<h2 id="running-regression-tests">
  회귀 테스트 실행
</h2>

VillageSQL 빌드 디렉터리에서 MySQL 테스트 러너를 사용하여 확장 회귀 테스트를 실행합니다.

### 전체 테스트 세트 실행

확장의 모든 테스트를 실행하려면:

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

### 개별 테스트 실행

단일 테스트 케이스를 실행하려면, 스위트 경로와 테스트 이름을 지정합니다:

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

## 새로운 테스트 만들기

새 기능 추가 또는 버그 수정 시 대응되는 회귀 테스트를 추가해야 합니다.

### 테스트 위치

확장 테스트는 VillageSQL 서버의 `mysql-test/suite/` 트리가 아닌, 확장 자체 리포지토리의 `test/` 디렉터리에 위치합니다.

* 테스트 파일은 `.test`로 끝나며 `test/t/`에 배치됩니다.
* 예상 결과 파일은 `.result`로 끝나며 `test/r/`에 배치됩니다.

예를 들어, `my_extension`이라는 확장의 경우:

* `test/t/my_new_test.test`
* `test/r/my_new_test.result`

### 테스트 파일 규칙

일반적인 확장 테스트는 확장 설치, SQL 실행, 제거를 포함합니다:

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

INSTALL EXTENSION my_extension;

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

UNINSTALL EXTENSION my_extension;
```

테스트 출력에 테스트 러너의 임시 디렉터리 경로가 포함된 경우, .test 파일 내에 이 지시어를 추가하여 경로를 정규화하세요. 그렇지 않으면 기록된 결과에 다른 머신에서 오류를 유발하는 절대 경로가 포함됩니다:

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

### 테스트 추가 단계

1. **`.test` 파일을** 확장의 `test/t/` 디렉터리에 생성합니다.
2. **빈 `.result` 파일을** 확장의 `test/r/` 디렉터리에 생성합니다.
3. **`--record`로 테스트를 실행하여** 예상 출력을 생성합니다:
   ```bash theme={null}
   cd $BUILD_HOME
   ./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test --record my_new_test
   ```
4. **생성된 `.result` 파일의 출력을 확인하여** 예상과 일치하는지 확인합니다.

## 테스트 디버깅

테스트가 실패하면 테스트 프레임워크가 상세한 로그를 제공합니다.

* **테스트 출력:** `mysql-test/var/log/mysqltest.log` (통합) 또는 `mysql-test/var/log/<test_name>/` (테스트별 디렉터리)를 확인합니다.
* **서버 오류 로그:** `mysql-test/var/log/mysqld.1.err`를 확인합니다. VillageSQL 전용 로그 메시지(`LogVSQL()`을 통해 출력됨)는 서버가 `--log-error-verbosity=3` 옵션으로 실행될 때만 표시됩니다.
* **차이점:** 프레임워크는 실제 출력과 예상된 `.result` 파일 간의 차이를 출력합니다.

테스트를 추가 디버그 정보와 함께 실행하려면:

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

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

## 참고 자료

* [확장 프로그램 생성](/docs/ko/mysql-8.4/0.0.4/create) — 엔드투엔드 빌드 단계, CMake 설정 및 설치
* [확장 API 참조](/docs/ko/mysql-8.4/0.0.4/extension-api-reference) — VDF 계약, null 처리 및 버퍼 크기 조정
* [확장 아키텍처](/docs/ko/mysql-8.4/0.0.4/architecture) — 라이프사이클, Victionary 캐싱, 성능 패턴 및 보안 모델
