1#[cfg(test)]
2use crate::db::query::plan::{PlanError, PolicyPlanError};
3use crate::{
4 db::{access::AccessPlanError, cursor::CursorPlanError},
5 patch::MergePatchError,
6};
7use std::fmt;
8use thiserror::Error as ThisError;
9
10#[derive(Debug, ThisError)]
111#[error("{message}")]
112pub struct InternalError {
113 pub class: ErrorClass,
114 pub origin: ErrorOrigin,
115 pub message: String,
116
117 pub detail: Option<ErrorDetail>,
120}
121
122impl InternalError {
123 pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
127 let message = message.into();
128
129 let detail = match (class, origin) {
130 (ErrorClass::Corruption, ErrorOrigin::Store) => {
131 Some(ErrorDetail::Store(StoreError::Corrupt {
132 message: message.clone(),
133 }))
134 }
135 (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
136 Some(ErrorDetail::Store(StoreError::InvariantViolation {
137 message: message.clone(),
138 }))
139 }
140 _ => None,
141 };
142
143 Self {
144 class,
145 origin,
146 message,
147 detail,
148 }
149 }
150
151 pub(crate) fn classified(
153 class: ErrorClass,
154 origin: ErrorOrigin,
155 message: impl Into<String>,
156 ) -> Self {
157 Self::new(class, origin, message)
158 }
159
160 pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
162 Self::classified(self.class, self.origin, message)
163 }
164
165 pub(crate) fn with_origin(self, origin: ErrorOrigin) -> Self {
169 Self::classified(self.class, origin, self.message)
170 }
171
172 pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
174 Self::new(
175 ErrorClass::InvariantViolation,
176 ErrorOrigin::Query,
177 message.into(),
178 )
179 }
180
181 pub(crate) fn planner_invariant(message: impl Into<String>) -> Self {
183 Self::new(
184 ErrorClass::InvariantViolation,
185 ErrorOrigin::Planner,
186 message.into(),
187 )
188 }
189
190 #[must_use]
192 pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
193 format!("executor invariant violated: {}", reason.into())
194 }
195
196 #[must_use]
198 pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
199 format!("invalid logical plan: {}", reason.into())
200 }
201
202 pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
204 Self::query_invariant(Self::executor_invariant_message(reason))
205 }
206
207 pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
209 Self::planner_invariant(Self::invalid_logical_plan_message(reason))
210 }
211
212 pub(crate) fn cursor_invariant(message: impl Into<String>) -> Self {
214 Self::new(
215 ErrorClass::InvariantViolation,
216 ErrorOrigin::Cursor,
217 message.into(),
218 )
219 }
220
221 pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
223 Self::new(
224 ErrorClass::InvariantViolation,
225 ErrorOrigin::Index,
226 message.into(),
227 )
228 }
229
230 pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
232 Self::new(
233 ErrorClass::InvariantViolation,
234 ErrorOrigin::Executor,
235 message.into(),
236 )
237 }
238
239 pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
241 Self::new(
242 ErrorClass::InvariantViolation,
243 ErrorOrigin::Store,
244 message.into(),
245 )
246 }
247
248 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
250 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
251 }
252
253 pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
255 Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
256 }
257
258 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
260 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
261 }
262
263 #[cfg(test)]
265 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
266 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
267 }
268
269 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
271 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
272 }
273
274 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
276 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
277 }
278
279 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
281 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
282 }
283
284 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
286 Self::new(
287 ErrorClass::Corruption,
288 ErrorOrigin::Serialize,
289 message.into(),
290 )
291 }
292
293 pub(crate) fn identity_corruption(message: impl Into<String>) -> Self {
295 Self::new(
296 ErrorClass::Corruption,
297 ErrorOrigin::Identity,
298 message.into(),
299 )
300 }
301
302 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
304 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
305 }
306
307 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
309 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
310 }
311
312 pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
314 Self::new(
315 ErrorClass::Unsupported,
316 ErrorOrigin::Executor,
317 message.into(),
318 )
319 }
320
321 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
323 Self::new(
324 ErrorClass::Unsupported,
325 ErrorOrigin::Serialize,
326 message.into(),
327 )
328 }
329
330 pub fn store_not_found(key: impl Into<String>) -> Self {
331 let key = key.into();
332
333 Self {
334 class: ErrorClass::NotFound,
335 origin: ErrorOrigin::Store,
336 message: format!("data key not found: {key}"),
337 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
338 }
339 }
340
341 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
343 let path = path.into();
344
345 Self::new(
346 ErrorClass::Unsupported,
347 ErrorOrigin::Store,
348 format!("unsupported entity path: '{path}'"),
349 )
350 }
351
352 #[must_use]
353 pub const fn is_not_found(&self) -> bool {
354 matches!(
355 self.detail,
356 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
357 )
358 }
359
360 #[must_use]
361 pub fn display_with_class(&self) -> String {
362 format!("{}:{}: {}", self.origin, self.class, self.message)
363 }
364
365 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
367 let message = message.into();
368 Self::new(
369 ErrorClass::Corruption,
370 origin,
371 format!("corruption detected ({origin}): {message}"),
372 )
373 }
374
375 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
377 Self::index_plan_corruption(ErrorOrigin::Index, message)
378 }
379
380 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
382 Self::index_plan_corruption(ErrorOrigin::Store, message)
383 }
384
385 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
387 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
388 }
389
390 pub(crate) fn index_plan_invariant(origin: ErrorOrigin, message: impl Into<String>) -> Self {
392 let message = message.into();
393 Self::new(
394 ErrorClass::InvariantViolation,
395 origin,
396 format!("invariant violation detected ({origin}): {message}"),
397 )
398 }
399
400 pub(crate) fn index_plan_store_invariant(message: impl Into<String>) -> Self {
402 Self::index_plan_invariant(ErrorOrigin::Store, message)
403 }
404
405 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
407 Self::new(
408 ErrorClass::Conflict,
409 ErrorOrigin::Index,
410 format!(
411 "index constraint violation: {path} ({})",
412 index_fields.join(", ")
413 ),
414 )
415 }
416
417 pub(crate) fn from_cursor_plan_error(err: CursorPlanError) -> Self {
419 let message = match &err {
420 CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
421 Self::executor_invariant_message(format!(
422 "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
423 ))
424 }
425 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch { value: None, .. } => {
426 Self::executor_invariant_message("pk cursor slot must be present")
427 }
428 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
429 value: Some(_), ..
430 } => Self::executor_invariant_message("pk cursor slot type mismatch"),
431 _ => err.to_string(),
432 };
433
434 Self::cursor_invariant(message)
435 }
436
437 #[cfg(test)]
439 pub(crate) fn from_group_plan_error(err: PlanError) -> Self {
440 let message = match &err {
441 PlanError::Group(inner) => format!("invalid logical plan: {inner}"),
442 _ => err.to_string(),
443 };
444
445 Self::planner_invariant(message)
446 }
447
448 pub(crate) fn from_executor_access_plan_error(err: AccessPlanError) -> Self {
450 Self::query_invariant(err.to_string())
451 }
452
453 #[cfg(test)]
456 pub(crate) fn plan_invariant_violation(err: PolicyPlanError) -> Self {
457 let reason = match err {
458 PolicyPlanError::EmptyOrderSpec => {
459 "order specification must include at least one field"
460 }
461 PolicyPlanError::DeletePlanWithPagination => "delete plans must not include pagination",
462 PolicyPlanError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
463 PolicyPlanError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
464 PolicyPlanError::UnorderedPagination => "pagination requires explicit ordering",
465 };
466
467 Self::planner_invariant(Self::executor_invariant_message(reason))
468 }
469}
470
471#[derive(Debug, ThisError)]
479pub enum ErrorDetail {
480 #[error("{0}")]
481 Store(StoreError),
482 #[error("{0}")]
483 ViewPatch(crate::patch::MergePatchError),
484 }
494
495impl From<MergePatchError> for InternalError {
496 fn from(err: MergePatchError) -> Self {
497 Self {
498 class: ErrorClass::Unsupported,
499 origin: ErrorOrigin::Interface,
500 message: err.to_string(),
501 detail: Some(ErrorDetail::ViewPatch(err)),
502 }
503 }
504}
505
506#[derive(Debug, ThisError)]
514pub enum StoreError {
515 #[error("key not found: {key}")]
516 NotFound { key: String },
517
518 #[error("store corruption: {message}")]
519 Corrupt { message: String },
520
521 #[error("store invariant violation: {message}")]
522 InvariantViolation { message: String },
523}
524
525#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532pub enum ErrorClass {
533 Corruption,
534 NotFound,
535 Internal,
536 Conflict,
537 Unsupported,
538 InvariantViolation,
539}
540
541impl fmt::Display for ErrorClass {
542 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543 let label = match self {
544 Self::Corruption => "corruption",
545 Self::NotFound => "not_found",
546 Self::Internal => "internal",
547 Self::Conflict => "conflict",
548 Self::Unsupported => "unsupported",
549 Self::InvariantViolation => "invariant_violation",
550 };
551 write!(f, "{label}")
552 }
553}
554
555#[derive(Clone, Copy, Debug, Eq, PartialEq)]
562pub enum ErrorOrigin {
563 Serialize,
564 Store,
565 Index,
566 Identity,
567 Query,
568 Planner,
569 Cursor,
570 Recovery,
571 Response,
572 Executor,
573 Interface,
574}
575
576impl fmt::Display for ErrorOrigin {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 let label = match self {
579 Self::Serialize => "serialize",
580 Self::Store => "store",
581 Self::Index => "index",
582 Self::Identity => "identity",
583 Self::Query => "query",
584 Self::Planner => "planner",
585 Self::Cursor => "cursor",
586 Self::Recovery => "recovery",
587 Self::Response => "response",
588 Self::Executor => "executor",
589 Self::Interface => "interface",
590 };
591 write!(f, "{label}")
592 }
593}
594
595#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::db::query::plan::validate::GroupPlanError;
603
604 #[test]
605 fn index_plan_index_corruption_uses_index_origin() {
606 let err = InternalError::index_plan_index_corruption("broken key payload");
607 assert_eq!(err.class, ErrorClass::Corruption);
608 assert_eq!(err.origin, ErrorOrigin::Index);
609 assert_eq!(
610 err.message,
611 "corruption detected (index): broken key payload"
612 );
613 }
614
615 #[test]
616 fn index_plan_store_corruption_uses_store_origin() {
617 let err = InternalError::index_plan_store_corruption("row/key mismatch");
618 assert_eq!(err.class, ErrorClass::Corruption);
619 assert_eq!(err.origin, ErrorOrigin::Store);
620 assert_eq!(err.message, "corruption detected (store): row/key mismatch");
621 }
622
623 #[test]
624 fn index_plan_serialize_corruption_uses_serialize_origin() {
625 let err = InternalError::index_plan_serialize_corruption("decode failed");
626 assert_eq!(err.class, ErrorClass::Corruption);
627 assert_eq!(err.origin, ErrorOrigin::Serialize);
628 assert_eq!(
629 err.message,
630 "corruption detected (serialize): decode failed"
631 );
632 }
633
634 #[test]
635 fn index_plan_store_invariant_uses_store_origin() {
636 let err = InternalError::index_plan_store_invariant("row/key mismatch");
637 assert_eq!(err.class, ErrorClass::InvariantViolation);
638 assert_eq!(err.origin, ErrorOrigin::Store);
639 assert_eq!(
640 err.message,
641 "invariant violation detected (store): row/key mismatch"
642 );
643 }
644
645 #[test]
646 fn query_executor_invariant_uses_invariant_violation_class() {
647 let err = InternalError::query_executor_invariant("route contract mismatch");
648 assert_eq!(err.class, ErrorClass::InvariantViolation);
649 assert_eq!(err.origin, ErrorOrigin::Query);
650 }
651
652 #[test]
653 fn executor_access_plan_error_mapping_stays_invariant_violation() {
654 let err = InternalError::from_executor_access_plan_error(AccessPlanError::IndexPrefixEmpty);
655 assert_eq!(err.class, ErrorClass::InvariantViolation);
656 assert_eq!(err.origin, ErrorOrigin::Query);
657 }
658
659 #[test]
660 fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
661 let err =
662 InternalError::plan_invariant_violation(PolicyPlanError::DeleteLimitRequiresOrder);
663 assert_eq!(err.class, ErrorClass::InvariantViolation);
664 assert_eq!(err.origin, ErrorOrigin::Planner);
665 assert_eq!(
666 err.message,
667 "executor invariant violated: delete limit requires explicit ordering",
668 );
669 }
670
671 #[test]
672 fn group_plan_error_mapping_uses_invalid_logical_plan_prefix() {
673 let err = InternalError::from_group_plan_error(PlanError::from(
674 GroupPlanError::UnknownGroupField {
675 field: "tenant".to_string(),
676 },
677 ));
678
679 assert_eq!(err.class, ErrorClass::InvariantViolation);
680 assert_eq!(err.origin, ErrorOrigin::Planner);
681 assert_eq!(
682 err.message,
683 "invalid logical plan: unknown group field 'tenant'",
684 );
685 }
686}