icydb_core/db/query/intent/
errors.rs1use 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#[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] QueryExecuteError),
35}
36
37impl QueryError {
38 pub(crate) fn execute(err: InternalError) -> Self {
40 Self::Execute(QueryExecuteError::from(err))
41 }
42}
43
44#[derive(Debug, ThisError)]
49pub enum QueryExecuteError {
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 QueryExecuteError {
70 #[must_use]
71 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 QueryExecuteError {
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#[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#[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#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::error::ErrorOrigin;
218
219 fn assert_execute_variant_for_class(err: &QueryExecuteError, class: ErrorClass) {
220 match class {
221 ErrorClass::Corruption => assert!(matches!(err, QueryExecuteError::Corruption(_))),
222 ErrorClass::InvariantViolation => {
223 assert!(matches!(err, QueryExecuteError::InvariantViolation(_)));
224 }
225 ErrorClass::Conflict => assert!(matches!(err, QueryExecuteError::Conflict(_))),
226 ErrorClass::NotFound => assert!(matches!(err, QueryExecuteError::NotFound(_))),
227 ErrorClass::Unsupported => assert!(matches!(err, QueryExecuteError::Unsupported(_))),
228 ErrorClass::Internal => assert!(matches!(err, QueryExecuteError::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 = QueryExecuteError::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(QueryExecuteError::Unsupported(inner))
267 if inner.class == ErrorClass::Unsupported
268 && inner.origin == ErrorOrigin::Cursor
269 ));
270 }
271
272 #[test]
273 fn planner_internal_mapping_preserves_boundary_origins_for_telemetry_matrix() {
274 let cases = [
275 (ErrorClass::Internal, ErrorOrigin::Planner),
276 (ErrorClass::Unsupported, ErrorOrigin::Cursor),
277 (ErrorClass::InvariantViolation, ErrorOrigin::Recovery),
278 (ErrorClass::Corruption, ErrorOrigin::Identity),
279 ];
280
281 for (class, origin) in cases {
282 let planner_internal = PlannerError::Internal(Box::new(InternalError::classified(
283 class, origin, "matrix",
284 )));
285 let query_err = QueryError::from(planner_internal);
286
287 let QueryError::Execute(execute_err) = query_err else {
288 panic!("planner internal errors must map to query execute errors");
289 };
290 assert_execute_variant_for_class(&execute_err, class);
291 assert_eq!(
292 execute_err.as_internal().origin,
293 origin,
294 "planner-internal mapping must preserve telemetry origin for {origin:?}",
295 );
296 }
297 }
298
299 #[test]
300 fn cursor_paging_policy_maps_to_invalid_paging_shape_intent_error() {
301 let order = IntentError::from(CursorPagingPolicyError::CursorRequiresOrder);
302 let limit = IntentError::from(CursorPagingPolicyError::CursorRequiresLimit);
303
304 assert!(matches!(
305 order,
306 IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresOrder)
307 ));
308 assert!(matches!(
309 limit,
310 IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresLimit)
311 ));
312 }
313
314 #[test]
315 fn fluent_paging_policy_maps_to_invalid_paging_shape_or_grouped_contract() {
316 let non_paged = IntentError::from(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
317 let grouped = IntentError::from(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
318
319 assert!(matches!(
320 non_paged,
321 IntentError::InvalidPagingShape(PagingIntentError::CursorRequiresPagedExecution)
322 ));
323 assert!(matches!(
324 grouped,
325 IntentError::GroupedRequiresExecuteGrouped
326 ));
327 }
328}