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 crates surrealdb and tokio.
Fetching all the fields of a record link
The following example shows a classroom joined to a few students by record links.
CREATE teacher:one SET name = id.id();
CREATE student:one, student:two, student:three SET name = id.id();
CREATE classroom SET
location = (-16.7, 64.4),
school_name = "Jöklaskóli",
teacher = teacher:one,
students = [student:one, student:two, student:three];
A query using SELECT * FROM classroom:one will show the teacher and all of the students, but only their record IDs.
Query and response
SELECT * FROM classroom:one;
[
{
id: classroom:one,
location: (-16.7, 64.4),
school_name: 'Jöklaskóli',
students: [
student:one,
student:two,
student:three
],
teacher: teacher:one
}
]
The .* operator for the teacher and student fields can be used in this case.
SELECT *, teacher.*, students.* FROM classroom;
Here is the result:
Output
[
{
id: classroom:one,
location: (-16.7, 64.4),
school_name: 'Jöklaskóli',
students: [
{
id: student:one,
name: 'one'
},
{
id: student:two,
name: 'two'
},
{
id: student:three,
name: 'three'
}
],
teacher: {
id: teacher:one,
name: 'one'
}
}
]
The Rust code
The code below that shows an example of FETCH is another example related to classes and students. Note that in one part it passes a Resource into the create method in order to return a Value and thus not have to specify a return type to deserialize into. For more information on this technique, see the page on flexible typing.
use surrealdb::{
engine::remote::ws::Ws,
opt::{auth::Root, Resource},
Surreal,
};
use surrealdb_types::{Datetime, RecordId, SurrealValue};
const DANCE: &str = "dance";
const STUDENT: &str = "student";
#[derive(Debug, SurrealValue)]
struct DanceClass {
id: RecordId,
name: String,
created_at: Datetime,
}
#[derive(Debug, SurrealValue)]
struct Student {
id: RecordId,
name: String,
classes: Vec<RecordId>,
created_at: Datetime,
}
#[derive(Debug, SurrealValue)]
#[allow(dead_code)]
struct StudentClasses {
id: RecordId,
name: String,
classes: Vec<DanceClass>,
created_at: Datetime,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
db.signin(Root {
username: "root".to_string(),
password: "secret".to_string(),
})
.await?;
db.use_ns("main").use_db("main").await?;
let classes: Option<DanceClass> = db
.create(DANCE)
.content(DanceClass {
id: RecordId::new(DANCE, "dc101"),
name: "Introduction to Dancing".to_owned(),
created_at: Datetime::default(),
})
.await?;
db.create(Resource::from(STUDENT))
.content(Student {
id: RecordId::new(STUDENT, "jane"),
name: "Jane Doe".to_owned(),
classes: classes.into_iter().map(|class| class.id).collect(),
created_at: Datetime::default(),
})
.await?;
let mut results = db.query(format!("SELECT * FROM {STUDENT} FETCH classes")).await?;
let students: Vec<StudentClasses> = results.take(0)?;
println!("Students = {:?}", students);
Ok(())
}
Here is the final output:
Students = [StudentClasses { id: RecordId { table: "student", key: String("jane") }, name: "Jane Doe", classes: [DanceClass { id: RecordId { table: "dance", key: String("dc101") }, name: "Introduction to Dancing", created_at: Datetime(2025-11-06T02:15:05.116807Z) }], created_at: Datetime(2025-11-06T02:15:05.117644Z) }]
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 --ns namespace --db database --pretty
Then use the cargo add command to add three crates: surrealdb and tokio, and with serde with the “serde_derive” feature (cargo add serde --features serde_derive). The dependencies inside Cargo.toml should look something like this:
cargo add serde —features serde_derive
[dependencies]
serde = { version = "1.0.228", features = ["serde_derive"] }
surrealdb = "2.4.1"
tokio = "1.49.0"
When to use FETCH
The following example shows a classroom joined to a few students by record links.
CREATE teacher:one SET name = id.id();
CREATE student:one, student:two, student:three SET name = id.id();
CREATE classroom SET
location = (-16.7, 64.4),
school_name = "Jöklaskóli",
teacher = teacher:one,
students = [student:one, student:two, student:three];
A query using SELECT * FROM classroom:one will show the teacher and all of the students, but only their record IDs.
Query and response
SELECT * FROM classroom:one;
[
{
id: classroom:one,
location: (-16.7, 64.4),
school_name: 'Jöklaskóli',
students: [
student:one,
student:two,
student:three
],
teacher: teacher:one
}
]
The .* operator for the teacher and student fields can be used in this case, but note that the .* must be used twice in the case of the students: once to access each member of the array, and once more to access all of its fields.
SELECT *, teacher.*, students.*.* FROM classroom;
Using FETCH may be a nicer option in this case. The syntax is a bit more readable, and there is no need to think about which fields are single records and which ones are arrays.
SELECT * FROM classroom FETCH teacher, students;
Here is the result:
Output
[
{
id: classroom:one,
location: (-16.7, 64.4),
school_name: 'Jöklaskóli',
students: [
{
id: student:one,
name: 'one'
},
{
id: student:two,
name: 'two'
},
{
id: student:three,
name: 'three'
}
],
teacher: {
id: teacher:one,
name: 'one'
}
}
]
The Rust code
The code below that shows an example of FETCH is another example related to classes and students. Note that in one part it passes a Resource into the create method in order to return a Value and thus not have to specify a return type to deserialize into. For more information on this technique, see the page on flexible typing.
use serde::{Deserialize, Serialize};
use surrealdb::{
engine::remote::ws::Ws,
opt::{auth::Root, Resource},
sql::Datetime,
RecordId, Surreal,
};
const DANCE: &str = "dance";
const STUDENT: &str = "student";
#[derive(Debug, Serialize, Deserialize)]
struct DanceClass {
id: RecordId,
name: String,
created_at: Datetime,
}
#[derive(Debug, Serialize)]
struct Student {
id: RecordId,
name: String,
classes: Vec<RecordId>,
created_at: Datetime,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct StudentClasses {
id: RecordId,
name: String,
classes: Vec<DanceClass>,
created_at: Datetime,
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("localhost:8000").await?;
db.signin(Root {
username: "root",
password: "secret",
})
.await?;
db.use_ns("namespace").use_db("database").await?;
let classes: Option<DanceClass> = db
.create(DANCE)
.content(DanceClass {
id: RecordId::from((DANCE, "dc101")),
name: "Introduction to Dancing".to_owned(),
created_at: Datetime::default(),
})
.await?;
db.create(Resource::from(STUDENT))
.content(Student {
id: RecordId::from((STUDENT, "jane")),
name: "Jane Doe".to_owned(),
classes: classes.into_iter().map(|class| class.id).collect(),
created_at: Datetime::default(),
})
.await?;
let mut results = db.query(format!("SELECT * FROM {STUDENT} FETCH classes")).await?;
let students: Vec<StudentClasses> = results.take(0)?;
println!("Students = {:?}", students);
Ok(())
}
Here is the final output:
Students = [StudentClasses { id: RecordId { table: "student", key: String("jane") }, name: "Jane Doe", classes: [DanceClass { id: RecordId { table: "dance", key: String("dc101") }, name: "Introduction to Dancing", created_at: Datetime(2025-11-06T02:15:05.116807Z) }], created_at: Datetime(2025-11-06T02:15:05.117644Z) }]