icydb_core/db/query/intent/
errors.rs1use 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#[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 pub(crate) fn execute(err: InternalError) -> Self {
45 Self::Execute(QueryExecutionError::from(err))
46 }
47}
48
49#[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 #[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#[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#[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#[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}