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::Conflict, ErrorOrigin::Executor),
214            (ErrorClass::NotFound, ErrorOrigin::Identity),
215            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
216            (ErrorClass::Internal, ErrorOrigin::Serialize),
217        ];
218
219        for (class, origin) in cases {
220            let internal = InternalError::classified(class, origin, "matrix");
221            let mapped = QueryExecuteError::from(internal);
222
223            assert_execute_variant_for_class(&mapped, class);
224            assert_eq!(mapped.as_internal().class, class);
225            assert_eq!(mapped.as_internal().origin, origin);
226        }
227    }
228
229    #[test]
230    fn planner_internal_mapping_preserves_runtime_class_and_origin() {
231        let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
232            ErrorClass::Unsupported,
233            ErrorOrigin::Cursor,
234            "cursor payload mismatch",
235        )));
236        let query_err = QueryError::from(planner_internal);
237
238        assert!(matches!(
239            query_err,
240            QueryError::Execute(QueryExecuteError::Unsupported(inner))
241                if inner.class == ErrorClass::Unsupported
242                    && inner.origin == ErrorOrigin::Cursor
243        ));
244    }
245}