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    /// Asset resolution occurred during query execution.
58    ///
59    /// This error is returned when `resolve_asset` is called while a query is
60    /// executing, and the resolved asset affects a dependency that the query
61    /// has already accessed. This would cause different parts of the query
62    /// to observe different asset values, violating consistency.
63    InconsistentAssetResolution,
64
65    /// User-defined error.
66    ///
67    /// This variant allows user errors to be propagated through the query system
68    /// using the `?` operator. Any type implementing `Into<anyhow::Error>` can be
69    /// converted to this variant.
70    ///
71    /// Unlike system errors (Suspend, Cycle, etc.), UserError results are cached
72    /// and participate in early cutoff optimization.
73    UserError(Arc<anyhow::Error>),
74}
75
76impl fmt::Display for QueryError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            QueryError::Suspend { asset } => {
80                write!(f, "query suspended: waiting for {}", asset.debug_repr())
81            }
82            QueryError::Cycle { path } => {
83                write!(f, "dependency cycle detected: {}", path.join(" -> "))
84            }
85            QueryError::Cancelled => write!(f, "query cancelled"),
86            QueryError::MissingDependency { description } => {
87                write!(f, "missing dependency: {}", description)
88            }
89            QueryError::DependenciesRemoved { missing_keys } => {
90                write!(
91                    f,
92                    "dependencies removed during execution: {:?}",
93                    missing_keys
94                )
95            }
96            QueryError::InconsistentAssetResolution => {
97                write!(
98                    f,
99                    "asset resolution occurred during query execution, causing inconsistent snapshot"
100                )
101            }
102            QueryError::UserError(e) => write!(f, "user error: {}", e),
103        }
104    }
105}
106
107impl<T: Into<anyhow::Error>> From<T> for QueryError {
108    fn from(err: T) -> Self {
109        QueryError::UserError(Arc::new(err.into()))
110    }
111}
112
113impl QueryError {
114    /// Returns a reference to the inner user error if this is a `UserError` variant.
115    pub fn user_error(&self) -> Option<&Arc<anyhow::Error>> {
116        match self {
117            QueryError::UserError(e) => Some(e),
118            _ => None,
119        }
120    }
121
122    /// Attempts to downcast the user error to a specific type.
123    ///
124    /// Returns `Some(&E)` if this is a `UserError` containing an error of type `E`,
125    /// otherwise returns `None`.
126    pub fn downcast_ref<E: std::error::Error + Send + Sync + 'static>(&self) -> Option<&E> {
127        self.user_error().and_then(|e| e.downcast_ref::<E>())
128    }
129
130    /// Returns `true` if this is a `UserError` containing an error of type `E`.
131    pub fn is<E: std::error::Error + Send + Sync + 'static>(&self) -> bool {
132        self.downcast_ref::<E>().is_some()
133    }
134}
135
136/// A typed wrapper around a user error that provides `Deref` access to the inner error type.
137///
138/// This struct holds an `Arc<anyhow::Error>` internally and provides safe access to
139/// the downcasted error reference. The `Arc` ensures the error remains valid for the
140/// lifetime of this wrapper.
141///
142/// # Example
143///
144/// ```ignore
145/// use query_flow::{QueryResultExt, TypedErr};
146///
147/// let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
148/// match result {
149///     Ok(value) => { /* success */ }
150///     Err(typed_err) => {
151///         // typed_err derefs to &MyError
152///         println!("Error code: {}", typed_err.code);
153///     }
154/// }
155/// ```
156#[derive(Clone)]
157pub struct TypedErr<E> {
158    arc: Arc<anyhow::Error>,
159    _marker: PhantomData<E>,
160}
161
162impl<E: std::error::Error + Send + Sync + 'static> TypedErr<E> {
163    fn new(arc: Arc<anyhow::Error>) -> Option<Self> {
164        // Verify the downcast is valid before constructing
165        if arc.downcast_ref::<E>().is_some() {
166            Some(Self {
167                arc,
168                _marker: PhantomData,
169            })
170        } else {
171            None
172        }
173    }
174
175    /// Returns a reference to the inner error.
176    pub fn get(&self) -> &E {
177        // Safe because we verified the type in `new`
178        self.arc.downcast_ref::<E>().unwrap()
179    }
180}
181
182impl<E: std::error::Error + Send + Sync + 'static> Deref for TypedErr<E> {
183    type Target = E;
184
185    fn deref(&self) -> &E {
186        self.get()
187    }
188}
189
190impl<E: std::error::Error + Send + Sync + 'static> fmt::Debug for TypedErr<E> {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        fmt::Debug::fmt(self.get(), f)
193    }
194}
195
196impl<E: std::error::Error + Send + Sync + 'static> fmt::Display for TypedErr<E> {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        fmt::Display::fmt(self.get(), f)
199    }
200}
201
202/// Extension trait for query results that provides ergonomic error downcasting.
203///
204/// This trait is implemented for `Result<Arc<T>, QueryError>` and allows you to
205/// downcast user errors to a specific type while propagating system errors.
206///
207/// # Example
208///
209/// ```ignore
210/// use query_flow::QueryResultExt;
211///
212/// // Downcast to MyError, propagating system errors and non-matching user errors
213/// let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
214///
215/// match result {
216///     Ok(value) => println!("Success: {:?}", value),
217///     Err(my_err) => println!("MyError: {}", my_err.code),
218/// }
219/// ```
220pub trait QueryResultExt<T> {
221    /// Attempts to downcast a `UserError` to a specific error type.
222    ///
223    /// # Returns
224    ///
225    /// - `Ok(Ok(value))` - The query succeeded with `value`
226    /// - `Ok(Err(typed_err))` - The query failed with a `UserError` of type `E`
227    /// - `Err(query_error)` - The query failed with a system error, or a `UserError`
228    ///   that is not of type `E`
229    ///
230    /// # Example
231    ///
232    /// ```ignore
233    /// // Handle specific error type, propagate others
234    /// let result = db.query(MyQuery::new()).downcast_err::<MyError>()?;
235    /// let value = result.map_err(|e| {
236    ///     eprintln!("MyError occurred: {}", e.message);
237    ///     e
238    /// })?;
239    /// ```
240    fn downcast_err<E: std::error::Error + Send + Sync + 'static>(
241        self,
242    ) -> Result<Result<Arc<T>, TypedErr<E>>, QueryError>;
243}
244
245impl<T> QueryResultExt<T> for Result<Arc<T>, QueryError> {
246    fn downcast_err<E: std::error::Error + Send + Sync + 'static>(
247        self,
248    ) -> Result<Result<Arc<T>, TypedErr<E>>, QueryError> {
249        match self {
250            Ok(value) => Ok(Ok(value)),
251            Err(QueryError::UserError(arc)) => match TypedErr::new(arc.clone()) {
252                Some(typed) => Ok(Err(typed)),
253                None => Err(QueryError::UserError(arc)),
254            },
255            Err(other) => Err(other),
256        }
257    }
258}