Use this file to discover all available pages before exploring further.
Custom types use Protocol 2 APIs, which are under active development and
subject to change before the beta release.
Custom types let you define new column types — like COMPLEX, UUID, or
VECTOR — that work with ORDER BY, indexes, and aggregate functions. This
page is Step 4 of the Creating Extensions
tutorial. Complete Steps 1–3 before continuing here.
Every custom type needs encode, decode, and compare operations, and optionally a hash operation. There are two ways to provide these: VDF names (VEF_PROTOCOL_2 or later, recommended) or function pointers. For each operation, provide one or the other — not both.VDF names — reference VDFs from the same extension by name (see VDF-Name Type Operations):
Using the template-based API (recommended):The vsql::make_type<kName>() template embeds encode, decode, compare, and hash
operations directly in the type object. VDF names are auto-generated as
TYPE::from_string, TYPE::to_string, TYPE::compare, and TYPE::hash
at compile time. Separate .func(make_type_encode<>(...)) calls are not needed.
build() fails to compile if from_string, to_string, or compare is missing.
Each template method validates the function pointer signature with static_assert.The type name is passed as a non-type template parameter (NTTP). Declare it as a
static constexpr const char[] array — the pointer identity is used to key
independent VDF name buffers, so two types sharing a function pointer still get
separate auto-generated names.Using function pointers:
VEF_GENERATE_ENTRY_POINTS( make_extension() .type(make_type(MYTYPE) .persisted_length(16) // Fixed storage size in bytes .max_decode_buffer_length(64) // Max string representation size .encode(&encode_mytype) .decode(&decode_mytype) .compare(&compare_mytype) // Enables ORDER BY and indexes .hash(&hash_mytype) // Optional custom hash .build()) .func(make_func<&mytype_constructor>("MYTYPE") // Constructor function .returns(MYTYPE) .param(REAL) .param(REAL) .build()));
With the template-based API, encode and decode VDFs are embedded in the type object
and registered automatically — no separate .func() calls are needed. The
auto-generated VDFs are SQL-callable:
-- Convert string to custom type (calls MYTYPE::from_string)SELECT MYTYPE::from_string('(1.0,2.0)');-- Convert custom type to string (calls MYTYPE::to_string)SELECT MYTYPE::to_string(my_column) FROM my_table;-- Explicit conversion in INSERTINSERT INTO my_table (id, value)VALUES (1, MYTYPE::from_string('(3.0,4.0)'));
When using the template-based API, .from_string<&fn>() and .to_string<&fn>() on the type builder automatically create SQL-callable conversion functions. No separate .func() registration is required.
When explicit conversion is required. VillageSQL implicitly converts a string
literal to a custom type on direct column assignment, so
INSERT INTO t (val) VALUES ('(1.0,2.0)') works without an explicit call. But
expressions that resolve to STRING type — CASE expressions, CONCAT, and
similar — are not implicitly coerced. Wrap them with TYPE::from_string:
UPDATE my_tableSET val = MYTYPE::from_string( CASE (pk MOD 2) WHEN 0 THEN '(1.0,2.0)' ELSE '(0.0,0.0)' END);
If you are using the function-pointer API instead, register conversion functions
explicitly:
Here’s a complete example implementing a COMPLEX number type:
#include <cstdio>#include <cstring>// Encode complex number from "(real,imag)" to 16 bytesbool encode_complex(std::string_view from, vsql::Span<unsigned char> buf, size_t *length) { if (buf.size() < 16) return true; double real, imag; if (sscanf(from.data(), "(%lf,%lf)", &real, &imag) != 2) return true; memcpy(buf.data(), &real, 8); memcpy(buf.data() + 8, &imag, 8); *length = 16; return false;}// Decode 16 bytes to "(real,imag)" stringbool decode_complex(vsql::Span<const unsigned char> data, vsql::Span<char> out, size_t *out_len) { if (data.size() < 16) return true; double real, imag; memcpy(&real, data.data(), 8); memcpy(&imag, data.data() + 8, 8); int len = snprintf(out.data(), out.size(), "(%.6f,%.6f)", real, imag); if (len < 0 || len >= (int)out.size()) return true; *out_len = len; return false;}// Compare for ORDER BY: compare real part first, then imaginaryint compare_complex(vsql::Span<const unsigned char> a, vsql::Span<const unsigned char> b) { if (a.size() < 16 || b.size() < 16) return 0; double a_real, a_imag, b_real, b_imag; memcpy(&a_real, a.data(), 8); memcpy(&a_imag, a.data() + 8, 8); memcpy(&b_real, b.data(), 8); memcpy(&b_imag, b.data() + 8, 8); if (a_real < b_real) return -1; if (a_real > b_real) return 1; if (a_imag < b_imag) return -1; if (a_imag > b_imag) return 1; return 0;}
After defining these operations, users can create tables with your custom type:
CREATE TABLE signals ( id INT PRIMARY KEY, impedance COMPLEX, frequency_response COMPLEX);INSERT INTO signals VALUES (1, '(50.0,10.0)', '(0.95,0.31)');-- ORDER BY works because we provided compare_complex!SELECT * FROM signals ORDER BY impedance;-- Prepared statements work with custom typesPREPARE stmt FROM 'SELECT * FROM signals WHERE impedance = ?';SET @val = '(50.0,10.0)';EXECUTE stmt USING @val;-- Aggregate operations work with custom typesSELECT COUNT(DISTINCT impedance), MIN(impedance), MAX(impedance), GROUP_CONCAT(impedance ORDER BY impedance) FROM signals;
VDFs can be used in generated column expressions. The VDF must be declared .deterministic() in the extension builder — the server blocks non-deterministic functions in this context.
CREATE TABLE signals ( id INT PRIMARY KEY, impedance COMPLEX, -- Generated column computed by a VDF magnitude DOUBLE GENERATED ALWAYS AS (complex_abs(impedance)) STORED);
complex_abs must be registered with .deterministic(). Traditional MySQL UDFs are not permitted in generated columns.
VDFs can be used in functional index expressions. The same .deterministic()
requirement from generated columns applies here because MySQL implements
functional indexes as hidden generated columns.
CREATE TABLE signals ( id INT PRIMARY KEY, sig COMPLEX, INDEX idx_magnitude ((COMPLEX_ABS(sig))));
The optimizer uses the index when the same VDF expression appears in WHERE,
ORDER BY, or GROUP BY. Cast the comparison value to the VDF’s return type
so the optimizer matches the expression:
SELECT id FROM signals WHERE COMPLEX_ABS(sig) > CAST(20.0 AS DOUBLE);