Skip to main content

Documentation Index

Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

This page is a reference for extension authors. For the step-by-step tutorial, see Creating Extensions. For custom column types, see Creating Custom Types.

VDF Function Contracts

These contracts govern how VDF implementation functions interact with the VEF runtime. Every function registered via make_func<> must follow them. The types referenced below are defined in #include <villagesql/vsql.h>.

Part A: VDF Function Contracts

1. VDF implementation functions are void — they never return a value. The server calls your function through this typedef:
typedef void (*vef_vdf_func_t)(vef_context_t *ctx, vef_vdf_args_t *args,
                                vef_vdf_result_t *result);
Extension authors write the per-argument form that the SDK wrapper bridges to this ABI:
void my_func_impl(vef_context_t* ctx, vef_invalue_t* input,
                  vef_vdf_result_t* result) {
    // ... compute result ...
    return;  // always void -- no return value
}
Communicate success, NULL, or error exclusively through result->type. 2. Set result->type to exactly one of the four result constants. Four constants exist in vef_return_value_type_t:
ConstantValueMeaning
VEF_RESULT_VALUE0Success — the output is in the appropriate union field
VEF_RESULT_NULL1The result is SQL NULL
VEF_RESULT_WARNING2Row-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_ERROR3Fatal error — statement execution is aborted; message is in result->error_msg
There are no type-specific variants. VEF_RESULT_VALUE is the single success constant for strings, integers, reals, and custom types alike. The type of output is determined by which union field you write to (str_buf, int_value, real_value, bin_buf). 3. Check input->is_null before reading any other field on an input argument. If is_null is true, every other field on that vef_invalue_t is undefined. Accessing str_value, int_value, or any other field without checking is_null first is undefined behavior.
void my_func_impl(vef_context_t* ctx, vef_invalue_t* input,
                  vef_vdf_result_t* result) {
    if (input->is_null) {
        result->type = VEF_RESULT_NULL;
        return;
    }
    // Safe to read input->str_value, input->int_value, etc.
}
4. For string results, write to result->str_buf and set result->actual_len. Check result->max_str_len before writing.
  • result->str_buf is a server-managed buffer. Write your output here.
  • result->actual_len must be set to the number of bytes written. There is no field named str_len on the result struct.
  • result->max_str_len is the size of str_buf in bytes. Always check it before writing. Do not use sizeof(result->str_buf).
void upper_impl(vef_context_t* ctx, vef_invalue_t* input,
                vef_vdf_result_t* result) {
    if (input->is_null) {
        result->type = VEF_RESULT_NULL;
        return;
    }

    size_t len = input->str_len;
    if (len > result->max_str_len) {
        result->type = VEF_RESULT_ERROR;
        snprintf(result->error_msg, VEF_MAX_ERROR_LEN,
                 "Input length %zu exceeds buffer size %zu",
                 len, result->max_str_len);
        return;
    }

    for (size_t i = 0; i < len; i++) {
        result->str_buf[i] = toupper(input->str_value[i]);
    }
    result->type = VEF_RESULT_VALUE;
    result->actual_len = len;
}
5. Write error messages to result->error_msg, not to result->str_buf. Use VEF_MAX_ERROR_LEN (512 bytes) as the snprintf size limit. error_msg is a separate, caller-provided buffer dedicated to error text. It is independent of str_buf. The maximum size including the null terminator is VEF_MAX_ERROR_LEN (512).
// Correct -- error goes to error_msg
result->type = VEF_RESULT_ERROR;
snprintf(result->error_msg, VEF_MAX_ERROR_LEN,
         "Invalid input: expected positive integer, got %lld",
         input->int_value);
return;

Part B: Encode/Decode Return Convention

The bool return convention applies only to encode and decode function pointers used by custom types. It does not apply to VDF-name type operations (which use the standard VDF void convention) or to VDF implementation functions.
typedef bool (*vef_encode_func_t)(unsigned char *buffer, size_t buffer_size,
                                  const char *from, size_t from_len,
                                  size_t *length);

typedef bool (*vef_decode_func_t)(const unsigned char *buffer,
                                  size_t buffer_size, char *to, size_t to_size,
                                  size_t *to_length);
Both functions return bool where:
  • false = success (the operation completed without error)
  • true = error (the conversion failed)
The encode/decode return convention is inverted from what most developers expect. false means success, true means error. This follows the general convention of MySQL internal C/C++ APIs, which frequently return 0 (false) for success. Double-check every return statement in your encode and decode implementations.

Implement Wrapper Functions

Implementation functions use the VEF API signature:
#include <algorithm>
#include <string>
#include <cstring>

// String reverse implementation
void my_reverse_impl(vef_context_t* ctx, vef_invalue_t* input,
                     vef_vdf_result_t* result) {
    if (input->is_null) {
        result->type = VEF_RESULT_NULL;
        return;
    }

    // Reverse the string
    size_t len = input->str_len;
    for (size_t i = 0; i < len; i++) {
        result->str_buf[i] = input->str_value[len - 1 - i];
    }
    result->type = VEF_RESULT_VALUE;
    result->actual_len = len;
}

// Count vowels implementation
void count_vowels_impl(vef_context_t* ctx, vef_invalue_t* input,
                       vef_vdf_result_t* result) {
    if (input->is_null) {
        result->type = VEF_RESULT_NULL;
        return;
    }

    int64_t count = 0;
    for (size_t i = 0; i < input->str_len; i++) {
        char lower = std::tolower(input->str_value[i]);
        if (lower == 'a' || lower == 'e' || lower == 'i' ||
            lower == 'o' || lower == 'u') {
            count++;
        }
    }
    result->int_value = count;
    result->type = VEF_RESULT_VALUE;
}

Handling NULL Values

Check for NULL via the is_null flag and return NULL by setting the result type:
void my_func_impl(vef_context_t* ctx, vef_invalue_t* input,
                  vef_vdf_result_t* result) {
    // Check if input is NULL
    if (input->is_null) {
        result->type = VEF_RESULT_NULL;
        return;
    }

    // Access input data
    const char* str = input->str_value;
    size_t len = input->str_len;

    // Process and write result to result->str_buf
    // ...

    result->type = VEF_RESULT_VALUE;
    result->actual_len = output_length;
}
NULL handling options:
  • Input NULL check: if (input->is_null)
  • Return NULL: result->type = VEF_RESULT_NULL
  • Return value: result->type = VEF_RESULT_VALUE + write to type-specific buffer
  • Return warning: result->type = VEF_RESULT_WARNING + warning message in result->error_msg (returns NULL for this row, adds SQL warning, continues execution; in strict mode, MySQL promotes this to an error on INSERT/UPDATE)
  • Return error: result->type = VEF_RESULT_ERROR + error message in result->error_msg

Error Handling

Return errors with custom messages for validation failures or invalid input:
void validate_age_impl(vef_context_t* ctx, vef_invalue_t* age_input,
                       vef_vdf_result_t* result) {
    if (age_input->is_null) {
        result->type = VEF_RESULT_NULL;
        return;
    }

    long long age = age_input->int_value;

    // Validate input range
    if (age < 0 || age > 150) {
        result->type = VEF_RESULT_ERROR;
        // Write error message
        const char* msg = "Age must be between 0 and 150";
        strcpy(result->error_msg, msg);
        return;
    }

    // Return valid result
    result->type = VEF_RESULT_VALUE;
    result->int_value = age;
}
Result types:
  • VEF_RESULT_VALUE - Success
  • VEF_RESULT_NULL - NULL value
  • VEF_RESULT_WARNING - Row-level warning (returns NULL, adds SQL warning, continues execution; strict mode promotes to error on INSERT/UPDATE)
  • VEF_RESULT_ERROR - Fatal error (aborts statement execution)

Per-Statement State with Prerun/Postrun

For VEF SDK functions that need setup/cleanup per SQL statement (not per row):
// Called once before the first row
void my_prerun(vef_context_t* ctx, vef_prerun_args_t* args,
               vef_prerun_result_t* result) {
    // Allocate state, open connections, etc.
    void* state = malloc(sizeof(MyState));
    result->user_data = state;
}

// Called once after all rows processed
void my_postrun(vef_context_t* ctx, vef_postrun_args_t* args,
                vef_postrun_result_t* result) {
    // Clean up state allocated in prerun
    if (args->user_data) {
        free(args->user_data);
    }
}

// Register with extension
VEF_GENERATE_ENTRY_POINTS(
  make_extension()
    .func(make_func<&my_func_impl>("my_func")
      .returns(STRING)
      .param(STRING)
      .prerun(&my_prerun)
      .postrun(&my_postrun)
      .build())
);
Most extensions don’t need prerun/postrun hooks. The VEF SDK automatically handles common cases like type checking and buffer allocation. 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, please 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 with make_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:
-- COUNT(DISTINCT) works with custom types
SELECT COUNT(DISTINCT impedance) FROM signals;

-- MIN and MAX work with custom types (requires compare function)
SELECT MIN(impedance), MAX(impedance) FROM signals;

-- GROUP_CONCAT works with custom types
SELECT GROUP_CONCAT(impedance ORDER BY impedance SEPARATOR ', ') FROM signals;
Extension functions are called in a per-row execution model:
  • Each function call processes one row with its own result buffer (thread-safe)
  • prerun/postrun provide 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
Best practice: Design functions to be stateless for simplicity and safety.

Window Functions

The following window functions work with custom types:
SELECT
    id,
    impedance,
    LAG(impedance)  OVER (ORDER BY id) AS prev_impedance,
    LEAD(impedance) OVER (ORDER BY id) AS next_impedance
FROM signals;

SELECT
    id,
    impedance,
    FIRST_VALUE(impedance) OVER w AS first_impedance,
    LAST_VALUE(impedance)  OVER w AS last_impedance,
    NTH_VALUE(impedance, 2) OVER w AS second_impedance
FROM signals
WINDOW w AS (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);

Temporary Tables

Custom types work in temporary tables. CREATE TEMPORARY TABLE, INSERT, and ALTER TABLE behave the same as with permanent tables.
CREATE TEMPORARY TABLE tmp_signals (
    id        INT PRIMARY KEY,
    impedance COMPLEX
);

INSERT INTO tmp_signals VALUES (1, '(10,5)'), (2, '(20,0)');
SELECT id, impedance FROM tmp_signals;

Preview APIs

Some VEF capabilities are available as opt-in headers under villagesql/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:
#include <villagesql/preview/keyring.h>        // vsql::preview_keyring::KeyringCapability
#include <villagesql/preview/thread_worker.h>  // vsql::preview_thread_worker::ThreadWorkerCapability
#include <villagesql/preview/sql_query.h>      // vsql::preview_sql_query::SqlQueryCapability
None of these headers are pulled in by <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.
Preview headers are not stable. An extension built against them may break when the server is updated. When a feature stabilizes, its headers move to a versioned stable SDK path.

Triggers

Triggers fire on tables with custom type columns. The trigger body can reference non-custom-type columns from NEW and OLD. Accessing custom type column values inside a trigger body is not yet supported.
CREATE TABLE signals (
    id        INT PRIMARY KEY,
    impedance COMPLEX,
    label     VARCHAR(50)
);
CREATE TABLE signal_log (
    id        INT,
    label     VARCHAR(50),
    logged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TRIGGER signals_after_insert
AFTER INSERT ON signals
FOR EACH ROW
    INSERT INTO signal_log (id, label) VALUES (NEW.id, NEW.label);

INSERT INTO signals VALUES (1, '(10,5)', 'sensor_a');
SELECT id, label FROM signal_log;