Skip to main content

icydb_core/db/query/intent/
errors.rs

1use crate::{
2    db::{
3        cursor::CursorPlanError,
4        predicate::ValidateError,
5        query::plan::{
6            CursorPagingPolicyError, FluentLoadPolicyViolation, IntentKeyAccessPolicyViolation,
7            PlanError, PlannerError, PolicyPlanError,
8        },
9        response::ResponseError,
10    },
11    error::{ErrorClass, InternalError},
12};
13use thiserror::Error as ThisError;
14
15///
16/// QueryError
17///
18
19#[derive(Debug, ThisError)]
20pub enum QueryError {
21    #[error("{0}")]
22    Validate(#[from] ValidateError),
23
24    #[error("{0}")]
25    Plan(Box<PlanError>),
26
27    #[error("{0}")]
28    Intent(#[from] IntentError),
29
30    #[error("{0}")]
31    Response(#[from] ResponseError),
32
33    #[error("{0}")]
34    Execute(#[from] QueryExecuteError),
35}
36
37impl QueryError {
38    /// Construct an execution-domain query error from one classified runtime error.
39    pub(crate) fn execute(err: InternalError) -> Self {
40        Self::Execute(QueryExecuteError::from(err))
41    }
42}
43
44///
45/// QueryExecuteError
46///
47
48#[derive(Debug, ThisError)]
49pub enum QueryExecuteError {
50    #[error("{0}")]
51    Corruption(InternalError),
52
53    #[error("{0}")]
54    InvariantViolation(InternalError),
55
56    #[error("{0}")]
57    Conflict(InternalError),
58
59    #[error("{0}")]
60    NotFound(InternalError),
61
62    #[error("{0}")]
63    Unsupported(InternalError),
64
65    #[error("{0}")]
66    Internal(InternalError),
67}
68
69impl QueryExecuteError {
70    #[must_use]
71    /// Borrow the wrapped classified runtime error.
72    pub const fn as_internal(&self) -> &InternalError {
73        match self {
74            Self::Corruption(err)
75            | Self::InvariantViolation(err)
76            | Self::Conflict(err)
77            | Self::NotFound(err)
78            | Self::Unsupported(err)
79            | Self::Internal(err) => err,
80        }
81    }
82}
83
84impl From<InternalError> for QueryExecuteError {
85    fn from(err: InternalError) -> Self {
86        match err.class {
87            ErrorClass::Corruption => Self::Corruption(err),
88            ErrorClass::InvariantViolation => Self::InvariantViolation(err),
89            ErrorClass::Conflict => Self::Conflict(err),
90            ErrorClass::NotFound => Self::NotFound(err),
91            ErrorClass::Unsupported => Self::Unsupported(err),
92            ErrorClass::Internal => Self::Internal(err),
93        }
94    }
95}
96
97impl From<PlannerError> for QueryError {
98    fn from(err: PlannerError) -> Self {
99        match err {
100            PlannerError::Plan(err) => Self::from(*err),
101            PlannerError::Internal(err) => Self::execute(*err),
102        }
103    }
104}
105
106impl From<PlanError> for QueryError {
107    fn from(err: PlanError) -> Self {
108        Self::Plan(Box::new(err))
109    }
110}
111
112///
113/// IntentError
114///
115
116#[derive(Clone, Copy, Debug, ThisError)]
117pub enum IntentError {
118    #[error("{0}")]
119    PlanShape(#[from] PolicyPlanError),
120
121    #[error("by_ids() cannot be combined with predicates")]
122    ByIdsWithPredicate,
123
124    #[error("only() cannot be combined with predicates")]
125    OnlyWithPredicate,
126
127    #[error("multiple key access methods were used on the same query")]
128    KeyAccessConflict,
129
130    #[error(
131        "{message}",
132        message = CursorPlanError::cursor_requires_order_message()
133    )]
134    CursorRequiresOrder,
135
136    #[error(
137        "{message}",
138        message = CursorPlanError::cursor_requires_limit_message()
139    )]
140    CursorRequiresLimit,
141
142    #[error("cursor tokens can only be used with .page().execute()")]
143    CursorRequiresPagedExecution,
144
145    #[error("grouped queries require execute_grouped(...)")]
146    GroupedRequiresExecuteGrouped,
147
148    #[error("HAVING requires GROUP BY")]
149    HavingRequiresGroupBy,
150}
151
152impl From<CursorPagingPolicyError> for IntentError {
153    fn from(err: CursorPagingPolicyError) -> Self {
154        match err {
155            CursorPagingPolicyError::CursorRequiresOrder => Self::CursorRequiresOrder,
156            CursorPagingPolicyError::CursorRequiresLimit => Self::CursorRequiresLimit,
157        }
158    }
159}
160
161impl From<IntentKeyAccessPolicyViolation> for IntentError {
162    fn from(err: IntentKeyAccessPolicyViolation) -> Self {
163        match err {
164            IntentKeyAccessPolicyViolation::KeyAccessConflict => Self::KeyAccessConflict,
165            IntentKeyAccessPolicyViolation::ByIdsWithPredicate => Self::ByIdsWithPredicate,
166            IntentKeyAccessPolicyViolation::OnlyWithPredicate => Self::OnlyWithPredicate,
167        }
168    }
169}
170
171impl From<FluentLoadPolicyViolation> for IntentError {
172    fn from(err: FluentLoadPolicyViolation) -> Self {
173        match err {
174            FluentLoadPolicyViolation::CursorRequiresPagedExecution => {
175                Self::CursorRequiresPagedExecution
176            }
177            FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped => {
178                Self::GroupedRequiresExecuteGrouped
179            }
180            FluentLoadPolicyViolation::CursorRequiresOrder => Self::CursorRequiresOrder,
181            FluentLoadPolicyViolation::CursorRequiresLimit => Self::CursorRequiresLimit,
182        }
183    }
184}
185
186///
187/// TESTS
188///
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::error::ErrorOrigin;
194
195    fn assert_execute_variant_for_class(err: &QueryExecuteError, class: ErrorClass) {
196        match class {
197            ErrorClass::Corruption => assert!(matches!(err, QueryExecuteError::Corruption(_))),
198            ErrorClass::InvariantViolation => {
199                assert!(matches!(err, QueryExecuteError::InvariantViolation(_)));
200            }
201            ErrorClass::Conflict => assert!(matches!(err, QueryExecuteError::Conflict(_))),
202            ErrorClass::NotFound => assert!(matches!(err, QueryExecuteError::NotFound(_))),
203            ErrorClass::Unsupported => assert!(matches!(err, QueryExecuteError::Unsupported(_))),
204            ErrorClass::Internal => assert!(matches!(err, QueryExecuteError::Internal(_))),
205        }
206    }
207
208    #[test]
209    fn query_execute_error_from_internal_preserves_class_and_origin_matrix() {
210        let cases = [
211            (ErrorClass::Corruption, ErrorOrigin::Store),
212            (ErrorClass::InvariantViolation, ErrorOrigin::Query),
213            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
214            (ErrorClass::Conflict, ErrorOrigin::Executor),
215            (ErrorClass::NotFound, ErrorOrigin::Identity),
216            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
217            (ErrorClass::Internal, ErrorOrigin::Serialize),
218            (ErrorClass::Internal, ErrorOrigin::Planner),
219        ];
220
221        for (class, origin) in cases {
222            let internal = InternalError::classified(class, origin, "matrix");
223            let mapped = QueryExecuteError::from(internal);
224
225            assert_execute_variant_for_class(&mapped, class);
226            assert_eq!(mapped.as_internal().class, class);
227            assert_eq!(mapped.as_internal().origin, origin);
228        }
229    }
230
231    #[test]
232    fn planner_internal_mapping_preserves_runtime_class_and_origin() {
233        let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
234            ErrorClass::Unsupported,
235            ErrorOrigin::Cursor,
236            "cursor payload mismatch",
237        )));
238        let query_err = QueryError::from(planner_internal);
239
240        assert!(matches!(
241            query_err,
242            QueryError::Execute(QueryExecuteError::Unsupported(inner))
243                if inner.class == ErrorClass::Unsupported
244                    && inner.origin == ErrorOrigin::Cursor
245        ));
246    }
247
248    #[test]
249    fn planner_internal_mapping_preserves_boundary_origins_for_telemetry_matrix() {
250        let cases = [
251            (ErrorClass::Internal, ErrorOrigin::Planner),
252            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
253            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
254            (ErrorClass::Corruption, ErrorOrigin::Identity),
255        ];
256
257        for (class, origin) in cases {
258            let planner_internal =
259                PlannerError::Internal(Box::new(InternalError::classified(class, origin, "matrix")));
260            let query_err = QueryError::from(planner_internal);
261
262            let QueryError::Execute(execute_err) = query_err else {
263                panic!("planner internal errors must map to query execute errors");
264            };
265            assert_execute_variant_for_class(&execute_err, class);
266            assert_eq!(
267                execute_err.as_internal().origin,
268                origin,
269                "planner-internal mapping must preserve telemetry origin for {origin:?}",
270            );
271        }
272    }
273}