Skip to content

Query Builder

The immutable fluent builder composes SurrealQL statements without executing them. Every method returns a new Query; the input is never mutated.

SELECT

use surql::query::helpers::{from_table, select};
use surql::types::operators::{eq, gt};

let q = select(Some(vec!["name".into(), "email".into()]))
    .from_table("user")?
    .where_(&gt("age", 18))
    .where_(&eq("status", "active"))
    .order_by("created_at", "DESC")
    .limit(10);

println!("{}", q.to_surql());

INSERT

use surql::query::helpers::insert;
use serde_json::json;

let q = insert(
    "user",
    [
        ("name", json!("Alice")),
        ("email", json!("alice@example.com")),
    ],
)?;

UPDATE / UPSERT / DELETE / RELATE

use surql::query::helpers::{update, upsert, delete, relate};

let u = update("user:alice", [("status", json!("active"))])?;
let d = delete("user:bob")?;
let r = relate("user:alice", "likes", "post:1")?;

What is escaped, and what is raw

The builder splices names and values into SurrealQL text, so each kind of input has one rule:

Input Rule
Table and edge names Must be identifiers ([A-Za-z_][A-Za-z0-9_]*), else a validation error.
Record-id targets (from_table, update, upsert, delete, relate, the CRUD and graph helpers) Parsed and re-rendered as a RecordID: the key is escaped (user:a-b renders user:⟨a-b⟩, and user:x; DELETE user targets the record keyed "x; DELETE user"). Array, object, range, and generated keys (user:[1, 2], user:ulid()) are refused unless quoted.
Fields in order_by, group_by, fulltext_search, vector_search, similarity_score, SET targets, GraphQuery::select / fetch, AggregateOpts aliases Must be field paths (identifiers joined by .).
search_score and reverse_traverse names Quoted as identifiers.
Data values (insert, update, upsert, relate, set, operator values) Always literals. A JSON object is an object literal, whatever its keys; keys that are not identifiers are quoted. A value nested more than 16 arrays and objects deep (MAX_INLINE_DEPTH) renders as its JSON text decoded by the engine, encoding::json::decode('…') (SurrealDB 3.1+), since the parser refuses a literal nested 20 levels deep.
Projections passed to select, string WHERE fragments, join, traverse paths, Expressions Raw SurrealQL by design: never build them from untrusted text.

Query::to_surql re-checks every name and target it renders, so a Query assembled by setting its public fields directly follows the same rules. Vector values and thresholds must be finite.

Raw values: functions and record references

Because a JSON value is always data, a function call or record reference goes in through an Expression:

use surql::query::expressions::time_now;
use surql::types::operators::{eq_expr, lt_expr};
use surql::types::record_ref;

// CREATE post CONTENT {title: 'hi', created_at: time::now()}
let q = insert("post", data)?.set_expr("created_at", time_now())?;

// WHERE author = type::record('user', 'alice') AND expires_at < time::now()
let q = select(None)
    .from_table("post")?
    .where_(eq_expr("author", record_ref("user", "alice")))
    .where_(lt_expr("expires_at", time_now()));

set / set_expr add SET assignments to an UPDATE and fields to the CONTENT of a CREATE / UPSERT / RELATE (replacing a same-named data key). SurrealFn, RecordRef, and RecordID convert into an Expression with .into().

Graph traversal

GraphQuery renders one step per out / r#in / both. Without a depth the step is a single hop onto the edge records; Some(n) is exactly n hops to the records at the far end, spelled out hop by hop (the form SurrealDB 3 accepts). to(table) narrows the last step's far end, in that step's direction:

use surql::query::GraphQuery;

// SELECT * FROM user:alice->follows->?->follows->user LIMIT 10
let q = GraphQuery::new("user:alice").out("follows", Some(2)).to("user").limit(10)?;

// SELECT * FROM user:alice<-follows<-user
let q = GraphQuery::new("user:alice").r#in("follows", None).to("user");

Depths run from 1 to 32 (also the cap on graph::shortest_path's max_depth). LIMIT renders before FETCH, the order the engine requires.

Where

where_ accepts:

  • a &str (raw SurrealQL)
  • a String
  • any &Operator from types::operators
  • a composed operator (and_, or_, not_)
use surql::types::operators::{and_, eq, gt};

let q = select(None)
    .from_table("user")?
    .where_(&and_(gt("age", 18), eq("status", "active")));

Expressions

Expressions build typed SurrealQL fragments you can embed in select lists or where_ clauses.

use surql::query::expressions::{as_, concat, count, field, math_mean};

let sel = vec![
    field("id").to_surql(),
    as_(&math_mean("score"), "avg_score").to_surql(),
    as_(&count(None), "total").to_surql(),
];

Hints

use surql::query::hints::{QueryHint, ParallelHint, TimeoutHint};

let q = q.hint(QueryHint::Parallel(ParallelHint::enabled()))
         .hint(QueryHint::Timeout(TimeoutHint::new(30.0)?));

Hints render as SurrealQL comments and are merged so that duplicates of the same kind are collapsed to the latest value. The server ignores comments, so hints label a statement without changing how it runs (see Query Hints).

Result wrappers

Once the async client lands, queries produce typed QueryResult<T> / RecordResult<T> / ListResult<T> / PaginatedResult<T> values via the result-extraction helpers in query::results.

What's next