> ## 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 API Reference

> Complete reference for the VillageSQL Rust SDK — InValue, VdfReturn, extension!, func!, agg_func!, varargs_func!, custom_type!, custom!, and manifest.json fields.

This page is a reference for the `villagesql` crate API. For the getting started tutorial, see [Creating Extensions in Rust](https://villagesql.com/docs/en/extensions/build/rust-sdk.md). For custom types, see [Custom Types in Rust](https://villagesql.com/docs/en/sdk/rust/custom-types.md).

## InValue

`InValue` is the enum the server passes for each function argument. Your function receives `args: &[InValue]` and must check each argument before using its value.

```rust theme={null}
pub enum InValue<'a> {
    String(&'a str),
    Real(f64),
    Int(i64),
    Null,
    Custom(&'a [u8]),
    CustomWithParams { bytes: &'a [u8], params: TypeParams<'a> },
}
```

| Variant | Rust type | Corresponding SQL type |
| - | - | - |
| `String(&str)` | String slice holding the argument's raw bytes, not checked for UTF-8 (a latin1 or binary argument keeps its bytes) | `STRING` / `VARCHAR` / `TEXT` |
| `Real(f64)` | 64-bit float | `REAL` / `DOUBLE` / `FLOAT` |
| `Int(i64)` | 64-bit signed integer | `INT` / `BIGINT` / `TINYINT` |
| `Null` | — | SQL `NULL` for any type |
| `Custom(&[u8])` | Raw binary bytes | Any custom type registered via `custom_type!` |
| `CustomWithParams { bytes, params }` | Raw bytes + `TypeParams` | A parameterized custom type registered via [`parameterized_type!`](https://villagesql.com/docs/en/sdk/rust/custom-types#parameterized-types) |

Always match on `Null` explicitly. Calling `.unwrap()` or pattern-matching only the value variants is a bug — SQL NULL is a normal input, not an error.

## VdfReturn

`VdfReturn` is what your function returns to the server. Construct it with one of the associated functions:

| Constructor | SQL effect |
| - | - |
| `VdfReturn::null()` | Returns SQL NULL for this row |
| `VdfReturn::string(s)` | Returns a `String` value; `s` is `impl Into<String>` |
| `VdfReturn::real(v)` | Returns an `f64` value |
| `VdfReturn::int(v)` | Returns an `i64` value |
| `VdfReturn::binary(bytes)` | Returns binary bytes for a custom type column; `bytes` is `Vec<u8>` |
| `VdfReturn::warning(msg)` | Returns NULL for this row, adds a SQL warning, execution continues |
| `VdfReturn::error(msg)` | Aborts the statement with a fatal error |

**Warning vs error:**

Use `warning` for user-input validation failures where continuing with the rest of the result set makes sense. A warning does not fail the statement, including `INSERT` and `UPDATE` in strict mode. Use `error` for conditions where proceeding is unsafe — corrupt stored data, internal invariant violations. A fatal error aborts the entire statement.

```rust theme={null}
fn validate_impl(args: &[InValue]) -> VdfReturn {
    match args.first() {
        Some(InValue::Int(n)) if *n >= 0 => VdfReturn::int(*n),
        Some(InValue::Int(_)) => VdfReturn::warning("value must be non-negative"),
        Some(InValue::Null) | None => VdfReturn::null(),
        _ => VdfReturn::error("validate: expected an INT argument"),
    }
}
```

Return a `VdfReturn` for every failure, and do not panic. The SDK does not
catch a panic in a function, aggregate, prerun, or postrun, or in a custom
type's operations, so a panic that reaches the SDK's `extern "C"` entry point
aborts the server process. This includes `.unwrap()` and `.expect()` on a `None` or
an `Err`, and slice indexing out of bounds.

## extension! macro

`extension!` generates the VEF entry points the server calls when loading your VEB file. It must appear exactly once in the crate.

```rust theme={null}
villagesql::extension! {
    funcs: [
        // One or more villagesql::func!(...) declarations
    ],
    types: [
        // One or more villagesql::custom_type!(...) declarations
    ],
    requires: [
        // Zero or more &'static capability references, e.g. &KEYRING
    ]
}
```

`types:` and `requires:` are optional on their own, but `funcs:` must always be present — write `funcs: []` for a type-only extension. A pure-function extension omits `types:`. An `extension!` block with `funcs: []` and no types is valid but produces an extension that does nothing.

`requires:` declares the [preview capabilities](https://villagesql.com/docs/en/sdk/rust/preview-capabilities.md) the extension uses, as references to `static` capability objects. It must come last, after a `funcs:` section — include `funcs: []` if the extension registers no functions.

## func! macro

`func!` declares a SQL-callable function. Eight forms — four without per-statement state (no parameters, `buffer_size` only, `deterministic` only, both), two that attach per-statement state through a `prerun` function, and two more that add an optional `postrun:` on top of those. See [Per-statement state](#per-statement-state) below.

```rust theme={null}
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, buffer_size: N)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, deterministic: true)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, buffer_size: N, deterministic: true)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, state: StateType, prerun: prerun_fn)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, state: StateType, prerun: prerun_fn, buffer_size: N, deterministic: true)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, state: StateType, prerun: prerun_fn, postrun: postrun_fn)
villagesql::func!(rust_fn, "sql_name", [param_types] -> return_type, state: StateType, prerun: prerun_fn, postrun: postrun_fn, buffer_size: N, deterministic: true)
```

| Argument | Description |
| - | - |
| `rust_fn` | The Rust function implementing the VDF. Signature: `fn(&[InValue]) -> VdfReturn`, or `fn(&mut StateType, &[InValue]) -> VdfReturn` for the `state:` / `prerun:` forms. |
| `"sql_name"` | The SQL function name as a string literal. This is what users call from SQL. |
| `[param_types]` | Comma-separated list of `villagesql::Type::*` or `villagesql::custom!("name")` values. Use `[]` for zero-arity functions. |
| `return_type` | `villagesql::Type::*` or `villagesql::custom!("name")`. |
| `buffer_size: N` | Optional. Size in bytes of the result buffer for string/binary returns, with a minimum of 1024. Use `0` or any value below 1024 for the server default (1024 bytes). When a function returns a string or binary value larger than the buffer, the function errors rather than truncating — declare a larger `buffer_size` to handle larger results. |
| `deterministic: true` | Optional. Declares the function deterministic — same inputs always produce the same output, no side effects. Only a deterministic function can appear in a generated column or a `CHECK` constraint. Only set this when it's true. |
| `postrun: postrun_fn` | Optional, only on the `state:`/`prerun:` forms. `fn(&mut StateType)`, called once after the last row, before the SDK drops the state. |

**Type constants** for use in `func!`:

| `villagesql::Type::*` | SQL type |
| - | - |
| `villagesql::Type::String` | `STRING` |
| `villagesql::Type::Real` | `REAL` |
| `villagesql::Type::Int` | `INT` |

### Per-statement state

Some functions need state that spans every row of a single statement — a call counter, an accumulator. Declare the state type with `state:` and a setup function with `prerun:`. The prerun function runs once, before the first row; the row function then runs once per row with `&mut` access to that state.

A prerun function has the signature `fn(PrerunArgs, PrerunResult<T>)`, and the row function it feeds takes the state first: `fn(state: &mut T, args: &[InValue]) -> VdfReturn`. `T` is the type named by `state:`, and the compiler checks that the prerun and the row function agree on it.

| `PrerunResult<T>` method | Effect |
| - | - |
| `set_state(self, state: T)` | Allocates the per-statement state and hands it to the server. It consumes `self` rather than borrowing it, so the compiler rejects a second call. |
| `request_buffer_size(&mut self, n: usize)` | Asks the server for a specific result-buffer size. |
| `error(self, msg: &str)` | Fails the whole statement with a message. |

`PrerunArgs::len()` is the number of arguments each row will receive, and `PrerunArgs::is_empty()` is true when the function was called with no arguments.

You must not free the state yourself: `func!` always generates the postrun that drops it when the statement ends, so freeing it is not your responsibility even when you supply your own teardown. That drop step is the opposite of the C++ SDK, where your postrun has to call `delete_state<T>()` — see [Per-Statement State](https://villagesql.com/docs/en/sdk/cpp/development.md#per-statement-state-prerun-and-postrun).

Name a `postrun: postrun_fn`, `fn(&mut StateType)`, to run your own teardown first: it runs once after the last row, while the state is still borrowable, and before the SDK's own drop. See `examples/vsql_postrun_order/src/lib.rs` in the [Rust SDK repo](https://github.com/villagesql/vsql-rust-sdk), whose recorded test output includes the line `prerun,postrun,drop|rows_seen=3`, confirming the order.

A complete extension whose function returns its own call index within the statement:

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

/// Per-statement state: how many times the row function has been called.
struct CallCounter {
    n: i64,
}

/// Runs once, before the first row: allocate the counter at zero.
fn call_index_prerun(_args: PrerunArgs, out: PrerunResult<CallCounter>) {
    out.set_state(CallCounter { n: 0 });
}

/// Runs once per row: bump the counter and return its new value.
fn call_index(state: &mut CallCounter, _args: &[InValue]) -> VdfReturn {
    state.n += 1;
    VdfReturn::int(state.n)
}

villagesql::extension! {
    funcs: [
        villagesql::func!(call_index, "call_index", [] -> villagesql::Type::Int,
            state: CallCounter, prerun: call_index_prerun),
    ]
}
```

Build and install it as described in [Creating Extensions in Rust](https://villagesql.com/docs/en/extensions/build/rust-sdk.md), then:

```sql theme={null}
INSTALL EXTENSION vsql_call_index;
CREATE TABLE t (id INT);
INSERT INTO t VALUES (10), (20), (30);
SELECT SUM(vsql_call_index.call_index()) AS total FROM t;
-- → 6
SELECT SUM(vsql_call_index.call_index()) AS total FROM t;
-- → 6
```

The table has three rows, so `call_index()` runs three times and returns `1`, then `2`, then `3` — one value per row. `SUM` adds those three values, which gives `6`.

The second `SELECT` returns the same total as the first, not a larger one: the counter is allocated for one statement and dropped when it finishes.

## agg\_func! macro

`agg_func!` declares an aggregate SQL function — SUM/COUNT-style, called over the rows of each group rather than once per row. Two forms:

```rust theme={null}
villagesql::agg_func!(result_fn, "sql_name", [param_types] -> return_type,
    state: StateType, clear: clear_fn, accumulate: accumulate_fn)
villagesql::agg_func!(result_fn, "sql_name", [param_types] -> return_type,
    state: StateType, clear: clear_fn, accumulate: accumulate_fn,
    buffer_size: N, deterministic: true)
```

| Argument | Description |
| - | - |
| `result_fn` | `fn(&State) -> VdfReturn`. Produces one group's output from the finished accumulator. Runs once per group, after the last row of that group has been folded in. Takes `&State`, not `&mut` — the result function reads the accumulator, it doesn't reset it. |
| `"sql_name"` | The SQL function name as a string literal. |
| `[param_types]` | The argument list, same as `func!` — `villagesql::Type::*` or `villagesql::custom!("name")` values. |
| `return_type` | `villagesql::Type::*` or `villagesql::custom!("name")`. |
| `state: StateType` | The accumulator type. It must implement `Default`: `agg_func!` generates the prerun for you, and that prerun value-initializes the accumulator with `StateType::default()`. Derive `Default` or implement it by hand. |
| `clear: clear_fn` | `fn(&mut State)`. Resets the accumulator at the start of each group. |
| `accumulate: accumulate_fn` | `fn(&mut State, &[InValue])`. Folds one row into the accumulator. Runs once per row. Returns nothing — the value only leaves through `result_fn`. |
| `buffer_size: N` / `deterministic: true` | Optional, but supplied together or not at all — `agg_func!` has no single-option forms like `func!` does. Same meaning as in `func!` when given. |

The accumulator is allocated once per statement and dropped when the statement ends — `agg_func!` generates both the prerun that creates it and the postrun that drops it, so you never write either. `clear_fn` is what gives you per-group behavior: with `GROUP BY`, the same accumulator is reused across groups, so any field that must not leak between groups has to be reset there.

A complete SUM-equivalent aggregate — the `vsql_agg_sum` example in the SDK repo:

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

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` matching only `InValue::Int` is what skips NULLs, matching built-in `SUM`. The `seen` flag is what makes an all-NULL group and an empty group return NULL rather than `0`:

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

## varargs\_func! macro

`varargs_func!` declares a VDF that accepts any number of arguments, of any type. The parameter list is written `[..]` — a required literal, not the `[]` used for a zero-arity `func!`.

> [!WARNING]
>
> The server performs no argument-count and no argument-type validation for a
> varargs VDF. There is no declared parameter list to check a call against, so
> every call reaches your function with whatever the SQL text passed,
> including zero arguments and types you never expected. Validation is
> entirely the `prerun` hook's job. Varargs registration also requires VEF
> Protocol 3 — older servers reject the extension at install time. This
> matches the C++ SDK, where [the framework cannot validate argument count or
> types for varargs VDFs](https://villagesql.com/docs/en/sdk/cpp/development.md#varargs-vdfs) either.

Eight forms — three shapes, each with a shorthand and a full form that adds
`buffer_size` and `deterministic` together (never singly). The `state:`/
`prerun:` shape has two more on top of that, adding an optional `postrun:` —
same meaning as in `func!`:

```rust theme={null}
// Per-statement state plus a validating prerun.
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    state: StateType, prerun: prerun_fn)
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    state: StateType, prerun: prerun_fn, buffer_size: N, deterministic: true)

// Per-statement state, validating prerun, and author postrun.
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    state: StateType, prerun: prerun_fn, postrun: postrun_fn)
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    state: StateType, prerun: prerun_fn, postrun: postrun_fn,
    buffer_size: N, deterministic: true)

// Validating prerun, no state.
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    prerun: prerun_fn)
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    prerun: prerun_fn, buffer_size: N, deterministic: true)

// Bare: no prerun, no state, no validation.
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type)
villagesql::varargs_func!(impl_fn, "sql_name", [..] -> return_type,
    buffer_size: N, deterministic: true)
```

| Argument | Description |
| - | - |
| `impl_fn` | The row function. `fn(&[InValue]) -> VdfReturn` for the prerun-only and bare forms; `fn(&mut StateType, &[InValue]) -> VdfReturn` for the `state:` forms. The `args` slice length varies from call to call. |
| `"sql_name"` | The SQL function name as a string literal. |
| `[..]` | Marks the function as varargs; there is no type list. |
| `return_type` | `villagesql::Type::*` or `villagesql::custom!("name")`. The return type is fixed even though the arguments are not. |
| `state: StateType` | Optional. Per-statement state, allocated by `prerun` and borrowed `&mut` by each row call. Requires `prerun:`. Unlike `agg_func!`, no `Default` bound — your prerun builds the value. |
| `prerun: prerun_fn` | Optional, and the only place argument validation can happen. `fn(PrerunArgs, PrerunResult<T>)`, where `T` is the type named by `state:`, or `()` in the prerun-only form. |
| `postrun: postrun_fn` | Optional, only on the `state:`/`prerun:` forms — means the same as it does in `func!`: `fn(&mut StateType)`, run once after the last row and before the SDK drops the state. |
| `buffer_size: N` / `deterministic: true` | Optional, supplied together or not at all. Same meaning as in `func!` when given — for varargs, a fixed value is often the wrong choice, since the result usually grows with the argument count; size it in the prerun with `request_buffer_size` instead. |

The bare form has no validation and accepts a zero-argument call — a legitimate choice for a function that's total over every input, but it means the row function alone is responsible for every input it can be handed. Only the `state:` form allocates and drops per-statement state; the prerun-only form uses `PrerunResult<()>` and stores nothing, so there is no postrun for it — such a prerun uses `PrerunResult` only for `error` and `request_buffer_size`, never `set_state`.

### Inspecting argument types in a prerun

Because the server validates nothing, a varargs prerun needs to see the argument types before the first row runs. `PrerunArgs::type_at` provides that view, alongside the `len()`/`is_empty()` and the `PrerunResult` methods described under [Per-statement state](#per-statement-state).

| `PrerunArgs` method | Returns |
| - | - |
| `type_at(i: usize)` | `Option<ArgType>` — the declared type of argument `i`, or `None` if `i` is out of range. |

| `ArgType` method | Returns |
| - | - |
| `is_str()` | `true` for a `STRING` argument. |
| `is_real()` | `true` for a `REAL` argument. A `DECIMAL` value, such as the literal `1.5`, is a `STRING` argument. |
| `is_int()` | `true` for an `INT` argument. |
| `is_custom()` | `true` for an argument of a custom type registered by any installed extension. |
| `custom_name()` | `Option<&str>` — the custom type's name. `Some` only when `is_custom()` is true; `None` for the scalar types. |

Pair `is_custom()` with `custom_name()` to accept exactly one custom type: `is_custom()` alone accepts every custom type in the server.

The `vsql_varargs` example in the SDK repo declares one function per form. A stateful varargs function, validated in prerun and carrying a per-statement call counter:

```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.
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))
}

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),
    ]
}
```

`str_join` still matches on `InValue` in the row function even though the prerun proved every argument is a string: prerun sees declared types, not values, and a `STRING` column can carry NULL on any given row.

```sql theme={null}
INSTALL EXTENSION vsql_varargs;
SELECT vsql_varargs.str_join('alpha', 'beta');
-- #1: alpha, beta
SELECT vsql_varargs.str_join('a', 'b', 'c', 'd');
-- #1: a, b, c, d
SELECT vsql_varargs.str_join('ok', 123);
-- ERROR 1123 (HY000): Can't initialize function 'str_join'; str_join: every argument must be a string
SELECT vsql_varargs.arg_count();
-- 0
SELECT vsql_varargs.arg_count(1, 2.5, 'mix');
-- 3
```

The example also declares `describe` (a prerun-only function that rejects zero arguments and non-scalar arguments, then formats a heterogeneous argument list) and `point_path` (which validates with `is_custom()` and `custom_name()`). See `examples/vsql_varargs/src/lib.rs` in the [Rust SDK repo](https://github.com/villagesql/vsql-rust-sdk).

## custom\_type! macro

`custom_type!` registers a new column type. `type_name`, `persisted_length`, `max_decode_buffer_length`, `encode`, `decode`, and `compare` are required. `hash` is optional but recommended. Give either `default` or `intrinsic_default_fn`, not both.

```rust theme={null}
villagesql::custom_type!(
    type_name: "sql_type_name",
    persisted_length: N,
    max_decode_buffer_length: M,
    encode: encode_fn,
    decode: decode_fn,
    compare: compare_fn,
    hash: hash_fn,
    default: "default_string",
)
```

| Field | Type | Description |
| - | - | - |
| `type_name` | `&str` literal | SQL name for the type. Case-insensitive in SQL. Refer to it as `<extension>.<type>` when more than one installed extension registers the same name. |
| `persisted_length` | `usize` | Fixed byte length for on-disk storage. All encoded values must produce exactly this many bytes. |
| `max_decode_buffer_length` | `usize` | Maximum byte length of the decoded string. Used to size the output buffer before calling `decode`. |
| `encode` | `fn(&str) -> Result<Vec<u8>, String>` | Called at `INSERT` time. Converts a SQL string literal to binary. `Err(msg)` always reports the value as invalid, the same handling MySQL gives a bad value for a built-in type: ERROR 1366 in strict mode, a warning under non-strict mode or `IGNORE`. For this path `msg` is discarded either way — the caller sees only the generic `Incorrect <type> value: '<input>' for column '<col>' at row <n>` text, not `msg` itself. |
| `decode` | `fn(&[u8]) -> Result<String, String>` | Called to display the value. Converts binary back to a string. |
| `compare` | `fn(&[u8], &[u8]) -> std::cmp::Ordering` | Called for `ORDER BY`, `MIN`, `MAX`. Return `Less`, `Equal`, or `Greater`. |
| `hash` | `fn(&[u8]) -> usize` | Optional. Called for `GROUP BY`, `DISTINCT`, and `UNION`. Values that compare `Equal` must hash to the same value. Recommended for indexed columns. |
| `default` | `&str` literal | Optional. A valid string the server encodes at type initialization to verify the callback works. Must encode to exactly `persisted_length` bytes. |
| `intrinsic_default_fn` | `fn() -> Result<String, String>` | Optional. Computes that string instead of stating it, for a default that cannot be written as a literal. |

The `default` field is not a column default value; an omitted column is `NULL`. It supplies the string the server encodes when it initializes the type, and that happens the first time a statement names the type rather than when the extension installs. Give neither `default` nor `intrinsic_default_fn` and the server encodes the empty string instead. Either way, an encode that returns `Err` fails that statement and leaves the type unusable:

```
ERROR 3219 (HY000): Type '<extension>.<type>' failed to initialize: from_string VDF failed to encode intrinsic default input '<default>' (expected persisted_length=<N>)
```

Giving both `default` and `intrinsic_default_fn` is refused when the extension installs, not deferred to first use.

## custom! macro

`villagesql::custom!("type_name")` references a custom type by name in a `func!` declaration:

```rust theme={null}
villagesql::func!(
    my_fn,
    "my_sql_func",
    [villagesql::custom!("mytype")] -> villagesql::custom!("mytype"),
    deterministic: true
)
```

Use it anywhere a `villagesql::Type::*` would appear in a parameter list or return type position. The string must match the `type_name` declared in the corresponding `custom_type!`.

## manifest.json fields

Every extension needs a `manifest.json` alongside its `Cargo.toml`:

```json theme={null}
{
  "name": "vsql_my_extension",
  "version": "0.1.0",
  "description": "Brief description of what the extension does",
  "author": "Your Name",
  "license": "GPL-2.0"
}
```

| Field | Required | Format | Description |
| - | - | - | - |
| `name` | Yes | letters, digits, `_`, `-` | Extension identifier. Must match the `INSTALL EXTENSION` name. Use underscores — hyphens require backtick quoting in SQL. |
| `version` | Yes | MAJOR.MINOR.PATCH | Semantic version. |
| `description` | No | String | Brief description of the extension. |
| `author` | No | String | Author name or organization. |
| `license` | No | String | License identifier. `GPL-2.0` recommended for open-source extensions. |

`name` validation rules: must start with a letter, end with a letter or digit, max 64 characters. An invalid manifest causes `INSTALL EXTENSION` to fail.


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