SurrealDB is designed to be run in many different ways and in many environments. Because the same query engine runs on every storage engine, SurrealDB can be run in embedded mode, from within a number of different language environments. In Rust, SurrealDB can be run as an in-memory database, it can persist data using a file-based storage engine, or on a distributed cluster.
Install the SDK
First, create a new project using cargo new and add the SurrealDB crate to your dependencies, enabling the key-value store you need:
# For an in memory database
cargo add surrealdb --features kv-mem
# For a SurrealKV database on disk
cargo add surrealdb --features kv-surrealkvSurrealKV is the on-disk engine for an embedded database, and is written in Rust, so it needs no C++ toolchain on the build machine. RocksDB (rocksdb://) and IndexedDB (indxdb://) were removed in SurrealDB 4.0, together with the kv-rocksdb and kv-indxdb features and the RocksDb and IndxDb engines. The rocksdb:// and indxdb:// schemes remain as deprecated spellings of surrealkv://: with the kv-surrealkv feature they open the path with SurrealKV and log a warning that says to use surrealkv:// instead. SurrealKV converts a RocksDB data directory written by SurrealDB the first time it opens it, so an application can open its existing path with SurrealKv. Take a copy of the directory first, because the conversion is one-way, as Moving a RocksDB database to SurrealKV explains.
You will need to add the following additional dependency:
cargo add tokio --features macros,rt-multi-threadThe structs in the example derive SurrealValue, which the SDK uses to convert Rust types to and from SurrealDB values. Types that only derive serde's Serialize and Deserialize do not implement it, and passing one to a method such as .content() or .select() fails to compile with the trait bound ...: SurrealValue is not satisfied.
Connect to SurrealDB
Open src/main.rs and replace everything in there with the following code to try out some basic operations using the SurrealDB SDK with an embedded database. Look at integrations to connect to a database.
use surrealdb::types::{RecordId, SurrealValue};
use surrealdb::Surreal;
// For an in memory database
use surrealdb::engine::local::Mem;
// For a SurrealKV database on disk
// use surrealdb::engine::local::SurrealKv;
#[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 {
#[allow(dead_code)]
id: RecordId,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
// Create database connection in memory
let db = Surreal::new::<Mem>(()).await?;
// Create database connection using SurrealKV
// let db = Surreal::new::<SurrealKv>("path/to/database-folder").await?;
// Select a specific namespace / database
db.use_ns("main").use_db("main").await?;
// Create a new person with a random id
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);
// Update a person record with a specific id
let updated: Option<Record> = db
.update(("person", "jaime"))
.merge(Responsibility { marketing: true })
.await?;
dbg!(updated);
// Select all people records
let people: Vec<Record> = db.select("person").await?;
dbg!(people);
// Perform a custom advanced query
let groups = db
.query("SELECT marketing, count() FROM type::table($table) GROUP BY marketing")
.bind(("table", "person"))
.await?;
dbg!(groups);
Ok(())
}Run your program from the command line with:
cargo runLogging and one process per datastore
The embedded engine logs through the tracing crate, so its output appears once your program installs a subscriber. With tracing-subscriber and its env-filter feature, the RUST_LOG environment variable chooses the level:
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
// Connect and query as usual
Ok(())
}RUST_LOG=surrealdb=info cargo runinfo shows the datastore starting up. trace also shows each statement as it is parsed and executed, which is useful for debugging a query but produces a lot of output, and it only works in a debug build such as plain cargo run. SurrealDB enables the release_max_level_debug feature of tracing, which removes trace-level logging from release builds at compile time. Cargo combines a crate's features across the whole build, so this also applies to trace! calls in your own code when your program depends on SurrealDB.
The limit applies when debug_assertions is off, not to optimised builds as such. To trace an optimised build, add a profile that inherits from release and keeps debug assertions on:
[profile.release-trace]
inherits = "release"
debug-assertions = trueRUST_LOG=surrealdb=trace cargo run --profile release-traceWithin one process, the SurrealKV engine runs many queries at once, including concurrent writes from many users to the same records, so the application has no single writer to wait for.
The datastore itself can only be open in one process at a time. SurrealKV locks the directory, so a second process that opens the same path, such as a surreal db start server pointed at it while your application runs, fails with an error like Database at mydb/LOCK is already locked by another process. To share the data between processes, or to look at it in SurrealDB Studio during development, run the database as a server with surreal db start and connect your application to it over ws://. With surrealdb::engine::any::connect, only the connection string changes between the embedded and the server setup, as Tips and tricks on using the Rust SDK shows. Stop the application's embedded connection before starting the server on the same path, because the lock is held for as long as the datastore is open.
The lock also applies within a single process. A second Surreal::new() or connect() call on a path that is already open fails.
SurrealKV reports Database at mydb/LOCK is already locked by another process even though the same process holds the lock.
Open the datastore once and clone the client wherever another handle is needed, because a clone shares the open datastore. Tests often hit this error, because each #[tokio::test] function that runs its own setup code opens the path again. Give each test its own directory, or connect with memory so that every test has a separate in-memory datastore.
SDK methods
The Rust SDK comes with a number of built-in functions.
Function | Description |
|---|---|
Surreal::init() | Initialises a static database engine |
db.connect(endpoint) | Connects to a specific database endpoint, saving the connection on the static client |
{"Surreal:: (endpoint)"} | Connects to a local or remote database endpoint |
db.use_ns(namespace).use_db(database) | Switch to a specific namespace and database |
db.signup(credentials) | Signs up a user using a specific record access method |
db.signin(credentials) | Signs this connection in using a specific access method or system user |
db.invalidate() | Invalidates the authentication for the current connection |
db.authenticate(token) | Authenticates the current connection with a JSON Web Token |
db.set(key, val) | Assigns a value as a parameter for this connection |
db.query(sql) | Runs a set of SurrealQL statements against the database |
db.select(resource) | Selects all records in a table, or a specific record |
db.create(resource).content(data) | Creates a record in the database |
db.update(resource).content(data) | Updates all records in a table, or a specific record |
db.update(resource).merge(data) | Modifies all records in a table, or a specific record |
db.update(resource).patch(data) | Applies JSON Patch changes to all records in a table, or a specific record |
db.delete(resource) | Deletes all records, or a specific record |
.init()
The DB static singleton ensures 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.
Surreal::init()Example usage
static DB: LazyLock<Surreal<Client>> = LazyLock::new(Surreal::init);
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
// Connect to the database
DB.connect::<Wss>("cloud.surrealdb.com").await?;
// Select a namespace + database
DB.use_ns("main").use_db("main").await?;
// Create or update a specific record
let tobie: Option<Record> = DB
.update(("person", "tobie"))
.content(Person { name: "Tobie" })
.await?;
Ok(())
} .connect()
Connects to a local or remote database endpoint.
db.connect(endpoint)Arguments
Argument | Description |
|---|---|
endpoint | The database endpoint to connect to. |
Example usage
// Connect to a local endpoint
DB.connect::<Ws>("127.0.0.1:8000").await?;
// Connect to a remote endpoint
DB.connect::<Wss>("cloud.surrealdb.com").await?; .new()
Connects to a local or remote database endpoint.
Surreal::new::<T>(endpoint)Arguments
Arguments | Description |
|---|---|
endpoint | The database endpoint to connect to. |
Example usage
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?; .use_ns() and .use_db()
Switch to a specific namespace and database.
db.use_ns(ns).use_db(db)Arguments
Arguments | Description |
|---|---|
ns | Switches to a specific namespace. |
db | Switches to a specific database. |
Example usage
db.use_ns("main").use_db("main").await?; .signup()
Signs up using a specific record access method.
db.signup(credentials)Arguments
Arguments | Description |
|---|---|
credentials | Variables used in a signup query. |
Example usage
use surrealdb::opt::auth::Record;
use surrealdb::types::SurrealValue;
#[derive(SurrealValue)]
struct Credentials {
email: String,
pass: String,
}
let jwt = db.signup(Record {
namespace: "main".to_string(),
database: "main".to_string(),
access: "user".to_string(),
params: Credentials {
email: "info@surrealdb.com".to_string(),
pass: "123456".to_string(),
},
}).await?;
// ⚠️: It is important to note that the token should be handled securely
// and protected from unauthorised access.
let token = jwt.access.as_insecure_token(); .signin()
Signs in using a specific access method or system user.
db.signin(credentials)Arguments
Arguments | Description |
|---|---|
credentials | Variables used in a signin query. |
Example usage
use surrealdb::opt::auth::Record;
use surrealdb::types::SurrealValue;
#[derive(SurrealValue)]
struct Credentials {
email: String,
pass: String,
}
let jwt = db.signin(Record {
namespace: "main".to_string(),
database: "main".to_string(),
access: "user".to_string(),
params: Credentials {
email: "info@surrealdb.com".to_string(),
pass: "123456".to_string(),
},
}).await?;
// ⚠️: It is important to note that the token should be handled securely
// and protected from unauthorised access.
let token = jwt.access.as_insecure_token(); .invalidate()
Invalidates the authentication for the current connection.
db.invalidate(credentials)Example usage
db.invalidate().await?; .authenticate()
Authenticates the current connection with a JWT token.
db.authenticate(token)Arguments
Arguments | Description |
|---|---|
token | The JWT authentication token. |
Example usage
db.authenticate(jwt).await?; .set()
Assigns a value as a parameter for this connection.
db.set(key, val)Arguments
Arguments | Description |
|---|---|
key | Specifies the name of the variable. |
val | Assigns the value to the variable name. |
Example usage
// Assign the variable on the connection
db.set("name", Name {
first: "Tobie",
last: "Morgan Hitchcock",
}).await?;
// Use the variable in a subsequent query
db.query("CREATE person SET name = $name").await?;
// Use the variable in a subsequent query
db.query("SELECT * FROM person WHERE name.first = $name.first").await?; .query()
Runs a set of SurrealQL statements against the database.
db.query(sql).bind(vars)Arguments
Arguments | Description |
|---|---|
sql | Specifies the SurrealQL statements. |
vars | Assigns variables which can be used in the query. |
Example usage
// Run some queries
let sql = "
CREATE person;
SELECT * FROM type::table($table);
";
let mut result = db
.query(sql)
.bind(("table", "person"))
.await?;
// Get the first result from the first query
let created: Option<Person> = result.take(0)?;
// Get all of the results from the second query
let people: Vec<Person> = result.take(1)?; .select()
Selects all records in a table, or a specific record, from the database.
db.select(resource)Arguments
Arguments | Description |
|---|---|
resource | The table name or a record ID to select. |
Example usage
// Select all records from a table
let people: Vec<Person> = db.select("person").await?;
// Select a specific record from a table
let person: Option<Person> = db.select(("person", "h5wxrf2ewk8xjxosxtyc")).await?;Translated query
This function will run the following query in the database:
SELECT * FROM $resource; .create()
Creates a record in the database.
db.create(resource).content(data)Arguments
Arguments | Description |
|---|---|
resource | The table name or the specific record ID to create. |
data | The document / record data to insert. |
Example usage
// Create a record with a random ID
let person: Option<Person> = db.create("person").await?;
// Create a record with a specific ID
let record: Record = db
.create(("person", "tobie"))
.content(Person {
name: "Tobie",
settings: {
active: true,
marketing: true,
},
}).await?;Translated query
This function will run the following query in the database:
CREATE $resource CONTENT $data; .update().content()
Updates all records in a table, or a specific record, in the database.
db.update(resource).content(data)This function replaces the current document / record data with the specified data.
Arguments
Arguments | Description |
|---|---|
resource | The table name or the specific record ID to create. |
data | The document / record data to insert. |
Example usage
// Update all records in a table
let people: Vec<Person> = db.update("person").await?;
// Update a record with a specific ID
let person: Option<Person> = db
.update(("person", "tobie"))
.content(Person {
name: "Tobie",
settings: {
active: true,
marketing: true,
},
}).await?;Translated query
This function will run the following query in the database:
UPDATE $resource CONTENT $data; .update().merge()
Modifies all records in a table, or a specific record, in the database.
db.update(resource).merge(data)This function merges the current document / record data with the specified data.
Arguments
Arguments | Description |
|---|---|
resource | The table name or the specific record ID to create. |
data | The document / record data to insert. |
Example usage
// Update all records in a table
let people: Vec<Person> = db.update("person")
.merge(Document {
updated_at: Datetime::default(),
})
.await?;
// Update a record with a specific ID
let person: Option<Person> = db.update(("person", "tobie"))
.merge(Document {
updated_at: Datetime::default(),
settings: Settings {
active: true,
},
})
.await?;Translated query
This function will run the following query in the database:
UPDATE $resource MERGE $data; .update().patch()
Applies JSON Patch changes to all records, or a specific record, in the database.
db.update(resource).patch(data)This function patches the current document / record data with the specified JSON Patch data.
Arguments
Arguments | Description |
|---|---|
resource | The table name or the specific record ID to modify. |
data | The JSON Patch data with which to modify the records. |
Example usage
// Update all records in a table
let people: Vec<Person> = db.update("person")
.patch(PatchOp::replace("/created_at", Datetime::default()))
.await?;
// Update a record with a specific ID
let person: Option<Person> = db.update(("person", "tobie"))
.patch(PatchOp::replace("/settings/active", false))
.patch(PatchOp::add("/tags", &["developer", "engineer"]))
.patch(PatchOp::remove("/temp"))
.await?;Translated query
This function will run the following query in the database:
UPDATE $resource PATCH $data; .delete()
Deletes all records in a table, or a specific record, from the database.
db.delete(resource)Arguments
Arguments | Description |
|---|---|
resource | The table name or a record ID to select. |
Example usage
// Delete all records from a table
let people: Vec<Person> = db.delete("person").await?;
// Delete a specific record from a table
let person: Option<Person> = db.delete(("person", "h5wxrf2ewk8xjxosxtyc")).await?;Translated query
This function will run the following query in the database:
DELETE FROM $resource RETURN BEFORE;