VDF Function Contracts
These contracts govern how VDF implementation functions interact with the VEF runtime. Every function registered viamake_func<> must follow them.
The types referenced below are available via #include <villagesql/vsql.h>.
Part A: VDF Function Contracts
1. VDF implementation functions arevoid — they never return a value.
out.set(...) / out.set_length(n), out.set_null(),
out.warning(msg), or out.error(msg).
2. Set result->type to exactly one of the four result constants.
Four constants exist in vef_return_value_type_t:
| Constant | Value | Meaning |
|---|---|---|
VEF_RESULT_VALUE | 0 | Success — the output is in the appropriate union field |
VEF_RESULT_NULL | 1 | The result is SQL NULL |
VEF_RESULT_WARNING | 2 | Row-level warning — execution continues, NULL is returned for this row, and a SQL warning is added. In strict mode, MySQL promotes this to an error on INSERT/UPDATE. |
VEF_RESULT_ERROR | 3 | Fatal error — statement execution is aborted; message is in result->error_msg |
VEF_RESULT_VALUE is the single success
constant for strings, integers, reals, and custom types alike. The output type
is determined by which result wrapper your function takes (StringResult,
IntResult, RealResult, CustomResult).
3. Check input.is_null() before calling input.value().
If is_null() returns true, calling value() is undefined behavior.
out.buffer() and call out.set_length(n). Check out.buffer().size() before writing.
out.buffer()returns aSpan<char>over the server-managed buffer.out.set_length(n)records how many bytes were written.out.buffer().size()is the maximum capacity. Always check it before writing.
out.error(msg). The message is truncated to VEF_MAX_ERROR_LEN (512 bytes) if necessary.
out.error(msg) accepts a std::string_view. It copies the message into a
server-managed buffer and sets the result state to error in one call.
Implement Wrapper Functions
Implementation functions use typed argument and result wrappers:Handling NULL Values
Check for NULL viais_null() and return NULL by calling set_null():
- Input NULL check:
input.is_null() - Return NULL:
out.set_null() - Return value:
out.set(v)(numeric/custom) orout.set_length(n)after writing intoout.buffer()(string) - Return warning:
out.warning(msg)— returns NULL for this row, adds a SQL warning, continues execution; in strict mode, MySQL promotes this to an error on INSERT/UPDATE. Call instead ofout.set(), not in addition to it. - Return error:
out.error(msg)— aborts statement execution
Error Handling
Return errors with custom messages for validation failures or invalid input:VEF_RESULT_VALUE- Success (out.set(v)/out.set_length(n))VEF_RESULT_NULL- NULL value (out.set_null())VEF_RESULT_WARNING- Row-level warning (returns NULL, adds SQL warning, continues execution; strict mode promotes to error on INSERT/UPDATE) (out.warning(msg))VEF_RESULT_ERROR- Fatal error, aborts statement execution (out.error(msg))
Per-Statement State with Prerun/Postrun
Prerun and postrun hooks use typed wrappers. The required signatures are:vef_prerun_args_t* / vef_postrun_args_t*) are rejected
at compile time by static_assert in .prerun<&Hook>() and .postrun<&Hook>().
PrerunArgs and PostrunArgs method details, see
Per-Statement State
in the Development guide.
Most extensions don’t need prerun/postrun hooks. The VEF SDK automatically
handles common cases like type checking and result buffer sizing — for both
STRING-returning and CUSTOM-returning VDFs, the result buffer is grown to fit
the resolved return type before the VDF body runs. Use prerun/postrun only when
you need expensive per-statement setup (like opening connections) that
shouldn’t happen per-row.If you find you need prerun/postrun for your use case, share your scenario on
the VillageSQL Discord — the team may be able
to add SDK support to handle it automatically.
Aggregate Functions
Built-in aggregates COUNT(DISTINCT), MIN, MAX, and GROUP_CONCAT work with custom types out of the box. MIN and MAX require a compare function registered on the type. Custom aggregate VDFs are also supported. Register one withmake_aggregate_func<State, &result_fn>("name"), then chain .returns(),
.param(), .clear<>(), and .accumulate<>() before calling .build().
Both .clear<>() and .accumulate<>() are required. See
Aggregate VDFs for the
builder API and callback signatures.
Built-in aggregate operations with custom types:
- Each function call processes one row with its own result buffer (thread-safe)
prerun/postrunprovide per-statement setup/teardown- Avoid global state - use function parameters and return values instead
- If you must use global state, protect it with mutexes/locks
Window Functions
The following window functions work with custom types:Temporary Tables
Custom types work in temporary tables.CREATE TEMPORARY TABLE, INSERT,
and ALTER TABLE behave the same as with permanent tables.
Preview APIs
Some VEF capabilities are available as opt-in headers undervillagesql/preview/ in the SDK include tree. The ABI and API are still
under active development and may change without notice.
To opt in, add the include to your extension source. For example:
<villagesql/vsql.h>; you must include it
directly when you opt in.
Namespace layout under vsql::preview is per-capability — there is no single
universal pattern. The keyring API uses vsql::preview_keyring::KeyringCapability;
the thread worker API uses vsql::preview_thread_worker::ThreadWorkerCapability;
the SQL query API uses vsql::preview_sql_query::SqlQueryCapability and must be
opened from a background worker thread handle (vef_thread_handle_t *).
Check each header for the exact namespace and class name it defines.
For the full preview API documentation, see Preview Capabilities.
Triggers
Triggers fire on tables with custom type columns. The trigger body can reference non-custom-type columns fromNEW and OLD. Accessing custom
type column values inside a trigger body is not yet supported.

