Flexible typing

Most examples in the Rust SDK feature strict types like the following that can be serialized and deserialized as needed.

#[derive(Debug, SurrealValue)]
struct Student {
name: String,
class_id: u32,
}

However, sometimes you will need to work with types that have a more dynamic structure. This page offers a few methods to use in such a case.

Getting started

Start a running database using the following command:

surreal start --user root --pass secret 

To follow along interactively, connect using Surrealist or the following command to open up the CLI:

surrealdb % surreal sql --user root --pass secret --pretty

Then use the cargo add command to add the surrealdb and tokio crates.

Strict vs. flexible typing

The following example is a typical one, featuring a Student struct that holds a name and a class_id, followed by the .select() function to display all the students.

use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::{Error, Surreal};
use surrealdb_types::SurrealValue;

#[derive(Debug, SurrealValue)]
struct Student {
name: String,
class_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
// Connect to the database server
let db = Surreal::new::<Ws>("localhost:8000").await?;

// Sign in into the server
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;

// Select the namespace and database to use
db.use_ns("main").use_db("main").await?;

let all_students: Vec<Student> = db.select("student").await?;
println!("All students: {all_students:?}");

Ok(())
}

Before running this code, first use Surrealist or the CLI to populate the database with two students.

CREATE student SET name = "Student 1", class_id = 10; CREATE student SET name = "Another student", class_id = 20;

Once that is done, running cargo run will display the following output.

All students: [Student { name: "Another student", class_id: 20 }, Student { name: "Student 1", class_id: 10 }]

So far so good, but what if we were still experimenting with the data and created a student that diverged from the Student struct?

CREATE student SET name = "Third student", class_id = 40, metadata = { teacher: teacher:mr_gundry_white, favourite_classes: ["Music", "Industrial arts"] };

In this case the output would still conform to the Student struct and the extra information in the third student would not show up.

All students: [Student { name: "Another student", class_id: 20 }, Student { name: "Student 1", class_id: 10 }, Student { name: "Third student", class_id: 40 }]

One possibility here is to use the .query() method, which will always return the output as received from the database.

use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::{Error, Surreal};
use surrealdb_types::SurrealValue;

#[derive(Debug, SurrealValue)]
struct Student {
name: String,
class_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
// Connect to the database server
let db = Surreal::new::<Ws>("localhost:8000").await?;

// Sign in into the server
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;

// Select the namespace and database to use
db.use_ns("main").use_db("main").await?;

let all_students = db.query("SELECT * FROM student").await?;
println!("All students: {all_students:?}");

Ok(())
}

However, the output is a bit noisy.

All students: IndexedResults { results: {0: (DbResultStats { execution_time: Some(139.916µs), query_type: Some(Other) }, Ok(Array(Array([Object(Object({"class_id": Number(Int(10)), "id": RecordId(RecordId { table: Table("student"), key: String("9q65y3sujtd6y2oq9qou") }), "name": String("Student 1")})), Object(Object({"class_id": Number(Int(20)), "id": RecordId(RecordId { table: Table("student"), key: String("emm1fhw2bxdm7vkcchni") }), "name": String("Another student")})), Object(Object({"class_id": Number(Int(40)), "id": RecordId(RecordId { table: Table("student"), key: String("vcwgksxms0819ivwsm3q") }), "metadata": Object(Object({"favourite_classes": Array(Array([String("Music"), String("Industrial arts")])), "teacher": RecordId(RecordId { table: Table("teacher"), key: String("mr_gundry_white") })})), "name": String("Third student")}))]))))}, live_queries: {} }

A better solution when working with dynamic structures in a situation like this is to pass in a Resource into the methods we use. Database methods that take a Resource will automatically return a Value which contains an enum of all the possible data types in SurrealDB, and thus does not require deserializing. In addition, a Value has the methods .to_sql() and .to_sql_pretty(), making the output similar to that in the CLI.

use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::opt::Resource;
use surrealdb::{Error, Surreal};
use surrealdb_types::{SurrealValue, ToSql};

#[derive(Debug, SurrealValue)]
struct Student {
name: String,
class_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
// Connect to the database server
let db = Surreal::new::<Ws>("localhost:8000").await?;

// Sign in into the server
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;

// Select the namespace and database to use
db.use_ns("main").use_db("main").await?;

let all_students = db.select(Resource::from("student")).await?;
println!("All students regular: {}\n", all_students.to_sql());
println!("All students pretty: {}", all_students.to_sql_pretty());

Ok(())
}

As the output shows, the first println! statement looks like the output in the CLI, while the second looks like the output in the CLI when the --pretty flag is passed in.

All students regular: [{ class_id: 10, id: student:9q65y3sujtd6y2oq9qou, name: 'Student 1' }, { class_id: 20, id: student:emm1fhw2bxdm7vkcchni, name: 'Another student' }, { class_id: 40, id: student:vcwgksxms0819ivwsm3q, metadata: { favourite_classes: ['Music', 'Industrial arts'], teacher: teacher:mr_gundry_white }, name: 'Third student' }]

All students pretty: [
{
class_id: 10,
id: student:9q65y3sujtd6y2oq9qou,
name: 'Student 1'
},
{
class_id: 20,
id: student:emm1fhw2bxdm7vkcchni,
name: 'Another student'
},
{
class_id: 40,
id: student:vcwgksxms0819ivwsm3q,
metadata: {
favourite_classes: [
'Music',
'Industrial arts'
],
teacher: teacher:mr_gundry_white
},
name: 'Third student'
}
]

A Value from serde_json can be passed into functions like .create(), allowing the json! macro to be used.

use serde_json::json;
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::opt::Resource;
use surrealdb::{Error, Surreal};
use surrealdb_types::{SurrealValue, ToSql};

#[derive(Debug, SurrealValue)]
struct Student {
name: String,
class_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
// Connect to the database server
let db = Surreal::new::<Ws>("localhost:8000").await?;

// Sign in into the server
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;

// Select the namespace and database to use
db.use_ns("main").use_db("main").await?;

let new_student = db.create(Resource::from("student")).content(json!({
"age": 15,
"weekly_allowance": 20.5
})).await?;

println!("{}", new_student.to_sql());

Ok(())
}

Output:

[{ age: 15, id: student:n89ugobw4iaw7gw3lh9b, weekly_allowance: 20.5f }]