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("{0}")]
131    InvalidPagingShape(#[from] PagingIntentError),
132
133    #[error("grouped queries require execute_grouped(...)")]
134    GroupedRequiresExecuteGrouped,
135
136    #[error("HAVING requires GROUP BY")]
137    HavingRequiresGroupBy,
138}
139
140///
141/// PagingIntentError
142///
143/// Canonical intent-level paging contract failures shared by planner and
144/// fluent/execution boundary gates.
145///
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
148#[expect(clippy::enum_variant_names)]
149pub enum PagingIntentError {
150    #[error(
151        "{message}",
152        message = CursorPlanError::cursor_requires_order_message()
153    )]
154    CursorRequiresOrder,
155
156    #[error(
157        "{message}",
158        message = CursorPlanError::cursor_requires_limit_message()
159    )]
160    CursorRequiresLimit,
161
162    #[error("cursor tokens can only be used with .page().execute()")]
163    CursorRequiresPagedExecution,
164}
165
166impl From<CursorPagingPolicyError> for PagingIntentError {
167    fn from(err: CursorPagingPolicyError) -> Self {
168        match err {
169            CursorPagingPolicyError::CursorRequiresOrder => Self::CursorRequiresOrder,
170            CursorPagingPolicyError::CursorRequiresLimit => Self::CursorRequiresLimit,
171        }
172    }
173}
174
175impl From<CursorPagingPolicyError> for IntentError {
176    fn from(err: CursorPagingPolicyError) -> Self {
177        Self::InvalidPagingShape(PagingIntentError::from(err))
178    }
179}
180
181impl From<IntentKeyAccessPolicyViolation> for IntentError {
182    fn from(err: IntentKeyAccessPolicyViolation) -> Self {
183        match err {
184            IntentKeyAccessPolicyViolation::KeyAccessConflict => Self::KeyAccessConflict,
185            IntentKeyAccessPolicyViolation::ByIdsWithPredicate => Self::ByIdsWithPredicate,
186            IntentKeyAccessPolicyViolation::OnlyWithPredicate => Self::OnlyWithPredicate,
187        }
188    }
189}
190
191impl From<FluentLoadPolicyViolation> for IntentError {
192    fn from(err: FluentLoadPolicyViolation) -> Self {
193        match err {
194            FluentLoadPolicyViolation::CursorRequiresPagedExecution => {
195                Self::InvalidPagingShape(PagingIntentError::CursorRequiresPagedExecution)
196            }
197            FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped => {
198                Self::GroupedRequiresExecuteGrouped
199            }
200            FluentLoadPolicyViolation::CursorRequiresOrder => {
201                Self::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
202            }
203            FluentLoadPolicyViolation::CursorRequiresLimit => {
204                Self::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
205            }
206        }
207    }
208}
209
210///
211/// TESTS
212///
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::error::ErrorOrigin;
218
219    fn assert_execute_variant_for_class(err: &QueryExecuteError, class: ErrorClass) {
220        match class {
221            ErrorClass::Corruption => assert!(matches!(err, QueryExecuteError::Corruption(_))),
222            ErrorClass::InvariantViolation => {
223                assert!(matches!(err, QueryExecuteError::InvariantViolation(_)));
224            }
225            ErrorClass::Conflict => assert!(matches!(err, QueryExecuteError::Conflict(_))),
226            ErrorClass::NotFound => assert!(matches!(err, QueryExecuteError::NotFound(_))),
227            ErrorClass::Unsupported => assert!(matches!(err, QueryExecuteError::Unsupported(_))),
228            ErrorClass::Internal => assert!(matches!(err, QueryExecuteError::Internal(_))),
229        }
230    }
231
232    #[test]
233    fn query_execute_error_from_internal_preserves_class_and_origin_matrix() {
234        let cases = [
235            (ErrorClass::Corruption, ErrorOrigin::Store),
236            (ErrorClass::InvariantViolation, ErrorOrigin::Query),
237            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
238            (ErrorClass::Conflict, ErrorOrigin::Executor),
239            (ErrorClass::NotFound, ErrorOrigin::Identity),
240            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
241            (ErrorClass::Internal, ErrorOrigin::Serialize),
242            (ErrorClass::Internal, ErrorOrigin::Planner),
243        ];
244
245        for (class, origin) in cases {
246            let internal = InternalError::classified(class, origin, "matrix");
247            let mapped = QueryExecuteError::from(internal);
248
249            assert_execute_variant_for_class(&mapped, class);
250            assert_eq!(mapped.as_internal().class, class);
251            assert_eq!(mapped.as_internal().origin, origin);
252        }
253    }
254
255    #[test]
256    fn planner_internal_mapping_preserves_runtime_class_and_origin() {
257        let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
258            ErrorClass::Unsupported,
259            ErrorOrigin::Cursor,
260            "cursor payload mismatch",
261        )));
262        let query_err = QueryError::from(planner_internal);
263
264        assert!(matches!(
265            query_err,
266            QueryError::Execute(QueryExecuteError::Unsupported(inner))
267                if inner.class == ErrorClass::Unsupported
268                    && inner.origin == ErrorOrigin::Cursor
269        ));
270    }
271
272    #[test]
273    fn planner_internal_mapping_preserves_boundary_origins_for_telemetry_matrix() {
274        let cases = [
275            (ErrorClass::Internal, ErrorOrigin::Planner),
276            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
277            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
278            (ErrorClass::Corruption, ErrorOrigin::Identity),
279        ];
280
281        for (class, origin) in cases {
282            let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
283                class, origin, "matrix",
284            )));
285            let query_err = QueryError::from(planner_internal);
286
287            let QueryError::Execute(execute_err) = query_err else {
288                panic!("planner internal errors must map to query execute errors");
289            };
290            assert_execute_variant_for_class(&execute_err, class);
291            assert_eq!(
292                execute_err.as_internal().origin,
293                origin,
294                "planner-internal mapping must preserve telemetry origin for {origin:?}",
295            );
296        }
297    }
298
299    #[test]
300    fn cursor_paging_policy_maps_to_invalid_paging_shape_intent_error() {
301        let order = IntentError::from(CursorPagingPolicyError::CursorRequiresOrder);
302        let limit = IntentError::from(CursorPagingPolicyError::CursorRequiresLimit);
303
304        assert!(matches!(
305            order,
306            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
307        ));
308        assert!(matches!(
309            limit,
310            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
311        ));
312    }
313
314    #[test]
315    fn fluent_paging_policy_maps_to_invalid_paging_shape_or_grouped_contract() {
316        let non_paged = IntentError::from(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
317        let grouped = IntentError::from(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
318
319        assert!(matches!(
320            non_paged,
321            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresPagedExecution)
322        ));
323        assert!(matches!(
324            grouped,
325            IntentError::GroupedRequiresExecuteGrouped
326        ));
327    }
328}