Extend MySQL Using Rust
VillageSQL launched with C++ as the language for writing extensions — C++ is familiar territory for MySQL developers. Rust has become a go-to language for systems and infrastructure developers: the same people who build database connectors, storage engines, and performance-critical services. VillageSQL's new Rust SDK adds it as a first-class option: write and ship a VillageSQL extension entirely in Rust without touching C++.
The architectural approach is similar to pgrx for PostgreSQL: bind directly to the server’s extension ABI rather than going through a client library. That's what opened PostgreSQL extension development to Rust developers, so it's the model we are following too.
What it looks like
We'll use a rot13 function to walk through the mechanics — ROT-13 shifts each letter 13 places in the alphabet, simple enough to read at a glance. At the end of the post you'll see the same pattern applied to a custom rational number type, which is closer to what you'd actually build.
Here's the complete extension:
use villagesql::{InValue, VdfReturn};
fn rot13_impl(args: &[InValue]) -> VdfReturn {
match args.first() {
Some(InValue::String(s)) => VdfReturn::string(rot13(s)),
Some(InValue::Null) | None => VdfReturn::null(),
_ => VdfReturn::error("rot13: expected a STRING argument"),
}
}
fn rot13(s: &str) -> String {
s.chars()
.map(|c| match c {
'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,
'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,
_ => c,
})
.collect()
}
villagesql::extension! {
funcs: [
villagesql::func!(rot13_impl, "rot13", [villagesql::Type::String] -> villagesql::Type::String),
]
}
The function signature is fn(&[InValue]) -> VdfReturn. InValue is an enum over the SQL types the server passes in — String(&str), Real(f64), Int(i64), Null, or Custom(&[u8]) for custom types. The &str borrows directly from the server's row buffer, no per-row allocation. This means zero per-row allocation overhead. VdfReturn is what you send back — including warning for a soft error that returns NULL and keeps execution going, or error to abort the statement entirely. In a batch query, that distinction is the difference between bad rows returning NULL and the whole operation being killed.
Your function body is safe Rust and that matters. Extensions run inside the database process; a memory error can take the server down with it. Rust's compile-time memory safety means that class of bug can't exist in your extension code. The macro-generated entry points contain the unsafe extern "C" FFI boundary, but that's code the SDK owns and you never write. If your function always produces the same output for the same input, add deterministic: true to the func! call — the optimizer uses this for expression folding and other query rewrites.
Two files round out the project:
Cargo.toml
# Cargo.toml
[package]
name = "vsql_rot13"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
villagesql = "0.0.1"
manifest.json
// manifest.json
{
"name": "vsql_rot13",
"version": "0.1.0",
"description": "ROT-13 encoding for VillageSQL",
"author": "Your Name",
"license": "GPL-2.0"
}
Cargo.toml and manifest.json sit alongside src/lib.rs at the crate root. The server reads manifest.json at install time — extension name, version, and description live there, not in code.
The tooling
Building, installing, and testing an extension all go through a single Cargo subcommand:
cargo vsql package # → dist/vsql_rot13.veb
cargo vsql install # package + copy to $VillageSQL_BUILD_DIR
cargo vsql test # install + run SQL test suite against a real server
cargo vsql test --record # regenerate expected results
It feels like normal Rust development. You don't learn a new build system, write a Makefile, or figure out how to wire a shared library into the server by hand. The .veb archive is the same format C++ extensions use — a flat tar with manifest.json and the compiled shared library. The Rust toolchain produces it; what lands in the server is identical.
cargo vsql test runs your extension's MySQL Test Framework suite against a real, running server. You write SQL tests, not mocks. If your function returns the wrong result for a NULL input, the test catches it before you install anything.
Once installed:
INSTALL EXTENSION 'vsql_rot13';
SELECT rot13('Hello, World!');
-- → 'Uryyb, Jbeyq!'
Custom types
The SDK supports custom SQL types — binary-stored types with their own on-disk layout, encoding, ordering, and hash semantics. That's what you'd use to build VECTOR, INET, or a type-safe domain value your schema can enforce.
You define a type with villagesql::custom_type!:
villagesql::custom_type!(
type_name: "rational",
persisted_length: 16,
max_decode_buffer_length: 42,
encode: rational_encode, // fn(&str) -> Result<Vec<u8>, String>
decode: rational_decode, // fn(&[u8]) -> Result<String, String>
compare: rational_compare, // fn(&[u8], &[u8]) -> std::cmp::Ordering
hash: rational_hash, // fn(&[u8]) -> usize — required for indexed columns
default: "0/1",
)
encode runs at insert time, decode when displaying a value. compare and hash must be consistent — equal values must hash equal. The SDK guarantees b.len() == persisted_length when calling these at runtime. Functions that take a custom type receive InValue::Custom(&[u8]); functions that return one use VdfReturn::Binary(Vec<u8>).
examples/vsql_rational in the SDK repo is a complete working implementation: a rational type stored as a 16-byte (i64 numerator, i64 denominator) pair, normalized to lowest terms, with arithmetic functions for add, subtract, multiply, divide, and conversion to REAL. It's the right starting point if you're building a custom type and want all the pieces together.
Get started
The fastest path from zero to a working extension is cargo generate:
cargo install cargo-generate cargo-vsql
cargo generate gh:villagesql/vsql-extension-template-rust --name vsql_my_extension
That scaffolds a complete project — src/lib.rs with a working passthrough function, manifest.json, an MTR test suite, and a GitHub Actions workflow for fmt, clippy, audit, and test. Swap in your logic and run cargo vsql test against a local server.
The examples in the SDK repo — vsql_rot13 and vsql_rational — are worth reading once you have the structure down. We welcome questions and feedback on Discord.
The SDK is on crates.io: villagesql = "0.0.1". The template is at github.com/villagesql/vsql-extension-template-rust.