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::Semantic(inner) => match inner.as_ref() {
442 crate::db::query::plan::SemanticPlanError::Group(inner) => {
443 format!("invalid logical plan: {inner}")
444 }
445 _ => err.to_string(),
446 },
447 PlanError::Cursor(_) => err.to_string(),
448 };
449
450 Self::planner_invariant(message)
451 }
452
453 pub(crate) fn from_executor_access_plan_error(err: AccessPlanError) -> Self {
455 Self::query_invariant(err.to_string())
456 }
457
458 #[cfg(test)]
461 pub(crate) fn plan_invariant_violation(err: PolicyPlanError) -> Self {
462 let reason = match err {
463 PolicyPlanError::EmptyOrderSpec => {
464 "order specification must include at least one field"
465 }
466 PolicyPlanError::DeletePlanWithPagination => "delete plans must not include pagination",
467 PolicyPlanError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
468 PolicyPlanError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
469 PolicyPlanError::UnorderedPagination => "pagination requires explicit ordering",
470 };
471
472 Self::planner_invariant(Self::executor_invariant_message(reason))
473 }
474}
475
476#[derive(Debug, ThisError)]
484pub enum ErrorDetail {
485 #[error("{0}")]
486 Store(StoreError),
487 #[error("{0}")]
488 ViewPatch(crate::patch::MergePatchError),
489 }
499
500impl From<MergePatchError> for InternalError {
501 fn from(err: MergePatchError) -> Self {
502 Self {
503 class: ErrorClass::Unsupported,
504 origin: ErrorOrigin::Interface,
505 message: err.to_string(),
506 detail: Some(ErrorDetail::ViewPatch(err)),
507 }
508 }
509}
510
511#[derive(Debug, ThisError)]
519pub enum StoreError {
520 #[error("key not found: {key}")]
521 NotFound { key: String },
522
523 #[error("store corruption: {message}")]
524 Corrupt { message: String },
525
526 #[error("store invariant violation: {message}")]
527 InvariantViolation { message: String },
528}
529
530#[derive(Clone, Copy, Debug, Eq, PartialEq)]
537pub enum ErrorClass {
538 Corruption,
539 NotFound,
540 Internal,
541 Conflict,
542 Unsupported,
543 InvariantViolation,
544}
545
546impl fmt::Display for ErrorClass {
547 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
548 let label = match self {
549 Self::Corruption => "corruption",
550 Self::NotFound => "not_found",
551 Self::Internal => "internal",
552 Self::Conflict => "conflict",
553 Self::Unsupported => "unsupported",
554 Self::InvariantViolation => "invariant_violation",
555 };
556 write!(f, "{label}")
557 }
558}
559
560#[derive(Clone, Copy, Debug, Eq, PartialEq)]
567pub enum ErrorOrigin {
568 Serialize,
569 Store,
570 Index,
571 Identity,
572 Query,
573 Planner,
574 Cursor,
575 Recovery,
576 Response,
577 Executor,
578 Interface,
579}
580
581impl fmt::Display for ErrorOrigin {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 let label = match self {
584 Self::Serialize => "serialize",
585 Self::Store => "store",
586 Self::Index => "index",
587 Self::Identity => "identity",
588 Self::Query => "query",
589 Self::Planner => "planner",
590 Self::Cursor => "cursor",
591 Self::Recovery => "recovery",
592 Self::Response => "response",
593 Self::Executor => "executor",
594 Self::Interface => "interface",
595 };
596 write!(f, "{label}")
597 }
598}
599
600#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::db::query::plan::validate::GroupPlanError;
608
609 #[test]
610 fn index_plan_index_corruption_uses_index_origin() {
611 let err = InternalError::index_plan_index_corruption("broken key payload");
612 assert_eq!(err.class, ErrorClass::Corruption);
613 assert_eq!(err.origin, ErrorOrigin::Index);
614 assert_eq!(
615 err.message,
616 "corruption detected (index): broken key payload"
617 );
618 }
619
620 #[test]
621 fn index_plan_store_corruption_uses_store_origin() {
622 let err = InternalError::index_plan_store_corruption("row/key mismatch");
623 assert_eq!(err.class, ErrorClass::Corruption);
624 assert_eq!(err.origin, ErrorOrigin::Store);
625 assert_eq!(err.message, "corruption detected (store): row/key mismatch");
626 }
627
628 #[test]
629 fn index_plan_serialize_corruption_uses_serialize_origin() {
630 let err = InternalError::index_plan_serialize_corruption("decode failed");
631 assert_eq!(err.class, ErrorClass::Corruption);
632 assert_eq!(err.origin, ErrorOrigin::Serialize);
633 assert_eq!(
634 err.message,
635 "corruption detected (serialize): decode failed"
636 );
637 }
638
639 #[test]
640 fn index_plan_store_invariant_uses_store_origin() {
641 let err = InternalError::index_plan_store_invariant("row/key mismatch");
642 assert_eq!(err.class, ErrorClass::InvariantViolation);
643 assert_eq!(err.origin, ErrorOrigin::Store);
644 assert_eq!(
645 err.message,
646 "invariant violation detected (store): row/key mismatch"
647 );
648 }
649
650 #[test]
651 fn query_executor_invariant_uses_invariant_violation_class() {
652 let err = InternalError::query_executor_invariant("route contract mismatch");
653 assert_eq!(err.class, ErrorClass::InvariantViolation);
654 assert_eq!(err.origin, ErrorOrigin::Query);
655 }
656
657 #[test]
658 fn executor_access_plan_error_mapping_stays_invariant_violation() {
659 let err = InternalError::from_executor_access_plan_error(AccessPlanError::IndexPrefixEmpty);
660 assert_eq!(err.class, ErrorClass::InvariantViolation);
661 assert_eq!(err.origin, ErrorOrigin::Query);
662 }
663
664 #[test]
665 fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
666 let err =
667 InternalError::plan_invariant_violation(PolicyPlanError::DeleteLimitRequiresOrder);
668 assert_eq!(err.class, ErrorClass::InvariantViolation);
669 assert_eq!(err.origin, ErrorOrigin::Planner);
670 assert_eq!(
671 err.message,
672 "executor invariant violated: delete limit requires explicit ordering",
673 );
674 }
675
676 #[test]
677 fn group_plan_error_mapping_uses_invalid_logical_plan_prefix() {
678 let err = InternalError::from_group_plan_error(PlanError::from(
679 GroupPlanError::UnknownGroupField {
680 field: "tenant".to_string(),
681 },
682 ));
683
684 assert_eq!(err.class, ErrorClass::InvariantViolation);
685 assert_eq!(err.origin, ErrorOrigin::Planner);
686 assert_eq!(
687 err.message,
688 "invalid logical plan: unknown group field 'tenant'",
689 );
690 }
691}