Writing Extension Functions
Extension functions are written in C++ and registered with VEF. Include a single header to access the full SDK:Argument and Result Types
VDF parameters and results are passed as type-safe argument and result types. The framework detects them from your function signature and adapts automatically — themake_func registration syntax is unchanged.
Argument types: IntArg, RealArg, StringArg, CustomArg — each
provides is_null() and value(). For parameterized custom types,
CustomArgWith<P> adds a params() accessor that returns the cached
parsed params struct (see Parameterized Types).
Result types: IntResult, RealResult, StringResult, CustomResult
— each provides set_null(), warning(msg), and error(msg). Scalar
results also provide set(value). Buffer results provide buffer() and
set_length(len). StringResult also provides
set(std::string_view), which copies up to buffer().size() bytes from the
view and sets the length in one call. For parameterized custom types,
CustomResultWith<P> adds a params() accessor.
Span type: value() and buffer() on the byte-oriented argument and
result types return a vsql::Span<T> — a non-owning view over a contiguous
run of T with data(), size(), empty(), begin()/end(), and
operator[]. Under C++20 it is an alias for std::span<T>; under C++17 this
SDK provides a minimal compatible implementation, so the same code compiles in
either standard. It is available through <villagesql/vsql.h>.
warning(msg) returns SQL NULL for the row and appends a SQL warning. In strict mode (STRICT_TRANS_TABLES), MySQL promotes it to a statement error on INSERT/UPDATE, so it behaves like error(msg) in strict contexts. Use it for recoverable bad input, such as an unparseable string in an encode function. Use error(msg) for corrupt stored data or any condition where continuing is unsafe. The message for both is truncated to fit the server’s internal error buffer if necessary.
Scalar example — add two integers:
StringResult and CustomResult, write into buffer(), then call
set_length() with the number of bytes written. buffer().size() is the
maximum capacity.
For VDFs that return a custom type (returns(CUSTOM(MYTYPE))), the server
sizes the result buffer to the resolved return type’s persisted_length
automatically — extension authors do not need to declare .buffer_size(...)
on the function builder for this case. If prerun grows the buffer further,
that larger size is preserved. This is what lets, for example,
SVECTOR::from_string('[…1024 floats…]') encode a wide vector without the
result buffer running out of room.
You can use different styles across functions in the same extension — each
function’s style is determined by its own signature.
Aggregate VDFs
Aggregate VDFs accumulate state across rows within eachGROUP BY group and
return a single result per group, like SQL SUM or COUNT. Use
make_aggregate_func<State, &result_fn>("name") to register one. The State
type is the per-group accumulation buffer; prerun and postrun are
auto-generated to allocate and delete it.
The result function must have the signature void(const State&, ResultType)
where ResultType is one of IntResult, RealResult, StringResult,
CustomResult, or CustomResultWith<P>. Call out.set(value) to return a
value or out.set_null() to return SQL NULL.
Both .clear<>() and .accumulate<>() are required. The builder enforces
this at compile time (via build()), and the server validates it again at
INSTALL EXTENSION time — clear resets state, accumulate folds rows, and
the result function reads the final state.
make_aggregate_func<State, &result_fn>()auto-generatesprerunandpostrun(value-initializes and deletesState)..clear<&fn>()registers yourvoid(State&)reset function..accumulate<&fn>()registers yourvoid(State&, TypedArgs...)fold function.TypedArgsare deduced from the function signature (IntArg,StringArg, etc.).- The result type (
IntResult,RealResult, etc.) is deduced from the result function signature.
StringResult aggregate VDF returns text: the result reports the
utf8mb4_bin charset and collation, so clients display it as characters
rather than hex — the same as the scalar VDF STRING path. It also honors
.max_result_length(n) the same way, sizing a materialized aggregate result
(a GROUP BY/DISTINCT temp table, CREATE TABLE ... SELECT, or UNION) so it
isn’t truncated at the argument width. See
Custom Buffer Sizes for the
sizing rules and cap.
Per-Statement State (Prerun and Postrun)
Some VDFs need state that spans every row a single query touches — a call counter, a cached result, an open resource. Allocate it in a prerun hook, access it from the VDF body, and release it in a postrun hook. Both hooks run once per statement; the VDF body runs once per row. Register them with.prerun<&Hook>() and .postrun<&Hook>(). The required
signatures are:
Use
PrerunResult::set_user_data(void*) to stash state; use PostrunArgs::delete_state<T>() to release it. If prerun calls set_user_data(new T{}), postrun must call delete_state<T>() — the SDK does not auto-free.
PrerunArgs::type_at(i) exposes the declared SQL type of each argument before any rows are read; the predicates is_int(), is_real(), is_str(), is_custom() on the returned PrerunArgType mirror the column types. Use this in prerun to validate argument types or call PrerunResult::request_buffer_size(n) to size the result buffer.
Varargs VDFs
A varargs VDF accepts any number of arguments of any SQL type. Declare one with.varargs() on the func builder, which is mutually exclusive with
.no_params() and .param(TYPE). The body receives a vsql::VarArgs argument
instead of the usual fixed-arity argument types.
The framework cannot validate argument count or types for varargs VDFs. Pair
every varargs registration with a prerun hook that calls PrerunResult::error()
on invalid input or PrerunResult::request_buffer_size(n) to size the result
buffer.
Iterate over arguments with range-for. Each AnyArg element requires a type
check before reading its value:
Check
is_null() before any accessor — all four are undefined on a null argument.
VEF_GENERATE_REGISTRATION
VEF_GENERATE_REGISTRATION creates an internal _vef_do_register() helper
that performs extension registration but does not define the extern "C" entry
points. Use it when you need to customize vef_register behavior — for
example, to patch descriptors after registration in a test build. For normal
extensions, use VEF_GENERATE_ENTRY_POINTS instead.
Custom Type Operations
For the full type operation builder reference — encode, decode, compare, hash, intrinsic defaults, and parameterized types — see Type Operations.Preview Capabilities
The following VEF capabilities are available as opt-in preview headers. ABI and API are still under active development; see Preview Capabilities for the full reference.- Extension system variables — Preview Capabilities → System Variables
- Extension status variables — Preview Capabilities → Status Variables
- Keyring access — Preview Capabilities → Keyring Access
- Column storage — Preview Capabilities → Column Storage
Inspecting Extension Registration Metadata
INFORMATION_SCHEMA.EXTENSION_REGISTRATION exposes the in-memory VEF
registration struct for each loaded extension as a JSON document. Use it to
verify that the server parsed your extension’s functions, types, and system
variables correctly after INSTALL EXTENSION.
See Also
- Creating Extensions in C++ — end-to-end build steps, CMake setup, and installation
- C++ Testing — local dev server, MTR, and debugging failures
- Type Operations — encode, decode, compare, hash, parameterized types
- C++ API Reference — VDF contracts, null handling, and buffer sizing
- Extension Architecture — lifecycle, Victionary caching, performance patterns, and security model

