This tutorial turns ordinary Rust code into a SurrealDB extension, from surreal ext init through to calling its functions from SurrealQL. Extensions were called modules before SurrealDB 4.0, and the earlier commands and statements are shown where they differ.
Getting started: regular Rust code
Starting a Surrealism project is best done using the surreal ext init command, which is similar to cargo new when starting a project in Rust. The Cargo.toml file will include the following line to instruct cargo to generate a .wasm file instead of the standard .rlib file when compiling Rust.
[lib]
crate-type = [ "cdylib" ]The command will also ask for the project and organisation name, which will lead to the following package.toml file. Before SurrealDB 4.0 the file was called surrealism.toml and held abi = 2.
target = "rust"
abi = 5
[package]
organisation = "my_organisation"
name = "surrealism_fun"
version = "1.0.0"
[capabilities]
allow_scripting = false
allow_arbitrary_queries = false
allow_write = false
allow_functions = []
allow_net = []
strict_timeout = true
[attach]You'll also notice a few sample functions inside lib.rs. To internalise how Surrealism works, let's delete them and begin with three of our own functions that we would like to call using SurrealQL.
One is a simple function that returns a bool.
fn can_drive(age: i64) -> bool {
age >= 18
}The second returns a Result.
fn parse_number(input: String) -> Result<i64, ParseIntError> {
input.parse::<i64>()
}And the third returns a User struct via a function called random_user() which uses the fake crate to create a user with a random English first name, French middle name, and German last name. This struct is annotated with the SurrealValue trait, allowing its return value to be understood on the SurrealDB side.
#[derive(Debug, SurrealValue)]
pub struct User {
first_name: String,
middle_name: String,
last_name: String,
age: i32
}
pub fn random_user() -> User {
User {
first_name: FirstName(EN).fake(),
middle_name: FirstName(FR_FR).fake(),
last_name: LastName(DE_DE).fake(),
age: random_range(10..=50)
}
}Here is all of the code.
use fake::faker::name::raw::*;
use fake::{Fake, locales::*};
use rand::random_range;
use surrealdb_types::SurrealValue;
use std::num::ParseIntError;
#[derive(Debug, SurrealValue)]
pub struct User {
first_name: String,
middle_name: String,
last_name: String,
age: i32
}
fn can_drive(age: i64) -> bool {
age >= 18
}
fn parse_number(input: String) -> Result<i64, ParseIntError> {
input.parse::<i64>()
}
pub fn random_user() -> User {
User {
first_name: FirstName(EN).fake(),
middle_name: FirstName(FR_FR).fake(),
last_name: LastName(DE_DE).fake(),
age: random_range(10..=50)
}
}Annotating functions
The functions that we want to call from inside SurrealQL can now be exposed by adding the #[surrealism] annotation.
use fake::faker::name::raw::*;
use fake::{Fake, locales::*};
use rand::random_range;
use surrealdb_types::SurrealValue;
use surrealism::surrealism;
use std::num::ParseIntError;
#[derive(Debug, SurrealValue)]
pub struct User {
first_name: String,
middle_name: String,
last_name: String,
age: i32
}
#[surrealism]
fn can_drive(age: i64) -> bool {
age >= 18
}
#[surrealism]
fn parse_number(input: String) -> Result<i64, ParseIntError> {
input.parse::<i64>()
}
#[surrealism]
pub fn random_user() -> User {
User {
first_name: FirstName(EN).fake(),
middle_name: FirstName(FR_FR).fake(),
last_name: LastName(DE_DE).fake(),
age: random_range(10..=50)
}
}With this done, the Rust code can now be compiled with the extension build command. The --out flag sets the output file name or path, such as demo.surli; without it, the archive is named after the organisation, name and version in package.toml. If you are in a separate folder, follow this with the path to the Rust code with functions to expose.
surreal ext build --out demo.surli /Users/my_name/my_rust_codeFor faster local iteration:
surreal ext build --debug --out demo.surli /Users/my_name/my_rust_codeThe file will now be compiled at either your current location or the one specified like in the command above: /Users/my_name/my_rust_code.
That's the extension archive that SurrealDB can point to!
Calling the Surrealism file from SurrealDB
The Surrealism file is ready to be used. All that is left now is to start a database with two environment variables. They are:
SURREAL_CAPS_ALLOW_EXPERIMENTAL=surrealism, which allows Surrealism extensions to be used in the first place. The archive is read from a bucket, which aDEFINE BUCKETstatement sets up.SURREAL_BUCKET_FOLDER_ALLOWLIST="/Users/my_name/my_rust_code/", which tells the database that it's okay to access this folder when using the files feature.
Now it's time to start the database with the surreal db start command and these two env vars.
SURREAL_CAPS_ALLOW_EXPERIMENTAL=surrealism SURREAL_BUCKET_FOLDER_ALLOWLIST="/Users/my_name/my_rust_code/" surreal db start --user root --pass secretYou can then connect through SurrealDB Studio or the CLI with the surreal db sql command:
SURREAL_CAPS_ALLOW_EXPERIMENTAL=surrealism SURREAL_BUCKET_FOLDER_ALLOWLIST="/Users/my_name/my_rust_code/" surreal db sql --user root --pass secretWe're almost there! Only two statements left and we can start accessing these functions.
The first is a DEFINE BUCKET statement to create a bucket called test, linked to the folder that we have allowed the database to access. The "file:/" prefix in this statement tells the DEFINE BUCKET statement that this path is to a file, not storage in memory.
DEFINE BUCKET test BACKEND "file:/users/my_name/my_rust_code";Now that we have a bucket called test, the demo.surli file will be available to us at the f"test:/demo.surli" path. The f there is an instruction to treat the string as a file path instead of just a regular string.
Then we can define an extension, giving it the name test and connecting it to the file. The statement changed in 3.3 and again in 4.0, so pick the tab that matches your version.
UNSIGNED says that the archive is loaded without checking a signature, which suits an extension you built yourself. A signed archive takes SIGNER instead, as described in SIGNER and UNSIGNED.
DEFINE EXTENSION ext::test FROM f"test:/demo.surli" UNSIGNED;The functions are now ready to call. The parse_number() function is available at the ext::test::parse_number path. Before SurrealDB 4.0, the path starts with mod:: instead.
ext::test::parse_number("10");
//- 10
ext::test::parse_number("Hi I'm number");
//- 'Thrown error: WASM function returned error: invalid digit found in string'Next, we can use random_user() to create some random users.
CREATE user CONTENT ext::test::random_user();
//- [{ age: 18, first_name: 'Thomas', id: user:hr8ohmn36zrpv3zthhnf, last_name: 'Meier', middle_name: 'Ninon' }]
CREATE user CONTENT ext::test::random_user();
//- [{ age: 13, first_name: 'Verda', id: user:zg0ucdjizdp71fzq9syc, last_name: 'Schuster', middle_name: 'Clarisse' }]
CREATE user CONTENT ext::test::random_user();
//- [{ age: 45, first_name: 'Zelda', id: user:rulp2bf82twrh94ifhsh, last_name: 'Berger', middle_name: 'Noah' }]That leaves us with one function left to try out, the can_drive() function.
SELECT
first_name + ' ' + middle_name + ' ' + last_name AS name,
ext::test::can_drive(age) AS can_drive
FROM user;Two of them can drive, but not Verda Clarisse Schuster who is far too young.
[
{ can_drive: true, name: 'Thomas Ninon Meier' },
{ can_drive: true, name: 'Zelda Noah Berger' },
{ can_drive: false, name: 'Verda Clarisse Schuster' }
]