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

# C++ 개발

> C++ 확장을 위한 VDF 작성 심층 참조 — 인자 및 결과 타입, 예외 안전성, 집계, prerun/postrun, 가변 인자, 등록.

이 가이드는 C++ VDF 구현을 작성하기 위한 심층 참조입니다. 종단간 빌드 단계를 다루는 [C++로 확장 만들기](https://villagesql.com/docs/ko/extensions/build/create.md), 그리고 테스트-반복 루프를 다루는 [C++ 테스트](https://villagesql.com/docs/ko/sdk/cpp/testing.md)의 동반 자료입니다.

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

## 확장 함수 작성

확장 함수는 C++로 작성되며 VEF에 등록됩니다. VDF, 타입, 확장 빌더 API에
액세스하려면 단일 헤더를 포함합니다:

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

## 예외 안전성

예외를 가두는 것은 확장의 책임입니다. SDK는 사용자의 구현을 `try`/`catch`로
감싸지 않으며 서버도 마찬가지이므로, 엔트리 포인트를 벗어난 예외는 그 위
어디에도 핸들러가 없는 상태로 C ABI 경계를 넘어 스택을 풀고 서버 프로세스가
종료됩니다. 일반적인 SQL 입력이 이 상황에 도달합니다: `std::stoi()`는 숫자가
아닌 문자열에서 예외를 던지고, `nlohmann::json::dump()`는 유효한 UTF-8이 아닌
텍스트에서 예외를 던집니다.

> [!NOTE]
>
> **0.0.8-dev에서 변경됨.** 개발 ABI(프로토콜 4, `-DVSQL_USE_DEV_ABI=ON`)에서는 SDK가 VDF 본문이나 집계 함수의 accumulate 또는 result 함수를 벗어난 예외를 잡습니다. 서버가 종료되는 대신 해당 문이 VDF 오류로 실패합니다. 안정 ABI는 변경되지 않습니다.

등록하는 모든 엔트리 포인트의 본문을 감싸세요: VDF, 집계 함수, prerun 및
postrun 훅, 그리고 사용자 정의 타입의 연산.

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

using namespace vsql;

void to_int_impl(StringArg in, IntResult out) {
  if (in.is_null()) { out.set_null(); return; }
  try {
    out.set(std::stoi(std::string(in.value())));  // throws on bad input
  } catch (const std::exception &e) {
    out.warning(e.what());
  } catch (...) {
    out.warning("unexpected error");
  }
}
```

`const std::exception &`에 더해 `...`도 잡으세요: 의존성이 `std::exception`을
상속하지 않는 타입을 던질 수 있으며, 그런 예외도 여전히 서버를 종료시킵니다.
`warning()`과 `error()` 중 무엇을 사용할지는 다른 모든 실패와 동일한 기준으로
선택합니다 — 문장이 계속 진행되어야 하는 잘못된 입력에는 `warning()`, 계속
실행이 안전하지 않은 경우에는 `error()`입니다.

## 인자 및 결과 타입

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

**인자 타입:** `IntArg`, `RealArg`, `StringArg`, `CustomArg` — 각각
`is_null()` 및 `value()`를 제공합니다. 매개변수화된 사용자 정의 타입의 경우,
`CustomArgWith<P>`는 캐시된 구문 분석된 매개변수 구조체를 반환하는 `params()`
액세서를 추가합니다(참조: [매개변수화된 타입](https://villagesql.com/docs/ko/sdk/cpp/type-operations.md#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 경고를 추가합니다. 그 NULL은 서버의 관점에서 일반적인 값이므로, 다른 모든 NULL과 같은 방식으로만 문장 오류가 됩니다: `NOT NULL` 열에 이를 쓰면 `INSERT INTO t VALUES (NULL)`과 마찬가지로 `STRICT_TRANS_TABLES`에서 실패합니다. NULL 허용 열에 쓰면 경고와 함께 성공합니다. 인코딩 함수의 구문 분석 불가능한 문자열과 같은 복구 가능한 잘못된 입력에는 `warning(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(MYTYPE)`)의 경우, 서버는
결과 버퍼를 해석된 반환 타입의 `persisted_length`에 맞춰 자동으로 크기
조정합니다 — 확장 작성자는 이 경우 함수 빌더에 `.buffer_size(...)`를 선언할
필요가 없습니다. `prerun`이 버퍼를 더 크게 늘리면 그 큰 크기가 유지됩니다.
예를 들어 이 덕분에 `SVECTOR::from_string('[…1024 floats…]')`이 결과 버퍼
공간이 부족해지는 일 없이 넓은 벡터를 인코딩할 수 있습니다.

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

## 집계 VDF

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

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

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

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

using namespace vsql;

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

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

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

빌더 메서드 작동 방식:

* `make_aggregate_func<State, &result_fn>()`은 `prerun` 및 `postrun`을 자동 생성합니다(`State`를 값 초기화하고 삭제합니다).
* `.clear<&fn>()`은 사용자의 `void(State&)` 재설정 함수를 등록합니다.
* `.accumulate<&fn>()`은 사용자의 `void(State&, TypedArgs...)` 결합 함수를 등록합니다. `TypedArgs`는 함수 시그니처에서 추론됩니다(`IntArg`, `StringArg` 등). `.accumulate<>()`는 모든 `.param(TYPE)` 호출 뒤에 호출하세요: 빌더는 타입 인자 개수를 선언된 SQL 매개변수 개수와 컴파일 타임에 대조하므로, 더 먼저 호출하면 빌드에 실패합니다.
* 결과 타입(`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); }
```

`StringResult` 집계 VDF는 텍스트를 반환합니다: 결과는 `utf8mb4_bin` 문자
집합 및 콜레이션을 보고하므로, 클라이언트는 이를 16진수가 아닌 문자로 표시합니다
— 스칼라 VDF STRING 경로와 동일합니다. 또한 `.max_result_length(n)`(선택적으로
활성화하는 개발 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)와 프로토콜 4가 필요합니다)을
동일한 방식으로 존중하여, 구체화된 집계 결과(`GROUP BY`/`DISTINCT` 임시 테이블,
`CREATE TABLE ... SELECT`, 또는 UNION)의 크기를 조정하여 인자 너비에서
잘리지 않도록 합니다. 크기 조정 규칙 및 상한선은
[사용자 정의 버퍼 크기](https://villagesql.com/docs/ko/extensions/build/create.md#custom-buffer-sizes)를
참조하세요.

## 문장별 상태 (Prerun 및 Postrun)

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

> [!NOTE]
>
> **0.0.8-dev에서 변경됨.** prerun이 성공했다면, 첫 번째 행 전에 문이 실패하는 경우에도 postrun이 실행됩니다.

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

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

`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 타입의 임의 개수의 인자를 수용합니다. func
빌더에 `.varargs()`로 선언하며, 이는 `.no_params()` 및 `.param(TYPE)`과 상호
배타적입니다. 본문은 일반적인 고정 인자 타입 대신 `vsql::VarArgs` 인자를
받습니다.

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

프레임워크는 가변 인자 VDF의 인자 개수 또는 타입을 검증할 수 없습니다. 모든
가변 인자 등록은 잘못된 입력에 `PrerunResult::error()`를 호출하거나 결과
버퍼 크기를 조정하기 위해 `PrerunResult::request_buffer_size(n)`을 호출하는
prerun 훅과 짝을 이루어야 합니다.

범위-기반 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 인자에서 정의되지 않습니다.

`PrerunArgType::custom_type()`은 인자의 사용자 정의 타입 이름을
`std::string_view`로 반환합니다 — 사용자 정의 타입이 아닌 인자에서는 비어
있습니다. `is_custom()`만으로는 서버에 등록된 모든 사용자 정의 타입을
수용합니다. `custom_type()` 비교와 짝을 이루면 가변 인자 호출을 하나의 특정
타입으로 좁힐 수 있습니다:

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

// POINT2D stores two little-endian int32 (x, y) in 8 bytes — registration
// omitted here; see Custom Types for make_type<>().

void point_path_prerun(PrerunArgs args, PrerunResult out) {
  if (args.size() == 0) {
    out.error("point_path requires at least one point");
    return;
  }
  for (size_t i = 0; i < args.size(); i++) {
    auto t = args.type_at(i);
    if (!t.is_custom() || t.custom_type() != "POINT2D") {
      out.error("point_path: every argument must be a POINT2D");
      return;
    }
  }
  out.request_buffer_size(16 + args.size() * 32);
}

void point_path(VarArgs args, StringResult out) {
  auto buf = out.buffer();
  size_t off = 0;
  bool first = true;
  for (auto a : args) {
    if (a.is_null()) { out.set_null(); return; }
    auto bytes = a.as_custom();
    int x, y;
    memcpy(&x, bytes.data(), 4);
    memcpy(&y, bytes.data() + 4, 4);
    if (!first) {
      memcpy(buf.data() + off, " -> ", 4);
      off += 4;
    }
    first = false;
    off += static_cast<size_t>(
        snprintf(buf.data() + off, buf.size() - off, "(%d,%d)", x, y));
  }
  out.set_length(off);
}

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

```sql theme={null}
SELECT point_path(POINT2D::from_string('0,0'), POINT2D::from_string('1,2'));
-- (0,0) -> (1,2)
SELECT point_path('1,2');
-- ERROR 1123 (HY000): Can't initialize function 'point_path'; point_path: every argument must be a POINT2D
```

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

constexpr size_t kBytearrayLen = 8;

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();
    if (off + bytes.size() > dst.size()) { out.error("ba_concat_all: result too long"); return; }
    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()
```

## 해석 시점에 함수의 타입 매개변수 결정

서버의 내장 규칙은 보통 인자의 선언된 타입으로부터 반환 타입의 매개변수를
알아냅니다. 때로는 그럴 수 없습니다. 예를 들어
`tvector_concat(TVECTOR(M), TVECTOR(N))`은 `TVECTOR(M+N)`을 반환합니다. 내장
규칙은 차원이 다른 두 `TVECTOR` 인자를 거부하며, 인자의 매개변수를 반환 타입으로
복사할 수는 있어도 더할 수는 없습니다. 이런 경우에는 func 빌더의
`.bind_and_check_types<&Hook>()`으로 다음 시그니처를 갖는
**bind-and-check-types 훅**을 등록합니다:

```cpp theme={null}
void my_hook(vsql::BindArgs args, vsql::BindResult out);
```

`.bind_and_check_types()`와 `.varargs()`는 동일한 함수에서 상호 배타적입니다 —
가변 인자 VDF에는 훅이 판단할 고정 인자 목록이 없습니다. 빌더 체인에서
`.bind_and_check_types<&Hook>()`보다 앞에 `.varargs()`를 선언하면 컴파일 타임
`static_assert` 실패가 발생합니다. 반대 순서로 선언하면 컴파일은 되지만, 서버가
로드할 때 확장이 거부됩니다: 등록은 모든 함수의 최종 빌드 상태를 순회하며 가변
인자이면서 bind 훅을 가진 첫 번째 함수를 다음과 같이 거부합니다:

```text theme={null}
ERROR 3219 (HY000): Failed to load VEF extension '<name>': vef_register returned an error: VDF '<function>' declares both .varargs() and .bind_and_check_types(). This is not currently supported.
```

> [!WARNING]
>
> `.bind_and_check_types<&Hook>()`과 `<villagesql/vsql/bind_check_types.h>`는
> 선택적으로 활성화하는 개발 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)에만 선언되어
> 있으며 VEF 프로토콜 4를 필요로 합니다. 기본 안정 ABI 빌드에는 둘 다 없으므로,
> 훅을 등록하면 컴파일에 실패합니다.

서버는 함수의 인자에 대해 수행하는 두 단계 사이에 훅을 호출합니다: 첫 번째
단계는 각 인자의 기본 타입을 검사하고, 다음으로 훅이 실행되며, 두 번째 단계는
훅이 답을 준 각 인자에 그 답을 적용합니다. 훅이 설치되어 있으면 첫 번째 단계는
동일한 사용자 정의 타입의 인자 간에 공유 매개변수를 수집하지 않으므로, 인자들이
서로 일치할 필요가 없습니다. 자체 타입이 아직 해석되지 않은 인자, 예를 들어 단순
문자열 리터럴은 훅에서만 매개변수를 얻습니다. 매개변수화된 반환 타입의
매개변수도 인자에서 복사하는 내장 규칙 대신 훅에서 옵니다.

서버 소스 트리의
[`vsql_tvector`](https://github.com/villagesql/villagesql-server/blob/main/villagesql/examples/vsql-tvector/src/tvector.cc)
예제 확장은 `tvector_concat`에 그러한 훅을 등록합니다:

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

// Excerpt from villagesql/examples/vsql-tvector/src/tvector.cc. That file
// also has the standard library includes, the constants kDefaultBytesPerElem
// (4) and kTVectorMaxDimension (4096), the TVECTOR type, the tvector_concat
// function body, and the helpers common_element_width() and
// bind_params_for_vector(), which read an argument's resolved params or count
// the elements of a constant literal.

struct TVectorParams {
  int64_t dimension;
  size_t bytes_per_elem;  // 4 for float, 8 for double

  static TVectorParams parse(const std::map<std::string, std::string> &params) {
    auto dim_it = params.find("dimension");
    int64_t dim = strtoll(dim_it->second.c_str(), nullptr, 10);
    size_t bytes = kDefaultBytesPerElem;
    auto type_it = params.find("type");
    if (type_it != params.end() && type_it->second == "double") bytes = 8;
    return TVectorParams{.dimension = dim, .bytes_per_elem = bytes};
  }

  // Inverse of parse: render a typed TVectorParams back into the canonical
  // key/value string form. Used by the SDK when the server needs to publish
  // inferred params (e.g., from a constant-string from_string) back in the
  // same shape that parse() consumes.
  static void to_strings(const TVectorParams &p,
                         std::map<std::string, std::string> &out) {
    out["dimension"] = std::to_string(p.dimension);
    out["type"] = (p.bytes_per_elem == 8) ? "double" : "float";
  }
};

// bind_and_check_types for tvector_concat.
//
// Two jobs the built-in rules cannot do:
//   1. permit arguments whose dimensions differ (parameter sharing rejects it);
//   2. compute the return dimension as M+N (the server return rule only
//      copies).
//
// A third job appears when an argument is a constant: a bare '[1,2,3]' has no
// type context, and with a hook installed nothing donates params to it, so the
// hook counts the elements itself and answers with set_arg(). The element type
// comes from whichever side the server already resolved, which is what the
// built-in rules would have donated.
void tvector_concat_bind(vsql::BindArgs args, vsql::BindResult out) {
  if (args.size() != 2) {
    out.error("expects 2 arguments");
    return;
  }

  std::string error_msg;
  size_t bpe = 0;
  if (common_element_width(args, bpe, error_msg)) {
    out.error(error_msg);
    return;
  }

  TVectorParams first{}, second{};
  bool publish_first = false, publish_second = false;

  if (bind_params_for_vector(args, 0, bpe, first, publish_first, error_msg) ||
      bind_params_for_vector(args, 1, bpe, second, publish_second, error_msg)) {
    out.error(error_msg);
    return;
  }

  // Hand back the params of whichever argument the server could not resolve.
  if (publish_first) out.set_arg(0, first);
  if (publish_second) out.set_arg(1, second);

  const int64_t total = first.dimension + second.dimension;
  if (total > kTVectorMaxDimension) {
    out.error("combined dimension " + std::to_string(total) +
              " exceeds the maximum of " +
              std::to_string(kTVectorMaxDimension));
    return;
  }
  out.set_return(TVectorParams{.dimension = total, .bytes_per_elem = bpe});
}

// Registration, inside make_extension():
// .func(make_func<&tvector_concat>("tvector_concat")
//     .returns(TVECTOR)
//     .param(TVECTOR)
//     .param(TVECTOR)
//     .bind_and_check_types<&tvector_concat_bind>()
//     .deterministic()
//     .build())
```

이 훅은 차원이 다른 두 벡터를 결합할 수 있게 하며, 상수 리터럴의 요소 개수를
셉니다. 서버의 소스 빌드에는 `vsql_tvector`가 포함되지만, 릴리스 tarball에는
포함되지 않습니다.

```sql theme={null}
INSTALL EXTENSION vsql_tvector;
CREATE TABLE v2 (v vsql_tvector.TVECTOR(2));
CREATE TABLE v3 (v vsql_tvector.TVECTOR(3));
INSERT INTO v2 VALUES ('[1,2]');
INSERT INTO v3 VALUES ('[3,4,5]');

SELECT vsql_tvector.tvector_concat(v2.v, v3.v) AS c,
       vsql_tvector.tvector_dim(vsql_tvector.tvector_concat(v2.v, v3.v)) AS dim
FROM v2, v3;
```

```text theme={null}
+-------------+------+
| c           | dim  |
+-------------+------+
| [1,2,3,4,5] |    5 |
+-------------+------+
```

```sql theme={null}
SELECT vsql_tvector.tvector_concat(v, '[6,7]') AS c FROM v3;
```

```text theme={null}
+-------------+
| c           |
+-------------+
| [3,4,5,6,7] |
+-------------+
```

## 확장 로드 및 언로드 훅

확장은 때때로 호출마다가 아니라 로드마다 한 번씩 수행해야 하는 작업이 있습니다
— CPU에 특화된 구현을 선택하거나, 자체 구성을 한 번 읽거나, VDF가 이후에 읽을
조회 테이블을 구축하는 작업입니다. 그 작업은 확장 빌더의 `.on_init<&Fn>()`으로
등록하고, 대응하는 정리 작업은 `.on_deinit<&Fn>()`으로 등록합니다. 둘 다
`void()` 시그니처를 갖는 함수를 받으며, 반드시 함께 등록해야 합니다 — 하나만
선언하고 다른 하나를 선언하지 않으면 컴파일에 실패합니다. 다른 방법으로 짝이
맞지 않는 상태로 서버에 도달한 확장은 대신 로드 시 거부됩니다:

```text theme={null}
ERROR 3219 (HY000): Failed to load VEF extension '<name>': extension registers an on_init hook without an on_deinit
```

> [!WARNING]
>
> `.on_init<&Fn>()` 및 `.on_deinit<&Fn>()`은 선택적으로 활성화하는 개발 ABI
> 헤더(`-DVSQL_USE_DEV_ABI=ON`)에만 선언되어 있습니다. 기본 안정 ABI 빌드에는 두
> 메서드가 모두 없으므로, 훅을 등록하면 컴파일에 실패합니다.

`on_init`은 로드마다 한 번 — `INSTALL EXTENSION` 시점, 그리고 서버가 시작될
때마다 — 서버가 확장을 검증하고 필요한 기능을 채운 후에 실행되므로, 등록된
시스템 변수는 이미 구성된 값을 보유하고 있습니다. 특히 시작 시에는 지속된 시스템
변수 값이 적용될 때까지 `on_init`이 지연되므로, `SET PERSIST`로 설정된 값 대신
컴파일된 기본값을 보는 일은 결코 없습니다. 서버가 거부한 확장에 대해서는 절대
실행되지 않습니다. `on_deinit`은 언로드 시점 — `UNINSTALL EXTENSION` 또는 서버
종료 시 — 에, 서버가 그 동일한 기능을 비우기 전에 실행되므로, `on_init`이 본 것과
동일한 라이브 상태를 봅니다.

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

static float *g_sqrt_table = nullptr;

void build_table() {
  g_sqrt_table = new float[256];
  for (int i = 0; i < 256; i++) g_sqrt_table[i] = std::sqrt(float(i));
}

void free_table() {
  delete[] g_sqrt_table;
  g_sqrt_table = nullptr;
}

void fast_sqrt(IntArg n, RealResult out) {
  if (n.is_null() || n.value() < 0 || n.value() > 255) {
    out.set_null();
    return;
  }
  out.set(g_sqrt_table[n.value()]);
}

VEF_GENERATE_ENTRY_POINTS(
    make_extension()
        .on_init<&build_table>()
        .on_deinit<&free_table>()
        .func(make_func<&fast_sqrt>("fast_sqrt")
                  .returns(REAL)
                  .param(INT)
                  .build()))
```

`INSTALL EXTENSION my_extension` 후에 `SELECT fast_sqrt(16)`은 `4`를
반환합니다 — `build_table`이 이미 실행된 경우에만 도달할 수 있는 결과입니다.

## VEF\_GENERATE\_REGISTRATION

`VEF_GENERATE_REGISTRATION`은 확장 등록을 수행하지만 `extern "C"` 엔트리
포인트를 정의하지 않는 내부 `_vef_do_register()` 헬퍼를 생성합니다.
`vef_register` 동작을 사용자 정의해야 할 때 사용합니다 — 예를 들어 테스트
빌드에서 등록 후 디스크립터를 패치하기 위해서입니다. 일반 확장의 경우 대신
`VEF_GENERATE_ENTRY_POINTS`를 사용합니다.

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

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

## 사용자 정의 타입 연산

전체 타입 연산 빌더 참조 — 인코딩, 디코딩, 비교, 해시, 내장 기본값, 매개변수화된 타입 — 는 [타입 연산](https://villagesql.com/docs/ko/sdk/cpp/type-operations.md)을 참조하세요.

## 미리보기 기능

다음 VEF 기능은 선택적으로 활성화하는 미리보기 헤더로 사용할 수 있습니다. ABI 및 API는 여전히 활발히 개발 중입니다. 전체 참조는 [미리보기 기능](https://villagesql.com/docs/ko/extensions/build/preview-capabilities.md)을 참조하세요.

* **확장 시스템 변수** — [미리보기 기능 → 시스템 변수](https://villagesql.com/docs/ko/extensions/build/preview-capabilities.md#system-variables)
* **확장 상태 변수** — [미리보기 기능 → 상태 변수](https://villagesql.com/docs/ko/extensions/build/preview-capabilities.md#status-variables)
* **키링 액세스** — [미리보기 기능 → 키링 액세스](https://villagesql.com/docs/ko/extensions/build/preview-capabilities.md#keyring-access)
* **컬럼 저장** — [미리보기 기능 → 컬럼 저장](https://villagesql.com/docs/ko/extensions/build/preview-capabilities.md#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` | `VARCHAR(65535)` | `funcs` 및 `types` 배열과, 확장이 빌드된 SDK 버전을 보고하는 `sdk_version` 키를 포함하는 `vef_registration_t` 구조체의 JSON 직렬화입니다. |

## 참고 자료

* [C++로 확장 만들기](https://villagesql.com/docs/ko/extensions/build/create.md) — 종단간 빌드 단계, CMake 설정 및 설치
* [C++ 테스트](https://villagesql.com/docs/ko/sdk/cpp/testing.md) — 로컬 개발 서버, MTR 및 실패 디버깅
* [타입 연산](https://villagesql.com/docs/ko/sdk/cpp/type-operations.md) — 인코딩, 디코딩, 비교, 해시, 매개변수화된 타입
* [C++ API 참조](https://villagesql.com/docs/ko/sdk/cpp/extension-api-reference.md) — VDF 계약, null 처리 및 버퍼 크기 조정
* [확장 아키텍처](https://villagesql.com/docs/ko/extensions/build/architecture.md) — 라이프사이클, Victionary 캐싱, 성능 패턴 및 보안 모델


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