> ## 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 はアルファ版です。リリース間で破壊的な API 変更が発生する可能性があります。関数のみの拡張機能、集約関数、可変長引数関数、およびカスタム型（encode、decode、compare、hash）がサポートされており、`sys_var`、`status_var`、`thread_worker`、`keyring` の各プレビュー機能もサポートされています。列ストレージ ABI は現在 C++ のみです。必要な場合は [C++ SDK](/docs/ja/mysql-9.7/stable/create) を使用してください。
</Warning>

カスタム型を使用すると、`ORDER BY`、インデックス、および集計関数で機能する`RATIONAL`、`VECTOR`、または`INET`のような新しいカラム型を定義できます。Rust SDKは、`custom_type!`マクロを通じて、またストレージサイズがカラムのパラメータに依存する型については`parameterized_type!`を通じて、これをサポートしています（[パラメータ化された型](#parameterized-types)を参照）。

このページでは、まず[Rustでの拡張機能の作成](/docs/ja/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`                   | 関数      | `INSERT`時に`&str`をバイナリバイトに変換します。                                                    |
| `decode`                   | 関数      | 表示用にバイナリバイトを`String`に変換します。                                                        |
| `compare`                  | 関数      | `ORDER BY`、`MIN`、`MAX`の`Ordering`を返します。                                            |
| `hash`                     | 関数      | `COUNT(DISTINCT)`およびセット操作の`usize`ハッシュを返します。オプションですが、インデックス化されたカラムには推奨されます。         |
| `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`型を実装する動作する拡張機能です。有理数を、リトルエンディアンのバイト順で2つの`i64`値のペア（分子、分母）、合計16バイトとして格納し、算術関数を提供します。

エンコード、デコード、比較、およびハッシュの実装のコアは次のとおりです。

```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ビット浮動小数点近似に変換します。表示または比較のために近似的な10進数が必要で、その値をカラムに精度が落ちる表現として保存したくない場合に便利です。

<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`に解析します。一意のパラメータの組み合わせごとに最大1回だけ実行されます。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::Custom(bytes)`ではなく`InValue::CustomWithParams { bytes, params }`を
受け取ります。`params`は\[`TypeParams`]であり、カラムに宣言された`key=value`の
ペアに対する読み取り専用でゼロコピーのビューです。

## 型を含む`extension!`ブロック

関数と型を両方登録する場合、`extension!`ブロックには2つのセクションがあります。

```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/ja/mysql-9.7/stable/rust-api-reference">
    `InValue`、`VdfReturn`、およびすべてのマクロの完全なリファレンス。
  </Card>

  <Card title="Rust で拡張機能を作成する" icon="wrench" href="/docs/ja/mysql-9.7/stable/rust-sdk">
    はじめに — Cargoのセットアップ、最初の関数、パッケージング、およびテスト。
  </Card>

  <Card title="C++カスタム型" icon="shapes" href="/docs/ja/mysql-9.7/stable/custom-types">
    C++のカスタム型 — `make_type<>`、エンコード/デコード/比較/ハッシュ、`ALTER TABLE`ルール。
  </Card>

  <Card title="拡張機能のアーキテクチャ" icon="sitemap" href="/docs/ja/mysql-9.7/stable/architecture">
    カスタム型が解決、キャッシュ、および保存される方法。
  </Card>
</CardGroup>
