> ## 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 中的自定义类型

> 在 VillageSQL Rust SDK 中定义新的列类型——二进制布局、编码、解码、比较、哈希和算术函数，使用 `custom_type!` 宏。

<Warning>
  Rust SDK 处于 alpha 阶段——各版本之间可能出现破坏性的 API 变更。支持纯函数扩展、聚合函数、变长参数函数以及自定义类型（encode、decode、compare、hash），同时也支持 `sys_var`、`status_var`、`thread_worker` 和 `keyring` 预览功能。列存储 ABI 目前仅在 C++ SDK 中提供——如果您需要它，请使用 [C++ SDK](/docs/zh/mysql-9.7/stable/create)。
</Warning>

自定义类型允许您定义新的列类型——例如 `RATIONAL`、`VECTOR` 或 `INET`——这些类型可以与 `ORDER BY`、索引和聚合函数一起使用。Rust SDK 通过 `custom_type!` 宏支持此功能，并且对于存储大小取决于列参数的类型，还通过 `parameterized_type!` 提供支持（请参阅[参数化类型](#parameterized-types)）。

本页假设您已经完成了 [使用 Rust 构建扩展](/docs/zh/mysql-9.7/stable/rust-sdk) 的相关步骤。设置（Cargo.toml、manifest.json、cargo-vsql）与之前相同。

## 何时使用自定义类型

在以下情况下使用自定义类型：

* 您需要一种二进制的磁盘布局，而标准的 SQL 类型无法表达这种布局（打包的浮点数、固定宽度的整数、二进制标识符）
* 您的类型具有其自身的排序语义，该语义与字符串字典序不同
* 您希望服务器正确地索引和哈希值，以便进行 `ORDER BY`、`COUNT(DISTINCT)` 和集合操作

如果您只需要可从 SQL 调用的函数，并且您的数据可以轻松地放入 `STRING`、`INT` 或 `REAL` 列中，则不需要自定义类型。

## `custom_type!` 宏

每个自定义类型都需要 4 个回调函数（编码、解码、比较、哈希）和一个默认值。以下是完整的宏签名：

```rust theme={null}
villagesql::custom_type!(
    type_name: "type_name_in_sql",
    persisted_length: N,
    max_decode_buffer_length: M,
    encode: your_encode_fn,
    decode: your_decode_fn,
    compare: your_compare_fn,
    hash: your_hash_fn,
    default: "a_valid_string_literal",
)
```

| 字段                         | 类型      | 描述                                                                   |
| -------------------------- | ------- | -------------------------------------------------------------------- |
| `type_name`                | 字符串字面量  | SQL 类型名称。在 SQL 中不区分大小写。                                              |
| `persisted_length`         | `usize` | 用于磁盘存储的固定字节长度。                                                       |
| `max_decode_buffer_length` | `usize` | 解码后的字符串表示形式的最大字节长度。                                                  |
| `encode`                   | 函数      | 将 `&str` 转换为 INSERT 时的二进制字节。                                         |
| `decode`                   | 函数      | 将二进制字节转换回 `String` 以进行显示。                                            |
| `compare`                  | 函数      | 返回 `Ordering`，用于 `ORDER BY`、`MIN`、`MAX`。                             |
| `hash`                     | 函数      | 返回 `usize` 哈希值，用于 `COUNT(DISTINCT)` 和集合操作。可选，但建议为索引列使用。              |
| `default`                  | 字符串字面量  | 一个有效的字符串，服务器可以在类型初始化时对其进行编码。必须编码为恰好 `persisted_length` 个字节。可选，但建议使用。 |

`type_name`、`persisted_length`、`max_decode_buffer_length`、`encode`、`decode` 和 `compare` 是必需的。`hash` 和 `default` 是可选的，但建议使用——`hash` 对于正确的 `COUNT(DISTINCT)` 和集合操作是必需的，`default` 用于类型初始化验证。

## 接收和返回二进制值

接受或返回自定义类型的函数使用原始字节。

**输入**——`InValue::Custom(b)` 携带存储的二进制数据，类型为 `&[u8]`：

```rust theme={null}
fn rational_numer_impl(args: &[InValue]) -> VdfReturn {
    match args.first() {
        Some(InValue::Custom(b)) => {
            let numer = read_i64(b, 0);
            VdfReturn::int(numer)
        }
        Some(InValue::Null) | None => VdfReturn::null(),
        _ => VdfReturn::error("rational_numer: expected a RATIONAL argument"),
    }
}
```

**输出**——`VdfReturn::Binary(bytes)` 将二进制字节发送回服务器：

```rust theme={null}
fn rational_add_impl(args: &[InValue]) -> VdfReturn {
    match (args.get(0), args.get(1)) {
        (Some(InValue::Custom(a)), Some(InValue::Custom(b))) => {
            let result = add_rationals(a, b);
            VdfReturn::Binary(result)
        }
        _ => VdfReturn::null(),
    }
}
```

要在 `func!` 声明中引用自定义类型，请使用 `villagesql::custom!("type_name")`：

```rust theme={null}
villagesql::func!(
    rational_add_impl,
    "rational_add",
    [villagesql::custom!("rational"), villagesql::custom!("rational")] -> villagesql::custom!("rational"),
    deterministic: true
)
```

## 示例：有理数类型

SDK 仓库中的 `examples/vsql_rational` 是一个可运行的扩展示例，它实现了 `RATIONAL` 类型。它以小端字节顺序存储一个有理数，作为 16 字节的 `i64` 值对（分子、分母），并提供算术函数。

以下是核心的编码、解码、比较和哈希实现：

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

// Binary layout: [numerator: i64 LE][denominator: i64 LE] — 16 bytes total.
// Always stored in reduced form (GCD = 1) with a positive denominator.
const BYTES: usize = 16;

fn to_bytes(num: i64, den: i64) -> Vec<u8> {
    let mut v = Vec::with_capacity(BYTES);
    v.extend_from_slice(&num.to_le_bytes());
    v.extend_from_slice(&den.to_le_bytes());
    v
}

fn from_bytes(b: &[u8]) -> (i64, i64) {
    let num = i64::from_le_bytes(b[..8].try_into().unwrap());
    let den = i64::from_le_bytes(b[8..16].try_into().unwrap());
    (num, den)
}

// encode: "3/4" -> 16 bytes
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!("rational numerator: {}", e))?;
    let den: i64 = den_s.trim().parse()
        .map_err(|e| format!("rational denominator: {}", e))?;
    let (n, d) = normalize(num as i128, den as i128)
        .ok_or_else(|| "rational: zero or overflowing denominator".to_string())?;
    Ok(to_bytes(n, d))
}

// decode: 16 bytes -> "3/4"
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))
}

// compare: for ORDER BY, MIN, MAX
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)
    let lhs = (n1 as i128) * (d2 as i128);
    let rhs = (n2 as i128) * (d1 as i128);
    lhs.cmp(&rhs)
}

// hash: for COUNT(DISTINCT) and set operations
pub fn rational_hash(b: &[u8]) -> usize {
    // FNV-1a over the 16 bytes
    let mut h: usize = 0xcbf29ce484222325u64 as usize;
    for &byte in b {
        h ^= byte as usize;
        h = h.wrapping_mul(0x100000001b3u64 as usize);
    }
    h
}
```

`custom_type!` 注册和算术 VDF（`rational_add`、`rational_sub` 等）都在完整的源代码 `examples/vsql_rational/src/lib.rs` 中。

安装扩展后：

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

CREATE TABLE fractions (
    id   INT PRIMARY KEY,
    val  RATIONAL
);

INSERT INTO fractions VALUES (1, '1/2'), (2, '3/4'), (3, '1/4');

-- ORDER BY uses rational_compare
SELECT val FROM fractions ORDER BY val;
-- → 1/4, 1/2, 3/4

-- Arithmetic with rational_add
SELECT rational_add('1/3', '1/6');
-- → 1/2

-- Extract numerator and denominator
SELECT rational_numer(val), rational_denom(val) FROM fractions;

-- Convert to floating-point approximation
SELECT rational_to_real('1/3');
-- → 0.3333333333333333
```

`rational_to_real(r RATIONAL) -> REAL` 将 `RATIONAL` 值转换为 64 位浮点近似值，方法是将分子除以分母。当您需要用于显示或比较的近似十进制数，但又不想在列中存储有损表示形式时，此函数很有用。

<h2 id="parameterized-types">
  参数化类型
</h2>

参数化类型需要一个在 `CREATE TABLE` 时读取的值——例如 `VECTOR(3)` 中的
`3`——才能确定其存储大小。`custom_type!` 无法表达这一点：它的
`persisted_length` 对于每一列都是单个固定常量。`parameterized_type!` 是其
参数化的对应物：持久化长度不再固定，而是通过 `int_to_params` 和
`resolve_params`，根据声明的参数为每一列计算得出。

```rust theme={null}
villagesql::parameterized_type!(
    type_name: "type_name_in_sql",
    max_persisted_length: N,
    max_decode_buffer_length: M,
    encode: your_encode_fn,
    decode: your_decode_fn,
    compare: your_compare_fn,
    int_to_params: your_int_to_params_fn,
    resolve_params: your_resolve_params_fn,
    params_type: YourParamsType,
    params_parse: your_params_parse_fn,
    params_to_strings: your_params_to_strings_fn,
    hash: your_hash_fn,
    default: "a_valid_string_literal",
)
```

| 字段                         | 类型      | 描述                                                          |
| -------------------------- | ------- | ----------------------------------------------------------- |
| `type_name`                | 字符串字面量  | SQL 类型名称。在 SQL 中不区分大小写。                                     |
| `max_persisted_length`     | `i64`   | 在所有有效的参数化组合中，磁盘上字节长度的上限。                                    |
| `max_decode_buffer_length` | `usize` | 解码后的字符串表示形式的最大字节长度。                                         |
| `encode`                   | 函数      | 在 INSERT 时将 `&str` 加上 `&mut MaybeParams<P>` 转换为二进制字节。       |
| `decode`                   | 函数      | 将二进制字节加上 `&P` 转换回 `String` 以进行显示。                           |
| `compare`                  | 函数      | 在给定 `&P` 的情况下，为 `ORDER BY`、`MIN`、`MAX` 返回 `Ordering`。       |
| `int_to_params`            | 函数      | 将 `TYPE(N)` 整数语法转换为规范的参数字符串。                                |
| `resolve_params`           | 函数      | 验证已解析的 `Params` 并返回一个 `Resolved`（存储大小，以及可选的重写后的参数）。         |
| `params_type`              | 类型      | 您解析后的参数结构体，例如 `PadintParams`。                               |
| `params_parse`             | 函数      | 将 `Params` 解析为 `params_type`。对于每个唯一的参数组合最多运行一次——SDK 会缓存结果。  |
| `params_to_strings`        | 函数      | `params_parse` 的逆操作：将 `params_type` 写回规范的 `(key, value)` 对。 |
| `hash`                     | 函数      | 在给定 `&P` 的情况下返回一个 `usize` 哈希值。可选，但建议为索引列使用。                 |
| `default`                  | 字符串字面量  | 一个有效的字符串，服务器可以在类型初始化时对其进行编码。可选，但建议使用。                       |

`type_name`、`max_persisted_length`、`max_decode_buffer_length`、`encode`、`decode`、`compare`、`int_to_params`、`resolve_params`、`params_type`、`params_parse` 和 `params_to_strings` 是必需的。`hash` 和 `default` 是可选的；`intrinsic_default_fn`（一个根据 `&P` 计算默认值的函数，用于默认值取决于参数的情况）也是可选的，并且与 `default` 互斥。

下面是 `padint`——一个以固定 8 个字节存储的 `i64`，带有一个控制补零显示宽度的 `width` 参数：

```rust theme={null}
pub struct PadintParams {
    pub width: u32,
}

pub fn padint_parse(params: Params) -> PadintParams {
    let width = params.get("width").and_then(|s| s.parse().ok()).unwrap_or(1);
    PadintParams { width }
}

pub fn padint_to_strings(p: &PadintParams) -> Vec<(String, String)> {
    vec![("width".to_string(), p.width.to_string())]
}

pub fn padint_int_to_params(n: i64) -> Result<String, String> {
    if n < 1 {
        return Err(format!("padint: width must be >= 1, got {n}"));
    }
    Ok(format!("width={n}"))
}

pub fn padint_resolve_params(params: Params) -> Result<Resolved, String> {
    let width: i64 = params
        .get("width")
        .ok_or_else(|| "padint: missing 'width' param".to_string())?
        .parse()
        .map_err(|e| format!("padint: bad width: {e}"))?;
    // Fixed 8-byte i64 storage; the decode buffer must fit the widest
    // rendering, which is the larger of an i64's digits and the padded width.
    Ok(Resolved::new(8, width.max(20)))
}

pub fn padint_encode(s: &str, params: &mut MaybeParams<PadintParams>) -> Result<Vec<u8>, String> {
    let n: i64 = s.trim().parse().map_err(|e| format!("padint: {e}"))?;
    if !params.is_known() {
        // Bare constant with no column to anchor it: infer width from digits.
        let digits = s.trim().trim_start_matches('-').len().max(1);
        params.set(PadintParams { width: u32::try_from(digits).unwrap_or(u32::MAX) });
    }
    Ok(n.to_le_bytes().to_vec())
}

pub fn padint_decode(b: &[u8], p: &PadintParams) -> Result<String, String> {
    let n = i64::from_le_bytes(b[..8].try_into().unwrap());
    Ok(format!("{n:0width$}", width = p.width as usize))
}

pub fn padint_compare(a: &[u8], b: &[u8], _p: &PadintParams) -> std::cmp::Ordering {
    i64::from_le_bytes(a[..8].try_into().unwrap()).cmp(&i64::from_le_bytes(b[..8].try_into().unwrap()))
}

villagesql::parameterized_type!(
    type_name: "padint",
    max_persisted_length: 8,
    max_decode_buffer_length: 32,
    encode: padint_encode,
    decode: padint_decode,
    compare: padint_compare,
    int_to_params: padint_int_to_params,
    resolve_params: padint_resolve_params,
    params_type: PadintParams,
    params_parse: padint_parse,
    params_to_strings: padint_to_strings,
    default: "0",
)
```

接收参数化自定义类型参数的函数看到的是
`InValue::CustomWithParams { bytes, params }`，而不是普通的
`InValue::Custom(bytes)`——`params` 是一个 \[`TypeParams`]，它是对该列所声明的
`key=value` 对的只读、零拷贝视图。

## 包含类型的 `extension!` 块

在注册函数和类型时，`extension!` 块有两个部分：

```rust theme={null}
villagesql::extension! {
    funcs: [
        // VDFs declared with func!
    ],
    types: [
        // Custom types declared with custom_type!
    ]
}
```

仅包含函数的扩展会省略 `types:`。仅包含类型的扩展会保留 `funcs: []`，其他部分都不省略。

## 后续步骤

<CardGroup cols={2}>
  <Card title="Rust API 参考" icon="book" href="/docs/zh/mysql-9.7/stable/rust-api-reference">
    关于 `InValue`、`VdfReturn` 和所有宏的完整参考。
  </Card>

  <Card title="使用 Rust 构建扩展" icon="wrench" href="/docs/zh/mysql-9.7/stable/rust-sdk">
    入门——Cargo 设置、第一个函数、打包和测试。
  </Card>

  <Card title="C++ 自定义类型" icon="shapes" href="/docs/zh/mysql-9.7/stable/custom-types">
    C++ 中的自定义类型——`make_type<>`、编码/解码/比较/哈希、`ALTER TABLE` 规则。
  </Card>

  <Card title="扩展架构" icon="sitemap" href="/docs/zh/mysql-9.7/stable/architecture">
    自定义类型是如何解析、缓存和存储的。
  </Card>
</CardGroup>
