// integration

Rust · surrealguard-rs

Compile-time-checked, typed SurrealQL for Rust. The query! macro runs the SurrealGuard analyzer against your schema during compilation — a wrong table, unknown field, bad arity, or kind mismatch is a cargo check error, and the result type is generated from the inferred response. No build script, no language server, no runtime schema fetch.

# until the crates.io release, depend on it by git in Cargo.toml
surrealguard-rs = { git = "https://github.com/DrewRidley/surrealguard" }

The macros

surrealguard-rs re-exports two proc-macros; a crate using them only needs surrealguard-rs in scope (it re-exports every dependency the expansion references).

use surrealguard_rs::{query, surql};

let users = query!("SELECT name, age FROM user");
//  users: Query<Vec<{ name: String, age: i64 }>>  ← nameless struct, inferred

let sql = surql!("UPDATE user SET age = 30");
//  sql: &'static str — validated, unchanged text

The compiler is the checker

The macro runs the real analyzer at compile time and turns each error-severity finding into a spanned compile_error!. On stable Rust the span covers the whole literal; the message carries every finding's code and text.

let bad = query!("SELECT name, ssn FROM user");
error: SurrealGuard rejected this query:
  [E1002] unknown field `ssn` on table `user`
 --> src/main.rs:7:15
  |
7 |     let bad = query!("SELECT name, ssn FROM user");
  |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With no schema configured, queries are still checked for everything that doesn't depend on one — syntax, function arity, operators, and so on.

Schema resolution

The macro resolves your schema at compile time from, in order:

  1. the SURREALGUARD_SCHEMA environment variable — a .surql file or a directory, taken relative to CARGO_MANIFEST_DIR unless absolute;
  2. otherwise a convention path under the crate root, first match wins: schema/, migrations/, then schema.surql.
# point the macro at an explicit schema for one build
SURREALGUARD_SCHEMA=db/schema.surql cargo check

A directory contributes every .surql/.surrealql file it contains, sorted by path — so zero-padded migrations (0001_*.surql, 0002_*.surql) apply in order. The macro emits an include_bytes! per schema file, so editing a schema forces the query's crate to recompile.

Nameless typed results

query! expands to a Query<T> whose T is rendered from the inferred response kind. Object shapes become structs defined inside the macro's own block scope — so the type escapes as a value but its name never does. You get nested field access without ever writing a type, the same way sqlx's query! returns an anonymous record.

let rows = query!("SELECT name, address.city AS city FROM user");
for row in rows.from_json(json)? {
    println!("{} — {}", row.name, row.city);   // fields, no type written
}

Generated result types derive serde::Deserialize. A statement that produces no response (e.g. a bare LET) yields ().

Execution. The shipped crate checks and types your queries; it does not itself run them. Query<T> carries the validated text (.text) and can decode a JSON encoding of the rows (.from_json). Running a query against a live database uses the official surrealdb Rust SDK — the execution layer is the next layer on top.

Kind → Rust type mapping

The inferred surrealdb_types::Kind renders to Rust as follows.

SurrealQL kindRust type
boolbool
inti64
float · number · decimalf64
string · regexString
datetimechrono::DateTime<Utc>
uuiduuid::Uuid
durationString (serialized as 1h30m)
bytesVec<u8>
none · null()
array<T> · set<T>Vec<T>
option<T>Option<T>
record<t>String (the id decodes as a string)
closed objecta nested block-local struct
open object · geometry · range · file · anyserde_json::Value

Literal kinds follow their base — a literal string is String, a literal integer is i64, a literal array becomes a tuple, and a literal object becomes a struct. Record links also surface as a typed RecordLink<T> in the runtime, distinguishing a record<post> field from a plain string as the execution layer lands.