> ## 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!` 宏。

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

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

## 何时使用自定义类型

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

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

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

## `custom_type!` 宏

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

```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` | `i64` | 用于磁盘存储的固定字节长度。 |
| `max_decode_buffer_length` | `i64` | 解码后的字符串表示形式的最大字节长度。 |
| `encode` | 函数 | 将 `&str` 转换为 INSERT 时的二进制字节。 |
| `decode` | 函数 | 将二进制字节转换回 `String` 以进行显示。 |
| `compare` | 函数 | 返回 `Ordering`，用于 `ORDER BY`、`MIN`、`MAX`。 |
| `hash` | 函数 | 返回 `usize` 哈希值，用于 `GROUP BY`、`DISTINCT` 以及 `UNION` 等集合操作。可选，但建议使用。 |
| `default` | 字符串字面量 | 一个有效的字符串，服务器可以在类型初始化时对其进行编码。必须编码为恰好 `persisted_length` 个字节。除非给出了 `intrinsic_default_fn`，否则为必需。 |
| `intrinsic_default_fn` | 函数 | 计算默认值而不是直接写出它：`fn() -> Result<String, String>`。当默认值无法写成字面量时使用。 |

`type_name`、`persisted_length`、`max_decode_buffer_length`、`encode`、`decode` 和 `compare` 是必需的。`hash` 是可选的，但建议使用，因为正确的 `GROUP BY`、`DISTINCT` 和集合操作依赖于它。请以两种方式之一为类型提供默认值：对于可以写成字面量的值使用 `default`，对于扩展必须计算的值使用 `intrinsic_default_fn`。两者都设置会使 `INSTALL EXTENSION` 失败。两者都不设置时扩展可以安装，但随后第一条使用该类型的语句会失败。

## 接收和返回二进制值

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

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

```rust theme={null}
fn rational_numer_impl(args: &[InValue]) -> VdfReturn {
    match args.first() {
        Some(InValue::Custom(b)) => {
            let numer = i64::from_le_bytes(b[..8].try_into().unwrap());
            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::extension! {
    funcs: [
        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 GROUP BY, 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!` 注册、`rational_encode` 所调用的 `gcd` 和 `normalize` 辅助函数，以及算术 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 位浮点近似值，方法是将分子除以分母。当您需要用于显示或比较的近似十进制数，但又不想在列中存储有损表示形式时，此函数很有用。

## 参数化类型

参数化类型需要一个在 `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` | `i64` | 解码后的字符串表示形式的最大字节长度。 |
| `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` 哈希值，用于 `GROUP BY`、`DISTINCT` 和集合操作。可选，但建议使用。 |
| `default` | 字符串字面量 | 一个有效的字符串，服务器可以在类型初始化时对其进行编码。除非给出了 `intrinsic_default_fn`，否则为必需。 |
| `intrinsic_default_fn` | 函数 | 计算默认值而不是直接写出它：`fn(&P) -> Result<String, String>`。当默认值取决于参数时使用。 |

`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` 计算默认值的函数，用于默认值取决于参数的情况）。两者都设置会使 `INSTALL EXTENSION` 失败。两者都不设置时扩展可以安装，但随后第一条使用该类型的语句会失败。

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

```rust theme={null}
use villagesql::{MaybeParams, Params, Resolved};

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::extension! {
    funcs: [],
    types: [
        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: []`，其他部分都不省略。

## 后续步骤

> [card] [Rust API 参考](https://villagesql.com/docs/zh/sdk/rust/api-reference.md)
>
> InValue、VdfReturn 以及 extension!、func!、agg\_func!、varargs\_func!、custom\_type! 和 custom! 宏的参考。

> [card] [使用 Rust 构建扩展](https://villagesql.com/docs/zh/extensions/build/rust-sdk.md)
>
> 入门——Cargo 设置、第一个函数、打包和测试。

> [card] [C++ 自定义类型](https://villagesql.com/docs/zh/sdk/cpp/custom-types.md)
>
> C++ 中的自定义类型——`make_type<>`、编码/解码/比较/哈希、`ALTER TABLE` 规则。

> [card] [扩展架构](https://villagesql.com/docs/zh/extensions/build/architecture.md)
>
> 自定义类型是如何解析、缓存和存储的。


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