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

# Rust 확장 예제

> Rust SDK를 사용하는 vsql_rot13, vsql_rational, vsql_agg_sum 및 vsql_varargs 참조 구현에서 배우기

이 페이지에서는 Rust SDK 리포지토리의 참조 확장 네 가지를 살펴봅니다 — 최소한의 함수 전용 예제, 산술, 정렬, 해싱을 갖춘 완전한 사용자 정의 타입, 집계 함수, 그리고 가변 인자 함수입니다. 리포지토리의 `examples/` 디렉터리에는 지원되는 [미리보기 기능](/docs/ko/mysql-9.7/stable/rust-preview-capabilities)별 예제도 하나씩 들어 있습니다.

**소스:** [vsql-rust-sdk](https://github.com/villagesql/vsql-rust-sdk/tree/main/examples)의 `examples/`

***

## vsql\_rot13 — 함수 전용 확장

가능한 한 가장 간단한 Rust 확장: STRING을 받아 STRING을 반환하는 VDF 하나입니다.

**사용법:**

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

SELECT rot13('Hello, World!');
-- 'Uryyb, Jbeyq!'

SELECT rot13(rot13('Hello, World!'));
-- 'Hello, World!' (rot13 is its own inverse)

SELECT rot13(NULL);
-- NULL
```

### 디렉터리 구조

```
vsql_rot13/
├── Cargo.toml          # cdylib crate, depends on villagesql
├── manifest.json       # Extension metadata
├── src/
│   └── lib.rs          # Implementation + extension! registration
└── mysql-test/
    └── t/*.test        # MTR test cases
```

### 구현

**파일: `src/lib.rs`**

```rust theme={null}
use villagesql::{InValue, VdfReturn};

/// SQL: rot13(s STRING) -> STRING
fn rot13_impl(args: &[InValue]) -> VdfReturn {
    match args.first() {
        Some(InValue::String(s)) => VdfReturn::string(rot13(s)),
        Some(InValue::Null) | None => VdfReturn::null(),
        _ => VdfReturn::error("rot13: expected a STRING argument"),
    }
}

fn rot13(s: &str) -> String {
    s.chars()
        .map(|c| match c {
            'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,
            'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,
            _ => c,
        })
        .collect()
}

villagesql::extension! {
    funcs: [
        villagesql::func!(rot13_impl, "rot13",
            [villagesql::Type::String] -> villagesql::Type::String),
    ]
}
```

**핵심 패턴:**

* VDF는 `&[InValue]`를 받고 `VdfReturn`을 반환합니다 — 둘 다 안전한 Rust 열거형입니다
* NULL은 양쪽 모두에서 일급 배리언트(variant)입니다; 직접 패턴 매치하세요
* `extension!` 매크로는 서버가 로드 시 호출하는 C 엔트리 포인트를 생성합니다
* `func!`는 SQL 시그니처를 선언합니다; 인수 및 반환 타입은 `villagesql::Type::*`를 사용합니다

### 매니페스트

**파일: `manifest.json`**

```json theme={null}
{
  "name": "vsql_rot13",
  "version": "0.1.0",
  "description": "Example VillageSQL extension: provides rot13(STRING) -> STRING",
  "author": "VillageSQL Community",
  "license": "GPL-2.0"
}
```

***

## vsql\_rational — 산술을 갖춘 사용자 정의 타입

완전한 사용자 정의 타입: 기약 형태의 `(numerator, denominator)`로 저장되는 유리수이며, 산술 함수, 정렬, 해싱을 갖춥니다.

**사용법:**

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

CREATE TABLE measurements (id INT, ratio rational);
INSERT INTO measurements VALUES
    (1, '1/2'),
    (2, '2/4'),   -- normalizes to '1/2' on storage
    (3, '-3/6'),  -- normalizes to '-1/2'
    (4, '0/1');

SELECT id, ratio FROM measurements ORDER BY ratio;

SELECT rational_add('1/2', '1/3');     -- '5/6'
SELECT rational_mul('2/3', '3/4');     -- '1/2'
SELECT rational_to_real('22/7');       -- 3.142857...
```

### 이진 저장 형식

`rational`은 **16바이트**를 저장합니다(리틀 엔디언):

* 바이트 0–7: 분자(`i64`)
* 바이트 8–15: 분모(`i64`)

값은 항상 양의 분모와 함께 기약 형태(GCD = 1)로 저장됩니다.

### 타입 시스템 함수

**파일: `src/lib.rs`**

이 타입은 네 가지 연산을 등록합니다: encode(문자열 → 바이트), decode(바이트 → 문자열), compare(ORDER BY용), hash(인덱싱용).

```rust theme={null}
pub fn rational_encode(s: &str) -> Result<Vec<u8>, String> {
    let (num_s, den_s) = s
        .split_once('/')
        .ok_or_else(|| format!("rational: expected 'n/d', got {s:?}"))?;
    let num: i64 = num_s.trim().parse().map_err(|e| format!("numerator: {e}"))?;
    let den: i64 = den_s.trim().parse().map_err(|e| format!("denominator: {e}"))?;
    let (n, d) = normalize(i128::from(num), i128::from(den))
        .ok_or_else(|| "rational: zero or overflowing denominator".to_string())?;
    Ok(to_bytes(n, d))
}

pub fn rational_decode(b: &[u8]) -> Result<String, String> {
    if b.len() < BYTES {
        return Err(format!("rational: expected {} bytes, got {}", BYTES, b.len()));
    }
    let (n, d) = from_bytes(b);
    Ok(format!("{n}/{d}"))
}

pub fn rational_compare(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
    let (n1, d1) = from_bytes(a);
    let (n2, d2) = from_bytes(b);
    // Cross-multiply; denominators are always positive after normalization
    let lhs = i128::from(n1) * i128::from(d2);
    let rhs = i128::from(n2) * i128::from(d1);
    lhs.cmp(&rhs)
}
```

### VDF 구현

사용자 정의 타입을 받는 VDF는 `InValue::Custom(&[u8])`를 받아 바이트를 직접 디코딩합니다:

```rust theme={null}
fn rational_add_impl(args: &[InValue]) -> VdfReturn {
    match (arg(args, 0), arg(args, 1)) {
        (Ok(Some((n1, d1))), Ok(Some((n2, d2)))) => {
            match normalize(
                i128::from(n1) * i128::from(d2) + i128::from(n2) * i128::from(d1),
                i128::from(d1) * i128::from(d2),
            ) {
                Some((n, d)) => VdfReturn::Binary(to_bytes(n, d)),
                None => VdfReturn::error("rational_add: overflow"),
            }
        }
        (Err(e), _) | (_, Err(e)) => VdfReturn::error(format!("rational_add: {e}")),
        _ => VdfReturn::null(),
    }
}
```

### 등록

`extension!` 매크로는 타입과 그 함수를 단일 선언으로 등록합니다:

```rust theme={null}
villagesql::extension! {
    funcs: [
        villagesql::func!(rational_add_impl, "rational_add",
            [villagesql::custom!("rational"), villagesql::custom!("rational")]
            -> villagesql::custom!("rational"),
            deterministic: true),
        villagesql::func!(rational_sub_impl, "rational_sub",
            [villagesql::custom!("rational"), villagesql::custom!("rational")]
            -> villagesql::custom!("rational"),
            deterministic: true),
        villagesql::func!(rational_mul_impl, "rational_mul",
            [villagesql::custom!("rational"), villagesql::custom!("rational")]
            -> villagesql::custom!("rational"),
            deterministic: true),
        villagesql::func!(rational_div_impl, "rational_div",
            [villagesql::custom!("rational"), villagesql::custom!("rational")]
            -> villagesql::custom!("rational"),
            deterministic: true),
        villagesql::func!(rational_numer_impl, "rational_numer",
            [villagesql::custom!("rational")] -> villagesql::Type::Int,
            deterministic: true),
        villagesql::func!(rational_denom_impl, "rational_denom",
            [villagesql::custom!("rational")] -> villagesql::Type::Int,
            deterministic: true),
        villagesql::func!(rational_to_real_impl, "rational_to_real",
            [villagesql::custom!("rational")] -> villagesql::Type::Real,
            deterministic: true),
    ],
    types: [
        villagesql::custom_type!(
            type_name: "rational",
            persisted_length: 16,
            max_decode_buffer_length: 42,
            encode: rational_encode,
            decode: rational_decode,
            compare: rational_compare,
            hash: rational_hash,
            default: "0/1",
        ),
    ]
}
```

**핵심 패턴:**

* `villagesql::custom!("name")`는 사용자 정의 타입을 인수 또는 반환으로 참조합니다
* `custom_type!`은 타입을 그 encode/decode/compare/hash 함수와 함께 등록합니다
* `default: "0/1"`은 내재적 기본값입니다 — 서버는 타입 초기화 시 이 문자열에 대해 `encode()`를 호출하므로 유효한 값이어야 합니다
* `persisted_length`는 `encode()`가 반환하는 바이트 길이와 일치해야 합니다
* `deterministic: true`는 최적화기가 상수 호출을 폴딩할 수 있게 합니다

***

## vsql\_agg\_sum — 집계 함수

`INT` 컬럼에 대해 `SUM`을 다시 구현한 집계 VDF입니다. 집계에 필요한 세 가지 훅 — `clear`, `accumulate`, 결과 함수 — 과 각 훅이 어떻게 같은 누산기를 보는지 보여줍니다.

**사용법:**

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

CREATE TABLE t (grp INT, val INT);
INSERT INTO t VALUES (1, 10), (1, 20), (2, 100), (2, 200), (2, 300);

SELECT grp, vsql_agg_sum.agg_sum(val) AS mine, SUM(val) AS builtin FROM t GROUP BY grp ORDER BY grp;
```

```
grp	mine	builtin
1	30	30
2	600	600
```

전부 NULL인 그룹을 추가해 보면, 누산기가 이월되는 것이 아니라 그룹 사이에서 재설정된다는 것을 알 수 있습니다:

```sql theme={null}
INSERT INTO t VALUES (3, NULL), (3, NULL);
SELECT grp, vsql_agg_sum.agg_sum(val) AS mine, SUM(val) AS builtin FROM t GROUP BY grp ORDER BY grp;
```

```
grp	mine	builtin
1	30	30
2	600	600
3	NULL	NULL
```

### 누산기 수명 주기

누산기는 문장마다 하나의 값이며, 모든 그룹에서 재사용됩니다. 서버는 정해진 순서로 이를 구동합니다:

| 단계         | 작성하는 함수                                          | 실행 시점                                                                                             |
| ---------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| 할당         | 없음 — `agg_func!`가 생성                             | 문장마다 한 번, 첫 번째 행 이전. SDK가 상태를 `Default::default()`로 할당하며, 상태 타입이 `Default`를 구현해야 하는 이유가 바로 이것입니다. |
| Clear      | `clear:` — `fn(&mut State)`                      | 각 그룹이 시작될 때.                                                                                      |
| Accumulate | `accumulate:` — `fn(&mut State, &[InValue])`     | 그룹의 행마다 한 번. 아무것도 반환하지 않으며, 유일한 효과는 상태에 대한 것입니다.                                                  |
| 결과         | `agg_func!`의 첫 번째 인수 — `fn(&State) -> VdfReturn` | 그룹마다 한 번, 마지막 행이 반영된 후.                                                                           |
| 드롭         | 없음 — `agg_func!`가 생성                             | 문장이 끝난 후.                                                                                         |

`clear`는 각 그룹이 시작될 때 누산기를 재설정합니다. 재설정을 빠뜨린 필드는 이전 그룹에서 누출됩니다.

서버는 인수가 NULL인 행을 포함해 모든 행에 대해 `accumulate`를 호출합니다. NULL을 건너뛰는 것은 함수의 몫입니다: 원하는 variant만 매치하고 나머지는 무시하세요.

### 구현

**파일: `src/lib.rs`**

```rust theme={null}
use villagesql::{InValue, VdfReturn};

/// Accumulator for `agg_sum`: the running total for the current group.
#[derive(Default)]
struct SumState {
    total: i64,
    seen: bool,
}

/// clear: reset the total at the start of each group.
fn agg_sum_clear(state: &mut SumState) {
    state.total = 0;
    state.seen = false;
}

/// accumulate: fold one row's int into the running total.
fn agg_sum_acc(state: &mut SumState, args: &[InValue]) {
    if let Some(InValue::Int(n)) = args.first() {
        state.total += *n;
        state.seen = true;
    }
}

/// result: emit the group's total once every row has been folded in.
fn agg_sum_result(state: &SumState) -> VdfReturn {
    if state.seen {
        VdfReturn::int(state.total)
    } else {
        VdfReturn::Null
    }
}
```

`seen` 플래그는 합계가 0인 그룹과 합산할 것이 없는 그룹을 구분해 줍니다. 이 플래그가 없으면 빈 그룹이나 전부 NULL인 그룹은 내장 `SUM`이 NULL을 반환하는 곳에서 `0`을 반환하게 됩니다.

### 등록

```rust theme={null}
villagesql::extension! {
    funcs: [
        villagesql::agg_func!(agg_sum_result, "agg_sum",
            [villagesql::Type::Int] -> villagesql::Type::Int,
            state: SumState, clear: agg_sum_clear, accumulate: agg_sum_acc),
    ]
}
```

**핵심 패턴:**

* 첫 번째 식별자는 행 함수가 아니라 **결과** 함수입니다 — 집계의 행별 작업은 `accumulate:`에 있습니다
* `state:`는 누산기 타입을 지정하며, 이 타입은 `Default`를 구현해야 합니다
* 선언된 매개변수 목록은 행별 인수 목록입니다: `[villagesql::Type::Int]`는 `accumulate`가 받는 것이고, 반환 타입은 결과 함수가 만들어 내는 것입니다
* `agg_func!`는 `accumulate:` 뒤에 `buffer_size:`와 `deterministic:`도 받습니다 — 함께, 그 순서로 제공해야 합니다

***

## vsql\_varargs — 가변 인자 함수

각각 임의 개수의 인수를 받는 네 개의 VDF입니다. 이들은 `varargs_func!`가 지원하는 세 가지 등록 형태 — prerun이 있는 상태형, prerun 전용, 기본(bare) — 에 더해 사용자 정의 타입 인수의 검증까지 함께 보여줍니다.

**사용법:**

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

SELECT vsql_varargs.str_join('alpha', 'beta');
SELECT vsql_varargs.str_join('a', 'b', 'c', 'd');
```

```
vsql_varargs.str_join('alpha', 'beta')
#1: alpha, beta
vsql_varargs.str_join('a', 'b', 'c', 'd')
#1: a, b, c, d
```

같은 함수가 두 인수 개수를 모두 처리합니다. `#1` 접두사는 문장별 호출 카운터로, 한 문장의 행들을 거치며 증가합니다:

```sql theme={null}
CREATE TABLE t (x VARCHAR(16), y VARCHAR(16));
INSERT INTO t VALUES ('1a', '1b'), ('2a', '2b'), ('3a', '3b');
SELECT vsql_varargs.str_join(x, y) AS joined FROM t ORDER BY x;
```

```
joined
#1: 1a, 1b
#2: 2a, 2b
#3: 3a, 3b
```

### 가변 인자 검증은 모두 prerun의 몫

가변 인자 함수에 대해 서버는 인수 검사를 전혀 하지 않습니다 — 개수도, 타입도 검사하지 않습니다. 보통은 선언된 시그니처가 있어서 잘못된 호출이 코드가 실행되기 전에 서버에서 거부되지만, 가변 인자 함수에는 선언된 시그니처가 없습니다. prerun 훅이 거부하지 않은 것은 무엇이든 행 함수에 도달합니다.

prerun은 어떤 행보다도 먼저, 한 번, *호출*을 거부합니다: 최적화기가 결정한 인수 타입을 보고 문장을 실패시킵니다. 행 함수는 여전히 각 *값*을 처리해야 합니다 — 타입 검증을 통과한 컬럼도 어느 행에서든 NULL을 담을 수 있기 때문입니다.

prerun의 거부는 문장 초기화를 실패시킵니다:

```sql theme={null}
SELECT vsql_varargs.str_join();
```

```
ERROR 1123 (HY000): Can't initialize function 'str_join'; str_join requires at least one argument
```

```sql theme={null}
SELECT vsql_varargs.str_join('ok', 123);
```

```
ERROR 1123 (HY000): Can't initialize function 'str_join'; str_join: every argument must be a string
```

prerun을 생략한다는 것은 모든 호출을 받아들인다는 뜻입니다. `arg_count`는 기본(bare) 형태로 등록되므로 인수가 0개인 호출도 유효합니다:

```sql theme={null}
SELECT vsql_varargs.arg_count();
SELECT vsql_varargs.arg_count(1, 2.5, 'mix');
```

```
vsql_varargs.arg_count()
0
vsql_varargs.arg_count(1, 2.5, 'mix')
3
```

### 구현

**파일: `src/lib.rs`**

prerun은 `PrerunArgs`와, `T`가 상태 타입과 일치하는 `PrerunResult<T>`를 받습니다. `PrerunArgs::len()`은 인수 개수이고, `type_at(i)`는 인수 `i`의 타입을 `ArgType`으로 반환합니다:

```rust theme={null}
use villagesql::{InValue, PrerunArgs, PrerunResult, VdfReturn};

/// Per-statement state: how many times the row handler has run this statement.
#[derive(Default)]
struct JoinState {
    calls: i64,
}

/// Validate the call and set up the statement. The server does no validation for
/// varargs, so this is the only gate.
fn str_join_prerun(args: PrerunArgs, mut out: PrerunResult<JoinState>) {
    // Reject a zero-argument call.
    if args.is_empty() {
        out.error("str_join requires at least one argument");
        return;
    }

    // Every argument must be a string.
    for i in 0..args.len() {
        if !args.type_at(i).is_some_and(|t| t.is_str()) {
            out.error("str_join: every argument must be a string");
            return;
        }
    }

    // Size the result buffer from the arg count.
    out.request_buffer_size(32 + args.len() * 64);

    // Hand the fresh counter to the server.
    out.set_state(JoinState::default());
}

/// Join a variable number of string arguments, prefixed with the per-statement
/// call count. `args` length varies with how many arguments the SQL call passed.
fn str_join(state: &mut JoinState, args: &[InValue]) -> VdfReturn {
    state.calls += 1;

    let mut joined = String::new();
    for (i, arg) in args.iter().enumerate() {
        match arg {
            InValue::String(s) => {
                if i > 0 {
                    joined.push_str(", ");
                }
                joined.push_str(s);
            }
            // A string column can carry NULL. SQL-style: NULL in -> NULL out.
            InValue::Null => return VdfReturn::Null,
            _ => return VdfReturn::error("str_join: non-string argument at runtime"),
        }
    }
    VdfReturn::string(format!("#{}: {joined}", state.calls))
}

/// Bare varargs: no prerun, no state, no validation. Returns how many arguments
/// it was called with, including zero.
fn arg_count(args: &[InValue]) -> VdfReturn {
    VdfReturn::int(i64::try_from(args.len()).unwrap_or(i64::MAX))
}
```

가변 인자에서는 prerun에서 `request_buffer_size`로 `args.len()`에 비례해 버퍼 크기를 정하세요 — 고정된 `buffer_size`는 인수 개수에 따라 늘어날 수 없습니다.

`ArgType`은 네 가지 조건식 — `is_int()`, `is_real()`, `is_str()`, `is_custom()` — 을 노출하므로, 모든 인수가 행 함수가 처리하는 모양 중 하나이기만 하면 prerun이 이질적인 호출도 받아들일 수 있습니다. `describe`는 세 가지 스칼라의 어떤 조합이든 받아들이고 그 외에는 모두 거부합니다:

```rust theme={null}
fn describe_prerun(args: PrerunArgs, mut out: PrerunResult<()>) {
    if args.is_empty() {
        out.error("describe requires at least one argument");
        return;
    }
    for i in 0..args.len() {
        let ok = args
            .type_at(i)
            .is_some_and(|t| t.is_int() || t.is_real() || t.is_str());
        if !ok {
            out.error("describe: arguments must be INT, REAL, or STRING");
            return;
        }
    }
    out.request_buffer_size(32 + args.len() * 48);
}
```

```sql theme={null}
SELECT vsql_varargs.describe(42, 3.14e0, 'hello');
```

```
vsql_varargs.describe(42, 3.14e0, 'hello')
int:42, real:3.14, str:hello
```

이 prerun은 아무것도 유지하지 않으므로 상태 타입이 `()`입니다: 검증하고 버퍼 크기를 정할 뿐, `set_state`는 결코 호출하지 않습니다.

### 사용자 정의 타입 가변 인자

`is_custom()`만으로는 인수가 *어떤* 사용자 정의 타입이라는 것만 알 수 있습니다. `custom_name()`은 어느 타입인지 반환하므로, prerun이 가변 인자 호출을 단일 타입으로 제한할 수 있습니다. 이 확장은 `point2d` 사용자 정의 타입을 등록하고 가변 개수의 `point2d` 값을 받아들입니다:

```rust theme={null}
fn point_path_prerun(args: PrerunArgs, mut out: PrerunResult<()>) {
    if args.is_empty() {
        out.error("point_path requires at least one point");
        return;
    }
    for i in 0..args.len() {
        let ok = args
            .type_at(i)
            .is_some_and(|t| t.is_custom() && t.custom_name() == Some("point2d"));
        if !ok {
            out.error("point_path: every argument must be a point2d");
            return;
        }
    }
    out.request_buffer_size(16 + args.len() * 32);
}
```

```sql theme={null}
SELECT vsql_varargs.point_path(point2d::from_string('0,0'), point2d::from_string('1,2'), point2d::from_string('3,5'));
```

```
vsql_varargs.point_path(point2d::from_string('0,0'), point2d::from_string('1,2'), point2d::from_string('3,5'))
(0,0) -> (1,2) -> (3,5)
```

일반 문자열은 그 바이트가 포인트로 파싱될 수 있더라도 첫 번째 행 이전에 거부됩니다:

```sql theme={null}
SELECT vsql_varargs.point_path('1,2');
```

```
ERROR 1123 (HY000): Can't initialize function 'point_path'; point_path: every argument must be a point2d
```

그다음 행 함수는 다른 사용자 정의 타입 VDF와 마찬가지로 `InValue::Custom(b)`를 매치하고 바이트를 직접 디코딩합니다.

### 등록

```rust theme={null}
villagesql::extension! {
    funcs: [
        villagesql::varargs_func!(str_join, "str_join", [..] -> villagesql::Type::String,
            state: JoinState, prerun: str_join_prerun),
        villagesql::varargs_func!(arg_count, "arg_count", [..] -> villagesql::Type::Int),
        villagesql::varargs_func!(describe, "describe", [..] -> villagesql::Type::String,
            prerun: describe_prerun),
        villagesql::varargs_func!(point_path, "point_path", [..] -> villagesql::Type::String,
            prerun: point_path_prerun),
    ],
    types: [
        villagesql::custom_type!(
            type_name: "point2d",
            persisted_length: 8,
            max_decode_buffer_length: 32,
            encode: point_encode,
            decode: point_decode,
            compare: point_compare,
            default: "0,0",
        ),
    ]
}
```

`describe`, `point_path`, 그리고 `point2d`의 `encode`/`decode`/`compare`는 위의 `str_join`과 `rational`에서 이미 보여준 것과 같은 `InValue` 매칭 및 바이트 인코딩 패턴을 따릅니다 — 전체 소스는 [Rust SDK 리포지토리](https://github.com/villagesql/vsql-rust-sdk)의 `examples/vsql_varargs/src/lib.rs`를 참조하세요.

**핵심 패턴:**

* 매개변수 목록 자리의 `[..]`가 함수를 가변 인자로 표시합니다
* 세 가지 형태가 각기 다른 행 함수 시그니처를 가집니다: `state:` + `prerun:`은 `fn(&mut State, &[InValue]) -> VdfReturn`, `prerun:` 단독과 기본(bare) 형태는 둘 다 `fn(&[InValue]) -> VdfReturn`
* 문장별 상태를 할당하고 드롭하는 것은 `state:` 형태뿐입니다
* 반환 타입은 여전히 선언되므로, 가변적인 것은 인수 목록뿐입니다
* 각 형태는 후행 쌍으로 `buffer_size:`와 `deterministic:`도 받습니다
* `point2d`는 `hash`를 등록하지 않는데, 이는 선택 사항입니다 — `ORDER BY`에는 `compare`만으로 충분합니다

***

## 핵심 구현 패턴

| 패턴                   | 사용법                                                                                       |
| -------------------- | ----------------------------------------------------------------------------------------- |
| **VDF 시그니처**         | `fn impl(args: &[InValue]) -> VdfReturn`                                                  |
| **NULL 처리**          | `InValue::Null`과 `None`을 명시적으로 매치                                                         |
| **오류 보고**            | `VdfReturn::error("message")`는 문 실행을 중단합니다                                                |
| **사용자 정의 타입 encode** | `Result<Vec<u8>, String>`을 반환                                                             |
| **사용자 정의 타입 decode** | `Result<String, String>`을 반환                                                              |
| **타입 인식 인수**         | `func!`에서 `villagesql::custom!("name")` 사용                                                |
| **집계 함수**            | `agg_func!(result_fn, ..., state: T, clear: f, accumulate: f)`; `T`는 `Default`를 구현해야 합니다  |
| **가변 인자 함수**         | `varargs_func!(f, "name", [..] -> ret)`; 인수 검증은 prerun만이 수행합니다                            |
| **prerun 타입 검사**     | `PrerunArgs::type_at(i)` → `ArgType::is_int`/`is_real`/`is_str`/`is_custom`/`custom_name` |
| **등록**               | 단일 `extension!` 블록이 함수와 타입을 선언                                                            |

***

## 테스트

네 예제 모두 C++ 확장과 마찬가지로 MTR(MySQL Test Runner)을 사용합니다:

```bash theme={null}
# From inside the example directory -- cargo-vsql passes the suite's full path:
cargo vsql test

# Or point mysql-test-run.pl at the suite directly. A bare --suite=vsql_rot13
# fails: that form only finds suites staged inside the server's own
# mysql-test/suite/ tree, and these live in the SDK repo.
cd /path/to/villagesql/build/mysql-test
./mysql-test-run.pl --suite=/path/to/vsql-rust-sdk/examples/vsql_rot13/mysql-test
./mysql-test-run.pl --suite=/path/to/vsql-rust-sdk/examples/vsql_rational/mysql-test
./mysql-test-run.pl --suite=/path/to/vsql-rust-sdk/examples/vsql_agg_sum/mysql-test
./mysql-test-run.pl --suite=/path/to/vsql-rust-sdk/examples/vsql_varargs/mysql-test
```

`--record`로 예상 결과를 생성하거나 업데이트합니다.

***

## 다음 단계

<CardGroup cols={2}>
  <Card title="Rust에서 확장 만들기" icon="rust" href="/docs/ko/mysql-9.7/stable/rust-sdk">
    SDK 설치, 빌드, extension! 매크로
  </Card>

  <Card title="Rust 사용자 정의 타입" icon="cube" href="/docs/ko/mysql-9.7/stable/rust-custom-types">
    encode, decode, compare, hash에 대한 심층 분석
  </Card>

  <Card title="Rust API 참조" icon="book" href="/docs/ko/mysql-9.7/stable/rust-api-reference">
    InValue, VdfReturn, 그리고 매크로 API
  </Card>

  <Card title="예제 소스" icon="github" href="https://github.com/villagesql/vsql-rust-sdk/tree/main/examples">
    네 가지 예제 전체 소스
  </Card>
</CardGroup>
