Skip to main content

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}