Skip to main content

icydb_core/db/query/intent/
errors.rs

1//! Module: query::intent::errors
2//! Responsibility: query-intent-facing typed error taxonomy and domain conversions.
3//! Does not own: planner rule evaluation or runtime execution policy decisions.
4//! Boundary: unifies intent/planner/cursor/resource errors into query API error classes.
5
6use crate::{
7    db::{
8        cursor::CursorPlanError,
9        predicate::ValidateError,
10        query::plan::{
11            CursorPagingPolicyError, FluentLoadPolicyViolation, IntentKeyAccessPolicyViolation,
12            PlanError, PlannerError, PolicyPlanError,
13        },
14        response::ResponseError,
15    },
16    error::{ErrorClass, InternalError},
17};
18use thiserror::Error as ThisError;
19
20///
21/// QueryError
22///
23
24#[derive(Debug, ThisError)]
25pub enum QueryError {
26    #[error("{0}")]
27    Validate(#[from] ValidateError),
28
29    #[error("{0}")]
30    Plan(Box<PlanError>),
31
32    #[error("{0}")]
33    Intent(#[from] IntentError),
34
35    #[error("{0}")]
36    Response(#[from] ResponseError),
37
38    #[error("{0}")]
39    Execute(#[from] QueryExecutionError),
40}
41
42impl QueryError {
43    /// Construct an execution-domain query error from one classified runtime error.
44    pub(crate) fn execute(err: InternalError) -> Self {
45        Self::Execute(QueryExecutionError::from(err))
46    }
47}
48
49///
50/// QueryExecutionError
51///
52
53#[derive(Debug, ThisError)]
54pub enum QueryExecutionError {
55    #[error("{0}")]
56    Corruption(InternalError),
57
58    #[error("{0}")]
59    InvariantViolation(InternalError),
60
61    #[error("{0}")]
62    Conflict(InternalError),
63
64    #[error("{0}")]
65    NotFound(InternalError),
66
67    #[error("{0}")]
68    Unsupported(InternalError),
69
70    #[error("{0}")]
71    Internal(InternalError),
72}
73
74impl QueryExecutionError {
75    /// Borrow the wrapped classified runtime error.
76    #[must_use]
77    pub const fn as_internal(&self) -> &InternalError {
78        match self {
79            Self::Corruption(err)
80            | Self::InvariantViolation(err)
81            | Self::Conflict(err)
82            | Self::NotFound(err)
83            | Self::Unsupported(err)
84            | Self::Internal(err) => err,
85        }
86    }
87}
88
89impl From<InternalError> for QueryExecutionError {
90    fn from(err: InternalError) -> Self {
91        match err.class {
92            ErrorClass::Corruption => Self::Corruption(err),
93            ErrorClass::InvariantViolation => Self::InvariantViolation(err),
94            ErrorClass::Conflict => Self::Conflict(err),
95            ErrorClass::NotFound => Self::NotFound(err),
96            ErrorClass::Unsupported => Self::Unsupported(err),
97            ErrorClass::Internal => Self::Internal(err),
98        }
99    }
100}
101
102impl From<PlannerError> for QueryError {
103    fn from(err: PlannerError) -> Self {
104        match err {
105            PlannerError::Plan(err) => Self::from(*err),
106            PlannerError::Internal(err) => Self::execute(*err),
107        }
108    }
109}
110
111impl From<PlanError> for QueryError {
112    fn from(err: PlanError) -> Self {
113        Self::Plan(Box::new(err))
114    }
115}
116
117///
118/// IntentError
119///
120
121#[derive(Clone, Copy, Debug, ThisError)]
122pub enum IntentError {
123    #[error("{0}")]
124    PlanShape(#[from] PolicyPlanError),
125
126    #[error("by_ids() cannot be combined with predicates")]
127    ByIdsWithPredicate,
128
129    #[error("only() cannot be combined with predicates")]
130    OnlyWithPredicate,
131
132    #[error("multiple key access methods were used on the same query")]
133    KeyAccessConflict,
134
135    #[error("{0}")]
136    InvalidPagingShape(#[from] PagingIntentError),
137
138    #[error("grouped queries require execute_grouped(...)")]
139    GroupedRequiresExecuteGrouped,
140
141    #[error("HAVING requires GROUP BY")]
142    HavingRequiresGroupBy,
143}
144
145///
146/// PagingIntentError
147///
148/// Canonical intent-level paging contract failures shared by planner and
149/// fluent/execution boundary gates.
150///
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
153#[expect(clippy::enum_variant_names)]
154pub enum PagingIntentError {
155    #[error(
156        "{message}",
157        message = CursorPlanError::cursor_requires_order_message()
158    )]
159    CursorRequiresOrder,
160
161    #[error(
162        "{message}",
163        message = CursorPlanError::cursor_requires_limit_message()
164    )]
165    CursorRequiresLimit,
166
167    #[error("cursor tokens can only be used with .page().execute()")]
168    CursorRequiresPagedExecution,
169}
170
171impl From<CursorPagingPolicyError> for PagingIntentError {
172    fn from(err: CursorPagingPolicyError) -> Self {
173        match err {
174            CursorPagingPolicyError::CursorRequiresOrder => Self::CursorRequiresOrder,
175            CursorPagingPolicyError::CursorRequiresLimit => Self::CursorRequiresLimit,
176        }
177    }
178}
179
180impl From<CursorPagingPolicyError> for IntentError {
181    fn from(err: CursorPagingPolicyError) -> Self {
182        Self::InvalidPagingShape(PagingIntentError::from(err))
183    }
184}
185
186impl From<IntentKeyAccessPolicyViolation> for IntentError {
187    fn from(err: IntentKeyAccessPolicyViolation) -> Self {
188        match err {
189            IntentKeyAccessPolicyViolation::KeyAccessConflict => Self::KeyAccessConflict,
190            IntentKeyAccessPolicyViolation::ByIdsWithPredicate => Self::ByIdsWithPredicate,
191            IntentKeyAccessPolicyViolation::OnlyWithPredicate => Self::OnlyWithPredicate,
192        }
193    }
194}
195
196impl From<FluentLoadPolicyViolation> for IntentError {
197    fn from(err: FluentLoadPolicyViolation) -> Self {
198        match err {
199            FluentLoadPolicyViolation::CursorRequiresPagedExecution => {
200                Self::InvalidPagingShape(PagingIntentError::CursorRequiresPagedExecution)
201            }
202            FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped => {
203                Self::GroupedRequiresExecuteGrouped
204            }
205            FluentLoadPolicyViolation::CursorRequiresOrder => {
206                Self::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
207            }
208            FluentLoadPolicyViolation::CursorRequiresLimit => {
209                Self::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
210            }
211        }
212    }
213}
214
215///
216/// TESTS
217///
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::error::ErrorOrigin;
223
224    fn assert_execute_variant_for_class(err: &QueryExecutionError, class: ErrorClass) {
225        match class {
226            ErrorClass::Corruption => assert!(matches!(err, QueryExecutionError::Corruption(_))),
227            ErrorClass::InvariantViolation => {
228                assert!(matches!(err, QueryExecutionError::InvariantViolation(_)));
229            }
230            ErrorClass::Conflict => assert!(matches!(err, QueryExecutionError::Conflict(_))),
231            ErrorClass::NotFound => assert!(matches!(err, QueryExecutionError::NotFound(_))),
232            ErrorClass::Unsupported => assert!(matches!(err, QueryExecutionError::Unsupported(_))),
233            ErrorClass::Internal => assert!(matches!(err, QueryExecutionError::Internal(_))),
234        }
235    }
236
237    #[test]
238    fn query_execute_error_from_internal_preserves_class_and_origin_matrix() {
239        let cases = [
240            (ErrorClass::Corruption, ErrorOrigin::Store),
241            (ErrorClass::InvariantViolation, ErrorOrigin::Query),
242            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
243            (ErrorClass::Conflict, ErrorOrigin::Executor),
244            (ErrorClass::NotFound, ErrorOrigin::Identity),
245            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
246            (ErrorClass::Internal, ErrorOrigin::Serialize),
247            (ErrorClass::Internal, ErrorOrigin::Planner),
248        ];
249
250        for (class, origin) in cases {
251            let internal = InternalError::classified(class, origin, "matrix");
252            let mapped = QueryExecutionError::from(internal);
253
254            assert_execute_variant_for_class(&mapped, class);
255            assert_eq!(mapped.as_internal().class, class);
256            assert_eq!(mapped.as_internal().origin, origin);
257        }
258    }
259
260    #[test]
261    fn planner_internal_mapping_preserves_runtime_class_and_origin() {
262        let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
263            ErrorClass::Unsupported,
264            ErrorOrigin::Cursor,
265            "cursor payload mismatch",
266        )));
267        let query_err = QueryError::from(planner_internal);
268
269        assert!(matches!(
270            query_err,
271            QueryError::Execute(QueryExecutionError::Unsupported(inner))
272                if inner.class == ErrorClass::Unsupported
273                    && inner.origin == ErrorOrigin::Cursor
274        ));
275    }
276
277    #[test]
278    fn planner_plan_mapping_stays_in_query_plan_error_boundary() {
279        let planner_plan = PlannerError::Plan(Box::new(PlanError::from(
280            PolicyPlanError::UnorderedPagination,
281        )));
282        let query_err = QueryError::from(planner_plan);
283
284        assert!(
285            matches!(query_err, QueryError::Plan(_)),
286            "planner plan errors must remain in query plan boundary, not execution boundary",
287        );
288    }
289
290    #[test]
291    fn planner_internal_mapping_preserves_boundary_origins_for_telemetry_matrix() {
292        let cases = [
293            (ErrorClass::Internal, ErrorOrigin::Planner),
294            (ErrorClass::Unsupported, ErrorOrigin::Cursor),
295            (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
296            (ErrorClass::Corruption, ErrorOrigin::Identity),
297        ];
298
299        for (class, origin) in cases {
300            let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
301                class, origin, "matrix",
302            )));
303            let query_err = QueryError::from(planner_internal);
304
305            let QueryError::Execute(execute_err) = query_err else {
306                panic!("planner internal errors must map to query execute errors");
307            };
308            assert_execute_variant_for_class(&execute_err, class);
309            assert_eq!(
310                execute_err.as_internal().origin,
311                origin,
312                "planner-internal mapping must preserve telemetry origin for {origin:?}",
313            );
314        }
315    }
316
317    #[test]
318    fn query_execute_storage_and_index_errors_stay_in_execution_boundary() {
319        let cases = [
320            InternalError::store_internal("store internal"),
321            InternalError::index_internal("index internal"),
322            InternalError::store_corruption("store corruption"),
323            InternalError::index_corruption("index corruption"),
324            InternalError::store_unsupported("store unsupported"),
325            InternalError::index_unsupported("index unsupported"),
326        ];
327
328        for internal in cases {
329            let class = internal.class;
330            let origin = internal.origin;
331
332            let query_err = QueryError::execute(internal);
333            let QueryError::Execute(execute_err) = query_err else {
334                panic!("storage/index runtime failures must stay in execution boundary");
335            };
336
337            assert_execute_variant_for_class(&execute_err, class);
338            assert_eq!(
339                execute_err.as_internal().origin,
340                origin,
341                "storage/index runtime failures must preserve origin taxonomy",
342            );
343        }
344    }
345
346    #[test]
347    fn cursor_paging_policy_maps_to_invalid_paging_shape_intent_error() {
348        let order = IntentError::from(CursorPagingPolicyError::CursorRequiresOrder);
349        let limit = IntentError::from(CursorPagingPolicyError::CursorRequiresLimit);
350
351        assert!(matches!(
352            order,
353            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
354        ));
355        assert!(matches!(
356            limit,
357            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
358        ));
359    }
360
361    #[test]
362    fn fluent_paging_policy_maps_to_invalid_paging_shape_or_grouped_contract() {
363        let non_paged = IntentError::from(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
364        let grouped = IntentError::from(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
365
366        assert!(matches!(
367            non_paged,
368            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresPagedExecution)
369        ));
370        assert!(matches!(
371            grouped,
372            IntentError::GroupedRequiresExecuteGrouped
373        ));
374    }
375
376    #[test]
377    fn fluent_cursor_order_and_limit_policy_map_to_intent_paging_shape() {
378        let requires_order = IntentError::from(FluentLoadPolicyViolation::CursorRequiresOrder);
379        let requires_limit = IntentError::from(FluentLoadPolicyViolation::CursorRequiresLimit);
380
381        assert!(matches!(
382            requires_order,
383            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
384        ));
385        assert!(matches!(
386            requires_limit,
387            IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
388        ));
389    }
390}