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