> ## 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 扩展示例

> 通过 vsql_rot13、vsql_rational、vsql_agg_sum 和 vsql_varargs 参考实现学习使用 Rust SDK

本页介绍 Rust SDK 仓库中的四个参考扩展——一个仅包含函数的最小示例，一个包含算术运算、排序和哈希的完整自定义类型，一个聚合函数，以及一个变长参数函数。该仓库的 `examples/` 目录还为每一项受支持的[预览功能](/docs/zh/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 在两端都是一等变体；直接对其进行模式匹配
* `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`**

该类型注册了四个操作：编码（字符串 → 字节）、解码（字节 → 字符串）、比较（用于 ORDER BY）和哈希（用于索引）。

```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:`——`fn(&mut State)`                    | 在每个分组开始时。                                                                |
| 累加 | `accumulate:`——`fn(&mut State, &[InValue])`   | 对分组中的每一行运行一次。不返回任何内容；它唯一的作用是修改状态。                                        |
| 结果 | `agg_func!` 的第一个参数——`fn(&State) -> VdfReturn` | 每个分组一次，在最后一行被折叠进累加器之后。                                                   |
| 丢弃 | 无——由 `agg_func!` 生成                           | 在语句结束之后。                                                                 |

`clear` 在每个分组开始时重置累加器。它忘记重置的字段会从上一个分组泄漏过来。

服务器会对每一行调用 `accumulate`，包括参数为 NULL 的行。跳过 NULL 是该函数自己的职责：只匹配您想要的变体，忽略其余部分。

### 实现

**文件：`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` 标志用于区分求和结果为零的分组和没有内容可求和的分组。没有它，空分组或全为 NULL 的分组会返回 `0`，而内置 `SUM` 返回的是 NULL。

### 注册

```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!` 还接受 `buffer_size:` 和 `deterministic:`，位于 `accumulate:` 之后，必须一起提供，并按此顺序

***

## vsql\_varargs — 变长参数函数

四个各自接受任意数量参数的 VDF。它们合起来覆盖了 `varargs_func!` 支持的三种注册形式——带 prerun 的有状态形式、仅 prerun 形式和裸形式——外加对自定义类型参数的验证。

**用法：**

```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` 以裸形式注册，因此零参数调用是合法的：

```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)` 以 `ArgType` 的形式返回参数 `i` 的类型：

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

随后行函数匹配 `InValue::Custom(b)` 并自行解码这些字节，与任何其他自定义类型 VDF 相同。

### 注册

```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:` 和裸形式都给出 `fn(&[InValue]) -> VdfReturn`
* 只有 `state:` 形式会分配和丢弃每个语句的状态
* 返回类型仍然要声明，因此只有参数列表是可变的
* 每种形式还接受作为尾部成对参数的 `buffer_size:` 和 `deterministic:`
* `point2d` 没有注册 `hash`，它是可选的——仅 `compare` 就足以支持 `ORDER BY`

***

## 关键实现模式

| 模式               | 用法                                                                                        |
| ---------------- | ----------------------------------------------------------------------------------------- |
| **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/zh/mysql-9.7/stable/rust-sdk">
    SDK 安装、构建以及 extension! 宏
  </Card>

  <Card title="Rust 自定义类型" icon="cube" href="/docs/zh/mysql-9.7/stable/rust-custom-types">
    深入了解 encode、decode、compare 和 hash
  </Card>

  <Card title="Rust API 参考" icon="book" href="/docs/zh/mysql-9.7/stable/rust-api-reference">
    InValue、VdfReturn 以及宏接口
  </Card>

  <Card title="示例源码" icon="github" href="https://github.com/villagesql/vsql-rust-sdk/tree/main/examples">
    全部四个示例的完整源码
  </Card>
</CardGroup>
