Custom types let you define new column types — likeDocumentation Index
Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
RATIONAL, VECTOR, or INET — that work with ORDER BY, indexes, and aggregate functions. The Rust SDK supports this through the custom_type! macro.
This page assumes you’ve already worked through Building 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
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:| Field | Type | Description |
|---|---|---|
type_name | string literal | The SQL type name. Case-insensitive in SQL. |
persisted_length | usize | Fixed byte length for on-disk storage. |
max_decode_buffer_length | usize | Maximum byte length of the decoded string representation. |
encode | fn | Converts a &str to binary bytes at INSERT time. |
decode | fn | Converts binary bytes back to a String for display. |
compare | fn | Returns Ordering for ORDER BY, MIN, MAX. |
hash | fn | Returns a usize hash for COUNT(DISTINCT) and set operations. Optional but recommended for indexed columns. |
default | string literal | A valid string the server can encode at type initialization. Must encode to exactly persisted_length bytes. Optional but recommended. |
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. Input —InValue::Custom(b) carries the stored binary as &[u8]:
VdfReturn::Binary(bytes) sends binary bytes back to the server:
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:
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.
The extension! block with types
When registering both functions and types, theextension! block has two sections:
funcs:; a function-only extension omits types:.
Next steps
Rust API Reference
Complete reference for InValue, VdfReturn, and all macros.
Building 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.

