Skip to main content
This page is the reference for the C++ type operation builders. For the tutorial-level introduction to custom types, see Custom Types in C++. Custom types require three operations the engine calls internally: encode (string to binary), decode (binary to string), and compare. Hash is optional. Implement them against these C++ signatures (all available via <villagesql/vsql.h>):

Fixed-Length Types

Register these operations using 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.
The type name must be a static constexpr const char[] variable — a string literal cannot be used as a non-type template parameter. Passing "MYTYPE" directly produces a compiler error like:
Declare the name as a named array as shown below.
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 a NOT 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 .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:
For fixed-length types, the default string must encode to exactly persisted_length bytes. Set an explicit default for any type where empty string is not a valid input.
String literal: .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>):
Breaking change: IntrinsicDefaultFunc and IntrinsicDefaultWithParamsFunc return std::string instead of const char*. Update any existing intrinsic default implementations to return std::string directly.
Return a 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 inverse to_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.
Register .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>().
.max_persisted_length() requires VEF Protocol 3 or higher. A type using it cannot be loaded by pre-Protocol-3 servers.
The parameterized variants — 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:
  • IntegerMYTYPE(N). The server routes N through int_to_params to build the parameter map. Requires .int_to_params<>().
  • StringMYTYPE('key=value,...'). The server normalizes the string and calls resolve_params directly; int_to_params is not involved. Available whenever .resolve_params<>() is registered — no extra builder call.
A type that registers only .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):
The rewritten map becomes the canonical parameter string the server persists and 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_type() raises the type’s required protocol to VEF Protocol 4. The server reads the variable_length flag only at protocol 4 or higher. Build against the opt-in dev ABI headers (-DVSQL_USE_DEV_ABI=ON); older servers do not read the 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 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:
Otherwise the type fails to initialize the first time a 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 in DECLARE variable declarations. The server resolves the custom type at routine execution time using the installed extension’s type metadata.

See Also