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] ExecutionError),
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(ExecutionError::from(err))
41    }
42}
43
44///
45/// ExecutionError
46///
47
48#[derive(Debug, ThisError)]
49pub enum ExecutionError {
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 ExecutionError {
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 ExecutionError {
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: &ExecutionError, class: ErrorClass) {
220        match class {
221            ErrorClass::Corruption => assert!(matches!(err, ExecutionError::Corruption(_))),
222            ErrorClass::InvariantViolation => {
223                assert!(matches!(err, ExecutionError::InvariantViolation(_)));
224            }
225            ErrorClass::Conflict => assert!(matches!(err, ExecutionError::Conflict(_))),
226            ErrorClass::NotFound => assert!(matches!(err, ExecutionError::NotFound(_))),
227            ErrorClass::Unsupported => assert!(matches!(err, ExecutionError::Unsupported(_))),
228            ErrorClass::Internal => assert!(matches!(err, ExecutionError::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 = ExecutionError::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(ExecutionError::Unsupported(inner))
267                if inner.class == ErrorClass::Unsupported
268                    && inner.origin == ErrorOrigin::Cursor
269        ));
270    }
271
272    #[test]
273    fn planner_plan_mapping_stays_in_query_plan_error_boundary() {
274        let planner_plan = PlannerError::Plan(Box::new(PlanError::from(
275            PolicyPlanError::UnorderedPagination,
276        )));
277        let query_err = QueryError::from(planner_plan);
278
279        assert!(
280            matches!(query_err, QueryError::Plan(_)),
281            "planner plan errors must remain in query plan boundary, not execution boundary",
282        );
283    }
284
285    #[test]
286    fn planner_internal_mapping_preserves_boundary_origins_for_telemetry_matrix() {
287        let cases = [
288            (ErrorClass::Internal, ErrorOrigin::Planner),
289            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
290            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
291            (ErrorClass::Corruption, ErrorOrigin::Identity),
292        ];
293
294        for (class, origin) in cases {
295            let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
296                class, origin, "matrix",
297            )));
298            let query_err = QueryError::from(planner_internal);
299
300            let QueryError::Execute(execute_err) = query_err else {
301                panic!("planner internal errors must map to query execute errors");
302            };
303            assert_execute_variant_for_class(&execute_err, class);
304            assert_eq!(
305                execute_err.as_internal().origin,
306                origin,
307                "planner-internal mapping must preserve telemetry origin for {origin:?}",
308            );
309        }
310    }
311
312    #[test]
313    fn query_execute_storage_and_index_errors_stay_in_execution_boundary() {
314        let cases = [
315            InternalError::store_internal("store internal"),
316            InternalError::index_internal("index internal"),
317            InternalError::store_corruption("store corruption"),
318            InternalError::index_corruption("index corruption"),
319            InternalError::store_unsupported("store unsupported"),
320            InternalError::index_unsupported("index unsupported"),
321        ];
322
323        for internal in cases {
324            let class = internal.class;
325            let origin = internal.origin;
326
327            let query_err = QueryError::execute(internal);
328            let QueryError::Execute(execute_err) = query_err else {
329                panic!("storage/index runtime failures must stay in execution boundary");
330            };
331
332            assert_execute_variant_for_class(&execute_err, class);
333            assert_eq!(
334                execute_err.as_internal().origin,
335                origin,
336                "storage/index runtime failures must preserve origin taxonomy",
337            );
338        }
339    }
340
341    #[test]
342    fn cursor_paging_policy_maps_to_invalid_paging_shape_intent_error() {
343        let order = IntentError::from(CursorPagingPolicyError::CursorRequiresOrder);
344        let limit = IntentError::from(CursorPagingPolicyError::CursorRequiresLimit);
345
346        assert!(matches!(
347            order,
348            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
349        ));
350        assert!(matches!(
351            limit,
352            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
353        ));
354    }
355
356    #[test]
357    fn fluent_paging_policy_maps_to_invalid_paging_shape_or_grouped_contract() {
358        let non_paged = IntentError::from(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
359        let grouped = IntentError::from(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
360
361        assert!(matches!(
362            non_paged,
363            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresPagedExecution)
364        ));
365        assert!(matches!(
366            grouped,
367            IntentError::GroupedRequiresExecuteGrouped
368        ));
369    }
370}