<villagesql/vsql.h>):
Fixed-Length Types
vsql::make_type<kTypeName>(). The type name
is passed as a non-type template parameter (NTTP) — a static constexpr const char[]
array. The builder auto-generates VDF names in the TYPE::method format
(e.g., "MYTYPE::from_string") from this NTTP, so no manual string matching
is required. Pass the built type object to .type() on the extension builder;
separate .func() calls for type operations are not needed.
build() fails at compile time if from_string, to_string, or compare
is missing. Each template method checks the function pointer signature via
static_assert.
Intrinsic Default
When aNOT NULL custom-type column receives NULL under IGNORE mode
(e.g., INSERT IGNORE or UPDATE IGNORE), the server calls the intrinsic
default to produce a fallback value rather than raising an error. The
intrinsic default provides a string representation; the server converts it
to binary using the type’s from_string function.
If you omit both For fixed-length types, the default string must encode to exactly
.intrinsic_default_str() and .intrinsic_default_vdf(),
the server calls from_string("") as a fallback. This happens when the type
is first used (at table creation), not at INSTALL EXTENSION. If your
encode function rejects empty string — or encodes it to the wrong number of
bytes — type initialization fails with an error visible in the SQL client:persisted_length bytes. Set an explicit default for any type where
empty string is not a valid input..intrinsic_default_str()
For a constant default, pass the string directly on the type builder (as
shown in the fixed-length example above with .intrinsic_default_str("0")).
VDF-based: .intrinsic_default_vdf() + make_intrinsic_default
When the default value depends on type parameters, implement a function
against one of these signatures (available via <villagesql/vsql.h>):
std::string representation of the default value. On error,
write a message to error_msg and return any value (the SDK checks
error_msg[0] != '\0' to detect errors). Register with
make_intrinsic_default<&fn>("vdf_name") (one argument: the VDF name) and
reference that name on the type builder with .intrinsic_default_vdf().
The parameterized types example below shows the full registration pattern.
Parameterized Types
Parameterized types need the column’s declared parameters at encode, decode, compare, and hash time to determine allocation sizes and layout. Define a params struct with a parse function and an inverseto_strings
function, register both on the type builder with
.params<P, &ParseFunc, &ToStringsFunc>(), and use const P& as the first
argument of your type operation functions. The SDK caches the parse result
per unique parameter combination, so the parse function runs at most once
per type instantiation. The to_strings function is the inverse of parse:
it writes a typed P back into the canonical key/value string form so the
server can publish inferred params in the same shape parse consumes.
.params<>() on the type builder. Use .int_to_params<&mytype_int_to_params_fn>()
to handle MYTYPE(N) integer syntax and .resolve_params<&mytype_resolve_params_fn>() to
validate parameters and compute storage sizes. Call .max_persisted_length(N) with an
upper bound on the persisted byte size across all valid parameterizations; the server
uses this only on the type parameter inference path, where it has not yet inferred
the params and so cannot consult resolve_params to size the encode buffer.
For a VDF-based intrinsic default, use .intrinsic_default_vdf() with the VDF name and
register the VDF separately via make_intrinsic_default<&mytype_default>().
TypeEncodeWithParamsFunc<P>,
TypeDecodeWithParamsFunc<P>, TypeCompareWithParamsFunc<P>, and
TypeHashWithParamsFunc<P> — together with ParamsToStringsFunc<P>
(void fn(const P&, std::map<std::string,std::string>&)) are available via
<villagesql/vsql.h>.
The vsql::make_type template methods detect the params argument and route
through the params cache automatically. Encode functions take
vsql::MaybeParams<P> & as the first argument; is_known() is always true
at runtime, and value() returns const P&. Decode, compare, and hash
variants take vsql::CustomArgWith<P>, whose params() accessor returns
const P&.
Supplying parameters in SQL. Two syntaxes reach resolve_params:
- Integer —
MYTYPE(N). The server routesNthroughint_to_paramsto build the parameter map. Requires.int_to_params<>(). - String —
MYTYPE('key=value,...'). The server normalizes the string and callsresolve_paramsdirectly;int_to_paramsis not involved. Available whenever.resolve_params<>()is registered — no extra builder call.
.resolve_params<>() accepts the string form and
rejects MYTYPE(N). SHOW CREATE TABLE preserves whichever form was written.
The serialized
key=value,... parameter string that int_to_params produces
and resolve_params consumes is capped at VEF_MAX_TYPE_PARAMS_STRING_LEN
(1024 bytes). A parameterization whose canonical string would exceed that limit
is rejected with a defined error rather than truncated silently — keep the
combined parameter names and values for a single type within 1024 bytes.Rewriting parameters and supplying defaults
resolve_params has a second, mutating overload: it takes the parameter map by
non-const reference so the type can rewrite it — typically to fill defaults the
author omitted. Register it the same way (.resolve_params<&fn>() accepts
either form; register only one):
SHOW CREATE TABLE prints, so the rewrite must be idempotent. A bare
declaration (MYTYPE, no length or params) now calls resolve_params with an
empty map instead of skipping it, so a type that supplies defaults gives every
column explicit params — vsql_bitfield_test’s BITFIELD resolves a bare
column to max_number_of_bits=4096:
Variable-Length Types
A variable-length custom type decides its persisted size per value rather than using a single fixed footprint. Declare one by calling.variable_length_type()
on the type builder, which sets the type’s variable_length flag.
Variable-length types must also call .max_persisted_length(N). build() fails
at compile time if it is omitted — the server needs the upper bound to allocate
a buffer for the backing field.
.variable_length_type() is monotonic: calling it before or after the Protocol 3
setters (max_persisted_length(), params(), int_to_params()) does not lower
the protocol requirement back below Protocol 4.
Like every custom type, a variable-length type must produce a usable
intrinsic default. The default is encoded into the
field’s maximum capacity, and any non-empty result of 1 to
Otherwise the type fails to initialize the first time a
max_persisted_length bytes is accepted. A type whose empty-string encode
produces zero bytes — an empty array or bit set, say — has no usable
default, so declare an explicit one that encodes to a non-empty value:NOT NULL column
references it, at CREATE TABLE — the same as a fixed-length type that cannot
encode from_string("").Custom Types in Stored Procedures
Custom extension types can be used as stored procedure parameter types and inDECLARE variable declarations. The server resolves the custom type at
routine execution time using the installed extension’s type metadata.
See Also
- Custom Types in C++ — tutorial introduction to custom types
- C++ API Reference — VDF contracts, null handling, and buffer sizing
- C++ Development — VDF authoring depth, argument and result types, aggregates, varargs

