icydb_core/db/query/intent/
errors.rs1use 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#[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 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 #[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#[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#[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#[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}