Skip to main content
This page walks through four of the reference extensions in the Rust SDK repository — a minimal function-only example, a complete custom type with arithmetic, ordering, and hashing, an aggregate function, and a varargs function. The repository’s examples/ directory also carries one example per supported preview capability. Source: examples/ in vsql-rust-sdk

vsql_rot13 — Function-Only Extension

The simplest possible Rust extension: one VDF that takes a STRING and returns a STRING. Usage:

Directory Structure

Implementation

File: src/lib.rs
Key patterns:
  • VDFs receive &[InValue] and return VdfReturn — both are safe Rust enums
  • NULL is a first-class variant on both sides; pattern-match it directly
  • The extension! macro generates the C entry points the server calls at load time
  • func! declares the SQL signature; argument and return types use villagesql::Type::*

Manifest

File: manifest.json

vsql_rational — Custom Type with Arithmetic

A complete custom type: rational numbers stored as (numerator, denominator) in reduced form, with arithmetic functions, ordering, and hashing. Usage:

Binary Storage Format

rational stores 16 bytes (little-endian):
  • Bytes 0–7: numerator (i64)
  • Bytes 8–15: denominator (i64)
Values are always stored in reduced form (GCD = 1) with a positive denominator.

Type-System Functions

File: src/lib.rs The type registers four operations: encode (string → bytes), decode (bytes → string), compare (for ORDER BY), and hash (for indexing).

VDF Implementations

VDFs that take a custom type receive InValue::Custom(&[u8]) and decode the bytes themselves:

Registration

The extension! macro registers both the type and its functions in a single declaration:
Key patterns:
  • villagesql::custom!("name") references a custom type as an argument or return
  • custom_type! registers the type alongside its encode/decode/compare/hash functions
  • default: "0/1" is the intrinsic default — the server calls encode() on this string at type initialization, so it must be a valid value
  • persisted_length must match the byte length encode() returns
  • deterministic: true lets the optimizer fold constant calls

vsql_agg_sum — Aggregate Function

An aggregate VDF that reimplements SUM over an INT column. It shows the three hooks an aggregate needs — clear, accumulate, and the result function — and how each one sees the same accumulator. Usage:
Adding an all-NULL group shows that the accumulator is reset between groups rather than carried forward:

Accumulator Lifecycle

The accumulator is one value per statement, reused across every group. The server drives it in a fixed order: clear resets the accumulator at the start of each group. A field it forgets to reset leaks from the previous group. The server calls accumulate for every row, including rows where the argument is NULL. Skipping NULLs is the function’s job: match only the variant you want and ignore the rest.

Implementation

File: src/lib.rs
The seen flag is what distinguishes a group that summed to zero from a group with nothing to sum. Without it, an empty or all-NULL group would return 0 where built-in SUM returns NULL.

Registration

Key patterns:
  • The first identifier is the result function, not the row function — an aggregate’s per-row work lives in accumulate:
  • state: names the accumulator type, which must implement Default
  • The declared parameter list is the per-row argument list: [villagesql::Type::Int] is what accumulate receives, and the return type is what the result function produces
  • agg_func! also accepts buffer_size: and deterministic: after accumulate:, supplied together, in that order

vsql_varargs — Varargs Function

Four VDFs that each take any number of arguments. Together they cover the three registration forms varargs_func! supports — stateful with a prerun, prerun-only, and bare — plus validation of custom-typed arguments. Usage:
The same function serves both arities. The #1 prefix is the per-statement call counter, which climbs across the rows of one statement:

Prerun Owns All Varargs Validation

For a varargs function the server does no argument checking at all — not the count, not the types. A declared signature is what normally makes the server reject a bad call before your code runs, and a varargs function has none. Whatever the prerun hook does not reject reaches the row function. Prerun rejects the call, once, before any row: it sees the argument types the optimizer resolved and fails the statement. The row function still has to handle each value, because a column whose type passed validation can still carry NULL on any given row. A prerun rejection fails statement initialization:
Omitting the prerun means accepting every call. arg_count is registered bare, so a zero-argument call is legal:

Implementation

File: src/lib.rs A prerun receives PrerunArgs and a PrerunResult<T> whose T matches the state type. PrerunArgs::len() is the argument count, and type_at(i) returns the type of argument i as an ArgType:
For varargs, size the buffer in the prerun with request_buffer_size, scaled to args.len() — a fixed buffer_size can’t grow with the argument count. ArgType exposes four predicates — is_int(), is_real(), is_str(), and is_custom() — so a prerun can accept a heterogeneous call as long as every argument is one of the shapes the row function handles. describe accepts any mix of the three scalars and rejects anything else:
The state type is () because this prerun keeps nothing: it validates and sizes the buffer, and never calls set_state.

Custom-Typed Varargs

is_custom() alone only says the argument is some custom type. custom_name() returns which one, so a prerun can restrict a varargs call to a single type. The extension registers a point2d custom type and accepts a variable number of point2d values:
A plain string is rejected before the first row, even though its bytes would parse as a point:
The row function then matches InValue::Custom(b) and decodes the bytes itself, the same as any other custom-typed VDF.

Registration

describe, point_path, and point2d’s encode/decode/compare follow the same InValue-matching and byte-encoding patterns already shown for str_join and rational above — see examples/vsql_varargs/src/lib.rs in the Rust SDK repo for their full source. Key patterns:
  • [..] in place of a parameter list is what marks the function varargs
  • Three forms, each with a different row-function signature: state: + prerun: gives fn(&mut State, &[InValue]) -> VdfReturn; prerun: alone and the bare form both give fn(&[InValue]) -> VdfReturn
  • Only the state: form allocates and drops per-statement state
  • The return type is still declared, so only the argument list is variable
  • Each form also accepts buffer_size: and deterministic: as a trailing pair
  • point2d registers no hash, which is optional — compare alone is enough for ORDER BY

Key Implementation Patterns


Testing

All four examples use MTR (the MySQL Test Runner) just like C++ extensions:
Generate or update expected results with --record.

Next Steps

Creating Extensions in Rust

SDK installation, build, and the extension! macro

Rust Custom Types

Deep dive on encode, decode, compare, and hash

Rust API Reference

InValue, VdfReturn, and the macro surface

Example Source

Complete source for all four examples