> ## 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.

# Preview Capabilities in Rust

> Use VillageSQL preview capabilities from the Rust SDK — declare status variables, system variables, background thread workers, SQL queries, and keyring access with the requires: list in extension!.

> [!WARNING]
>
> Preview capability APIs are not stable. An extension built against a preview
> capability may fail to load after a server update.

Preview capabilities are server features exposed to extensions before their
APIs are finalized. The Rust SDK wraps five of them: status variables, system
variables, background thread workers, SQL queries from a worker thread, and
keyring access. This page covers
declaring and using each one from Rust. For the concept, the preview tier,
and the capabilities that exist only in C++, see
[Preview Capabilities](https://villagesql.com/docs/mysql-8.4/0.0.6/preview-capabilities.md).

## Prerequisites

An extension that uses any preview capability installs only when
`vsql_allow_preview_extensions` is `ON`:

```sql theme={null}
SET PERSIST vsql_allow_preview_extensions = ON;
```

See [Enabling the Preview Tier](https://villagesql.com/docs/mysql-8.4/0.0.6/preview-capabilities.md#enabling-the-preview-tier)
for the details, including why `SET GLOBAL` is rejected for this variable.

You also need a working Rust extension setup — the
[Creating Extensions in Rust](https://villagesql.com/docs/mysql-8.4/0.0.6/rust-sdk.md) walkthrough covers the
toolchain, `cargo-vsql`, and your first `extension!` block.

## What the Rust SDK Wraps

| Capability | Rust module | SDK example |
| - | - | - |
| `vsql::status_var` | `villagesql::preview::status_var` | [`vsql_status_var`](https://github.com/villagesql/vsql-rust-sdk/tree/main/examples/vsql_status_var) |
| `vsql::sys_var` | `villagesql::preview::sys_var` | [`vsql_sys_var`](https://github.com/villagesql/vsql-rust-sdk/tree/main/examples/vsql_sys_var) |
| `vsql::preview::thread_worker` | `villagesql::preview::thread_worker` | [`vsql_thread_worker`](https://github.com/villagesql/vsql-rust-sdk/tree/main/examples/vsql_thread_worker) |
| `vsql::preview::sql_query` | `villagesql::preview::sql_query` | [`vsql_sql_query`](https://github.com/villagesql/vsql-rust-sdk/tree/main/examples/vsql_sql_query) |
| `vsql::preview::keyring` | `villagesql::preview::keyring` | [`vsql_keyring`](https://github.com/villagesql/vsql-rust-sdk/tree/main/examples/vsql_keyring) |

The remaining preview capabilities — `auth`, `mysql_services`,
`statement_event`, and the column storage ABI — are C++-only today. Use the
[C++ SDK](https://villagesql.com/docs/mysql-8.4/0.0.6/preview-capabilities.md) if you need one of those.

## Registration Pattern

Declare each capability as a `static`, then list it by reference in the
`requires:` section of `extension!` — the Rust equivalent of the C++ SDK's
`.with()`:

```rust theme={null}
use villagesql::preview::keyring::KeyringCapability;

static KEYRING: KeyringCapability = KeyringCapability::new();

villagesql::extension! {
    funcs: [
        // ...
    ],
    requires: [
        &KEYRING,
    ]
}
```

The server populates the capability object at load time; before that, its
accessor methods report the capability as unavailable rather than crashing.
The `static` is required — the server holds pointers into the capability for
the extension's whole lifetime.

## Status Variables

The `status_var` capability exposes extension-owned counters through
`SHOW GLOBAL STATUS`. Your extension owns the storage as `'static` atomics
and writes to them; the server reads through the pointers each time the
status variable is queried.

Declare each variable as a `StatusVarSpec` — `Int` backed by an
`AtomicI64`, or `Double` backed by the SDK's `AtomicF64` (Rust's standard
library has no atomic `f64`, so the SDK provides one with `new`, `load`, and
`store`):

```rust theme={null}
use std::sync::atomic::{AtomicI64, Ordering};

use villagesql::preview::status_var::{AtomicF64, StatusVarCapability, StatusVarSpec};
use villagesql::{InValue, VdfReturn};

static REQUESTS: AtomicI64 = AtomicI64::new(0);
static LOAD: AtomicF64 = AtomicF64::new(0.5);

static SPECS: &[StatusVarSpec] = &[
    StatusVarSpec::Int {
        name: c"requests",
        value: &REQUESTS,
    },
    StatusVarSpec::Double {
        name: c"load",
        value: &LOAD,
    },
];

static STATUS_VAR: StatusVarCapability = StatusVarCapability::new(SPECS);

fn bump_impl(_args: &[InValue]) -> VdfReturn {
    let n = REQUESTS.fetch_add(1, Ordering::Relaxed) + 1;
    VdfReturn::int(n)
}

villagesql::extension! {
    funcs: [
        villagesql::func!(bump_impl, "bump", [] -> villagesql::Type::Int),
    ],
    requires: [
        &STATUS_VAR,
    ]
}
```

After `INSTALL EXTENSION`, the variables appear with the extension name as a
prefix:

```sql theme={null}
SELECT vsql_status_var.bump();
-- 1
SHOW GLOBAL STATUS LIKE 'vsql_status_var%';
```

```
Variable_name	Value
vsql_status_var.load	0.500000
vsql_status_var.requests	1
```

## System Variables

The `sys_var` capability registers MySQL system variables owned by your
extension. Three types are supported: `Bool`, `Int` (with `min`/`max`
bounds), and `Str`. Every spec carries a name, a comment shown in
`SHOW VARIABLES` metadata, a default, and an optional `on_change` callback.

Names and string defaults are `&'static CStr` values — write them as C-string
literals (`c"enabled"`):

```rust theme={null}
use villagesql::preview::sys_var::{SysVarCapability, SysVarSpec};

static SPECS: &[SysVarSpec] = &[
    SysVarSpec::Bool {
        name: c"enabled",
        comment: c"Enable the feature",
        default: true,
        on_change: None,
    },
    SysVarSpec::Int {
        name: c"threshold",
        comment: c"Threshold in milliseconds",
        default: 1000,
        min: 0,
        max: 60000,
        on_change: None,
    },
    SysVarSpec::Str {
        name: c"log_path",
        comment: c"Path to the log file",
        default: c"/tmp/vsql_sys_var.log",
        on_change: None,
    },
];

static SYS_VAR: SysVarCapability = SysVarCapability::new(SPECS);

villagesql::extension! {
    funcs: [],
    requires: [
        &SYS_VAR,
    ]
}
```

The `funcs:` section must be present before `requires:`, even when empty.

After install, the variables are addressable with the extension name as a
prefix:

```sql theme={null}
SELECT @@global.vsql_sys_var.enabled;
-- 1
SELECT @@global.vsql_sys_var.threshold;
-- 1000
SET GLOBAL vsql_sys_var.enabled = 0;
```

### Reacting to Changes

`on_change` is a raw C callback, invoked by the server after a variable is
set. The server calls it while it holds its global system-variable lock, so
keep it quick, and don't panic — a panic here crosses the FFI boundary. The
callback receives a `*const vef_sys_var_change_t` from the raw ABI layer:

```rust theme={null}
use std::sync::atomic::{AtomicU64, Ordering};
use villagesql::sys::vef_sys_var_change_t;

static CHANGE_COUNT: AtomicU64 = AtomicU64::new(0);

unsafe extern "C" fn on_enabled_change(_change: *const vef_sys_var_change_t) {
    CHANGE_COUNT.fetch_add(1, Ordering::Relaxed);
}
```

> [!WARNING]
>
> Calling `SysVarCapability::get` or `SysVarCapability::set`, running SQL, or
> waiting on a thread that does either deadlocks on that lock. Keep the callback
> to bookkeeping in your own statics, as above, and hand work that needs SQL to a
> [thread worker](#thread-worker).

Wire it into a spec with `on_change: Some(on_enabled_change)`.

### Reading and Writing from Extension Code

`SysVarCapability` also exposes `get()` and `set()` for programmatic access
through the server (so range validation and persistence are handled for
you). `set()` takes a `scope` argument that selects persistence: `null`
changes only the running value, so it reverts on restart; `"PERSIST"` changes
the running value and writes it to the persisted config; `"PERSIST_ONLY"`
writes to the persisted config without touching the running value, so it
applies on the next restart. Both are `unsafe` FFI methods taking
NUL-terminated C strings, and
both use the inverted C convention: `Some(false)` means success, `Some(true)`
means the server reported an error, and `None` means the capability is
unavailable. On a successful `get`, the server writes a `malloc`'d string
you must release with the C `free()`. The
[`vsql_sys_var` example](https://github.com/villagesql/vsql-rust-sdk/blob/main/examples/vsql_sys_var/src/lib.rs)
shows the full pattern, including the `free` extern and the safety
comments.

## Thread Worker

The `thread_worker` capability runs a function you provide on a
server-managed background thread. The server registers a control system
variable at load time; while it is `ON`, your work function is called on a
periodic timer, on file-descriptor readiness, or on enable/disable
transitions.

Your work function is plain safe Rust:

```rust theme={null}
use std::sync::atomic::{AtomicI64, Ordering};
use std::time::Duration;

use villagesql::preview::thread_worker::{
    NextWakeup, ThreadHandle, ThreadWorkerCapability, WakeupReason,
};
use villagesql::{InValue, VdfReturn};

static TICKS: AtomicI64 = AtomicI64::new(0);

fn worker(reason: WakeupReason, _handle: &ThreadHandle) -> NextWakeup {
    if reason == WakeupReason::Periodic {
        TICKS.fetch_add(1, Ordering::Relaxed);
    }
    NextWakeup::unchanged()
}

static WORKER: ThreadWorkerCapability =
    ThreadWorkerCapability::new(worker, "ticker", Duration::from_millis(100), None);

fn ticks_impl(_args: &[InValue]) -> VdfReturn {
    VdfReturn::int(TICKS.load(Ordering::Relaxed))
}

villagesql::extension! {
    funcs: [
        villagesql::func!(ticks_impl, "ticks", [] -> villagesql::Type::Int),
    ],
    requires: [
        &WORKER,
    ]
}
```

`ThreadWorkerCapability::new` takes the work function, a thread-name suffix,
the initial sleep interval, and an optional control-variable name override.
When the override is `None`, the control variable is named
`{suffix}_enabled` and registered under the extension's prefix:

```sql theme={null}
SHOW GLOBAL VARIABLES LIKE '%ticker%';
```

```
Variable_name	Value
vsql_thread_worker.ticker_enabled	OFF
```

```sql theme={null}
SET GLOBAL vsql_thread_worker.ticker_enabled = ON;
SELECT SLEEP(0.5);
SELECT vsql_thread_worker.ticks();
-- 4  (varies with timing — one tick per 100 ms while enabled)
SET GLOBAL vsql_thread_worker.ticker_enabled = OFF;
```

### Wakeups

`WakeupReason` tells you why the server called: `Enable`, `Periodic`,
`PollFd`, or `Disable`. Your return value adjusts the next wakeup:

* `NextWakeup::unchanged()` — keep the current sleep interval and poll fd.
* `NextWakeup::after(duration)` — wake again after `duration`.
* Set the `poll_fd` field to a file descriptor greater than zero to also
  wake when it becomes readable, or to `-1` to clear a previously set one.

A zero-length `Duration` collapses to "no change" — the underlying C ABI
reserves `0` for that, so an instant wakeup can't be expressed.

If the work function panics, the SDK catches the panic at the FFI boundary
and treats the call as returning `NextWakeup::unchanged()` — the worker
keeps running.

The `ThreadHandle` parameter arrives by reference; pass it to
`SqlQueryCapability::open` in `villagesql::preview::sql_query` to run SQL
from inside the worker.

## Keyring Access

The `keyring` capability reads and writes secrets stored in the MySQL
keyring component — API keys, encryption keys, anything that shouldn't live
in a table. A keyring component (for example `component_keyring_file`) must
be installed on the server; without one, every read and write fails with
`KeyringError::NoComponent`.

```rust theme={null}
use std::ffi::CString;

use villagesql::preview::keyring::KeyringCapability;
use villagesql::{InValue, VdfReturn};

static KEYRING: KeyringCapability = KeyringCapability::new();

const MAX_SECRET_LEN: usize = 1024;

fn keyring_read_impl(args: &[InValue]) -> VdfReturn {
    let Some(&InValue::String(data_id)) = args.first() else {
        return VdfReturn::null();
    };
    let Ok(data_id) = CString::new(data_id) else {
        return VdfReturn::null();
    };

    let mut buf = [0u8; MAX_SECRET_LEN];
    match KEYRING.read(&data_id, None, &mut buf) {
        Ok(Some(n)) => match std::str::from_utf8(&buf[..n]) {
            Ok(s) => VdfReturn::string(s),
            Err(_) => VdfReturn::null(),
        },
        Ok(None) | Err(_) => VdfReturn::null(),
    }
}

villagesql::extension! {
    funcs: [
        villagesql::func!(
            keyring_read_impl, "keyring_read",
            [villagesql::Type::String] -> villagesql::Type::String,
            buffer_size: MAX_SECRET_LEN
        ),
    ],
    requires: [
        &KEYRING,
    ]
}
```

`read(data_id, auth_id, buf)` fills the buffer you pass and returns
`Ok(Some(n))` with the secret's length, or `Ok(None)` when no secret exists
under `data_id` — a normal outcome, not an error. `write(data_id, auth_id,
data)` returns `Ok(())` on success. `auth_id` is the owning user; pass
`None` for internal keys not associated with a specific user.

Both return `Err(KeyringError)` on failure: `CapabilityUnavailable` (the
capability was never wired up), `NoComponent` (no keyring component on the
server), or `Other`.

The keyring has no size probe: a secret larger than the buffer you pass to
`read` comes back as `Ok(None)`, indistinguishable from a missing key. Size
your buffer for the largest secret you expect to store.

## Next Steps

> **[Preview Capabilities (C++)](https://villagesql.com/docs/mysql-8.4/0.0.6/preview-capabilities.md)**
>
> The full capability index, the preview tier, and the C++-only
> capabilities: auth, mysql\_services, statement\_event, and column
> storage.

> **[Rust API Reference](https://villagesql.com/docs/mysql-8.4/0.0.6/rust-api-reference.md)**
>
> InValue, VdfReturn, extension!, func!, and custom\_type! — all fields.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.