> ## 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++ 유형 연산 빌더에 대한 참조입니다. 사용자 정의 유형에 대한 튜토리얼 수준의 소개는 [C++에서 사용자 정의 유형 만들기](https://villagesql.com/docs/ko/sdk/cpp/custom-types.md)를 참조하세요.

사용자 정의 유형은 엔진이 내부적으로 호출하는 세 가지 연산을 필요로 합니다: 인코딩(문자열에서 바이너리로), 디코딩(바이너리에서 문자열로), 비교. 해시는 선택 사항입니다. 다음 C++ 서명에 맞춰 구현하세요(모두 `<villagesql/vsql.h>`를 통해 사용 가능):

## 고정 길이 유형

```cpp theme={null}
// Encode: string -> binary. Write the encoded bytes via out.buffer() and
// out.set_length(n). out.set_null(), out.warning(msg), out.error(msg), or no
// call at all reports the value as invalid: ERROR 1366 in strict mode, a
// warning under IGNORE. msg is discarded.
using TypeEncodeFunc = void (*)(std::string_view from, vsql::CustomResult out);

// Decode: binary -> string. Report the result by calling out.set_length(n)
// or out.set(sv). out.set_null(), out.warning(msg), out.error(msg), or no call
// at all fails the read with ERROR 3219; msg goes to the server log, and the
// default message is "failed to decode value".
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 GROUP BY, DISTINCT and UNION).
using TypeHashFunc = size_t (*)(vsql::CustomArg in);
```

> [!NOTE]
>
> **0.0.8-dev에서 변경됨.** 인코딩 함수에서 `out.set_null()`은 SQL `NULL`을 저장하고, 비어 있지 않은 메시지를 담은 `out.error(msg)`는 SQL 모드와 관계없이 그 메시지로 문을 중단합니다. `out.warning(msg)`를 호출하거나 아무것도 호출하지 않으면 여전히 값이 유효하지 않다고 보고됩니다.

사용자 정의 해시 함수를 생략하면 유형의 원시 저장 바이트를 해시하는 방식으로
대체됩니다. 동일한 논리적 값에 대해 인코딩 함수가 둘 이상의 바이너리 표현을
생성할 수 있는 유형에서는 이 방식이 안전하지 않습니다 — 대표적인 경우가 부동
소수점 필드로, `-0.0`과 `+0.0`은 비교 시 같지만 비트 단위로는 다르므로
`GROUP BY`와 `SELECT DISTINCT`가 이들을 서로 다른 값으로 취급하게 됩니다.

이러한 연산은 `vsql::make_type<kTypeName>()`을 사용해 등록합니다. 유형 이름은
비유형 템플릿 매개변수(NTTP) — `static constexpr const char[]` 배열 — 로 전달됩니다.
빌더는 이 NTTP로부터 `TYPE::method` 형식(예: `"MYTYPE::from_string"`)의 VDF 이름을
자동 생성하므로, 수동으로 문자열을 일치시킬 필요가 없습니다. 빌드된 유형 객체를
확장 빌더의 `.type()`에 전달하세요. 유형 연산을 위한 별도의 `.func()` 호출은 필요하지 않습니다.

> [!WARNING]
>
> 유형 이름은 반드시 `static constexpr const char[]` 변수여야 합니다 — 문자열 리터럴은 비유형 템플릿 매개변수로 사용할 수 없습니다. `"MYTYPE"`을 직접 전달하면 다음과 같은 컴파일러 오류가 발생합니다:
>
> ```
> error: '"MYTYPE"' is not a valid template argument for type 'const char*'
> ```
>
> 아래와 같이 이름을 명명된 배열로 선언하세요.

```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"; set it for types that store floating-point values
        .intrinsic_default_str("0")  // string-literal intrinsic default
        .build();

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

`from_string`, `to_string`, `compare` 중 하나가 누락되면 `build()`가 컴파일 타임에
실패합니다. 각 템플릿 메서드는 `static_assert`로 함수 포인터 서명을 검증합니다.

사용자 정의 유형은 값당 최대 **65532바이트**의 저장 공간을 선언할 수 있습니다.
사용자 정의 컬럼은 `VARBINARY` 필드로 뒷받침되며, MySQL의 행당 65535바이트 예산은
해당 필드의 길이 바이트 2개와 행의 null 바이트도 함께 부담합니다. 컬럼의 nullable
여부는 `CREATE TABLE` 시점에 컬럼별로 결정되고 nullable이 기본값이므로, 이 상한값은
null 바이트를 위한 공간을 남겨 두어야 합니다. `persisted_length`와
`max_persisted_length` 모두 이 값에 대해 검사되며, 이 검사는 `CREATE TABLE`이 아니라
`INSTALL EXTENSION` 시점에 실행되므로 너무 넓은 유형은 확장 전체와 함께 거부됩니다:

```text theme={null}
ERROR 3219 (HY000): Failed to install extension 'vsql_huge_fixed_test': type 'HUGE_FIELD' declares 70000 bytes of storage per value, which exceeds the maximum of 65532 bytes supported for custom-type columns today
```

상한값 아래에 머무른다고 해서 컬럼이 반드시 들어맞는 것은 아닙니다. 테이블의 다른
모든 컬럼도 동일한 65535바이트 행 예산을 사용하며, 이는 `CREATE TABLE`만이 가늠할 수
있으므로 상한값에 가깝거나 도달한 유형은 여전히 그 시점에 `ER_TOO_BIG_ROWSIZE`로
실패할 수 있습니다. 동일한 폭의 일반 `VARBINARY`도 같은 방식으로 동작합니다.

## 내장 기본값

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

> [!NOTE]
>
> `.intrinsic_default_str()`과 `.intrinsic_default_vdf()`를 모두 생략하면,
> 서버는 대체 방안으로 `from_string("")`을 호출합니다. 이는 유형이 `INSTALL EXTENSION` 시점이 아니라
> **처음 사용될 때**(테이블 생성 시) 발생합니다. 인코딩 함수가 빈 문자열을 거부하면 유형 초기화가 실패하며 SQL 클라이언트에 다음과 같은 오류가 표시됩니다:
>
> ```
> ERROR 3219 (HY000): Type 'my_ext.MYTYPE' failed to initialize: from_string VDF failed to encode intrinsic default input '' (expected persisted_length=M)
> ```
>
> 빈 문자열을 잘못된 바이트 수로 인코딩하면 오류는 다음과 같습니다:
>
> ```
> ERROR 3219 (HY000): Type 'my_ext.MYTYPE' failed to initialize: from_string VDF encoded intrinsic default input '' to N bytes, expected persisted_length=M
> ```
>
> 고정 길이 유형의 경우, 기본 문자열은 정확히 `persisted_length` 바이트로 인코딩되어야 합니다.
> 빈 문자열이 유효한 입력이 아닌 모든 유형에 대해 명시적 기본값을 설정하세요.

내장 기본값은 두 가지 방법 중 하나로 제공합니다. 두 방법은 상호 배타적입니다:
`.intrinsic_default_str()`과 `.intrinsic_default_vdf()`를 모두 선언하면
개발용 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)에서는 컴파일 타임 오류가 발생하고,
안정 헤더에서는 서버가 `INSTALL EXTENSION` 시점에 유형을 거부합니다.

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

상수 기본값의 경우, 유형 빌더에 문자열을 직접 전달하세요(위 고정 길이 예제에서
`.intrinsic_default_str("0")`으로 표시된 것처럼).

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

기본값이 유형 매개변수에 의존할 때는, 다음 서명 중 하나에 맞춰 함수를
구현하세요(`<villagesql/vsql.h>`를 통해 사용 가능):

```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 */;
}
```

## 매개변수화 유형

매개변수화 유형은 할당 크기와 레이아웃을 결정하기 위해 인코딩, 디코딩,
비교, 해시 시점에 컬럼의 선언된 매개변수를 필요로 합니다. 파싱 함수와
역방향 `to_strings` 함수를 가진 params 구조체를 정의하고, `.params<P, &ParseFunc, &ToStringsFunc>()`로
유형 빌더에 둘 다 등록한 뒤, 유형 연산 함수의 첫 번째 인수로 `vsql::MaybeParams<P> &`(인코딩)
또는 `vsql::CustomArgWith<P>`(디코딩, 비교, 해시)를 받으세요.
SDK는 고유한 매개변수 조합별로 파싱 결과를 캐시하므로, 파싱 함수는 유형 인스턴스화당
최대 한 번만 실행됩니다. `to_strings` 함수는 `parse`의 역함수입니다:
이는 유형이 지정된 `P`를 표준 키/값 문자열 형식으로 다시 작성하여 서버가
추론된 매개변수를 `parse`가 소비하는 것과 동일한 형태로 게시할 수 있도록 합니다.

파싱 캐시는 params 구조체 자체를 키로 합니다: `TypeParamsCache<P>`는 `P`당 하나의
인스턴스이며, 그 `P`로 선언된 모든 사용자 정의 유형이 공유합니다. 둘 이상의 유형이
동일한 params 구조체를 사용하면, 그 각각은 동일한 파싱 함수를 등록해야 합니다.
다르게 파싱해야 하는 유형에는 자체 params 구조체가 필요합니다.

```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 false when the server infers the params from a constant string
  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) {
  auto it = params.find("dimension");
  if (it == params.end()) {
    snprintf(error_msg, VEF_MAX_ERROR_LEN, "MYTYPE: dimension parameter is required");
    return true;
  }
  char *endptr = nullptr;
  int64_t dim = std::strtoll(it->second.c_str(), &endptr, 10);
  if (endptr == it->second.c_str() || *endptr != '\0') {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: invalid dimension value '%s'", it->second.c_str());
    return true;
  }
  if (dim <= 0) {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: dimension must be a positive integer");
    return true;
  }
  if (dim > INT64_MAX / 4) {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: dimension %" PRId64 " is too large", dim);
    return true;
  }
  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>()`을 사용하세요. 모든 유효한
매개변수화에 걸친 저장 바이트 크기의 상한값으로 `.max_persisted_length(N)`을 호출하세요.
서버는 이를 유형 매개변수 추론 경로에서 인코딩 버퍼 크기를 조정하는 데 사용하는데,
이 경로에서는 아직 매개변수를 추론하지 않아 `resolve_params`를 참조할 수 없기 때문입니다.
또한 이 값을 초과하는 해석된 고정 `persisted_length`를 거부합니다.
VDF 기반 내장 기본값의 경우, `.intrinsic_default_vdf()`에 VDF 이름을 사용하고
`make_intrinsic_default<&mytype_default>("vdf_name")`을 통해 VDF를 별도로 등록하세요.

개발용 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)로 빌드할 때, `.params<>()`나
`.int_to_params<>()`가 등록되면 `build()`는 `.resolve_params<>()`를 요구합니다:
`int_to_params`는 `MYTYPE(N)` 축약 형식을 매개변수 문자열로 재작성하기만 하는 반면,
`resolve_params`는 매개변수를 검증하고 저장 크기를 해석하는 관문입니다. 그곳에서 이를
생략하면 컴파일 타임 오류가 발생합니다. 안정 헤더에서는 서버가 `INSTALL EXTENSION`
시점에 유형을 거부합니다.

> [!WARNING]
>
> `.max_persisted_length()`는 VEF 프로토콜 3 이상을 필요로 합니다. 이를 사용하는 유형은
> 프로토콜 3 이전 서버에서 로드될 수 없습니다. 이는 가변 길이 유형 또는 매개변수화
> 유형에만 유효합니다. 매개변수화되지 않은 고정 길이 유형이 이를 설정하면 개발용 ABI
> 헤더(`-DVSQL_USE_DEV_ABI=ON`)에서는 컴파일에 실패하고, 안정 헤더로 빌드하면 서버가
> `INSTALL EXTENSION` 시점에 거부합니다.

`resolve_params`가 보고하는 크기는 컬럼의 매개변수가 확정된 뒤 `CREATE TABLE`
시점에도 검증됩니다. 고정 길이 유형의 경우, 해석된 `persisted_length`는 선언된
`max_persisted_length`를 초과해서는 안 됩니다. 계산된 사용량(예: `dimension * 4`
크기 조정)이 그 한도를 넘는 매개변수화는 구문 오류로 거부됩니다. 길이 종류와
무관하게, `resolve_params`는 0보다 큰 `max_decode_buffer_length`를 해석해야 합니다.
이 값이 디코딩 스크래치 버퍼의 크기를 결정하기 때문이며, 0 이하의 결과는 동일한
부류의 오류로 거부됩니다.

```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)    // fixed length, sized per column by resolve_params
        .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` 템플릿 메서드는 params 인수를 감지해 자동으로 params 캐시를
통해 라우팅합니다. 인코딩 함수는 첫 번째 인수로 `vsql::MaybeParams<P> &`를
받습니다. `is_known()`은 서버가 상수 문자열에서 매개변수를 추론할 때 false이며,
`value()`는 매개변수가 확정되면 `const P&`를 반환합니다.
디코딩, 비교, 해시 변형은 `vsql::CustomArgWith<P>`를 받으며, 그 `params()`
접근자는 `const P&`를 반환합니다.

**SQL에서 매개변수 제공.** 두 가지 구문이 `resolve_params`에 도달합니다:

* **정수** — `MYTYPE(N)`. 서버는 `N`을 `int_to_params`를 통해 라우팅해
  매개변수 맵을 구성합니다. `.int_to_params<>()`가 필요합니다.
* **문자열** — `MYTYPE('key=value,...')`. 서버는 문자열을 정규화하고
  `resolve_params`를 직접 호출합니다. `int_to_params`는 관여하지 않습니다.
  `.resolve_params<>()`가 등록되어 있으면 언제든 사용 가능합니다 — 추가 빌더 호출이 필요하지 않습니다.

`.resolve_params<>()`만 등록하는 유형은 문자열 형식을 허용하고
`MYTYPE(N)`을 거부합니다.

`SHOW CREATE TABLE`은 작성자가 쓴 내용을 그대로 되풀이하지 않습니다. 서버는 해석된
매개변수 맵을 보관하고 이로부터 표시 형식을 다시 구성하며, `int_to_params(N)`이 맵
전체를 재현할 수 있으면 `MYTYPE(N)`을 우선하고, 그렇지 않으면 키가 정렬된 표준
`MYTYPE('key=value,...')` 형식으로 대체합니다. 따라서 `MYTYPE('dimension=8')`로 작성된
컬럼은 `MYTYPE(8)`로 다시 읽힙니다.

```sql theme={null}
CREATE TABLE t (v ext.MYTYPE(8));              -- integer form (.int_to_params)
CREATE TABLE t2 (v ext.MYTYPE('dimension=8')); -- string form (.resolve_params only)
```

> [!NOTE]
>
> `int_to_params`가 생성하고 `resolve_params`가 소비하는 직렬화된 `key=value,...`
> 매개변수 문자열은 `VEF_MAX_TYPE_PARAMS_STRING_LEN`(1024바이트)으로 제한됩니다.
> 표준 문자열이 그 한도를 초과하는 매개변수화는 조용히 잘리는 대신 정의된 오류로
> 거부됩니다 — 단일 유형에 대한 매개변수 이름과 값의 합계를 1024바이트 이내로 유지하세요.

어느 구문이 매개변수를 제공하든, 서버는 `resolve_params`를 호출하기 전에 결과 맵을
검증합니다: 모든 키는 비어 있지 않아야 하고, 한 번만 나타나야 하며, 비어 있지 않은
값을 가져야 합니다. 위반은 유형 이름을 명시하는 구문 오류입니다. 빈 매개변수 이름의
경우를 제외하고, 문제가 된 키의 이름도 명시합니다:

```sql theme={null}
-- vsql_tvector ships in the villagesql/server Docker image; with a tarball install, build it from villagesql/examples/vsql-tvector in the server source
INSTALL EXTENSION vsql_tvector;
CREATE TABLE t3 (v vsql_tvector.TVECTOR('dimension=3,dimension=4'));
-- ERROR 1064 (42000) at line 1: Type 'vsql_tvector.TVECTOR': duplicate parameter 'dimension' near 'vsql_tvector.TVECTOR('dimension=3,dimension=4'))' at line 1
```

> [!NOTE]
>
> **0.0.8-dev에서 변경됨.** 유형 매개변수 텍스트는 연결 문자 집합에서 UTF-8로 변환되며, 유효한 UTF-8이 아니거나 NUL 바이트를 포함하는 텍스트는 거부됩니다. 중복 매개변수 이름은 대소문자와 악센트를 구분하지 않고 비교하므로, `café`와 `cafe`는 같은 이름입니다.

### 매개변수 재작성 및 기본값 제공

`resolve_params`에는 선택적으로 활성화하는 개발용 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)에만
선언된 두 번째 변형(mutating overload)이 있습니다: 매개변수 맵을
비상수 참조로 받아 유형이 이를 재작성할 수 있게 합니다 — 일반적으로 작성자가 생략한
기본값을 채우기 위해서입니다. 동일한 방식으로 등록하세요(`.resolve_params<&fn>()`은
두 형식 중 하나를 허용합니다. 하나만 등록하세요):

```cpp theme={null}
bool mytype_resolve_params_fn(std::map<std::string, std::string> &params,
                              vsql::ResolvedTypeParams *result, char *error_msg) {
  if (params.find("dimension") == params.end())
    params["dimension"] = "128";                 // supply a default
  const std::string &dim_str = params.at("dimension");
  char *endptr = nullptr;
  int64_t dim = std::strtoll(dim_str.c_str(), &endptr, 10);
  if (endptr == dim_str.c_str() || *endptr != '\0') {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: invalid dimension value '%s'", dim_str.c_str());
    return true;
  }
  if (dim <= 0) {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: dimension must be a positive integer");
    return true;
  }
  if (dim > INT64_MAX / 4) {
    snprintf(error_msg, VEF_MAX_ERROR_LEN,
             "MYTYPE: dimension %" PRId64 " is too large", dim);
    return true;
  }
  result->persisted_length = dim * 4;
  result->max_decode_buffer_length = 64;
  return false;                                  // success
}
```

변형 `resolve_params` 콜백은 매개변수를 추가하거나 변경할 수 있지만, 호출자가 작성한
매개변수를 제거하는 것은 오류입니다: `Type '<extension>.<type>':
resolve_params removed parameter '<key>'; parameters may be added or changed
but not removed`.

재작성된 맵은 서버가 저장하고 `SHOW CREATE TABLE`이 출력하는 표준 매개변수
문자열이 되므로, 재작성은 멱등적이어야 합니다. **매개변수 없는** 선언(`MYTYPE`, 길이나 매개변수 없음)은
유형이 `.int_to_params<>()`를 등록하지 않은 경우 빈 맵으로 `resolve_params`를 호출하므로,
기본값을 제공하는 유형은 모든 컬럼에 명시적 매개변수를 부여합니다 — `vsql_bitfield_test`의 `BITFIELD`는
매개변수 없는 컬럼을 `max_number_of_bits=4096`으로 해석합니다:

```sql theme={null}
-- vsql_bitfield_test is built from villagesql/test-extensions/vsql-bitfield-test in the server source; the release does not ship it
INSTALL EXTENSION vsql_bitfield_test;
CREATE TABLE bits (id INT PRIMARY KEY, b vsql_bitfield_test.BITFIELD);
SHOW CREATE TABLE bits;   -- b persists as BITFIELD('max_number_of_bits=4096')
```

## 가변 길이 유형

가변 길이 사용자 정의 유형은 단일 고정 크기를 사용하는 대신 값별로 저장되는
크기를 결정합니다. 유형 빌더에서 `.variable_length_type()`을 호출해 선언하며,
이는 유형의 `variable_length` 플래그를 설정합니다.

> [!WARNING]
>
> `.variable_length_type()`은 유형에 필요한 프로토콜을 VEF 프로토콜 4로 높입니다.
> 서버는 `variable_length` 플래그를 프로토콜 4 이상에서만 읽습니다. opt-in 방식의
> 개발용 ABI 헤더(`-DVSQL_USE_DEV_ABI=ON`)로 빌드하세요. 이전 서버는
> 플래그를 읽지 않습니다.

가변 길이 유형은 반드시 `.max_persisted_length(N)`도 호출해야 합니다. 이를 생략하면
`build()`가 컴파일 타임에 실패합니다 — 서버는 백킹 필드에 버퍼를 할당하기 위해
상한값이 필요합니다. 가변 길이 유형은 `.persisted_length()`를 함께 호출해서는
안 됩니다: 크기가 값별로 결정되므로 `build()`가 이 조합을 컴파일 타임에 거부합니다.

`.variable_length_type()`은 단조적입니다: 프로토콜 3 설정자
(`max_persisted_length()`, `params()`, `int_to_params()`) 앞이나 뒤에 호출해도
프로토콜 요구 사항을 프로토콜 4 아래로 낮추지 않습니다.

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

static constexpr const char kMyTypeName[] = "MYTYPE";
constexpr int64_t kMyTypeMaxPersistedLength = 4096;

constexpr auto MYTYPE =
    vsql::make_type<kMyTypeName>()
        .variable_length_type()  // per-value sizing; requires Protocol 4
        .max_persisted_length(kMyTypeMaxPersistedLength)
        .max_decode_buffer_length(64)
        .from_string<&my_encode>()
        .to_string<&my_decode>()
        .compare<&my_compare>()
        .build();

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

> [!NOTE]
>
> 모든 사용자 정의 유형과 마찬가지로, 가변 길이 유형은 사용 가능한
> [내장 기본값](#intrinsic-default)을 생성해야 합니다. 기본값은 필드의 최대 용량에
> 인코딩되며, 1에서 `max_persisted_length` 바이트 사이의 비어 있지 않은 모든 결과가
> 허용됩니다. 빈 문자열 인코딩이 **0** 바이트를 생성하는 유형 — 예를 들어 빈 배열이나
> 비트 집합 — 은 사용 가능한 기본값이 없으므로, 비어 있지 않은 값으로 인코딩되는
> 명시적 기본값을 선언하세요:
>
> ```cpp theme={null}
>         .max_persisted_length(kMyTypeMaxPersistedLength)
>         .intrinsic_default_str("[0]")  // empty "[]" would encode to zero bytes
> ```
>
> 그렇지 않으면 유형은 컬럼이 처음 이를 참조할 때, `CREATE TABLE` 시점에
> 초기화에 실패합니다 — `from_string("")`을 인코딩할 수 없는 고정 길이 유형과 동일합니다.

## 저장 프로시저에서의 사용자 정의 유형

사용자 정의 확장 유형은 저장 프로시저 매개변수 유형 및 `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 ;
```

## 참고 자료

* [C++에서 사용자 정의 유형 만들기](https://villagesql.com/docs/ko/sdk/cpp/custom-types.md) — 사용자 정의 유형에 대한 튜토리얼 소개
* [C++ API 참조](https://villagesql.com/docs/ko/sdk/cpp/extension-api-reference.md) — VDF 계약, null 처리 및 버퍼 크기 조정
* [C++ 개발](https://villagesql.com/docs/ko/sdk/cpp/development.md) — VDF 작성 심화, 인수 및 결과 유형, 집계, 가변 인수


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