Introduction
SurrealGuard is static analysis and type inference for SurrealQL. It parses your .surql schema and queries into a typed, span-carrying AST, infers the response type of every statement, and reports violations of each construct's contract — before a query ever reaches SurrealDB.
The same engine powers four surfaces: a CLI for CI and agents, a language server, a Rust proc-macro, and a set of TypeScript packages. You write ordinary SurrealQL; SurrealGuard checks it and hands back a fully typed result.
Why contract-first
SurrealDB is permissive at runtime. Cross-kind comparisons order by kind instead of failing, coercions succeed silently, and a misspelled field just returns NONE. That is exactly where bugs hide — the database accepts them.
SurrealGuard is contract-first: every construct has a contract (what the author must mean for the statement to make sense), and the analyzer reports violations of that contract even when the engine would happily execute the query. Severity is intrinsic to each finding; whether SurrealDB throws, tolerates, or coerces is irrelevant to whether the contract was broken.
What it analyzes
- Full statement coverage — SELECT (projections, graph traversals, FETCH/SPLIT/GROUP/OMIT), the six mutations, RELATE, LET/RETURN/IF/FOR/blocks, transactions, DEFINE/REMOVE/ALTER, LIVE SELECT/KILL, and the rest.
- Real type inference — response kinds as upstream
surrealdb_types::Kind: closed object literals for known rows, IF/ELSE unions, record-link and graph-edge shapes, the full builtin function table plusfn::declarations, closure and subquery inference, and constant-value evaluation. - 90 contract diagnostics across eight families — schema references, types, graph, statement misuse, functions, parameters, lints, and version compatibility.
- Control-flow type narrowing — occurrence typing that narrows
option<T>andrecord<A|B>unions through= NONEguards andtype::table()discriminants (including nested field paths), so a branch sees the narrowed type and unreachable code after a diverging branch is flagged. - PERMISSIONS-predicate analysis — the boolean inside a
PERMISSIONS FORclause is checked for undefined fields, undefinedfn::calls, non-boolean results, and always-false predicates. - Parameter constraints — every
$parama source reads is exported with the kind and value domain its uses imply (UPDATE user SET age = $age→age: int). - Byte-precise spans on every finding, so editor squiggles and inline edits land on the exact token — even inside a query embedded in a host file.
The compiler is the checker (Rust)
In Rust, the query! macro runs the real analyzer during compilation and generates the result type. A wrong table, unknown field, bad arity, or kind mismatch is a cargo check error — no codegen step, no language server, no runtime schema fetch.
use surrealguard_rs::query; let users = query!("SELECT name, age FROM user"); // users: Query<Vec<{ name: String, age: i64 }>> ← nameless, inferred let bad = query!("SELECT name, ssn FROM user"); // error: SurrealGuard rejected this query: // [E1002] unknown field `ssn` on table `user`
Typed queries, no wrapper (TypeScript)
SurrealGuardClient extends the SurrealDB SDK, so new SurrealGuardClient() has every SDK method plus typed queries. Pass a string literal to db.query and its result and parameters are inferred from the query text. Missing or wrong params are compile errors; dynamic strings degrade to unknown[] and still run.
// One import: the generated file re-exports a ready client and loads the registry. import { SurrealGuardClient } from "./surrealguard.generated"; const db = new SurrealGuardClient(); await db.connect("ws://localhost:8000/rpc"); // SurrealDB returns one result per statement, so destructure the first result. const [users] = await db.query("SELECT name FROM user WHERE team = $team", { team: "red" }); // ^ Array<{ name: string }> — params required + typed from the text
Install
# CLI — zero install via npm npx surrealguard check --json # or install from source (Rust) cargo install --git https://github.com/DrewRidley/surrealguard surrealguard # Rust library — git dependency in Cargo.toml (until the crates.io release) # surrealguard-rs = { git = "https://github.com/DrewRidley/surrealguard" } # TypeScript packages — published on npm npm i @surrealguard/client
See Getting started for a full walkthrough — init, surrealguard.toml, and your first check.