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 guide covers writing VDF implementations and running regression tests for VillageSQL extensions. It is the companion to Creating Extensions, which covers the end-to-end build steps.
VEF (Protocol 2) APIs are still evolving. Extension rebuilds may be required when upgrading to a new server version.
If you are contributing to the VillageSQL server itself (not building an extension), see Build from Source, which covers the full server developer workflow including running tests with mysql-test-run.pl directly.
Setting Up Your Environment
To develop and test extensions, you need a built VillageSQL server. Follow the Clone and Build from Source guide to compile the server binaries.
Once you have a build, use the villagesql CLI to manage a local dev server instance. Run all commands from the directory where VillageSQL was installed.
Starting a Local Dev Server
Initialize and start a server instance:
./villagesql init # initialize database and seed bundled extensions
./villagesql start # start the server (default port 3307)
./villagesql status # check the server is running
./villagesql connect # open a mysql shell
./villagesql stop # stop the server
To set a root password on init:
./villagesql init --password
./villagesql start
Pass --dir <path> before any command to manage multiple independent instances, or use --here to create a server directory in the current working directory:
./villagesql --here init
./villagesql --here start
Managing Extension Files
Before installing an extension via SQL, its .veb file must be present on the server. The CLI manages the server’s lib/veb/ directory:
./villagesql veb add /path/to/my_extension.veb # copy a .veb to the server
./villagesql veb ls # list available .veb files
./villagesql veb rm my_extension # remove a .veb file
.veb files placed in lib/veb/ before init are seeded automatically. After adding a file, install the extension via SQL:
INSTALL EXTENSION my_extension;
Writing Extension Functions
Extension functions are written in C++ and registered with VEF. Include a
single header to access the full SDK:
#include <villagesql/vsql.h>
Typed Wrappers (Recommended)
Typed wrappers provide a type-safe interface for VDF parameters and results.
The framework detects wrapper types in your function signature and adapts
automatically — the make_func registration syntax is unchanged.
Input wrappers: IntArg, RealArg, StringArg, CustomArg — each
provides is_null() and value(). For parameterized custom types,
CustomArgWith<P> adds a params() accessor that returns the cached
parsed params struct (see Parameterized Types).
Result wrappers: IntResult, RealResult, StringResult, CustomResult
— each provides set_null(), warning(msg), and error(msg). Scalar
results also provide set(value). Buffer results provide buffer() and
set_length(len). StringResult additionally provides
set(std::string_view), which copies up to buffer().size() bytes from the
view and sets the length in one call. For parameterized custom types,
CustomResultWith<P> adds a params() accessor.
error(msg) sets the function result to a SQL error with the given message string. Use it for invalid input that should surface as an error to the caller, as opposed to set_null() which silently produces NULL. The message is truncated to fit the server’s internal error buffer if necessary.
Scalar example — add two integers:
using namespace vsql;
void add_impl(IntArg a, IntArg b, IntResult out) {
if (a.is_null() || b.is_null()) { out.set_null(); return; }
out.set(a.value() + b.value());
}
// Registration is unchanged:
make_func<&add_impl>("add").returns(INT).param(INT).param(INT).build();
Binary example — transform a custom type buffer in place:
using namespace vsql;
void rot13_impl(CustomArg in, CustomResult out) {
if (in.is_null()) { out.set_null(); return; }
auto src = in.value(); // vsql::Span<const unsigned char>
auto dst = out.buffer(); // vsql::Span<unsigned char>
for (size_t i = 0; i < src.size(); i++) { dst[i] = transform(src[i]); }
out.set_length(src.size());
}
For StringResult and CustomResult, write into buffer(), then call
set_length() with the number of bytes written. buffer().size() is the
maximum capacity.
You can use different styles across functions in the same extension — each
function’s style is determined by its own signature.
VDF Return Codes
Set result->type (or call the corresponding typed-wrapper method) to
indicate the outcome of a VDF call:
| Code | Value | Typed wrapper method | Behavior |
|---|
VEF_RESULT_VALUE | 0 | set(v) / set_length(n) | Returns the computed value. |
VEF_RESULT_NULL | 1 | set_null() | Returns SQL NULL. |
VEF_RESULT_WARNING | 2 | warning(msg) | Returns NULL for this row and adds a SQL warning. Execution continues. In strict mode (STRICT_TRANS_TABLES), MySQL promotes the warning to an error on INSERT/UPDATE. |
VEF_RESULT_ERROR | 3 | error(msg) | Aborts statement execution immediately. |
Use VEF_RESULT_WARNING for recoverable bad input (e.g., an unparseable
string passed to an encode function). Use VEF_RESULT_ERROR for conditions
that make it unsafe to continue (e.g., corrupt stored data).
Aggregate VDFs
Aggregate VDFs accumulate state across rows within each GROUP BY group and
return a single result per group, like SQL SUM or COUNT. Use
make_aggregate_func<State, &result_fn>("name") to register one. The State
type is the per-group accumulation buffer; prerun and postrun are
auto-generated to allocate and delete it.
The result function must have the signature void(const State&, ResultWrapper)
where ResultWrapper is one of IntResult, RealResult, StringResult,
CustomResult, or CustomResultWith<P>. Call out.set(value) to return a
value or out.set_null() to return SQL NULL.
Both .clear<>() and .accumulate<>() are required. The builder enforces
this at compile time (via build()), and the server validates it again at
INSTALL EXTENSION time — clear resets state, accumulate folds rows, and
the result function reads the final state.
#include <villagesql/vsql.h>
#include <optional>
using namespace vsql;
// State type: nullopt means no non-NULL rows seen yet.
using SumState = std::optional<long long>;
void my_clear(SumState &s) { s = std::nullopt; }
void my_acc(SumState &s, IntArg v) {
if (!v.is_null()) s = s.value_or(0) + v.value();
}
void my_result(const SumState &s, IntResult out) {
if (!s.has_value()) { out.set_null(); return; }
out.set(s.value());
}
// Registration:
// make_aggregate_func<SumState, &my_result>("my_sum")
// .returns(INT)
// .param(INT)
// .clear<&my_clear>()
// .accumulate<&my_acc>()
// .build()
How the builder methods work:
make_aggregate_func<State, &result_fn>() auto-generates prerun and postrun (value-initializes and deletes State).
.clear<&fn>() wraps void(State&) → vef_vdf_clear_func_t
.accumulate<&fn>() wraps void(State&, TypedArgs...) → vef_vdf_accumulate_func_t. TypedArgs are deduced from the function signature (IntArg, StringArg, etc.).
- The
ResultWrapper type (IntResult, RealResult, etc.) is deduced from the result function signature.
For a counter that never returns NULL, use a plain state type:
using CountState = long long;
void count_clear(CountState &s) { s = 0; }
void count_acc(CountState &s, IntArg v) { if (!v.is_null()) s++; }
void count_result(const CountState &s, IntResult out) { out.set(s); }
VEF_GENERATE_REGISTRATION
VEF_GENERATE_REGISTRATION creates an internal _vef_do_register() helper
that performs extension registration but does not define the extern "C" entry
points. Use it when you need to customize vef_register behavior — for
example, to patch descriptors after registration in a test build. For normal
extensions, use VEF_GENERATE_ENTRY_POINTS instead.
VEF_GENERATE_REGISTRATION(
make_extension()
.func(make_func<&my_impl>("my_func").returns(INT).build()))
// Then define your own extern "C" vef_register/vef_unregister that call
// _vef_do_register() and optionally modify the result.
Type Operation Builders
Only needed if your extension defines a custom column type. If you’re writing functions only, skip ahead to Running Regression Tests.
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 defined in vsql/func_builder.h):
Fixed-Length Types
// Encode: string -> binary. Write the encoded bytes via out.buffer() and
// out.set_length(n); call out.set_null() for SQL NULL, out.warning(msg) for
// recoverable bad input, or out.error(msg) to abort the statement. Returning
// without calling any of these surfaces a default warning.
using TypeEncodeFunc = void (*)(std::string_view from, vsql::CustomResult out);
// Decode: binary -> string. Report the outcome by calling
// out.set_length(n), out.set(sv), out.set_null(), out.warning(msg), or
// out.error(msg). If none is called the wrapper falls back to a default
// "failed to decode value" ERROR.
using TypeDecodeFunc = void (*)(vsql::CustomArg in, vsql::StringResult out);
// Compare: returns -1, 0, or 1 (used for ORDER BY and indexes).
using TypeCompareFunc = int (*)(vsql::CustomArg a, vsql::CustomArg b);
// Hash: returns hash code (used for hash joins).
using TypeHashFunc = size_t (*)(vsql::CustomArg in);
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:error: '"MYTYPE"' is not a valid template argument for type 'const char*'
Declare the name as a named array as shown below.
#include <villagesql/vsql.h>
using namespace vsql;
static constexpr const char kMyTypeName[] = "MYTYPE";
constexpr auto MYTYPE =
vsql::make_type<kMyTypeName>()
.persisted_length(8)
.max_decode_buffer_length(64)
.from_string<&my_encode>() // auto: "MYTYPE::from_string"
.to_string<&my_decode>() // auto: "MYTYPE::to_string"
.compare<&my_compare>() // auto: "MYTYPE::compare"
.hash<&my_hash>() // optional, auto: "MYTYPE::hash"
.intrinsic_default_str("0") // string-literal intrinsic default
.build();
VEF_GENERATE_ENTRY_POINTS(
make_extension()
.type(MYTYPE))
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.
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 (defined in vsql/func_builder.h):
Breaking change (baa54d31100): IntrinsicDefaultFunc and
IntrinsicDefaultWithParamsFunc return std::string instead of const char*.
Update any existing intrinsic default implementations to return std::string directly.
// Fixed (no type parameters):
using IntrinsicDefaultFunc = std::string (*)(char *error_msg);
// Parameterized (receives cached parsed params):
template <typename P>
using IntrinsicDefaultWithParamsFunc = std::string (*)(const P &,
char *error_msg);
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.
std::string mytype_default(const MyTypeParams &p, char * /*error_msg*/) {
return /* build string representation based on p */;
}
Parameterized Types
Variable-length 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.
struct MyTypeParams {
int64_t dimension;
static MyTypeParams parse(const std::map<std::string, std::string> &p) {
return {.dimension = stoll(p.at("dimension"))};
}
static void to_strings(const MyTypeParams &p,
std::map<std::string, std::string> &out) {
out["dimension"] = std::to_string(p.dimension);
}
};
void mytype_encode(vsql::MaybeParams<MyTypeParams> ¶ms,
std::string_view from, vsql::CustomResult out) {
const MyTypeParams &p = params.value(); // is_known() is always true at runtime
size_t bytes = (size_t)p.dimension * 4;
auto buf = out.buffer();
if (buf.size() < bytes) { out.error("MYTYPE: buffer too small"); return; }
// ... parse from, write to buf ...
out.set_length(bytes);
}
void mytype_decode(vsql::CustomArgWith<MyTypeParams> in,
vsql::StringResult out) {
const MyTypeParams &p = in.params();
// ... read p.dimension floats from in.value(), write to out.buffer() ...
out.set_length(bytes_written);
}
int mytype_compare(vsql::CustomArgWith<MyTypeParams> a,
vsql::CustomArgWith<MyTypeParams> b) {
// Returns -1, 0, or 1.
}
size_t mytype_hash(vsql::CustomArgWith<MyTypeParams> in) {
// Returns hash code.
}
// Converts MYTYPE(N) integer syntax to a parameter map.
// Signature: IntToTypeParamsFunc from vsql/func_builder.h.
bool mytype_int_to_params_fn(int64_t value,
std::map<std::string, std::string> ¶ms,
char *error_msg) {
if (value <= 0) {
snprintf(error_msg, VEF_MAX_ERROR_LEN,
"MYTYPE: dimension must be a positive integer");
return true;
}
params["dimension"] = std::to_string(value);
return false; // success
}
// Validates parameters and computes storage sizes.
// Signature: ResolveTypeParamsFunc from vsql/func_builder.h.
bool mytype_resolve_params_fn(const std::map<std::string, std::string> ¶ms,
vsql::ResolvedTypeParams *result,
char *error_msg) {
int64_t dim = std::stoll(params.at("dimension"));
result->persisted_length = dim * 4;
result->max_decode_buffer_length = 64;
return false; // success
}
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>().
static constexpr const char kMyTypeName[] = "MYTYPE";
// Maximum valid dimension for MYTYPE.
constexpr int64_t kMyTypeMaxDimension = 1024; // your max valid dimension
// Upper bound on MYTYPE's persisted byte size across all valid params.
constexpr int64_t kMyTypeMaxPersistedLength = kMyTypeMaxDimension * 4;
constexpr auto MYTYPE =
vsql::make_type<kMyTypeName>()
.persisted_length(-1)
.max_decode_buffer_length(16)
.max_persisted_length(kMyTypeMaxPersistedLength)
.params<MyTypeParams, &MyTypeParams::parse, &MyTypeParams::to_strings>()
.int_to_params<&mytype_int_to_params_fn>()
.resolve_params<&mytype_resolve_params_fn>()
.from_string<&mytype_encode>()
.to_string<&mytype_decode>()
.compare<&mytype_compare>()
.intrinsic_default_vdf("mytype_intrinsic_default")
.build();
using namespace vsql;
VEF_GENERATE_ENTRY_POINTS(
make_extension()
.type(MYTYPE)
.func(make_intrinsic_default<&mytype_default>(
"mytype_intrinsic_default")))
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 defined in
vsql/func_builder.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&.
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.
DELIMITER //
CREATE PROCEDURE insert_complex(IN val COMPLEX)
BEGIN
DECLARE tmp COMPLEX;
SET tmp = val;
INSERT INTO t1 VALUES (tmp);
END //
DELIMITER ;
Extension System Variables
Extensions can declare MySQL component system variables so that users configure
extension behavior through standard SET / SELECT @@ SQL syntax. System
variables are exposed through the preview capability pattern: build a
SysVarCapability from a list of descriptors and attach it to the extension
with .with(...). The server registers the variables on the extension’s
behalf at load time and unregisters them at unload time.
Extension system variables are a preview capability — under active
development and subject to change before the beta release. See
Preview Capabilities
for the full reference.
Declaring Variables
Include <villagesql/preview/sys_var.h> and build a capability using the
type-specific factories (make_bool, make_int, make_str). Each factory
takes the variable name, a comment shown in metadata, a pointer to a global
in the extension’s .so, a default value, and — for numeric types — min/max
bounds.
#include <villagesql/vsql.h>
#include <villagesql/preview/sys_var.h>
namespace sv = vsql::preview_sys_var;
static bool g_enabled = true;
static long long g_threshold = 1000;
static char *g_log_file = nullptr;
static auto SYS_VARS = sv::make_capability({
sv::make_bool("enabled", "Enable feature", &g_enabled, true),
sv::make_int ("threshold_ms", "Threshold in ms", &g_threshold, 1000, 0, 3600000),
sv::make_str ("log_file", "Log file path", &g_log_file, "/tmp/myext.log"),
});
VEF_GENERATE_ENTRY_POINTS(
make_extension()
.func(make_func<&some_impl>("some_func").returns(INT).build())
.with(SYS_VARS))
The factory signatures are:
| Factory | Storage type | Extra parameters |
|---|
sv::make_bool | bool * | def_val |
sv::make_int | long long * | def_val, min_val, max_val |
sv::make_str | char ** | def_val |
The capability must have static storage duration. MySQL writes directly to
the storage pointers when the user sets the variable.
On-Change Callbacks
To react when a variable changes, chain .on_change<&fn>() on the descriptor.
The callback receives a sv::SysVarChange with var_name() and typed
accessors (as_int(), as_real(), as_str()).
void on_threshold_change(sv::SysVarChange c) {
// c.var_name() == "threshold_ms"
// c.as_int().value() is the new value
}
static auto SYS_VARS = sv::make_capability({
sv::make_int("threshold_ms", "Threshold", &g_threshold, 1000, 0, 3600000)
.on_change<&on_threshold_change>(),
});
Accessing Variables in SQL
After INSTALL EXTENSION my_ext, variables are accessible using the extension
name as a component prefix:
SELECT @@global.my_ext.threshold_ms;
SET GLOBAL my_ext.threshold_ms = 500;
SET GLOBAL my_ext.log_file = '/var/log/myext.log';
Reading and Writing from Extension Code
For integer and boolean variables, read the global storage pointer
directly — MySQL updates those atomically. To update a variable through MySQL
(so locking, range validation, and persistence are handled by the server),
call SYS_VARS.set(extension_name, var_name, scope, value). Both set
and get return false on success and true on error.
void write_threshold_impl(IntArg value, IntResult out) {
if (value.is_null()) { out.set(1); return; }
bool err = SYS_VARS.set("my_ext", "threshold_ms", nullptr, value.value());
out.set(err ? 1 : 0);
}
The scope argument controls persistence:
| Scope | Behavior |
|---|
nullptr | Update running value only (GLOBAL), not persisted. |
"PERSIST" | Update running value and write to mysqld-auto.cnf. |
"PERSIST_ONLY" | Write to mysqld-auto.cnf only; takes effect on next restart. |
Extension Status Variables
Extensions can expose read-only counters and gauges as MySQL status variables,
visible in SHOW STATUS / SHOW GLOBAL STATUS and
performance_schema.global_status. The extension owns the storage and writes
to it; the server reads it at query time.
Extension status variables are a preview capability — under active
development and subject to change before the beta release. See
Preview Capabilities
for the full reference.
Include <villagesql/preview/status_var.h> and build a capability with
vsql::preview_status_var::make_capability(). Pass make_int(name, ptr) for
long long * counters or make_double(name, ptr) for double * gauges. The
storage pointers must remain valid for the lifetime of the extension.
#include <villagesql/vsql.h>
#include <villagesql/preview/status_var.h>
using namespace vsql;
namespace sv = vsql::preview_status_var;
static long long g_add_calls = 0;
void add_impl(IntArg a, IntArg b, IntResult out) {
g_add_calls++;
if (a.is_null() || b.is_null()) { out.set_null(); return; }
out.set(a.value() + b.value());
}
static auto STATUS_VARS = sv::make_capability({
sv::make_int("add_calls", &g_add_calls),
});
VEF_GENERATE_ENTRY_POINTS(
make_extension()
.func(make_func<&add_impl>("add")
.returns(INT).param(INT).param(INT).build())
.with(STATUS_VARS))
After INSTALL EXTENSION my_ext, the variable is visible with the extension
name as a prefix:
SHOW GLOBAL STATUS LIKE 'my_ext%';
Variable_name Value
my_ext.add_calls 0
Concurrent increments from multiple query threads using a non-atomic ++ may
occasionally be lost; this is acceptable for an approximate call counter
exposed via SHOW STATUS.
Keyring Access
Keyring access is a preview capability — see
Preview Capabilities
for the full API reference, result codes, and a complete example.
INFORMATION_SCHEMA.EXTENSION_REGISTRATION exposes the in-memory VEF
registration struct for each loaded extension as a JSON document. Use it to
verify that the server parsed your extension’s functions, types, and system
variables correctly after INSTALL EXTENSION.
SELECT EXTENSION_NAME, NEGOTIATED_PROTOCOL, REGISTRATION_JSON
FROM INFORMATION_SCHEMA.EXTENSION_REGISTRATION
WHERE EXTENSION_NAME = 'my_ext';
| Column | Type | Description |
|---|
EXTENSION_NAME | VARCHAR(64) | Name of the installed extension. |
NEGOTIATED_PROTOCOL | BIGINT UNSIGNED | VEF protocol version negotiated between the extension and the server. |
REGISTRATION_JSON | TEXT | JSON serialization of the vef_registration_t struct, including funcs and types arrays. |
Running Regression Tests
Run extension regression tests using the MySQL Test Runner from your VillageSQL build directory.
Running the Full Suite
To run all tests for your extension:
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test --parallel=auto
Running Individual Tests
To run a single test case, specify the suite path and test name:
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test my_test_name
Creating New Tests
When adding new features or fixing bugs, you should add corresponding regression tests.
Test Location
Tests are located in mysql-test/suite/villagesql/. Choose the appropriate sub-directory based on the SQL statement or feature being tested.
- Test files end with
.test and go in the t/ directory.
- Expected result files end with
.result and go in the r/ directory.
Test File Conventions
VillageSQL tests often involve the custom COMPLEX type. We follow a specific pattern to ensure tests are skipped gracefully if the feature is not yet fully implemented:
# Description of the test
# Grammar: (Optional) SQL syntax reference
--source include/villagesql/not_implemented_complex_type.inc
--source include/villagesql/install_complex_extension.inc
# ... Your Test Code Here ...
CREATE TABLE t1 (val COMPLEX);
INSERT INTO t1 VALUES ('(1.0,2.0)');
SELECT * FROM t1;
DROP TABLE t1;
--source include/villagesql/uninstall_complex_extension.inc
Steps to Add a Test
- Create the
.test file in the appropriate t/ directory (e.g., mysql-test/suite/villagesql/select/t/my_new_test.test).
- Create an empty
.result file in the corresponding r/ directory (e.g., mysql-test/suite/villagesql/select/r/my_new_test.result).
- Run the test with
--record to generate the expected output:
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --suite=/path/to/your/extension/test --record my_new_test
- Verify the output in the generated
.result file to ensure it matches your expectations.
Debugging Tests
If a test fails, the test framework provides detailed logs.
- Test output: Check
mysql-test/var/log/my_new_test.log.
- Server error log: Check
mysql-test/var/log/mysqld.1.err.
- Diff: The framework outputs a diff between the actual output and the expected
.result file.
To run a test with extra debug information:
cd $BUILD_HOME
./mysql-test/mysql-test-run.pl --verbose --suite=/path/to/your/extension/test my_new_test
See Also