Skip to main content

surrealdb_types/
error.rs

1use std::convert::Infallible;
2use std::fmt;
3use std::time::Duration;
4
5use crate::{Kind, SurrealValue, ToSql, Value};
6
7// -----------------------------------------------------------------------------
8// JSON-RPC 2.0 and SurrealDB-specific error codes (wire backwards compatibility)
9// -----------------------------------------------------------------------------
10
11/// Numeric error codes used on the wire for RPC. Kept for backwards compatibility.
12#[allow(missing_docs)]
13mod code {
14	pub const PARSE_ERROR: i64 = -32700;
15	pub const INVALID_REQUEST: i64 = -32600;
16	pub const METHOD_NOT_FOUND: i64 = -32601;
17	pub const METHOD_NOT_ALLOWED: i64 = -32602;
18	pub const INVALID_PARAMS: i64 = -32603;
19	pub const LIVE_QUERY_NOT_SUPPORTED: i64 = -32604;
20	pub const BAD_LIVE_QUERY_CONFIG: i64 = -32605;
21	pub const BAD_GRAPHQL_CONFIG: i64 = -32606;
22	pub const INTERNAL_ERROR: i64 = -32000;
23	pub const CLIENT_SIDE_ERROR: i64 = -32001;
24	pub const INVALID_AUTH: i64 = -32002;
25	pub const QUERY_NOT_EXECUTED: i64 = -32003;
26	pub const QUERY_TIMEDOUT: i64 = -32004;
27	pub const QUERY_CANCELLED: i64 = -32005;
28	pub const QUERY_TRANSACTION_CONFLICT: i64 = -32009;
29	pub const THROWN: i64 = -32006;
30	pub const SERIALIZATION_ERROR: i64 = -32007;
31	pub const DESERIALIZATION_ERROR: i64 = -32008;
32}
33
34/// Default wire code when none is specified (e.g. for deserialization of older wire format).
35fn default_code() -> i64 {
36	code::INTERNAL_ERROR
37}
38
39// -----------------------------------------------------------------------------
40// Public API error type (wire-friendly, non-lossy, supports chaining)
41// -----------------------------------------------------------------------------
42
43/// Represents an error in SurrealDB
44///
45/// Designed to be returned from public APIs (including over the wire). It is
46/// wire-friendly and non-lossy: serialization preserves `kind`, `message`,
47/// and optional `details`. Use this type whenever an error crosses
48/// an API boundary (e.g. server response, SDK method return).
49///
50/// The `details` field is flattened into the serialized object, so the wire
51/// format contains `kind` (string) and optionally `details` (object) at the
52/// same level as `code` and `message`. The optional `cause` field allows
53/// error chaining so that SDKs can receive and display full error chains.
54#[derive(Debug, Clone, PartialEq, Eq, SurrealValue)]
55#[surreal(crate = "crate")]
56pub struct Error {
57	/// Wire-only error code for RPC backwards compatibility.
58	#[surreal(default = "default_code")]
59	code: i64,
60	/// Human-readable error message describing the error.
61	message: String,
62	/// The error kind and optional structured details. The kind is derived from the variant.
63	/// Flattened into the parent object: contributes `kind` and optionally `details` fields.
64	#[surreal(flatten)]
65	details: ErrorDetails,
66	/// Optional underlying cause for error chaining. Serialized as a nested error object on the
67	/// wire.
68	#[surreal(default)]
69	cause: Option<Box<Error>>,
70}
71
72impl Error {
73	/// Validation error (parse error, invalid request or params), with optional structured details.
74	/// When `details` is provided, the wire code is set from the variant (e.g. `Parse` →
75	/// `PARSE_ERROR`).
76	pub fn validation(message: String, details: impl Into<Option<ValidationError>>) -> Self {
77		let details = details.into();
78		let code = details
79			.as_ref()
80			.map(|d| match d {
81				ValidationError::Parse => code::PARSE_ERROR,
82				ValidationError::InvalidRequest => code::INVALID_REQUEST,
83				ValidationError::InvalidParams => code::INVALID_PARAMS,
84				ValidationError::NamespaceEmpty
85				| ValidationError::DatabaseEmpty
86				| ValidationError::InvalidParameter {
87					..
88				}
89				| ValidationError::InvalidContent {
90					..
91				}
92				| ValidationError::InvalidMerge {
93					..
94				} => code::INVALID_REQUEST,
95			})
96			.unwrap_or(code::INTERNAL_ERROR);
97		Self {
98			message,
99			code,
100			details: ErrorDetails::Validation(details),
101			cause: None,
102		}
103	}
104
105	/// Not-allowed error (e.g. method, scripting, function, net target), with optional
106	/// structured details. When `details` is provided, the wire code is set from the variant.
107	pub fn not_allowed(message: String, details: impl Into<Option<NotAllowedError>>) -> Self {
108		let details = details.into();
109		let code = details
110			.as_ref()
111			.map(|d| match d {
112				NotAllowedError::Auth(auth_error) => match auth_error {
113					AuthError::TokenExpired => code::INVALID_AUTH,
114					AuthError::SessionExpired => code::INTERNAL_ERROR,
115					AuthError::InvalidAuth
116					| AuthError::UnexpectedAuth
117					| AuthError::MissingUserOrPass
118					| AuthError::NoSigninTarget
119					| AuthError::InvalidPass
120					| AuthError::TokenMakingFailed
121					| AuthError::InvalidRole {
122						..
123					}
124					| AuthError::NotAllowed {
125						..
126					}
127					| AuthError::InvalidSignup => code::INVALID_AUTH,
128				},
129				NotAllowedError::Method {
130					..
131				}
132				| NotAllowedError::Scripting
133				| NotAllowedError::Function {
134					..
135				}
136				| NotAllowedError::Target {
137					..
138				} => code::METHOD_NOT_ALLOWED,
139			})
140			.unwrap_or(code::INTERNAL_ERROR);
141		Self {
142			message,
143			code,
144			details: ErrorDetails::NotAllowed(details),
145			cause: None,
146		}
147	}
148
149	/// Configuration error (feature or config not supported), with optional structured details.
150	/// When `details` is provided, the wire code is set from the variant.
151	pub fn configuration(message: String, details: impl Into<Option<ConfigurationError>>) -> Self {
152		let details = details.into();
153		let code = details
154			.as_ref()
155			.map(|d| match d {
156				ConfigurationError::LiveQueryNotSupported => code::LIVE_QUERY_NOT_SUPPORTED,
157				ConfigurationError::BadLiveQueryConfig => code::BAD_LIVE_QUERY_CONFIG,
158				ConfigurationError::BadGraphqlConfig => code::BAD_GRAPHQL_CONFIG,
159			})
160			.unwrap_or(code::INTERNAL_ERROR);
161		Self {
162			message,
163			code,
164			details: ErrorDetails::Configuration(details),
165			cause: None,
166		}
167	}
168
169	/// User-thrown error (e.g. from THROW in SurrealQL). Sets wire code for RPC.
170	pub fn thrown(message: String) -> Self {
171		Self {
172			message,
173			code: code::THROWN,
174			details: ErrorDetails::Thrown,
175			cause: None,
176		}
177	}
178
179	/// Query execution error (not executed, timeout, cancelled), with optional structured details.
180	/// When `details` is provided, the wire code is set from the variant.
181	pub fn query(message: String, details: impl Into<Option<QueryError>>) -> Self {
182		let details = details.into();
183		let code = details
184			.as_ref()
185			.map(|d| match d {
186				QueryError::NotExecuted => code::QUERY_NOT_EXECUTED,
187				QueryError::TimedOut {
188					..
189				} => code::QUERY_TIMEDOUT,
190				QueryError::Cancelled => code::QUERY_CANCELLED,
191				QueryError::TransactionConflict => code::QUERY_TRANSACTION_CONFLICT,
192			})
193			.unwrap_or(code::INTERNAL_ERROR);
194		Self {
195			message,
196			code,
197			details: ErrorDetails::Query(details),
198			cause: None,
199		}
200	}
201
202	/// Serialisation or deserialisation error, with optional structured details.
203	/// When `details` is provided, the wire code is set from the variant.
204	pub fn serialization(message: String, details: impl Into<Option<SerializationError>>) -> Self {
205		let details = details.into();
206		let code = details
207			.as_ref()
208			.map(|d| match d {
209				SerializationError::Serialization => code::SERIALIZATION_ERROR,
210				SerializationError::Deserialization => code::DESERIALIZATION_ERROR,
211			})
212			.unwrap_or(code::INTERNAL_ERROR);
213		Self {
214			message,
215			code,
216			details: ErrorDetails::Serialization(details),
217			cause: None,
218		}
219	}
220
221	/// Resource not found (e.g. table, record, namespace, RPC method), with optional
222	/// structured details. When `details` is `NotFoundError::Method`, the wire code is set to
223	/// `METHOD_NOT_FOUND` for RPC backwards compatibility.
224	pub fn not_found(message: String, details: impl Into<Option<NotFoundError>>) -> Self {
225		let details = details.into();
226		let code = details
227			.as_ref()
228			.and_then(|d| match d {
229				NotFoundError::Method {
230					..
231				} => Some(code::METHOD_NOT_FOUND),
232				_ => None,
233			})
234			.unwrap_or(code::INTERNAL_ERROR);
235		Self {
236			message,
237			code,
238			details: ErrorDetails::NotFound(details),
239			cause: None,
240		}
241	}
242
243	/// Resource already exists (e.g. table, record), with optional structured details.
244	pub fn already_exists(message: String, details: impl Into<Option<AlreadyExistsError>>) -> Self {
245		let details = details.into();
246		Self {
247			message,
248			code: code::INTERNAL_ERROR,
249			details: ErrorDetails::AlreadyExists(details),
250			cause: None,
251		}
252	}
253
254	/// Connection error (e.g. uninitialised, already connected), with optional structured details.
255	/// Used in the SDK for client-side connection state errors.
256	pub fn connection(message: String, details: impl Into<Option<ConnectionError>>) -> Self {
257		let details = details.into();
258		Self {
259			message,
260			code: code::CLIENT_SIDE_ERROR,
261			details: ErrorDetails::Connection(details),
262			cause: None,
263		}
264	}
265
266	/// Internal or unexpected error (server or client). Sets wire code for RPC.
267	pub fn internal(message: String) -> Self {
268		Self {
269			message,
270			code: code::INTERNAL_ERROR,
271			details: ErrorDetails::Internal,
272			cause: None,
273		}
274	}
275
276	/// Context for error chaining.
277	fn context(message: String) -> Self {
278		Self {
279			code: code::INTERNAL_ERROR,
280			message,
281			details: ErrorDetails::Context,
282			cause: None,
283		}
284	}
285
286	/// Build an error from a message and pre-parsed [`ErrorDetails`].
287	/// Uses [`default_code`] for the wire code. Intended for deserialization paths
288	/// that already have a typed `ErrorDetails` (e.g. via `ErrorDetails::from_value`).
289	#[doc(hidden)]
290	pub fn from_details(message: String, details: ErrorDetails) -> Self {
291		Self {
292			code: default_code(),
293			message,
294			details,
295			cause: None,
296		}
297	}
298
299	/// Build an error from message, details, and optional cause. For deserialization when cause is
300	/// present.
301	#[doc(hidden)]
302	pub fn from_details_with_cause(
303		message: String,
304		details: ErrorDetails,
305		cause: Option<Box<Error>>,
306	) -> Self {
307		Self {
308			code: default_code(),
309			message,
310			details,
311			cause,
312		}
313	}
314
315	/// Build an error from the query-result wire shape (message, optional kind string, details).
316	/// Used when deserialising query result error payloads that do not include `code`. Uses
317	/// [`default_code`] and defaults kind to "Internal" when not present.
318	#[doc(hidden)]
319	pub fn from_parts(message: String, kind: Option<&str>, details: Option<Value>) -> Self {
320		let kind_str = kind.unwrap_or("Internal");
321		let typed_details = match details {
322			Some(v) => ErrorDetails::from_value_with_kind_str(kind_str, v)
323				.unwrap_or_else(|_| ErrorDetails::from_kind_str(kind_str)),
324			None => ErrorDetails::from_kind_str(kind_str),
325		};
326		Self {
327			code: default_code(),
328			message,
329			details: typed_details,
330			cause: None,
331		}
332	}
333
334	/// Returns the optional underlying cause for error chaining.
335	pub fn cause(&self) -> Option<&Error> {
336		self.cause.as_deref()
337	}
338
339	/// Returns an error with the given cause attached. Consumes `self`.
340	pub fn with_cause(mut self, cause: Error) -> Self {
341		self.cause = Some(Box::new(cause));
342		self
343	}
344
345	/// Build an [`Error`] from an [`anyhow::Error`], surfacing only the outermost message.
346	///
347	/// SECURITY: do not copy the anyhow source chain to clients. Inner chain messages
348	/// frequently contain raw field/computed values, storage-engine details, and other
349	/// internal context that was attached by intermediate `?` / `with_context` callers
350	/// without any redaction. Boundary call sites that want to attach an explicit,
351	/// reviewed cause can use `with_cause` directly.
352	#[doc(hidden)]
353	#[allow(clippy::needless_pass_by_value)] // Public API: callers pass owned `anyhow::Error`.
354	pub fn from_anyhow_with_chain(error: anyhow::Error) -> Self {
355		Self::internal(error.to_string())
356	}
357
358	/// Returns the kind string for this error (e.g. "NotAllowed", "Internal").
359	pub fn kind_str(&self) -> &'static str {
360		self.details.kind_str()
361	}
362
363	/// Returns the human-readable error message.
364	pub fn message(&self) -> &str {
365		&self.message
366	}
367
368	/// Returns the error details (always present). The variant determines the error kind.
369	pub fn details(&self) -> &ErrorDetails {
370		&self.details
371	}
372
373	/// Returns true if this is a validation error.
374	pub fn is_validation(&self) -> bool {
375		self.details.is_validation()
376	}
377
378	/// Returns true if this is a configuration error.
379	pub fn is_configuration(&self) -> bool {
380		self.details.is_configuration()
381	}
382
383	/// Returns true if this is a query error.
384	pub fn is_query(&self) -> bool {
385		self.details.is_query()
386	}
387
388	/// Returns true if this is a serialization error.
389	pub fn is_serialization(&self) -> bool {
390		self.details.is_serialization()
391	}
392
393	/// Returns true if this is a not-allowed error.
394	pub fn is_not_allowed(&self) -> bool {
395		self.details.is_not_allowed()
396	}
397
398	/// Returns true if this is a not-found error.
399	pub fn is_not_found(&self) -> bool {
400		self.details.is_not_found()
401	}
402
403	/// Returns true if this is an already-exists error.
404	pub fn is_already_exists(&self) -> bool {
405		self.details.is_already_exists()
406	}
407
408	/// Returns true if this is a connection error.
409	pub fn is_connection(&self) -> bool {
410		self.details.is_connection()
411	}
412
413	/// Returns true if this is a user-thrown error.
414	pub fn is_thrown(&self) -> bool {
415		self.details.is_thrown()
416	}
417
418	/// Returns true if this is an internal error.
419	pub fn is_internal(&self) -> bool {
420		self.details.is_internal()
421	}
422
423	/// Returns true if this is a context error (used for error chaining).
424	pub fn is_context(&self) -> bool {
425		self.details.is_context()
426	}
427
428	/// Returns structured validation error details, if this is a validation error with specifics.
429	pub fn validation_details(&self) -> Option<&ValidationError> {
430		match &self.details {
431			ErrorDetails::Validation(d) => d.as_ref(),
432			_ => None,
433		}
434	}
435
436	/// Returns structured not-allowed error details, if this is a not-allowed error with specifics.
437	pub fn not_allowed_details(&self) -> Option<&NotAllowedError> {
438		match &self.details {
439			ErrorDetails::NotAllowed(d) => d.as_ref(),
440			_ => None,
441		}
442	}
443
444	/// Returns structured configuration error details, if this is a configuration error with
445	/// specifics.
446	pub fn configuration_details(&self) -> Option<&ConfigurationError> {
447		match &self.details {
448			ErrorDetails::Configuration(d) => d.as_ref(),
449			_ => None,
450		}
451	}
452
453	/// Returns structured serialization error details, if this is a serialization error with
454	/// specifics.
455	pub fn serialization_details(&self) -> Option<&SerializationError> {
456		match &self.details {
457			ErrorDetails::Serialization(d) => d.as_ref(),
458			_ => None,
459		}
460	}
461
462	/// Returns structured not-found error details, if this is a not-found error with specifics.
463	pub fn not_found_details(&self) -> Option<&NotFoundError> {
464		match &self.details {
465			ErrorDetails::NotFound(d) => d.as_ref(),
466			_ => None,
467		}
468	}
469
470	/// Returns structured query error details, if this is a query error with specifics.
471	pub fn query_details(&self) -> Option<&QueryError> {
472		match &self.details {
473			ErrorDetails::Query(d) => d.as_ref(),
474			_ => None,
475		}
476	}
477
478	/// Returns structured already-exists error details, if this is an already-exists error with
479	/// specifics.
480	pub fn already_exists_details(&self) -> Option<&AlreadyExistsError> {
481		match &self.details {
482			ErrorDetails::AlreadyExists(d) => d.as_ref(),
483			_ => None,
484		}
485	}
486
487	/// Returns structured connection error details, if this is a connection error with specifics.
488	pub fn connection_details(&self) -> Option<&ConnectionError> {
489		match &self.details {
490			ErrorDetails::Connection(d) => d.as_ref(),
491			_ => None,
492		}
493	}
494}
495
496// -----------------------------------------------------------------------------
497// ErrorDetails enum (typed wrapper for all detail variants)
498// -----------------------------------------------------------------------------
499
500/// Typed error details. Each variant represents an error kind and optionally
501/// wraps the detail enum for that kind. This replaces the separate `kind` field
502/// on [`Error`] -- the kind is derived from the variant.
503///
504/// Rust users can pattern-match directly:
505/// ```ignore
506/// match error.details() {
507///     ErrorDetails::NotAllowed(Some(NotAllowedError::Auth(AuthError::TokenExpired))) => ...,
508///     ErrorDetails::NotFound(Some(NotFoundError::Table { name })) => ...,
509///     ErrorDetails::Internal => ...,
510///     _ => ...,
511/// }
512/// ```
513#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
514#[non_exhaustive]
515#[surreal(crate = "crate")]
516#[surreal(tag = "kind", content = "details", skip_content_if = "Value::is_empty")]
517pub enum ErrorDetails {
518	/// Validation error (parse error, invalid request/params).
519	Validation(Option<ValidationError>),
520	/// Configuration error (feature/config not supported).
521	Configuration(Option<ConfigurationError>),
522	/// Query execution error (timeout, cancelled, not executed).
523	Query(Option<QueryError>),
524	/// Serialization/deserialization error.
525	Serialization(Option<SerializationError>),
526	/// Permission or authorization error.
527	NotAllowed(Option<NotAllowedError>),
528	/// Resource not found.
529	NotFound(Option<NotFoundError>),
530	/// Duplicate resource.
531	AlreadyExists(Option<AlreadyExistsError>),
532	/// Client connection error (SDK-side).
533	Connection(Option<ConnectionError>),
534	/// User-thrown error (THROW in SurrealQL). No detail type.
535	Thrown,
536	/// Internal/unexpected error. No detail type.
537	/// Acts as a catch-all for unknown kinds during deserialization (forward compatibility).
538	#[surreal(other)]
539	Internal,
540	/// Context for error chaining.
541	Context,
542}
543
544impl ErrorDetails {
545	/// Returns the kind string for wire serialization (e.g. "NotAllowed", "Internal").
546	pub fn kind_str(&self) -> &'static str {
547		match self {
548			Self::Validation(_) => "Validation",
549			Self::Configuration(_) => "Configuration",
550			Self::Query(_) => "Query",
551			Self::Serialization(_) => "Serialization",
552			Self::NotAllowed(_) => "NotAllowed",
553			Self::NotFound(_) => "NotFound",
554			Self::AlreadyExists(_) => "AlreadyExists",
555			Self::Connection(_) => "Connection",
556			Self::Thrown => "Thrown",
557			Self::Internal => "Internal",
558			Self::Context => "Context",
559		}
560	}
561
562	/// Returns true if this is a validation error.
563	pub fn is_validation(&self) -> bool {
564		matches!(self, Self::Validation(_))
565	}
566	/// Returns true if this is a configuration error.
567	pub fn is_configuration(&self) -> bool {
568		matches!(self, Self::Configuration(_))
569	}
570	/// Returns true if this is a query error.
571	pub fn is_query(&self) -> bool {
572		matches!(self, Self::Query(_))
573	}
574	/// Returns true if this is a serialization error.
575	pub fn is_serialization(&self) -> bool {
576		matches!(self, Self::Serialization(_))
577	}
578	/// Returns true if this is a not-allowed error.
579	pub fn is_not_allowed(&self) -> bool {
580		matches!(self, Self::NotAllowed(_))
581	}
582	/// Returns true if this is a not-found error.
583	pub fn is_not_found(&self) -> bool {
584		matches!(self, Self::NotFound(_))
585	}
586	/// Returns true if this is an already-exists error.
587	pub fn is_already_exists(&self) -> bool {
588		matches!(self, Self::AlreadyExists(_))
589	}
590	/// Returns true if this is a connection error.
591	pub fn is_connection(&self) -> bool {
592		matches!(self, Self::Connection(_))
593	}
594	/// Returns true if this is a user-thrown error.
595	pub fn is_thrown(&self) -> bool {
596		matches!(self, Self::Thrown)
597	}
598	/// Returns true if this is an internal error.
599	pub fn is_internal(&self) -> bool {
600		matches!(self, Self::Internal)
601	}
602	/// Returns true if this is a context error (used for error chaining).
603	pub fn is_context(&self) -> bool {
604		matches!(self, Self::Context)
605	}
606
607	/// Create an `ErrorDetails` from a kind string, with no inner details.
608	/// Unknown kind strings fall back to `Internal` (forward compatibility).
609	pub(crate) fn from_kind_str(kind: &str) -> Self {
610		match kind {
611			"Validation" => Self::Validation(None),
612			"Configuration" => Self::Configuration(None),
613			"Query" => Self::Query(None),
614			"Serialization" => Self::Serialization(None),
615			"NotAllowed" => Self::NotAllowed(None),
616			"NotFound" => Self::NotFound(None),
617			"AlreadyExists" => Self::AlreadyExists(None),
618			"Connection" => Self::Connection(None),
619			"Thrown" => Self::Thrown,
620			"Context" => Self::Context,
621			// Unknown kinds fall back to Internal (forward compat)
622			_ => Self::Internal,
623		}
624	}
625
626	/// Deserialize details using the kind string to select the right variant.
627	/// O(1) dispatch -- no trial-and-error parsing.
628	pub(crate) fn from_value_with_kind_str(kind: &str, value: Value) -> Result<Self, Error> {
629		match kind {
630			"Validation" => {
631				ValidationError::from_value(value).map(|v| ErrorDetails::Validation(Some(v)))
632			}
633			"Configuration" => {
634				ConfigurationError::from_value(value).map(|v| ErrorDetails::Configuration(Some(v)))
635			}
636			"Query" => QueryError::from_value(value).map(|v| ErrorDetails::Query(Some(v))),
637			"Serialization" => {
638				SerializationError::from_value(value).map(|v| ErrorDetails::Serialization(Some(v)))
639			}
640			"NotAllowed" => {
641				NotAllowedError::from_value(value).map(|v| ErrorDetails::NotAllowed(Some(v)))
642			}
643			"NotFound" => NotFoundError::from_value(value).map(|v| ErrorDetails::NotFound(Some(v))),
644			"AlreadyExists" => {
645				AlreadyExistsError::from_value(value).map(|v| ErrorDetails::AlreadyExists(Some(v)))
646			}
647			"Connection" => {
648				ConnectionError::from_value(value).map(|v| ErrorDetails::Connection(Some(v)))
649			}
650			"Thrown" => Ok(Self::Thrown),
651			"Context" => Ok(Self::Context),
652			_ => Ok(Self::Internal),
653		}
654	}
655
656	/// Returns true if this variant has inner detail data.
657	pub fn has_details(&self) -> bool {
658		match self {
659			Self::Validation(d) => d.is_some(),
660			Self::Configuration(d) => d.is_some(),
661			Self::Query(d) => d.is_some(),
662			Self::Serialization(d) => d.is_some(),
663			Self::NotAllowed(d) => d.is_some(),
664			Self::NotFound(d) => d.is_some(),
665			Self::AlreadyExists(d) => d.is_some(),
666			Self::Connection(d) => d.is_some(),
667			Self::Thrown | Self::Internal | Self::Context => false,
668		}
669	}
670}
671
672// -----------------------------------------------------------------------------
673// Structured error details (wire format in Error.details)
674// -----------------------------------------------------------------------------
675
676/// Auth failure reason for [`ErrorKind::NotAllowed`] errors.
677#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
678#[surreal(crate = "crate")]
679#[surreal(tag = "kind", content = "details")]
680#[non_exhaustive]
681pub enum AuthError {
682	/// The token used for authentication has expired.
683	#[surreal(skip_content)]
684	TokenExpired,
685	/// The session has expired.
686	#[surreal(skip_content)]
687	SessionExpired,
688	/// Authentication failed (invalid credentials or similar).
689	#[surreal(skip_content)]
690	InvalidAuth,
691	/// Unexpected error while performing authentication.
692	#[surreal(skip_content)]
693	UnexpectedAuth,
694	/// Username or password was not provided.
695	#[surreal(skip_content)]
696	MissingUserOrPass,
697	/// No signin target (SC, DB, NS, or KV) specified.
698	#[surreal(skip_content)]
699	NoSigninTarget,
700	/// The password did not verify.
701	#[surreal(skip_content)]
702	InvalidPass,
703	/// Failed to create the authentication token.
704	#[surreal(skip_content)]
705	TokenMakingFailed,
706	/// Signup failed.
707	#[surreal(skip_content)]
708	InvalidSignup,
709	/// Invalid role (IAM). Carries the role name.
710	InvalidRole {
711		/// Name of the invalid role.
712		name: String,
713	},
714	/// Not enough permissions to perform the action (IAM). Carries actor, action, resource.
715	NotAllowed {
716		/// Actor that attempted the action.
717		actor: String,
718		/// Action that was attempted.
719		action: String,
720		/// Resource the action was attempted on.
721		resource: String,
722	},
723}
724
725impl From<AuthError> for Option<NotAllowedError> {
726	fn from(auth_error: AuthError) -> Self {
727		Some(NotAllowedError::Auth(auth_error))
728	}
729}
730
731/// Validation failure reason for [`ErrorKind::Validation`] errors.
732#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
733#[surreal(crate = "crate")]
734#[surreal(tag = "kind", content = "details")]
735#[non_exhaustive]
736pub enum ValidationError {
737	/// Parse error (invalid message or request format).
738	#[surreal(skip_content)]
739	Parse,
740	/// Invalid request structure.
741	#[surreal(skip_content)]
742	InvalidRequest,
743	/// Invalid parameters.
744	#[surreal(skip_content)]
745	InvalidParams,
746	/// Namespace is empty.
747	#[surreal(skip_content)]
748	NamespaceEmpty,
749	/// Database is empty.
750	#[surreal(skip_content)]
751	DatabaseEmpty,
752	/// Invalid parameter with name.
753	InvalidParameter {
754		/// Name of the invalid parameter.
755		name: String,
756	},
757	/// Invalid content value.
758	InvalidContent {
759		/// The invalid value.
760		value: String,
761	},
762	/// Invalid merge value.
763	InvalidMerge {
764		/// The invalid value.
765		value: String,
766	},
767}
768
769/// Not-allowed reason for [`ErrorKind::NotAllowed`] errors.
770#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
771#[surreal(crate = "crate")]
772#[surreal(tag = "kind", content = "details")]
773#[non_exhaustive]
774pub enum NotAllowedError {
775	/// Scripting not allowed.
776	#[surreal(skip_content)]
777	Scripting,
778	/// Authentication or authorisation failure.
779	Auth(AuthError),
780	/// RPC method not allowed.
781	Method {
782		/// Name of the method.
783		name: String,
784	},
785	/// Function not allowed.
786	Function {
787		/// Name of the function.
788		name: String,
789	},
790	/// Net target not allowed.
791	Target {
792		/// Name of the net target.
793		name: String,
794	},
795}
796
797/// Configuration failure reason for [`ErrorKind::Configuration`] errors.
798#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
799#[surreal(crate = "crate")]
800#[surreal(tag = "kind", skip_content)]
801#[non_exhaustive]
802pub enum ConfigurationError {
803	/// Live query not supported.
804	LiveQueryNotSupported,
805	/// Bad live query config.
806	BadLiveQueryConfig,
807	/// Bad GraphQL config.
808	BadGraphqlConfig,
809}
810
811/// Serialisation failure reason for [`ErrorKind::Serialization`] errors.
812#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
813#[surreal(crate = "crate")]
814#[surreal(tag = "kind", skip_content)]
815#[non_exhaustive]
816pub enum SerializationError {
817	/// Serialisation error.
818	Serialization,
819	/// Deserialisation error.
820	Deserialization,
821}
822
823/// Not-found reason for [`ErrorKind::NotFound`] errors.
824#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
825#[surreal(crate = "crate")]
826#[surreal(tag = "kind", content = "details")]
827#[non_exhaustive]
828pub enum NotFoundError {
829	/// RPC method not found.
830	Method {
831		/// Name of the method.
832		name: String,
833	},
834	/// Session not found.
835	Session {
836		/// Optional session ID that was not found.
837		id: Option<String>,
838	},
839	/// Table not found.
840	Table {
841		/// Name of the table.
842		name: String,
843	},
844	/// Record not found.
845	Record {
846		/// ID of the record.
847		id: String,
848	},
849	/// Namespace not found.
850	Namespace {
851		/// Name of the namespace.
852		name: String,
853	},
854	/// Database not found.
855	Database {
856		/// Name of the database.
857		name: String,
858	},
859	/// Transaction not found.
860	#[surreal(skip_content)]
861	Transaction,
862}
863
864/// Query failure reason for [`ErrorKind::Query`] errors.
865#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
866#[surreal(crate = "crate")]
867#[surreal(tag = "kind", content = "details")]
868#[non_exhaustive]
869pub enum QueryError {
870	/// Query was not executed.
871	#[surreal(skip_content)]
872	NotExecuted,
873	/// Query timed out.
874	TimedOut {
875		/// Duration after which the query timed out.
876		duration: Duration,
877	},
878	/// Query was cancelled.
879	#[surreal(skip_content)]
880	Cancelled,
881	/// Transaction conflict; the operation can be retried.
882	#[surreal(skip_content)]
883	TransactionConflict,
884}
885
886/// Already-exists reason for [`ErrorKind::AlreadyExists`] errors.
887#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
888#[surreal(crate = "crate")]
889#[surreal(tag = "kind", content = "details")]
890#[non_exhaustive]
891pub enum AlreadyExistsError {
892	/// Session already exists.
893	Session {
894		/// Optional session ID that already exists.
895		id: String,
896	},
897	/// Table already exists.
898	Table {
899		/// Name of the table.
900		name: String,
901	},
902	/// Record already exists.
903	Record {
904		/// ID of the record.
905		id: String,
906	},
907	/// Namespace already exists.
908	Namespace {
909		/// Name of the namespace.
910		name: String,
911	},
912	/// Database already exists.
913	Database {
914		/// Name of the database.
915		name: String,
916	},
917}
918
919/// Connection failure reason for [`ErrorKind::Connection`] errors.
920/// Used in the SDK for client-side connection state errors.
921#[derive(Clone, Debug, PartialEq, Eq, SurrealValue)]
922#[surreal(crate = "crate")]
923#[surreal(tag = "kind", skip_content)]
924#[non_exhaustive]
925pub enum ConnectionError {
926	/// Connection was used before being initialised.
927	Uninitialised,
928	/// Connect was called on an instance that is already connected.
929	AlreadyConnected,
930	/// Connection or transport failed (e.g. network error, DNS failure, WebSocket error).
931	ConnectionFailed,
932}
933
934impl fmt::Display for Error {
935	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
936		write!(f, "{}", self.message)
937	}
938}
939
940impl std::error::Error for Error {
941	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
942		self.cause.as_deref().map(|e| e as &(dyn std::error::Error + 'static))
943	}
944}
945
946// -----------------------------------------------------------------------------
947// Error chaining trait
948// -----------------------------------------------------------------------------
949
950/// Extension trait for wrapping errors with context, producing a chained [`Error`].
951///
952/// # Examples
953///
954/// ```ignore
955/// use surrealdb_types::{Error, Chain};
956/// use std::io;
957///
958/// let result: io::Result<()> = Err(io::Error::new(io::ErrorKind::NotFound, "file missing"));
959/// let err = result.chain("Failed to load config").unwrap_err();
960/// assert!(err.is_internal());
961/// assert!(err.cause().is_some());
962/// ```
963pub trait Chain<T, E> {
964	/// Wrap the error with the given context message. The original error becomes the cause.
965	fn chain<C>(self, context: C) -> Result<T, Error>
966	where
967		C: fmt::Display + Send + Sync + 'static;
968
969	/// Wrap the error with lazily-evaluated context.
970	fn chain_with<C, F>(self, f: F) -> Result<T, Error>
971	where
972		C: fmt::Display + Send + Sync + 'static,
973		F: FnOnce() -> C;
974}
975
976impl<T, E> Chain<T, E> for Result<T, E>
977where
978	E: Into<Error>,
979{
980	fn chain<C>(self, context: C) -> Result<T, Error>
981	where
982		C: fmt::Display + Send + Sync + 'static,
983	{
984		self.map_err(|e| Error::context(context.to_string()).with_cause(e.into()))
985	}
986
987	fn chain_with<C, F>(self, f: F) -> Result<T, Error>
988	where
989		C: fmt::Display + Send + Sync + 'static,
990		F: FnOnce() -> C,
991	{
992		self.map_err(|e| Error::context(f().to_string()).with_cause(e.into()))
993	}
994}
995
996impl<T> Chain<T, Infallible> for Option<T> {
997	fn chain<C>(self, context: C) -> Result<T, Error>
998	where
999		C: fmt::Display + Send + Sync + 'static,
1000	{
1001		self.ok_or_else(|| Error::context(context.to_string()))
1002	}
1003
1004	fn chain_with<C, F>(self, f: F) -> Result<T, Error>
1005	where
1006		C: fmt::Display + Send + Sync + 'static,
1007		F: FnOnce() -> C,
1008	{
1009		self.ok_or_else(|| Error::context(f().to_string()))
1010	}
1011}
1012
1013// -----------------------------------------------------------------------------
1014// From conversions (std and optional)
1015// -----------------------------------------------------------------------------
1016
1017/// Allow conversion from [`std::io::Error`].
1018/// Maps [`std::io::ErrorKind`] to the most appropriate typed error where possible.
1019impl From<std::io::Error> for Error {
1020	fn from(error: std::io::Error) -> Self {
1021		use std::io::ErrorKind;
1022		let msg = error.to_string();
1023		match error.kind() {
1024			ErrorKind::NotFound => Error::not_found(msg, None),
1025			ErrorKind::AlreadyExists => Error::already_exists(msg, None),
1026			ErrorKind::PermissionDenied
1027			| ErrorKind::ReadOnlyFilesystem
1028			| ErrorKind::ResourceBusy
1029			| ErrorKind::ExecutableFileBusy => Error::not_allowed(msg, None),
1030			ErrorKind::ConnectionRefused
1031			| ErrorKind::ConnectionReset
1032			| ErrorKind::ConnectionAborted
1033			| ErrorKind::NotConnected
1034			| ErrorKind::HostUnreachable
1035			| ErrorKind::NetworkUnreachable
1036			| ErrorKind::NetworkDown
1037			| ErrorKind::TimedOut
1038			| ErrorKind::BrokenPipe
1039			| ErrorKind::AddrInUse
1040			| ErrorKind::AddrNotAvailable
1041			| ErrorKind::StaleNetworkFileHandle => Error::connection(msg, ConnectionError::ConnectionFailed),
1042			ErrorKind::InvalidData | ErrorKind::UnexpectedEof => {
1043				Error::serialization(msg, SerializationError::Deserialization)
1044			}
1045			ErrorKind::InvalidInput
1046			| ErrorKind::InvalidFilename
1047			| ErrorKind::NotADirectory
1048			| ErrorKind::IsADirectory
1049			| ErrorKind::DirectoryNotEmpty => Error::validation(msg, None),
1050			_ => Error::internal(msg),
1051		}
1052	}
1053}
1054
1055#[cfg(feature = "convert")]
1056impl From<async_channel::RecvError> for Error {
1057	fn from(error: async_channel::RecvError) -> Self {
1058		Error::connection(
1059			format!("Channel receive error: {}", error),
1060			ConnectionError::ConnectionFailed,
1061		)
1062	}
1063}
1064
1065#[cfg(feature = "convert")]
1066impl From<url::ParseError> for Error {
1067	fn from(error: url::ParseError) -> Self {
1068		Error::validation(error.to_string(), ValidationError::InvalidRequest)
1069	}
1070}
1071
1072#[cfg(feature = "convert")]
1073impl From<semver::Error> for Error {
1074	fn from(error: semver::Error) -> Self {
1075		Error::validation(error.to_string(), ValidationError::InvalidParams)
1076	}
1077}
1078
1079#[cfg(feature = "reqwest")]
1080impl From<reqwest::Error> for Error {
1081	fn from(error: reqwest::Error) -> Self {
1082		Error::connection(error.to_string(), ConnectionError::ConnectionFailed)
1083	}
1084}
1085
1086// -----------------------------------------------------------------------------
1087// Type conversion errors (internal to the types layer)
1088// -----------------------------------------------------------------------------
1089
1090/// Errors that can occur when working with SurrealDB types
1091#[derive(Debug, Clone)]
1092#[non_exhaustive]
1093pub enum TypeError {
1094	/// Failed to convert between types
1095	Conversion(ConversionError),
1096	/// Value is out of range for the target type
1097	OutOfRange(OutOfRangeError),
1098	/// Array or tuple length mismatch
1099	LengthMismatch(LengthMismatchError),
1100	/// Invalid format or structure
1101	Invalid(String),
1102}
1103
1104/// Error when converting between types
1105#[derive(Debug, Clone)]
1106#[non_exhaustive]
1107pub struct ConversionError {
1108	/// The expected kind
1109	pub expected: Kind,
1110	/// The actual kind that was received
1111	pub actual: Kind,
1112	/// Optional context about what was being converted
1113	pub context: Option<String>,
1114}
1115
1116/// Error when a value is out of range for the target type
1117#[derive(Debug, Clone)]
1118#[non_exhaustive]
1119pub struct OutOfRangeError {
1120	/// The value that was out of range
1121	pub value: String,
1122	/// The target type name
1123	pub target_type: String,
1124	/// Optional additional context
1125	pub context: Option<String>,
1126}
1127
1128/// Error when an array or tuple has the wrong length
1129#[derive(Debug, Clone)]
1130#[non_exhaustive]
1131pub struct LengthMismatchError {
1132	/// The expected length
1133	pub expected: usize,
1134	/// The actual length received
1135	pub actual: usize,
1136	/// The target type name
1137	pub target_type: String,
1138}
1139
1140impl ConversionError {
1141	/// Create a new conversion error
1142	pub fn new(expected: Kind, actual: Kind) -> Self {
1143		Self {
1144			expected,
1145			actual,
1146			context: None,
1147		}
1148	}
1149
1150	/// Create a conversion error from a value
1151	pub fn from_value(expected: Kind, value: &Value) -> Self {
1152		Self {
1153			expected,
1154			actual: value.kind(),
1155			context: None,
1156		}
1157	}
1158
1159	/// Add context to the error
1160	pub fn with_context(mut self, context: impl Into<String>) -> Self {
1161		self.context = Some(context.into());
1162		self
1163	}
1164}
1165
1166impl OutOfRangeError {
1167	/// Create a new out of range error
1168	pub fn new(value: impl fmt::Display, target_type: impl Into<String>) -> Self {
1169		Self {
1170			value: value.to_string(),
1171			target_type: target_type.into(),
1172			context: None,
1173		}
1174	}
1175
1176	/// Add context to the error
1177	pub fn with_context(mut self, context: impl Into<String>) -> Self {
1178		self.context = Some(context.into());
1179		self
1180	}
1181}
1182
1183impl LengthMismatchError {
1184	/// Create a new length mismatch error
1185	pub fn new(expected: usize, actual: usize, target_type: impl Into<String>) -> Self {
1186		Self {
1187			expected,
1188			actual,
1189			target_type: target_type.into(),
1190		}
1191	}
1192}
1193
1194impl fmt::Display for TypeError {
1195	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1196		match self {
1197			TypeError::Conversion(e) => write!(f, "{e}"),
1198			TypeError::OutOfRange(e) => write!(f, "{e}"),
1199			TypeError::LengthMismatch(e) => write!(f, "{e}"),
1200			TypeError::Invalid(e) => write!(f, "Invalid: {e}"),
1201		}
1202	}
1203}
1204
1205impl fmt::Display for ConversionError {
1206	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1207		write!(f, "Expected {}, got {}", self.expected.to_sql(), self.actual.to_sql())?;
1208		if let Some(context) = &self.context {
1209			write!(f, " ({})", context)?;
1210		}
1211		Ok(())
1212	}
1213}
1214
1215impl fmt::Display for OutOfRangeError {
1216	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1217		write!(f, "Value {} is out of range for type {}", self.value, self.target_type)?;
1218		if let Some(context) = &self.context {
1219			write!(f, " ({})", context)?;
1220		}
1221		Ok(())
1222	}
1223}
1224
1225impl fmt::Display for LengthMismatchError {
1226	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1227		write!(
1228			f,
1229			"Length mismatch for {}: expected {}, got {}",
1230			self.target_type, self.expected, self.actual
1231		)
1232	}
1233}
1234
1235impl std::error::Error for TypeError {}
1236impl std::error::Error for ConversionError {}
1237impl std::error::Error for OutOfRangeError {}
1238impl std::error::Error for LengthMismatchError {}
1239
1240impl From<ConversionError> for Error {
1241	fn from(e: ConversionError) -> Self {
1242		Error::internal(e.to_string())
1243	}
1244}
1245
1246impl From<OutOfRangeError> for Error {
1247	fn from(e: OutOfRangeError) -> Self {
1248		Error::internal(e.to_string())
1249	}
1250}
1251
1252impl From<LengthMismatchError> for Error {
1253	fn from(e: LengthMismatchError) -> Self {
1254		Error::internal(e.to_string())
1255	}
1256}
1257
1258impl From<TypeError> for Error {
1259	fn from(e: TypeError) -> Self {
1260		Error::internal(e.to_string())
1261	}
1262}
1263
1264/// Helper function to create a conversion error
1265pub fn conversion_error(expected: Kind, value: impl Into<Value>) -> Error {
1266	let value = value.into();
1267	ConversionError::from_value(expected, &value).into()
1268}
1269
1270/// Helper function to create an out of range error
1271pub fn out_of_range_error(value: impl fmt::Display, target_type: impl Into<String>) -> Error {
1272	OutOfRangeError::new(value, target_type).into()
1273}
1274
1275/// Helper function to create a length mismatch error
1276pub fn length_mismatch_error(
1277	expected: usize,
1278	actual: usize,
1279	target_type: impl Into<String>,
1280) -> Error {
1281	LengthMismatchError::new(expected, actual, target_type).into()
1282}
1283
1284/// Helper function to create a conversion error for union types (Either)
1285/// where the value doesn't match any of the possible types
1286pub fn union_conversion_error(expected: Kind, value: impl Into<Value>) -> Error {
1287	let value = value.into();
1288	ConversionError::from_value(expected, &value)
1289		.with_context("Value does not match any variant in union type")
1290		.into()
1291}