Skip to main content

surql/
error.rs

1//! Error types for the `surql` crate.
2//!
3//! Follows a strict no-panic policy: every fallible operation returns
4//! [`Result<T, SurqlError>`]. The [`SurqlError`] enum unifies every error
5//! kind the library can produce. Per-subsystem error variants mirror the
6//! Python (`oneiriq-surql`) exception hierarchy.
7//!
8//! ## Examples
9//!
10//! ```
11//! use surql::error::{Context, Result, SurqlError};
12//!
13//! fn parse_id(raw: &str) -> Result<(String, String)> {
14//!     let (table, id) = raw.split_once(':').ok_or_else(|| {
15//!         SurqlError::Validation {
16//!             reason: format!("expected table:id, got {raw:?}"),
17//!         }
18//!     })?;
19//!     Ok((table.to_owned(), id.to_owned()))
20//! }
21//!
22//! let outcome = parse_id("user").context("parsing record id").unwrap_err();
23//! assert!(outcome.to_string().contains("parsing record id"));
24//! ```
25
26use std::fmt;
27
28/// Convenient alias for results produced by this crate.
29pub type Result<T> = std::result::Result<T, SurqlError>;
30
31/// Unified error type for the `surql` crate.
32///
33/// Each variant corresponds to one subsystem. Variants can be wrapped with
34/// additional context via [`Context::context`].
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum SurqlError {
37    /// General database error (analogue of Python `DatabaseError`).
38    Database {
39        /// Human-readable explanation.
40        reason: String,
41    },
42    /// Connection failed, timed out, or was closed unexpectedly.
43    Connection {
44        /// Human-readable explanation.
45        reason: String,
46    },
47    /// A query failed at the database or during result decoding.
48    Query {
49        /// Human-readable explanation.
50        reason: String,
51    },
52    /// A transaction could not be started, committed, or rolled back.
53    Transaction {
54        /// Human-readable explanation.
55        reason: String,
56    },
57    /// Ambient connection context was missing or misconfigured.
58    Context {
59        /// Human-readable explanation.
60        reason: String,
61    },
62    /// Named-connection registry lookup or registration failed.
63    Registry {
64        /// Human-readable explanation.
65        reason: String,
66    },
67    /// Live/streaming query error.
68    Streaming {
69        /// Human-readable explanation.
70        reason: String,
71    },
72    /// Input failed validation (invalid identifier, malformed id, etc.).
73    Validation {
74        /// Human-readable explanation.
75        reason: String,
76    },
77    /// Schema parser could not understand the schema text or response.
78    SchemaParse {
79        /// Human-readable explanation.
80        reason: String,
81    },
82    /// Error while discovering migration files on disk.
83    MigrationDiscovery {
84        /// Human-readable explanation.
85        reason: String,
86    },
87    /// Error while loading an individual migration.
88    MigrationLoad {
89        /// Human-readable explanation.
90        reason: String,
91    },
92    /// Error while generating a migration from a schema diff.
93    MigrationGeneration {
94        /// Human-readable explanation.
95        reason: String,
96    },
97    /// Error while executing a migration against the database.
98    MigrationExecution {
99        /// Human-readable explanation.
100        reason: String,
101    },
102    /// Error while reading or writing migration history.
103    MigrationHistory {
104        /// Human-readable explanation.
105        reason: String,
106    },
107    /// Error while squashing migrations.
108    MigrationSquash {
109        /// Human-readable explanation.
110        reason: String,
111    },
112    /// Error raised by the schema file watcher.
113    MigrationWatcher {
114        /// Human-readable explanation.
115        reason: String,
116    },
117    /// Multi-environment orchestration failed.
118    Orchestration {
119        /// Human-readable explanation.
120        reason: String,
121    },
122    /// JSON encode or decode failure.
123    Serialization {
124        /// Human-readable explanation.
125        reason: String,
126    },
127    /// Filesystem / generic I/O error.
128    Io {
129        /// Human-readable explanation.
130        reason: String,
131    },
132    /// An existing [`SurqlError`] with extra context prepended.
133    WithContext {
134        /// Underlying error.
135        source: Box<SurqlError>,
136        /// Context description added at the call site.
137        context: String,
138    },
139}
140
141impl fmt::Display for SurqlError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            Self::Database { reason } => write!(f, "database error: {reason}"),
145            Self::Connection { reason } => write!(f, "connection error: {reason}"),
146            Self::Query { reason } => write!(f, "query error: {reason}"),
147            Self::Transaction { reason } => write!(f, "transaction error: {reason}"),
148            Self::Context { reason } => write!(f, "context error: {reason}"),
149            Self::Registry { reason } => write!(f, "registry error: {reason}"),
150            Self::Streaming { reason } => write!(f, "streaming error: {reason}"),
151            Self::Validation { reason } => write!(f, "validation error: {reason}"),
152            Self::SchemaParse { reason } => write!(f, "schema parse error: {reason}"),
153            Self::MigrationDiscovery { reason } => {
154                write!(f, "migration discovery error: {reason}")
155            }
156            Self::MigrationLoad { reason } => write!(f, "migration load error: {reason}"),
157            Self::MigrationGeneration { reason } => {
158                write!(f, "migration generation error: {reason}")
159            }
160            Self::MigrationExecution { reason } => {
161                write!(f, "migration execution error: {reason}")
162            }
163            Self::MigrationHistory { reason } => write!(f, "migration history error: {reason}"),
164            Self::MigrationSquash { reason } => write!(f, "migration squash error: {reason}"),
165            Self::MigrationWatcher { reason } => write!(f, "migration watcher error: {reason}"),
166            Self::Orchestration { reason } => write!(f, "orchestration error: {reason}"),
167            Self::Serialization { reason } => write!(f, "serialization error: {reason}"),
168            Self::Io { reason } => write!(f, "io error: {reason}"),
169            Self::WithContext { source, context } => write!(f, "{context}: {source}"),
170        }
171    }
172}
173
174impl std::error::Error for SurqlError {
175    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
176        match self {
177            Self::WithContext { source, .. } => Some(source.as_ref()),
178            _ => None,
179        }
180    }
181}
182
183impl From<std::io::Error> for SurqlError {
184    fn from(err: std::io::Error) -> Self {
185        Self::Io {
186            reason: err.to_string(),
187        }
188    }
189}
190
191impl From<serde_json::Error> for SurqlError {
192    fn from(err: serde_json::Error) -> Self {
193        Self::Serialization {
194            reason: err.to_string(),
195        }
196    }
197}
198
199/// Extension trait for attaching contextual messages to a [`Result`].
200///
201/// Mirrors the `Context` trait used in `oniq`; the attached message is
202/// formatted as `"{context}: {source}"` in [`SurqlError::WithContext`].
203///
204/// ## Examples
205///
206/// ```
207/// use surql::error::{Context, SurqlError};
208///
209/// let result: Result<(), SurqlError> = Err(SurqlError::Query {
210///     reason: "syntax near SELECT".into(),
211/// });
212/// let wrapped = result.context("loading user").unwrap_err();
213/// assert!(wrapped.to_string().starts_with("loading user: query error"));
214/// ```
215pub trait Context<T> {
216    /// Attach the given context to this result's error (no-op on `Ok`).
217    fn context(self, context: impl Into<String>) -> Result<T>;
218}
219
220impl<T, E> Context<T> for std::result::Result<T, E>
221where
222    E: Into<SurqlError>,
223{
224    fn context(self, context: impl Into<String>) -> Result<T> {
225        self.map_err(|e| SurqlError::WithContext {
226            source: Box::new(e.into()),
227            context: context.into(),
228        })
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn display_includes_reason() {
238        let err = SurqlError::Query {
239            reason: "missing table".into(),
240        };
241        assert_eq!(err.to_string(), "query error: missing table");
242    }
243
244    #[test]
245    fn context_wraps_error() {
246        let base: Result<()> = Err(SurqlError::Connection {
247            reason: "refused".into(),
248        });
249        let wrapped = base.context("dialing surrealdb").unwrap_err();
250        assert_eq!(
251            wrapped.to_string(),
252            "dialing surrealdb: connection error: refused"
253        );
254    }
255
256    #[test]
257    fn context_is_noop_on_ok() {
258        let ok: Result<u32> = Ok(1);
259        assert_eq!(ok.context("should not fire").unwrap(), 1);
260    }
261
262    #[test]
263    fn from_serde_json_error() {
264        let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
265        let err: SurqlError = json_err.into();
266        assert!(matches!(err, SurqlError::Serialization { .. }));
267    }
268
269    #[test]
270    fn from_io_error() {
271        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
272        let err: SurqlError = io_err.into();
273        assert!(matches!(err, SurqlError::Io { .. }));
274    }
275
276    #[test]
277    fn source_chain_is_reported() {
278        let err = SurqlError::WithContext {
279            source: Box::new(SurqlError::Validation {
280                reason: "bad".into(),
281            }),
282            context: "outer".into(),
283        };
284        let source = std::error::Error::source(&err);
285        assert!(source.is_some());
286    }
287}