Extend MySQL with Rust

deesix1 pts1 comments

Extend MySQL Using Rust

Sign in<br>Subscribe

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 {<br>match args.first() {<br>Some(InValue::String(s)) => VdfReturn::string(rot13(s)),<br>Some(InValue::Null) | None => VdfReturn::null(),<br>_ => VdfReturn::error("rot13: expected a STRING argument"),

fn rot13(s: &str) -> String {<br>s.chars()<br>.map(|c| match c {<br>'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,<br>'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,<br>_ => c,<br>})<br>.collect()

villagesql::extension! {<br>funcs: [<br>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<br>[package]<br>name = "vsql_rot13"<br>version = "0.1.0"<br>edition = "2021"

[lib]<br>crate-type = ["cdylib"]

[dependencies]<br>villagesql = "0.0.1"

manifest.json

// manifest.json<br>"name": "vsql_rot13",<br>"version": "0.1.0",<br>"description": "ROT-13 encoding for VillageSQL",<br>"author": "Your Name",<br>"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<br>cargo vsql install # package + copy to $VillageSQL_BUILD_DIR<br>cargo vsql test # install + run SQL test suite against a real server<br>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';<br>SELECT rot13('Hello, World!');<br>-- → '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!(<br>type_name: "rational",<br>persisted_length: 16,<br>max_decode_buffer_length: 42,<br>encode: rational_encode, // fn(&str) -> Result, String><br>decode: rational_decode, // fn(&[u8]) -> Result<br>compare: rational_compare, // fn(&[u8], &[u8]) -> std::cmp::Ordering<br>hash: rational_hash, // fn(&[u8]) -> usize —...

villagesql rust extension server cargo string

Related Articles