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