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).
query!("…")— check and return a typedQuery<T>whoseTis the inferred, nameless result type.surql!("…")— check and expand to the query text as a&'static str(the lighter form when you only want validation).
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");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Schema resolution
The macro resolves your schema at compile time from, in order:
- the
SURREALGUARD_SCHEMAenvironment variable — a.surqlfile or a directory, taken relative toCARGO_MANIFEST_DIRunless absolute; - otherwise a convention path under the crate root, first match wins:
schema/,migrations/, thenschema.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 ().
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 kind | Rust type |
|---|---|
| bool | bool |
| int | i64 |
| float · number · decimal | f64 |
| string · regex | String |
| datetime | chrono::DateTime<Utc> |
| uuid | uuid::Uuid |
| duration | String (serialized as 1h30m) |
| bytes | Vec<u8> |
| none · null | () |
| array<T> · set<T> | Vec<T> |
| option<T> | Option<T> |
| record<t> | String (the id decodes as a string) |
| closed object | a nested block-local struct |
| open object · geometry · range · file · any | serde_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.