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