Sign In

Methods

query

The .query() method for the SurrealDB Rust SDK runs one or more SurrealQL statements against the database.

Runs one or more SurrealQL statements against the database.

Method Syntax
db.query(query)

Argument

Type

Description

queryquery

Specifies the SurrealQL statements.

The .query() method serves as a default way to pass queries into the Rust SDK. The simplest usage of this method is by passing in a &str and returning an IndexedResults.

use surrealdb::engine::any::connect;
use surrealdb::opt::auth::Root;

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let 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?;

    let query = r#"
        LET $data = ["J. Jonah Jameson", "James Earl Jones"];
        RETURN $data.map(|$name| {
            LET $names = $name.split(' ');
            {
                first_name:  $names[0],
                middle_name: $names[1],
                last_name:   $names[2]
            }
        });
    "#;

    let result = db.query(query).await?;
    println!("Number of statements: {}", result.num_statements());
    dbg!(result);
    Ok(())
}

The .take() method can be used to pull out one of the responses into a deserialised format. Note that in the next example the LET statement is the first statement received by the database, and thus .take(1) is used to grab the output of the second statement to deserialise into a Person struct.

use surrealdb::engine::any::connect;
use surrealdb::opt::auth::Root;
use surrealdb::types::SurrealValue;

#[derive(Debug, SurrealValue)]
struct Person {
    first_name: String,
    middle_name: String,
    last_name: String,
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let 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?;

    let query = r#"
    LET $data = ["J. Jonah Jameson", "James Earl Jones"];
    RETURN $data.map(|$name| {
    LET $names = $name.split(' ');
    {
       first_name:  $names[0],
       middle_name: $names[1],
       last_name:   $names[2]
    }
    });"#;

    let mut result = db.query(query).await?;
    let people: Vec<Person> = result.take(1)?;
    dbg!(people);
    Ok(())
}

The return value from this method is Result<Response, Error>. A Result::Ok(Response) only means that the query or queries were successfully executed, but does not mean that each query contained in the Response was successful.

Take the following code for example which contains one successful query, followed by one with incorrect syntax (an integer where a string is expected).

use surrealdb::engine::any::connect;

#[tokio::main]
async fn main() {
    let db = connect("memory").await.unwrap();
    let res = db.query("
        LET $x = 9;
        LET $x: string = 9") // valid SurrealQL but wrong type
        .await;
    println!("{res:?}");
}

The .query() method returns an Ok(IndexedResults), showing that the database was able to understand and process the queries, even though the latter returned an error.

Ok(IndexedResults { results: {0: (DbResultStats { execution_time: Some(392.875µs), query_type: Some(Other) }, Ok(None)), 1: (DbResultStats { execution_time: Some(426.042µs), query_type: Some(Other) }, Err(InternalError("Tried to set `$x`, but couldn't coerce value: Expected `string` but found `9`")))}, live_queries: {} })

But if the function contains input that the database is unable to parse into a query in the first place, an Err will be returned for the entire .query() call.
If the string syntax is changed to something nonsensical like Hi how are you?, the database is unable to process the query in the first place and .query() will return an Err for the whole call.

use surrealdb::engine::any::connect;

#[tokio::main]
async fn main() {
    let db = connect("memory").await.unwrap();
    let res = db.query("
        LET $x = 9;
        Hi how are you?;") // invalid SurrealQL
        .await;
    println!("{res:?}");
}
Err(ParseError("Parse error: Unexpected token `an identifier`, expected Eof\n --> [3:12]\n  |\n3 | Hi how are you?;\n  |    ^^^\n"))

The IndexedResults struct contains helper metods such as .check() to check for errors, or .take_errors() which removes the errors from the main IndexedResults.

use surrealdb::engine::any::connect;

#[tokio::main]
async fn main() {
    let db = connect("memory").await.unwrap();
    db.use_ns("ns").use_db("db").await.unwrap();
    let mut res = db
        .query(
        "LET $x = 9;
        LET $x: string = 9;
        LET $x: bool = 9;
        CREATE person",
        )
        .await
        .unwrap();

    println!("Errors: {:?}\n", res.take_errors());
    println!("Successes: {:?}", res);
}

Output:

Errors: {2: InternalError("Tried to set `$x`, but couldn't coerce value: Expected `bool` but found `9`"), 1: InternalError("Tried to set `$x`, but couldn't coerce value: Expected `string` but found `9`")}

Successes: IndexedResults { results: {0: (DbResultStats { execution_time: Some(301.375µs), query_type: Some(Other) }, Ok(None)), 3: (DbResultStats { execution_time: Some(2.278083ms), query_type: Some(Other) }, Ok(Array(Array([Object(Object({"id": RecordId(RecordId { table: Table("person"), key: String("yq7gxgm3ffkr4kibumeb") })}))]))))}, live_queries: {} }

The .bind() method sets the parameters that a query refers to with SurrealQL's $ syntax. It accepts anything that implements IntoVariables, which covers any type implementing SurrealValue that converts into an object, plus key-value pairs.

Form

Example

A single key-value pair

.bind(("table", "person"))

The

vars!

macro

.bind(vars! { table: "person", min_age: 18 })

The

object!

macro

.bind(object! { table: "person" })

A struct deriving

SurrealValue
.bind(Filters { min_age: 18 })

A map, such as

HashMap<String, Value>
.bind(map)

The vars! macro is the most direct way to set several parameters at once, as each pair is written in place rather than chained one call at a time.

use surrealdb::engine::any::connect;
use surrealdb::types::{RecordId, SurrealValue, vars};

#[derive(Debug, SurrealValue)]
struct Person {
    id: RecordId,
    name: String,
    age: i64,
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let db = connect("memory").await?;
    db.use_ns("ns").use_db("db").await?;

    let statements = "
        CREATE type::table($table) SET name = $name, age = $age;
        SELECT * FROM type::table($table) WHERE age >= $min_age;
    ";

    let mut result = db
        .query(statements)
        .bind(vars! {
            table: "person",
            name: "Aeon",
            age: 30,
            min_age: 18,
        })
        .await?;

    let created: Option<Person> = result.take(0)?;
    dbg!(created);
    let adults: Vec<Person> = result.take(1)?;
    dbg!(adults);
    Ok(())
}

Calls to .bind() accumulate rather than replace, so parameters can be gathered from more than one place before the query is awaited. A later call wins if it repeats a name.

let mut result = db
    .query("RETURN [$a, $b]")
    .bind(vars! { a: 1 })
    .bind(vars! { b: 2 })
    .await?;
Note

A binding error is not raised at the point of the .bind() call. It is held until the query is awaited, and surfaces there as the result of the whole query.

The .with_stats() method can be used on the query builder before awaiting the future. The awaited value is WithStats<IndexedResults>, which wraps the usual IndexedResults so each .take(...) can return both Stats (including execution time) and the deserialised statement result.

use surrealdb::engine::any::connect;
use surrealdb::types::Value;

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let db = connect("memory").await?;
    db.use_ns("ns").use_db("db").await?;

    let mut response = db
        .query("CREATE person:ada SET name = 'Ada'; SELECT * FROM person;")
        .with_stats()
        .await?;

    if let Some((stats, res)) = response.take(1) {
        let records: Vec<Value> = res?;
        println!("time = {:?}, records = {:?}", stats.execution_time, records);
    }
    Ok(())
}

See WithStats::take on Docs.rs for the supported .take shapes (statement index, nested paths, and tuples).

After awaiting .query(...), IndexedResults::stream turns the live-query slot at a given statement index into a QueryStream. Pass a statement index (0, 1, …), or pass () to merge every LIVE SELECT in that response. The stream yields Notification values (or raw Value) and implements futures::Stream. This can be polled with the StreamExt trait from the futures crate.

If you prefer not to embed LIVE SELECT in SurrealQL, the same live subscription can be started with db.select(resource).live() on top of select(); both approaches yield a stream of notifications.

use futures::StreamExt;
use surrealdb::engine::any::connect;
use surrealdb::opt::auth::Root;
use surrealdb::types::Value;

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let db = connect("ws://localhost:8000").await?;
    db.use_ns("main").use_db("main").await?;
    db.signin(Root {
        username: "root".into(),
        password: "secret".into(),
    })
    .await?;

    // Use 2 or 3 instead of () to only LIVE SELECT
    // either person or cat
    let mut response = db
        .query(
            "DEFINE TABLE IF NOT EXISTS person SCHEMALESS;
            DEFINE TABLE IF NOT EXISTS cat SCHEMALESS;
             LIVE SELECT * FROM person;
             LIVE SELECT * FROM cat;",
        )
        .await?;
    let mut stream = response.stream::<Value>(())?;

    while let Some(item) = stream.next().await {
        let notification = item?;
        println!("{:?}", notification);
    }
    Ok(())
}

To test the live stream, either log in using SurrealDB Studio or the CLI using the surreal sql --user root --pass secret command in another terminal window. You should see notifications similar to the following whenever a new record is created from the person or cat table, but not for others.

Notification { query_id: Uuid(3bad02bb-fd1e-402b-9a43-5b3eae88f279), action: Create, data: Object(Object({"id": RecordId(RecordId { table: Table("person"), key: String("zhby5ibqh8b2hfyyao30") })})) }
Notification { query_id: Uuid(829d7ec7-d67c-47ef-bd7c-8b3e10b8d149), action: Create, data: Object(Object({"id": RecordId(RecordId { table: Table("cat"), key: String("cgu921pkfco7uk7ajeym") })})) }

Available since: v3.3.0

Awaiting a query gives an IndexedResults, which holds the entire result set in memory and yields nothing until the last row has been read. The .stream_items() method returns an ItemStream instead, so a large SELECT can be processed while the server is still producing it. The stream implements futures::Stream and yields StreamItem values, of which there are two:

  • StreamItem::Row carries one row along with the index of the statement that produced it.

  • StreamItem::StatementEnd marks a statement as finished, and carries its stats and its Result.

use futures::StreamExt;
use surrealdb::engine::any::connect;
use surrealdb::method::StreamItem;

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let db = connect("memory").await?;
    db.use_ns("ns").use_db("db").await?;

    db.query("CREATE person:ada SET name = 'Ada'; CREATE person:grace SET name = 'Grace';")
        .await?
        .check()?;

    let mut rows = db.query("SELECT name FROM person").stream_items()?;

    while let Some(item) = rows.next().await {
        match item? {
            StreamItem::Row { statement, value } => {
                println!("statement {statement} produced {value:?}");
            }
            StreamItem::StatementEnd { statement, stats, result } => {
                result?;
                println!("statement {statement} finished in {:?}", stats.execution_time);
            }
        }
    }
    Ok(())
}

Output:

statement 0 produced Object(Object({"name": String("Ada")}))
statement 0 produced Object(Object({"name": String("Grace")}))
statement 0 finished in Some(1.8045ms)
Important

Rows are provisional until their statement ends. A statement can still fail on a later row, and a BEGIN … COMMIT block can still roll back, so a StatementEnd carrying an error retracts every row that preceded it. Code that acts on rows as they arrive has to be able to undo that.

Two further points worth knowing:

  • Only some engines stream incrementally. The embedded engines (mem://, rocksdb://, surrealkv:// and file paths) and the gRPC remote engine (grpc://) deliver rows as the server produces them. The WebSocket and HTTP engines have no incremental path, so they run the query to completion and then replay the items. Every engine yields the same items in the same order; on those two they simply do not arrive any earlier than awaiting the query would.

  • Dropping the stream stops the query, as the execution behind it holds an open transaction that has to be finalised rather than abandoned.

LIVE SELECT is not served by this method. A live query's ID arrives as an ordinary row and nothing subscribes to it. Use the awaited form described in Stream LIVE SELECT output for live queries, and .stream_items() for reading rows.

As the .query() method can be used to pass any SurrealQL query on to the database, it is an easy go-to when using complex queries. However, be sure to keep the following best practices in mind when doing so.

Thus, instead of using user input to directly construct a string:

let bad_sql = format!("
CREATE {user_input};
SELECT * FROM {user_input};");

You can insert a parameter using SurrealQL's $ parameter syntax,

let sql = "
    CREATE person;
    SELECT * FROM type::table($table);
";

And then apply the .bind() method to pass the parameter in.

use surrealdb::engine::any::connect;
use surrealdb::opt::auth::Root;
use surrealdb::types::{RecordId, SurrealValue};

#[derive(Debug, SurrealValue)]
struct Person {
    id: RecordId,
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let 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?;

    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)?;
    dbg!(created);
    // Get all of the results from the second query
    let people: Vec<Person> = result.take(1)?;
    dbg!(people);
    Ok(())
}

Was this page helpful?