First, create a new project using cargo new and add the following dependencies:
surrealdbtokio, in order to to use the database inside fn main(). You will most likely want to enable the macros and rt-multi-thread features so that the #[tokio::main] attribute can be used on top of fn main().
The two main ways to connect to SurrealDB when getting started are by connecting to a running instance wia the protocol-ws feature, or by running an embedded instance in memory using the kv-mem feature. Each of these can be added via a feature flag in the SDK.
All together, that leads to the following commands to get started:
cargo new my_project
cd my_project
cargo add surrealdb --features kv-mem,protocol-ws
cargo add tokio --features macros,rt-multi-thread
The examples inside this SDK manual assume that all of these crates and features are present.
To maximize performance when compiling in release mode, it is recommended to use the following profile inside Cargo.toml, the same as the profile used by SurrealDB when building each version for release.
[profile.release]
lto = true
strip = true
opt-level = 3
panic = 'abort'
codegen-units = 1
Start SurrealDB
Before using cargo run to try out your code, make sure that the SurrealDB server is running by using the surreal start command. The following command will start an in-memory server with a single root user at the default address 127.0.0.1:8000.
surreal start --user root --pass secret
If you prefer to do everything through Surrealist, you can also use the Start serving button to do the same as long as you have Surrealist installed locally on your computer.
Connect to SurrealDB
Open src/main.rs and replace everything with the following code to try out some basic operations using the SurrealDB SDK.
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::Surreal;
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;
db.use_ns("test").use_db("test").await?;
let some_queries = db.query("
RETURN 9;
RETURN 10;
SELECT * FROM { is: 'Nice database' };
").await?;
dbg!(some_queries);
Ok(())
}
Note that the .query() method is able to hold more than one statement, in this case three statements; i.e. two RETURN statements and one SELECT statement. The IndexedResults struct returned contains a field called results which holds the output of each statement. Note that each result has its own index. This will become useful when using the .take() method in the example to follow, which can access a result by its index number.
Example output
results: {
0: (
DbResultStats {
execution_time: Some(
43.583µs,
),
query_type: Some(
Other,
),
},
Ok(
Number(
Int(
9,
),
),
),
),
1: (
DbResultStats {
execution_time: Some(
6.5µs,
),
query_type: Some(
Other,
),
},
Ok(
Number(
Int(
10,
),
),
),
),
2: (
DbResultStats {
execution_time: Some(
88.333µs,
),
query_type: Some(
Other,
),
},
Ok(
Array(
Array(
[
Object(
Object(
{
"is": String(
"Nice database",
),
},
),
),
],
),
),
),
),
}
Now that we have the basics down, it is time to try out some other methods like CREATE and UPDATE. The most ergonomic way to do this is to use a struct that implements SurrealValue which allows for both serialization and deserialization between the Rust code and the database.
use surrealdb::Surreal;
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::Resource;
use surrealdb::opt::auth::Root;
use surrealdb_types::{RecordId, SurrealValue, Value};
#[derive(Debug, SurrealValue)]
struct Name {
first: String,
last: String,
}
#[derive(Debug, SurrealValue)]
struct Person {
title: String,
name: Name,
marketing: bool,
}
#[derive(Debug, SurrealValue)]
struct Responsibility {
marketing: bool,
}
#[derive(Debug, SurrealValue)]
struct Record {
id: RecordId,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;
db.use_ns("test").use_db("test").await?;
let created: Option<Record> = db
.create("person")
.content(Person {
title: "Founder & CEO".to_string(),
name: Name {
first: "Tobie".to_string(),
last: "Morgan Hitchcock".to_string(),
},
marketing: true,
})
.await?;
dbg!(created);
db.update(Resource::from(("person", "jaime")))
.merge(Responsibility { marketing: true })
.await?;
let people: Vec<Record> = db.select("person").await?;
dbg!(people);
let mut groups = db
.query("SELECT marketing, count() FROM type::table($table) GROUP BY marketing")
.bind(("table", "person"))
.await?;
dbg!(groups.take::<Value>(0).unwrap());
Ok(())
}
Using a static singleton
A static singleton can be used to ensure that a single database instance is available across very large or complicated applications. With the singleton, only one connection to the database is instantiated, and the database connection does not have to be shared across components or controllers.
The LazyLock struct below has been available in stable Rust since version 1.80, making it usable without a single external crate.
use std::sync::LazyLock;
use surrealdb::Surreal;
use surrealdb::engine::remote::ws::{Client, Ws};
use surrealdb::opt::auth::Root;
use surrealdb_types::{RecordId, SurrealValue};
#[derive(Debug, SurrealValue)]
struct Record {
id: RecordId,
}
#[derive(Debug, SurrealValue)]
struct Person {
name: String,
marketing: bool,
}
static DB: LazyLock<Surreal<Client>> = LazyLock::new(Surreal::init);
async fn upsert_tobie() -> surrealdb::Result<()> {
let tobie: Option<Record> = DB
.upsert(("person", "tobie"))
.content(Person {
name: "Tobie".to_string(),
marketing: true,
})
.await?;
dbg!(tobie);
Ok(())
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
DB.connect::<Ws>("localhost:8000").await?;
DB.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;
DB.use_ns("main").use_db("main").await?;
upsert_tobie().await?;
Ok(())
}
Other ways to see the results
Besides printing out the results inside the Rust code above, you can sign in to the database using the CLI or Surrealist to view them.
surreal sql --user root --pass secret --pretty
Inside Surrealist, do the following:
- Hover over the current connection and click on “Change connection”
- Hover over “New connection” and click the pencil icon
- Change the “Method” to “root”. Enter “root” for the username and “secret” for the password.
- You will now be connected as the root user, and can define and then select the namespace and databases called “main”.
Here is the last query in the example above to get started:
SELECT marketing, count() FROM person GROUP BY marketing;
Using capabilities and experimental capabilities
The Rust SDK has a single Capabilities struct that is used to allow or limit what users are allowed to do using queries. Each method on this struct is used to configure the capabilities for the database in the same way that capabilities flags are passed in to the surreal start command.
use surrealdb::{
Error,
engine::any::connect,
opt::{Config, auth::Root, capabilities::Capabilities},
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let mut capabilities = Capabilities::all();
capabilities.deny_function("http").unwrap();
let config = Config::default().capabilities(capabilities);
let db = connect(("ws://localhost:8000", config)).await?;
db.use_ns("main").use_db("main").await?;
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;
println!(
"{:?}",
db.query("http::get('http://www.surrealdb.com')").await
);
Ok(())
}
The output shows that this function was not allowed to be called.
Ok(IndexedResults { results: {0: (DbResultStats { execution_time: Some(1.703459ms), query_type: Some(Other) }, Err(Error { code: -32000, message: "Access to network target 'www.surrealdb.com:80' is not allowed", details: NotAllowed(Some(Target { name: "www.surrealdb.com:80" })) }))}, live_queries: {} })
SurrealDB also has a number of experimental capabilities which need to be specifically opted into and are not included inside an --allow-all flag or struct created by the Capabilities::all() function. These can be passed in individually using a slice of the ExperimentalFeature enum inside the .with_experimental_features_allowed method, or all at once with .with_all_experimental_features_allowed().
This configuration on the SDK side is not needed if connecting to a remote instance that has the --allow-experimental flag passed in, but is if using an embedded instance like in the example below.
use surrealdb::{
Error,
engine::any::connect,
opt::{Config, capabilities::Capabilities},
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let mut capabilities = Capabilities::all();
capabilities.allow_all_experimental_features();
let config = Config::default().capabilities(capabilities);
let db = connect(("memory", config)).await?;
db.use_ns("main").use_db("main").await?;
println!(
"{:?}",
db.query("DEFINE BUCKET my_bucket BACKEND 'memory'").await?
);
Ok(())
}
Experimenting with an embedded instance
An embedded in-memory instance of SurrealDB does not expose any endpoints. To experiment with such an instance without having to recompile the Rust code, a server can be spun up with a single endpoint that accepts a query. The following example uses Axum to create a localhost:8080/query endpoint that feeds any input into the .query() method, deserializes it into a Value and returns this as a String.
use axum::{Router, routing::post};
use surrealdb_types::{ToSql, Value};
use std::sync::OnceLock;
use surrealdb::{
Surreal,
engine::any::{Any, connect},
};
use tokio::net::TcpListener;
static DB: OnceLock<Surreal<Any>> = OnceLock::new();
pub async fn query(query: String) -> Result<String, String> {
let mut res = DB.get().unwrap().query(query.to_string()).await.map_err(|e| e.to_string())?;
Ok(res.take::<Value>(0).map_err(|e| e.to_string())?.to_sql())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
DB.set(connect("memory").await?).unwrap();
DB.get().unwrap().use_ns("main").use_db("main").await?;
let listener = TcpListener::bind("localhost:8080").await?;
let router = Router::new().route("/query", post(query));
axum::serve(listener, router).await?;
Ok(())
}
Two examples of output:
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d 'CREATE user SET name = "Billy"'
[{ id: user:oktnnywt6mj9f5oka7d6, name: 'Billy' }]
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d 'LET $x: string = 9'
Internal error: Tried to set `$x`,
but couldn't coerce value: Expected `string` but found `9`
For more on web frameworks using the Rust SDK, see the pages on Actix, Axum, and Rocket.
First, create a new project using cargo new and add the following dependencies:
surrealdbtokio, in order to to use the database inside fn main(). You will most likely want to enable the macros and rt-multi-thread features so that the #[tokio::main] attribute can be used on top of fn main().serde with the derive feature enabled in order to use the Serialize and Deserialize attribute macros on top of your Rust data types to match those sent to and returned from the database.
All together, that leads to the following commands to get started:
cargo new my_project
cd my_project
cargo add surrealdb
cargo add tokio --features macros,rt-multi-thread
cargo add serde --features derive
The examples inside this SDK manual assume that all of these crates and features are present.
To maximize performance when compiling in release mode, it is recommended to use the following profile inside Cargo.toml, the same as the profile used by SurrealDB when building each version for release.
[profile.release]
lto = true
strip = true
opt-level = 3
panic = 'abort'
codegen-units = 1
Start SurrealDB
Before using cargo run to try out your code, make sure that the SurrealDB server is running by using the surreal start command. The following command will start an in-memory server with a single root user at the default address 127.0.0.1:8000.
surreal start --user root --pass secret
If you prefer to do everything through Surrealist, you can also use the Start serving button to do the same as long as you have Surrealist installed locally on your computer.
Connect to SurrealDB
Open src/main.rs and replace everything with the following code to try out some basic operations using the SurrealDB SDK.
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::Surreal;
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
db.signin(Root {
username: "root",
password: "secret",
})
.await?;
db.use_ns("test").use_db("test").await?;
let some_queries = db.query("
RETURN 9;
RETURN 10;
SELECT * FROM { is: 'Nice database' };
").await?;
dbg!(some_queries);
Ok(())
}
Note that the .query() method is able to hold more than one statement, in this case three statements; i.e. two RETURN statements and one SELECT statement. The Response struct returned contains a field called results which holds the output of each statement. Note that each result has its own index. This will become useful when using the .take() method in the example to follow, which can access a result by its index number.
Example output
results: {
0: (
Stats {
execution_time: Some(
64.125µs,
),
},
Ok(
Number(
Int(
9,
),
),
),
),
1: (
Stats {
execution_time: Some(
19.791µs,
),
},
Ok(
Number(
Int(
10,
),
),
),
),
2: (
Stats {
execution_time: Some(
97.75µs,
),
},
Ok(
Array(
Array(
[
Object(
Object(
{
"is": Strand(
Strand(
"Nice database",
),
),
},
),
),
],
),
),
),
),
}
Now that we have the basics down, it is time to try out some other methods like CREATE and UPDATE. The most ergonomic way to do this is to use a struct that implements Serialize for anything we want to pass in, and Deserialize for anything we have received from the database and want to turn back into a Rust type.
use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::opt::Resource;
use surrealdb::RecordId;
use surrealdb::Surreal;
use surrealdb::Value;
#[derive(Debug, Serialize)]
struct Name<'a> {
first: &'a str,
last: &'a str,
}
#[derive(Debug, Serialize)]
struct Person<'a> {
title: &'a str,
name: Name<'a>,
marketing: bool,
}
#[derive(Debug, Serialize)]
struct Responsibility {
marketing: bool,
}
#[derive(Debug, Deserialize)]
struct Record {
id: RecordId,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
db.signin(Root {
username: "root",
password: "secret",
})
.await?;
db.use_ns("test").use_db("test").await?;
let created: Option<Record> = db
.create("person")
.content(Person {
title: "Founder & CEO",
name: Name {
first: "Tobie",
last: "Morgan Hitchcock",
},
marketing: true,
})
.await?;
dbg!(created);
db.update(Resource::from(("person", "jaime")))
.merge(Responsibility { marketing: true })
.await?;
let people: Vec<Record> = db.select("person").await?;
dbg!(people);
let mut groups = db
.query("SELECT marketing, count() FROM type::table($table) GROUP BY marketing")
.bind(("table", "person"))
.await?;
dbg!(groups.take::<Value>(0).unwrap());
Ok(())
}
Using a static singleton
A static singleton can be used to ensure that a single database instance is available across very large or complicated applications. With the singleton, only one connection to the database is instantiated, and the database connection does not have to be shared across components or controllers.
The LazyLock struct below has been available in stable Rust since version 1.80, making it usable without a single external crate.
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use surrealdb::engine::remote::ws::{Client, Ws};
use surrealdb::opt::auth::Root;
use surrealdb::RecordId;
use surrealdb::Surreal;
#[derive(Debug, Deserialize)]
struct Record {
id: RecordId,
}
#[derive(Debug, Serialize)]
struct Person<'a> {
name: &'a str,
marketing: bool,
}
static DB: LazyLock<Surreal<Client>> = LazyLock::new(Surreal::init);
async fn upsert_tobie() -> surrealdb::Result<()> {
let tobie: Option<Record> = DB
.upsert(("person", "tobie"))
.content(Person {
name: "Tobie",
marketing: true,
})
.await?;
dbg!(tobie);
Ok(())
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
DB.connect::<Ws>("localhost:8000").await?;
DB.signin(Root {
username: "root",
password: "secret",
})
.await?;
DB.use_ns("test").use_db("test").await?;
upsert_tobie().await?;
Ok(())
}
Other ways to see the results
Besides printing out the results inside the Rust code above, you can sign in to the database using the CLI or Surrealist to view them.
surreal sql --user root --pass secret --namespace test --database test --pretty
Inside Surrealist, do the following:
- Hover over the current connection and click on “Change connection”
- Hover over “New connection” and click the pencil icon
- Change the “Method” to “root”. Enter “root” for the username and “secret” for the password.
- You will now be connected as the root user, and can define and then select the namespace and databases called “test”.
Here is the last query in the example above to get started:
SELECT marketing, count() FROM person GROUP BY marketing;
Using capabilities and experimental capabilities
The Rust SDK has a single Capabilities struct that is used to allow or limit what users are allowed to do using queries. Each method on this struct is used to configure the capabilities for the database in the same way that capabilities flags are passed in to the surreal start command.
surreal start --allow-all --deny-funcs "http"
surreal sql --ns ns --db db
#[tokio::main]
async fn main() -> Result<(), Error> {
let config = Config::default().capabilities(Capabilities::all().with_deny_function("http")?);
let db = connect(("mem://", config)).await?;
db.use_ns("ns").use_db("db").await?;
println!("{:?}", db.query("http::get('http://www.surrealdb.com')").await);
Ok(())
}
SurrealDB also has a number of experimental capabilities which need to be specifically opted into and are not included inside an --allow-all flag or struct created by the Capabilities::all() function. These can be passed in individually using a slice of the ExperimentalFeature enum inside the .with_experimental_features_allowed method, or all at once with .with_all_experimental_features_allowed().
#[tokio::main]
async fn main() -> Result<(), Error> {
let config = Config::default().capabilities(Capabilities::all().with_all_experimental_features_allowed());
let db = connect(("mem://", config)).await?;
db.use_ns("ns").use_db("db").await?;
println!("{:?}", db.query("DEFINE FIELD comics ON person TYPE option<array<record<comic_book>>> REFERENCE"));
Ok(())
}
Experimenting with an embedded instance
An embedded in-memory instance of SurrealDB does not expose any endpoints. To experiment with such an instance without having to recompile the Rust code, a server can be spun up with a single endpoint that accepts a query. The following example uses Axum to create a localhost:8080/query endpoint that feeds any input into the .query() method, deserializes it into a Value and returns this as a String.
use axum::{Router, routing::post};
use std::sync::OnceLock;
use surrealdb::{
Surreal,
engine::any::{Any, connect},
};
use tokio::net::TcpListener;
static DB: OnceLock<Surreal<Any>> = OnceLock::new();
use surrealdb::Value;
pub async fn query(query: String) -> Result<String, String> {
let mut res = DB.get().unwrap().query(query.to_string()).await.map_err(|e| e.to_string())?;
Ok(res.take::<Value>(0).map_err(|e| e.to_string())?.to_string())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
DB.set(connect("memory").await?).unwrap();
DB.get().unwrap().use_ns("test").use_db("test").await?;
let listener = TcpListener::bind("localhost:8080").await?;
let router = Router::new().route("/query", post(query));
axum::serve(listener, router).await?;
Ok(())
}
Two examples of output:
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d 'CREATE user SET name = "Billy"'
[{ id: user:oktnnywt6mj9f5oka7d6, name: 'Billy' }]
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d 'LET $x: string = 9'
Found 9 for param $x, but expected a string
For more on web frameworks using the Rust SDK, see the pages on Actix, Axum, and Rocket.