query_flow/error.rs
1//! Error types for query execution.
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::ops::Deref;
6use std::sync::Arc;
7
8use crate::asset::PendingAsset;
9use crate::key::FullCacheKey;
10
11/// Query errors including both system-level and user errors.
12///
13/// User errors can be propagated using the `?` operator, which automatically
14/// converts any `Into<anyhow::Error>` type into `QueryError::UserError`.
15#[derive(Debug, Clone)]
16pub enum QueryError {
17 /// Query is waiting for async loading to complete.
18 ///
19 /// This is returned when a dependency is still loading via a background task.
20 /// Use `runtime.query_async()` to wait for loading to complete, or handle
21 /// explicitly in your query logic.
22 ///
23 /// The `asset` field contains information about the pending asset, which can
24 /// be downcast to the original key type using `asset.key::<K>()`.
25 Suspend {
26 /// The pending asset that caused the suspension.
27 asset: PendingAsset,
28 },
29
30 /// Dependency cycle detected.
31 ///
32 /// The query graph contains a cycle, which would cause infinite recursion.
33 /// The `path` contains a debug representation of the cycle.
34 Cycle {
35 /// Debug representation of the queries forming the cycle.
36 path: Vec<String>,
37 },
38
39 /// Query execution was cancelled.
40 Cancelled,
41
42 /// A required dependency is missing.
43 MissingDependency {
44 /// Description of the missing dependency.
45 description: String,
46 },
47
48 /// Dependencies were removed during query execution.
49 ///
50 /// This can happen if another thread removes queries or assets
51 /// while this query is being registered.
52 DependenciesRemoved {
53 /// Keys that were not found during registration.
54 missing_keys: Vec<FullCacheKey>,
55 },
56
57 /// User-defined error.
58 ///
59 /// This variant allows user errors to be propagated through the query system
60 /// using the `?` operator. Any type implementing `Into<anyhow::Error>` can be
61 /// converted to this variant.
62 ///
63 /// Unlike system errors (Suspend, Cycle, etc.), UserError results are cached
64 /// and participate in early cutoff optimization.
65 UserError(Arc<anyhow::Error>),
66}
67
68impl fmt::Display for QueryError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 QueryError::Suspend { asset } => {
72 write!(f, "query suspended: waiting for {}", asset.debug_repr())
73 }
74 QueryError::Cycle { path } => {
75 write!(f, "dependency cycle detected: {}", path.join(" -> "))
76 }
77 QueryError::Cancelled => write!(f, "query cancelled"),
78 QueryError::MissingDependency { description } => {
79 write!(f, "missing dependency: {}", description)
80 }
81 QueryError::DependenciesRemoved { missing_keys } => {
82 write!(
83 f,
84 "dependencies removed during execution: {:?}",
85 missing_keys
86 )
87 }
88 QueryError::UserError(e) => write!(f, "user error: {}", e),
89 }
90 }
91}
92
93impl<T: Into<anyhow::Error>> From<T> for QueryError {
94 fn from(err: T) -> Self {
95 QueryError::UserError(Arc::new(err.into()))
96 }
97}
98
99impl QueryError {
100 /// Returns a reference to the inner user error if this is a `UserError` variant.
101 pub fn user_error(&self) -> Option<&Arc<anyhow::Error>> {
102 match self {
103 QueryError::UserError(e) => Some(e),
104 _ => None,
105 }
106 }
107
108 /// Attempts to downcast the user error to a specific type.
109 ///
110 /// Returns `Some(&E)` if this is a `UserError` containing an error of type `E`,
111 /// otherwise returns `None`.
112 pub fn downcast_ref<E: std::error::Error + Send + Sync + 'static>(&self) -> Option<&E> {
113 self.user_error().and_then(|e| e.downcast_ref::<E>())
114 }
115
116 /// Returns `true` if this is a `UserError` containing an error of type `E`.
117 pub fn is<E: std::error::Error + Send + Sync + 'static>(&self) -> bool {
118 self.downcast_ref::<E>().is_some()
119 }
120}
121
122/// A typed wrapper around a user error that provides `Deref` access to the inner error type.
123///
124/// This struct holds an `Arc<anyhow::Error>` internally and provides safe access to
125/// the downcasted error reference. The `Arc` ensures the error remains valid for the
126/// lifetime of this wrapper.
127///
128/// # Example
129///
130/// ```ignore
131/// use query_flow::{QueryResultExt, TypedErr};
132///
133/// let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
134/// match result {
135/// Ok(value) => { /* success */ }
136/// Err(typed_err) => {
137/// // typed_err derefs to &MyError
138/// println!("Error code: {}", typed_err.code);
139/// }
140/// }
141/// ```
142#[derive(Clone)]
143pub struct TypedErr<E> {
144 arc: Arc<anyhow::Error>,
145 _marker: PhantomData<E>,
146}
147
148impl<E: std::error::Error + Send + Sync + 'static> TypedErr<E> {
149 fn new(arc: Arc<anyhow::Error>) -> Option<Self> {
150 // Verify the downcast is valid before constructing
151 if arc.downcast_ref::<E>().is_some() {
152 Some(Self {
153 arc,
154 _marker: PhantomData,
155 })
156 } else {
157 None
158 }
159 }
160
161 /// Returns a reference to the inner error.
162 pub fn get(&self) -> &E {
163 // Safe because we verified the type in `new`
164 self.arc.downcast_ref::<E>().unwrap()
165 }
166}
167
168impl<E: std::error::Error + Send + Sync + 'static> Deref for TypedErr<E> {
169 type Target = E;
170
171 fn deref(&self) -> &E {
172 self.get()
173 }
174}
175
176impl<E: std::error::Error + Send + Sync + 'static> fmt::Debug for TypedErr<E> {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 fmt::Debug::fmt(self.get(), f)
179 }
180}
181
182impl<E: std::error::Error + Send + Sync + 'static> fmt::Display for TypedErr<E> {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 fmt::Display::fmt(self.get(), f)
185 }
186}
187
188/// Extension trait for query results that provides ergonomic error downcasting.
189///
190/// This trait is implemented for `Result<Arc<T>, QueryError>` and allows you to
191/// downcast user errors to a specific type while propagating system errors.
192///
193/// # Example
194///
195/// ```ignore
196/// use query_flow::QueryResultExt;
197///
198/// // Downcast to MyError, propagating system errors and non-matching user errors
199/// let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
200///
201/// match result {
202/// Ok(value) => println!("Success: {:?}", value),
203/// Err(my_err) => println!("MyError: {}", my_err.code),
204/// }
205/// ```
206pub trait QueryResultExt<T> {
207 /// Attempts to downcast a `UserError` to a specific error type.
208 ///
209 /// # Returns
210 ///
211 /// - `Ok(Ok(value))` - The query succeeded with `value`
212 /// - `Ok(Err(typed_err))` - The query failed with a `UserError` of type `E`
213 /// - `Err(query_error)` - The query failed with a system error, or a `UserError`
214 /// that is not of type `E`
215 ///
216 /// # Example
217 ///
218 /// ```ignore
219 /// // Handle specific error type, propagate others
220 /// let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
221 /// let value = result.map_err(|e| {
222 /// eprintln!("MyError occurred: {}", e.message);
223 /// e
224 /// })?;
225 /// ```
226 fn downcast_err<E: std::error::Error + Send + Sync + 'static>(
227 self,
228 ) -> Result<Result<Arc<T>, TypedErr<E>>, QueryError>;
229}
230
231impl<T> QueryResultExt<T> for Result<Arc<T>, QueryError> {
232 fn downcast_err<E: std::error::Error + Send + Sync + 'static>(
233 self,
234 ) -> Result<Result<Arc<T>, TypedErr<E>>, QueryError> {
235 match self {
236 Ok(value) => Ok(Ok(value)),
237 Err(QueryError::UserError(arc)) => match TypedErr::new(arc.clone()) {
238 Some(typed) => Ok(Err(typed)),
239 None => Err(QueryError::UserError(arc)),
240 },
241 Err(other) => Err(other),
242 }
243 }
244}