Skip to main content
The Rust SDK is in alpha — expect breaking API changes between releases. Function-only extensions, aggregate functions, varargs functions, and custom types (encode, decode, compare, hash) are supported, as are the sys_var, status_var, thread_worker and keyring preview capabilities. The column storage ABI is C++-only today — use the C++ SDK if you need it.
Custom types let you define new column types — like RATIONAL, VECTOR, or INET — that work with ORDER BY, indexes, and aggregate functions. The Rust SDK supports this through the custom_type! macro, and through parameterized_type! for types whose storage size depends on column parameters (see Parameterized Types). This page assumes you’ve already worked through Creating Extensions in Rust. The setup (Cargo.toml, manifest.json, cargo-vsql) is the same.

When to use a custom type

Use a custom type when:
  • You need a binary on-disk layout that a standard SQL type can’t express (packed floats, fixed-width integers, binary identifiers)
  • Your type has its own ordering semantics that differ from lexicographic string ordering
  • You want the server to index and hash values correctly for ORDER BY, COUNT(DISTINCT), and set operations
If you only need SQL-callable functions and your data fits comfortably in STRING, INT, or REAL columns, you don’t need a custom type.

The custom_type! macro

Every custom type needs 4 callbacks (encode, decode, compare, hash) and a default value. Here’s the full macro signature:
type_name, persisted_length, max_decode_buffer_length, encode, decode, and compare are required. hash and default are optional but recommended — hash is needed for correct COUNT(DISTINCT) and set operations, and default is needed for type initialization verification.

Receiving and returning binary values

Functions that take or return a custom type work with raw bytes. InputInValue::Custom(b) carries the stored binary as &[u8]:
OutputVdfReturn::Binary(bytes) sends binary bytes back to the server:
To reference a custom type in a func! declaration, use villagesql::custom!("type_name"):

Example: rational number type

examples/vsql_rational in the SDK repo is a working extension implementing a RATIONAL type. It stores a rational number as a 16-byte pair of i64 values (numerator, denominator) in little-endian byte order and provides arithmetic functions. Here are the core encode, decode, compare, and hash implementations:
The custom_type! registration and the arithmetic VDFs (rational_add, rational_sub, etc.) are in the full source at examples/vsql_rational/src/lib.rs. With the extension installed:
rational_to_real(r RATIONAL) -> REAL converts a RATIONAL value to a 64-bit floating-point approximation by dividing the numerator by the denominator. Useful when you need an approximate decimal for display or comparison but don’t want to store the lossy representation in the column.

Parameterized types

A parameterized type needs a value read at CREATE TABLE time — like the 3 in VECTOR(3) — to know its storage size. custom_type! can’t express this: its persisted_length is a single fixed constant for every column. parameterized_type! is the parameterized counterpart: persisted length is computed per column from the declared parameters, through int_to_params and resolve_params, instead of being fixed.
type_name, max_persisted_length, max_decode_buffer_length, encode, decode, compare, int_to_params, resolve_params, params_type, params_parse, and params_to_strings are required. hash and default are optional; intrinsic_default_fn (a function computing the default from &P, for when the default depends on parameters) is also optional and mutually exclusive with default. Here’s padint — an i64 stored in a fixed 8 bytes, with a width parameter controlling zero-padded display width:
A function receiving a parameterized custom-type argument sees InValue::CustomWithParams { bytes, params } rather than plain InValue::Custom(bytes)params is a [TypeParams], a read-only, zero-copy view over the column’s declared key=value pairs.

The extension! block with types

When registering both functions and types, the extension! block has two sections:
A function-only extension omits types:. A type-only extension keeps funcs: [] and omits nothing else.

Next steps

Rust API Reference

Complete reference for InValue, VdfReturn, and all macros.

Creating Extensions in Rust

Getting started — Cargo setup, first function, packaging, and testing.

C++ Custom Types

Custom types in C++ — make_type<>, encode/decode/compare/hash, ALTER TABLE rules.

Extension Architecture

How custom types are resolved, cached, and stored.