Writing Extension Functions
Extension functions are written in C++ and registered with VEF. Include a single header to access the full SDK:Exception Safety
Containing exceptions is the extension’s responsibility. The SDK does not wrap your implementation in atry/catch, and neither does the server, so an
exception that escapes an entry point unwinds across the C ABI boundary with no
handler anywhere above it and the server process terminates. Ordinary SQL input
reaches this: std::stoi() throws on a non-numeric string, and
nlohmann::json::dump() throws on text that is not valid UTF-8.
Wrap the body of every entry point you register: VDFs, aggregate result
functions, and the from_string/to_string operations of a custom type.
... in addition to const std::exception &: a dependency may throw a
type that does not derive from std::exception, and that one still terminates
the server. Choose between warning() and error() on the same basis as any
other failure — warning() for bad input the statement should survive,
error() where continuing is unsafe.
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) — which requires the opt-in dev-ABI headers
(-DVSQL_USE_DEV_ABI=ON) and Protocol 4 — 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.
PrerunArgType::custom_type() returns the argument’s custom type name as a
std::string_view — empty for non-custom arguments. is_custom() alone
accepts every custom type registered on the server; pairing it with a
custom_type() comparison narrows a varargs call to one specific type:
Extension Load and Unload Hooks
An extension sometimes has work to do once per load rather than once per call — picking a CPU-specific implementation, or building a lookup table the VDFs then read. Register that work with.on_init<&Fn>() on the extension builder, and
the matching teardown with .on_deinit<&Fn>(). Both take a function with the
signature void().
on_init runs at every extension load: at INSTALL EXTENSION and again on each
server startup, after the server has validated and accepted the extension. It
never runs for an extension the server rejected. on_deinit runs at unload —
UNINSTALL EXTENSION or server shutdown.
Both hooks run inside the extension process with no access to the server:
they cannot run SQL, read system variables, or reach server state. That is why
they are the wrong place for setup that has to talk to the server — use a
preview capability’s populate step for that.
INSTALL EXTENSION my_extension, SELECT fast_sqrt(16) returns 4 —
a result that is only reachable if build_table already ran.
on_deinit is called from the vef_unregister entry point that
VEF_GENERATE_ENTRY_POINTS generates. VEF_GENERATE_REGISTRATION does not
define that entry point, so an extension using it must call its teardown from
the vef_unregister it writes itself.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

