surrealguard_rs/lib.rs
1//! Compile-time-checked, typed SurrealQL for Rust.
2//!
3//! `surrealguard-rs` runs the [SurrealGuard](https://github.com/DrewRidley/surrealguard)
4//! analyzer against your schema *during compilation*. A wrong table, unknown
5//! field, bad function arity, or kind mismatch becomes a `cargo check` error,
6//! the result type is generated from the inferred response, and the query's
7//! parameters are type-checked against the kinds their uses imply — no build
8//! script, no language server, no runtime schema fetch.
9//!
10//! ```rust,ignore
11//! use surrealguard_rs::query;
12//!
13//! let adults = query!("SELECT name, age FROM user WHERE age >= $min", min = 18)
14//! .fetch_all(&db)
15//! .await?;
16//!
17//! for row in adults {
18//! println!("{} is {}", row.name, row.age); // String, i64 — inferred
19//! }
20//! ```
21//!
22//! # The macros
23//!
24//! This crate re-exports the proc-macros from
25//! [`surrealguard-macros`](https://crates.io/crates/surrealguard-macros) and
26//! provides the runtime types their output refers to, so depending on
27//! `surrealguard-rs` alone is enough.
28//!
29//! - [`query!`] checks a query **and** returns a typed [`Query<T>`](Query),
30//! where `T` is the inferred result rendered as a *nameless* struct. You get
31//! nested field access without ever writing a type — the same ergonomics as
32//! sqlx's anonymous `query!` record.
33//! - [`query_file!`] is [`query!`] with the SurrealQL read from a file at
34//! compile time, resolved relative to the crate root (as `sqlx::query_file!`
35//! does).
36//! - [`surql!`] checks a query and expands to its validated text as a
37//! `&'static str` — the lighter form when you only want validation.
38//!
39//! # Executing
40//!
41//! With the default `runtime` feature, a [`Query<T>`](Query) runs against the
42//! official `surrealdb` SDK. The verbs mirror sqlx:
43//!
44//! | method | returns | available when |
45//! |--------------------|------------------|------------------------------------|
46//! | [`fetch`] | `T` | always — `T` is the analyzed shape |
47//! | [`fetch_all`] | `Vec<T::Row>` | the query yields a row set |
48//! | [`fetch_one`] | `T::Row` | the query yields a row set |
49//! | [`fetch_optional`] | `Option<T::Row>` | the query yields a row set |
50//! | [`execute`] | `()` | always |
51//!
52//! [`fetch`]: Query::fetch
53//! [`fetch_all`]: Query::fetch_all
54//! [`fetch_one`]: Query::fetch_one
55//! [`fetch_optional`]: Query::fetch_optional
56//! [`execute`]: Query::execute
57//!
58//! [`fetch`](Query::fetch) is the honest one: it hands back exactly the type
59//! the analyzer inferred, whatever its shape. The row-set verbs are gated on
60//! the [`Rows`] trait, which is implemented for the shapes a row set can take —
61//! `Vec<R>` (a plain `SELECT`) and `Option<R>` (`SELECT … FROM ONLY …`). A
62//! query that returns a scalar therefore *cannot* call `fetch_all`; that is a
63//! compile error, not a runtime surprise:
64//!
65//! ```rust,ignore
66//! query!("RETURN 1 + 1").fetch_all(&db).await?;
67//! // error: this query does not return a set of rows
68//! // note: use `.fetch(&db)` to get the value this query actually returns
69//! ```
70//!
71//! # Parameters
72//!
73//! Parameters are supplied by name and checked against the kind the analyzer
74//! inferred for each use site. There is no unchecked bind: the macro rejects a
75//! missing parameter, an unknown one, and a value whose Rust type cannot become
76//! the inferred kind.
77//!
78//! ```rust,ignore
79//! query!("SELECT name FROM user WHERE age > $min", min = 18) // ok
80//! query!("SELECT name FROM user WHERE age > $min", min = "18") // compile error
81//! query!("SELECT name FROM user WHERE age > $min") // compile error: missing `min`
82//! query!("SELECT name FROM user", limit = 10) // compile error: no `$limit`
83//! ```
84//!
85//! # Multiple statements
86//!
87//! When more than one statement in a query responds, `T` is a tuple of the
88//! responding statements' types, in source order. Non-responding statements
89//! (a bare `LET`, a `DEFINE`) are skipped — they still occupy a slot in the
90//! SDK's response, and this crate accounts for that so the tuple lines up with
91//! what you wrote.
92//!
93//! ```rust,ignore
94//! let (users, posts) = query!("SELECT name FROM user; SELECT title FROM post;")
95//! .fetch(&db)
96//! .await?;
97//! ```
98//!
99//! Because a tuple is not a row set, `fetch_all` on a multi-statement query is
100//! a compile error — you must use [`fetch`](Query::fetch) and destructure.
101//!
102//! # Schema awareness
103//!
104//! The macros resolve your schema at compile time from, in order:
105//!
106//! 1. the `SURREALGUARD_SCHEMA` environment variable — a `.surql` file or a
107//! directory, relative to `CARGO_MANIFEST_DIR` unless absolute;
108//! 2. otherwise a convention path under the crate root, tried in turn:
109//! `schema/`, then `migrations/`, then `schema.surql`.
110//!
111//! A directory contributes every `.surql`/`.surrealql` file **sorted by name**,
112//! so zero-padded migrations (`0001_*.surql`, `0002_*.surql`, …) apply in
113//! order. Each schema file is tracked with `include_bytes!`, so editing it
114//! forces a rebuild of the crate that calls the macro. With no schema
115//! configured, queries are still checked for everything that does not depend on
116//! one (syntax, function arity, operators, …).
117//!
118//! # `Kind` → Rust mapping
119//!
120//! Generated types implement [`surrealdb_types::SurrealValue`], which is what
121//! the SDK's `take` actually requires — *not* `serde::Deserialize`. The mapping
122//! is chosen so every generated type decodes the `Value` the server really
123//! sends: the SDK performs no coercion whatsoever, so an approximate mapping is
124//! a runtime failure rather than a lossy read.
125//!
126//! | SurrealQL kind | Rust type |
127//! |---------------------|------------------------------------------|
128//! | `bool` | `bool` |
129//! | `int` | `i64` |
130//! | `float` | `f64` |
131//! | `decimal` | [`Decimal`](surrealdb_types::Decimal) |
132//! | `number` | [`Number`](surrealdb_types::Number) |
133//! | `string` | `String` |
134//! | `datetime` | [`Datetime`](surrealdb_types::Datetime) |
135//! | `duration` | [`Duration`](surrealdb_types::Duration) |
136//! | `uuid` | [`Uuid`](surrealdb_types::Uuid) |
137//! | `bytes` | [`Bytes`](surrealdb_types::Bytes) |
138//! | `regex` | [`Regex`](surrealdb_types::Regex) |
139//! | `record<t>` | [`RecordId`](surrealdb_types::RecordId) |
140//! | `geometry` | [`Geometry`](surrealdb_types::Geometry) |
141//! | `option<T>` | `Option<T>` |
142//! | `array<T>` / `set` | `Vec<T>` |
143//! | closed `object` | a nested, nameless struct |
144//! | `any` / open object | [`Value`](surrealdb_types::Value) |
145//!
146//! # Feature flags
147//!
148//! - **`runtime`** (default) — pulls in the `surrealdb` SDK and enables the
149//! `fetch*` / `execute` methods. Turn it off (`default-features = false`) to
150//! get checking and typing with no SDK dependency; [`Query::decode`] still
151//! decodes a response obtained some other way.
152//!
153//! The SDK is depended on with `default-features = false`, so it contributes no
154//! engine or protocol of its own — the executing crate picks those through its
155//! own `surrealdb` dependency, and Cargo unifies the two.
156
157use std::fmt;
158use std::marker::PhantomData;
159
160pub use surrealdb_types;
161use surrealdb_types::{SurrealValue, Value};
162pub use surrealguard_macros::{query, query_file, surql};
163
164#[doc(hidden)]
165pub mod _rt;
166
167/// The result of running a compile-time-checked query.
168pub type Result<T> = std::result::Result<T, Error>;
169
170/// What went wrong running a [`Query`].
171///
172/// Unlike the SDK's error (and unlike `sqlx::Error`), this carries the text of
173/// the query that produced it, so a failure names itself without the caller
174/// having to correlate it back to a call site.
175/// Boxed so that `Result<T, Error>` — which every method on [`Query`] returns —
176/// stays pointer-sized. The SDK's own error is several words wide, and this
177/// wraps it.
178#[derive(Debug)]
179pub struct Error(Box<ErrorInner>);
180
181#[derive(Debug)]
182struct ErrorInner {
183 query: &'static str,
184 kind: ErrorKind,
185}
186
187/// The specific failure behind an [`Error`].
188#[derive(Debug)]
189#[non_exhaustive]
190pub enum ErrorKind {
191 /// The database rejected or failed to run the query.
192 Database(surrealdb_types::Error),
193 /// A statement's result could not be decoded into its inferred type.
194 ///
195 /// Reaching this means the analyzer and the server disagree about a
196 /// shape — a schema that has drifted from the one compiled against is the
197 /// usual cause.
198 Decode {
199 /// The zero-based index of the statement whose result failed.
200 statement: usize,
201 /// The underlying conversion failure.
202 source: surrealdb_types::Error,
203 },
204 /// [`Query::fetch_one`] found no rows.
205 RowNotFound,
206}
207
208impl Error {
209 /// The text of the query that failed.
210 #[must_use]
211 pub const fn query(&self) -> &'static str {
212 self.0.query
213 }
214
215 /// The specific failure.
216 #[must_use]
217 pub const fn kind(&self) -> &ErrorKind {
218 &self.0.kind
219 }
220
221 fn new(query: &'static str, kind: ErrorKind) -> Self {
222 Self(Box::new(ErrorInner { query, kind }))
223 }
224}
225
226impl fmt::Display for Error {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match &self.0.kind {
229 ErrorKind::Database(source) => write!(f, "query failed: {source}")?,
230 ErrorKind::Decode { statement, source } => write!(
231 f,
232 "statement {statement} returned a value that does not match its inferred type: {source}",
233 )?,
234 ErrorKind::RowNotFound => f.write_str("expected at least one row, got none")?,
235 }
236 write!(f, "\n in: {}", self.0.query)
237 }
238}
239
240impl std::error::Error for Error {
241 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
242 match &self.0.kind {
243 ErrorKind::Database(source) | ErrorKind::Decode { source, .. } => Some(source),
244 ErrorKind::RowNotFound => None,
245 }
246 }
247}
248
249/// A result shape that is a set of rows.
250///
251/// Implemented for the shapes a SurrealQL row set can take: `Vec<R>` for a
252/// plain `SELECT`, and `Option<R>` for `SELECT … FROM ONLY …`. The row-set
253/// verbs on [`Query`] are bounded by this, so calling [`Query::fetch_all`] on a
254/// query that returns a scalar — or on a multi-statement query, whose result is
255/// a tuple — is a compile error.
256#[diagnostic::on_unimplemented(
257 message = "this query does not return a set of rows",
258 label = "not a row set",
259 note = "`fetch_all`, `fetch_one` and `fetch_optional` apply to queries whose result is a row set (`SELECT`, `CREATE`, `UPDATE`, `DELETE`)",
260 note = "use `.fetch(&db)` to get the value this query actually returns"
261)]
262pub trait Rows {
263 /// One row of the set.
264 type Row;
265
266 /// The rows, in order.
267 fn into_rows(self) -> Vec<Self::Row>;
268}
269
270impl<R> Rows for Vec<R> {
271 type Row = R;
272
273 fn into_rows(self) -> Vec<R> {
274 self
275 }
276}
277
278impl<R> Rows for Option<R> {
279 type Row = R;
280
281 fn into_rows(self) -> Vec<R> {
282 self.into_iter().collect()
283 }
284}
285
286/// A compile-time-checked query paired with its inferred result type `T`.
287///
288/// [`query!`] expands to one of these, carrying the already-validated query
289/// text, the parameters bound at the call site, and a `T` that is the inferred
290/// response rendered as a nameless struct.
291pub struct Query<T> {
292 text: &'static str,
293 statements: usize,
294 vars: Vec<(String, Value)>,
295 decode: fn(Vec<Value>) -> _rt::DecodeResult<T>,
296 _marker: PhantomData<fn() -> T>,
297}
298
299impl<T> fmt::Debug for Query<T> {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 f.debug_struct("Query")
302 .field("text", &self.text)
303 .field("bound", &self.bound())
304 .finish_non_exhaustive()
305 }
306}
307
308impl<T> Query<T> {
309 /// Builds a query. Called only by the macro expansion, which is the only
310 /// thing that can supply a `decode` matching the analyzed `text`.
311 #[doc(hidden)]
312 #[must_use]
313 pub const fn new(
314 text: &'static str,
315 statements: usize,
316 decode: fn(Vec<Value>) -> _rt::DecodeResult<T>,
317 ) -> Self {
318 Self {
319 text,
320 statements,
321 vars: Vec::new(),
322 decode,
323 _marker: PhantomData,
324 }
325 }
326
327 /// Binds one parameter. Called only by the macro expansion, which pins the
328 /// value's type to the kind the analyzer inferred for that parameter.
329 #[doc(hidden)]
330 #[must_use]
331 pub fn bind(mut self, name: &str, value: impl SurrealValue) -> Self {
332 self.vars.push((name.to_owned(), value.into_value()));
333 self
334 }
335
336 /// The validated SurrealQL text.
337 #[must_use]
338 pub const fn text(&self) -> &'static str {
339 self.text
340 }
341
342 /// The number of top-level statements, which is also the number of result
343 /// slots the server returns.
344 #[must_use]
345 pub const fn statements(&self) -> usize {
346 self.statements
347 }
348
349 /// The names of the parameters bound at the call site, in bind order.
350 #[must_use]
351 pub fn bound(&self) -> Vec<&str> {
352 self.vars.iter().map(|(name, _)| name.as_str()).collect()
353 }
354
355 /// Decodes one [`Value`] per statement into the inferred result type.
356 ///
357 /// This is the whole typed surface with the `runtime` feature off, and the
358 /// seam the `fetch*` methods go through — a response obtained any other way
359 /// can be decoded with it. `values` must hold one entry per statement, in
360 /// source order.
361 ///
362 /// # Errors
363 ///
364 /// Returns [`ErrorKind::Decode`] if a statement's value does not match the
365 /// type inferred for it.
366 pub fn decode(&self, values: Vec<Value>) -> Result<T> {
367 (self.decode)(values).map_err(|error| {
368 Error::new(
369 self.text,
370 ErrorKind::Decode {
371 statement: error.statement,
372 source: error.source,
373 },
374 )
375 })
376 }
377}
378
379#[cfg(feature = "runtime")]
380impl<T> Query<T> {
381 /// Runs the query and returns one [`Value`] per statement.
382 async fn raw<C: surrealdb::Connection>(&self, db: &surrealdb::Surreal<C>) -> Result<Vec<Value>> {
383 let mut request = db.query(self.text);
384 for (name, value) in &self.vars {
385 request = request.bind((name.clone(), value.clone()));
386 }
387 let mut response = request
388 .await
389 .map_err(|source| Error::new(self.text, ErrorKind::Database(source)))?;
390
391 let mut values = Vec::with_capacity(self.statements);
392 for statement in 0..self.statements {
393 values.push(
394 response
395 .take::<Value>(statement)
396 .map_err(|source| Error::new(self.text, ErrorKind::Database(source)))?,
397 );
398 }
399 Ok(values)
400 }
401
402 /// Runs the query and returns exactly the type the analyzer inferred.
403 ///
404 /// This is the shape-preserving verb: a `SELECT` gives a `Vec` of rows, a
405 /// `RETURN` gives the value, `SELECT … FROM ONLY` gives an `Option`, and a
406 /// multi-statement query gives a tuple. When the result is a row set,
407 /// [`fetch_all`](Self::fetch_all) and friends read better.
408 ///
409 /// # Errors
410 ///
411 /// Returns [`ErrorKind::Database`] if the query failed, or
412 /// [`ErrorKind::Decode`] if the response does not match the inferred type.
413 pub async fn fetch<C: surrealdb::Connection>(self, db: &surrealdb::Surreal<C>) -> Result<T> {
414 let values = self.raw(db).await?;
415 self.decode(values)
416 }
417
418 /// Runs the query and discards its result.
419 ///
420 /// # Errors
421 ///
422 /// Returns [`ErrorKind::Database`] if the query failed.
423 pub async fn execute<C: surrealdb::Connection>(self, db: &surrealdb::Surreal<C>) -> Result<()> {
424 self.raw(db).await.map(|_| ())
425 }
426}
427
428/// The row-set verbs.
429///
430/// `T: Rows` is a bound on each *method* rather than on the impl block on
431/// purpose: an unsatisfied method bound is an E0277, which honours the
432/// `#[diagnostic::on_unimplemented]` note on [`Rows`], whereas an unsatisfied
433/// impl-block bound is an E0599 that reports only `i64: Rows` and no guidance.
434#[cfg(feature = "runtime")]
435impl<T> Query<T> {
436 /// Runs the query and returns every row.
437 ///
438 /// # Errors
439 ///
440 /// Returns [`ErrorKind::Database`] if the query failed, or
441 /// [`ErrorKind::Decode`] if a row does not match its inferred type.
442 pub async fn fetch_all<C: surrealdb::Connection>(
443 self,
444 db: &surrealdb::Surreal<C>,
445 ) -> Result<Vec<T::Row>>
446 where
447 T: Rows,
448 {
449 self.fetch(db).await.map(Rows::into_rows)
450 }
451
452 /// Runs the query and returns its first row, failing if there are none.
453 ///
454 /// # Errors
455 ///
456 /// Returns [`ErrorKind::RowNotFound`] if the query matched nothing, plus
457 /// the errors [`fetch_all`](Self::fetch_all) returns.
458 pub async fn fetch_one<C: surrealdb::Connection>(
459 self,
460 db: &surrealdb::Surreal<C>,
461 ) -> Result<T::Row>
462 where
463 T: Rows,
464 {
465 let text = self.text;
466 let mut rows = self.fetch(db).await.map(Rows::into_rows)?;
467 if rows.is_empty() {
468 return Err(Error::new(text, ErrorKind::RowNotFound));
469 }
470 Ok(rows.swap_remove(0))
471 }
472
473 /// Runs the query and returns its first row, if any.
474 ///
475 /// # Errors
476 ///
477 /// Returns [`ErrorKind::Database`] if the query failed, or
478 /// [`ErrorKind::Decode`] if a row does not match its inferred type.
479 pub async fn fetch_optional<C: surrealdb::Connection>(
480 self,
481 db: &surrealdb::Surreal<C>,
482 ) -> Result<Option<T::Row>>
483 where
484 T: Rows,
485 {
486 self.fetch(db)
487 .await
488 .map(|rows| rows.into_rows().into_iter().next())
489 }
490}