1use crate::{
2 db::{
3 access::AccessPlanError,
4 cursor::CursorPlanError,
5 query::{plan::PlanError, policy::PlanPolicyError},
6 },
7 patch::MergePatchError,
8};
9use std::fmt;
10use thiserror::Error as ThisError;
11
12#[derive(Debug, ThisError)]
113#[error("{message}")]
114pub struct InternalError {
115 pub class: ErrorClass,
116 pub origin: ErrorOrigin,
117 pub message: String,
118
119 pub detail: Option<ErrorDetail>,
122}
123
124impl InternalError {
125 pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
129 let message = message.into();
130
131 let detail = match (class, origin) {
132 (ErrorClass::Corruption, ErrorOrigin::Store) => {
133 Some(ErrorDetail::Store(StoreError::Corrupt {
134 message: message.clone(),
135 }))
136 }
137 (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
138 Some(ErrorDetail::Store(StoreError::InvariantViolation {
139 message: message.clone(),
140 }))
141 }
142 _ => None,
143 };
144
145 Self {
146 class,
147 origin,
148 message,
149 detail,
150 }
151 }
152
153 pub(crate) fn classified(
155 class: ErrorClass,
156 origin: ErrorOrigin,
157 message: impl Into<String>,
158 ) -> Self {
159 Self::new(class, origin, message)
160 }
161
162 pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
164 Self::classified(self.class, self.origin, message)
165 }
166
167 pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
169 Self::new(
170 ErrorClass::InvariantViolation,
171 ErrorOrigin::Query,
172 message.into(),
173 )
174 }
175
176 #[must_use]
178 pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
179 format!("executor invariant violated: {}", reason.into())
180 }
181
182 #[must_use]
184 pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
185 format!("invalid logical plan: {}", reason.into())
186 }
187
188 pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
190 Self::query_invariant(Self::executor_invariant_message(reason))
191 }
192
193 pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
195 Self::query_invariant(Self::invalid_logical_plan_message(reason))
196 }
197
198 pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
200 Self::new(
201 ErrorClass::InvariantViolation,
202 ErrorOrigin::Index,
203 message.into(),
204 )
205 }
206
207 pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
209 Self::new(
210 ErrorClass::InvariantViolation,
211 ErrorOrigin::Executor,
212 message.into(),
213 )
214 }
215
216 pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
218 Self::new(
219 ErrorClass::InvariantViolation,
220 ErrorOrigin::Store,
221 message.into(),
222 )
223 }
224
225 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
227 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
228 }
229
230 pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
232 Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
233 }
234
235 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
237 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
238 }
239
240 #[cfg(test)]
242 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
243 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
244 }
245
246 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
248 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
249 }
250
251 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
253 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
254 }
255
256 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
258 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
259 }
260
261 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
263 Self::new(
264 ErrorClass::Corruption,
265 ErrorOrigin::Serialize,
266 message.into(),
267 )
268 }
269
270 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
272 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
273 }
274
275 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
277 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
278 }
279
280 pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
282 Self::new(
283 ErrorClass::Unsupported,
284 ErrorOrigin::Executor,
285 message.into(),
286 )
287 }
288
289 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
291 Self::new(
292 ErrorClass::Unsupported,
293 ErrorOrigin::Serialize,
294 message.into(),
295 )
296 }
297
298 pub fn store_not_found(key: impl Into<String>) -> Self {
299 let key = key.into();
300
301 Self {
302 class: ErrorClass::NotFound,
303 origin: ErrorOrigin::Store,
304 message: format!("data key not found: {key}"),
305 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
306 }
307 }
308
309 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
311 let path = path.into();
312
313 Self::new(
314 ErrorClass::Unsupported,
315 ErrorOrigin::Store,
316 format!("unsupported entity path: '{path}'"),
317 )
318 }
319
320 #[must_use]
321 pub const fn is_not_found(&self) -> bool {
322 matches!(
323 self.detail,
324 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
325 )
326 }
327
328 #[must_use]
329 pub fn display_with_class(&self) -> String {
330 format!("{}:{}: {}", self.origin, self.class, self.message)
331 }
332
333 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
335 let message = message.into();
336 Self::new(
337 ErrorClass::Corruption,
338 origin,
339 format!("corruption detected ({origin}): {message}"),
340 )
341 }
342
343 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
345 Self::index_plan_corruption(ErrorOrigin::Index, message)
346 }
347
348 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
350 Self::index_plan_corruption(ErrorOrigin::Store, message)
351 }
352
353 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
355 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
356 }
357
358 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
360 Self::new(
361 ErrorClass::Conflict,
362 ErrorOrigin::Index,
363 format!(
364 "index constraint violation: {path} ({})",
365 index_fields.join(", ")
366 ),
367 )
368 }
369
370 pub(crate) fn from_cursor_plan_error(err: PlanError) -> Self {
372 let message = match &err {
373 PlanError::Cursor(inner) => match inner.as_ref() {
374 CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
375 Self::executor_invariant_message(format!(
376 "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
377 ))
378 }
379 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
380 value: None, ..
381 } => Self::executor_invariant_message("pk cursor slot must be present"),
382 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
383 value: Some(_),
384 ..
385 } => Self::executor_invariant_message("pk cursor slot type mismatch"),
386 _ => err.to_string(),
387 },
388 _ => err.to_string(),
389 };
390
391 Self::query_invariant(message)
392 }
393
394 #[cfg(test)]
396 pub(crate) fn from_group_plan_error(err: PlanError) -> Self {
397 let message = match &err {
398 PlanError::Group(inner) => format!("invalid logical plan: {inner}"),
399 _ => err.to_string(),
400 };
401
402 Self::query_invariant(message)
403 }
404
405 pub(crate) fn from_executor_access_plan_error(err: AccessPlanError) -> Self {
407 Self::query_invariant(err.to_string())
408 }
409
410 pub(crate) fn plan_invariant_violation(err: PlanPolicyError) -> Self {
413 let reason = match err {
414 PlanPolicyError::EmptyOrderSpec => {
415 "order specification must include at least one field"
416 }
417 PlanPolicyError::DeletePlanWithPagination => "delete plans must not include pagination",
418 PlanPolicyError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
419 PlanPolicyError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
420 PlanPolicyError::UnorderedPagination => "pagination requires explicit ordering",
421 };
422
423 Self::query_executor_invariant(reason)
424 }
425}
426
427#[derive(Debug, ThisError)]
435pub enum ErrorDetail {
436 #[error("{0}")]
437 Store(StoreError),
438 #[error("{0}")]
439 ViewPatch(crate::patch::MergePatchError),
440 }
450
451impl From<MergePatchError> for InternalError {
452 fn from(err: MergePatchError) -> Self {
453 Self {
454 class: ErrorClass::Unsupported,
455 origin: ErrorOrigin::Interface,
456 message: err.to_string(),
457 detail: Some(ErrorDetail::ViewPatch(err)),
458 }
459 }
460}
461
462#[derive(Debug, ThisError)]
470pub enum StoreError {
471 #[error("key not found: {key}")]
472 NotFound { key: String },
473
474 #[error("store corruption: {message}")]
475 Corrupt { message: String },
476
477 #[error("store invariant violation: {message}")]
478 InvariantViolation { message: String },
479}
480
481#[derive(Clone, Copy, Debug, Eq, PartialEq)]
488pub enum ErrorClass {
489 Corruption,
490 NotFound,
491 Internal,
492 Conflict,
493 Unsupported,
494 InvariantViolation,
495}
496
497impl fmt::Display for ErrorClass {
498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499 let label = match self {
500 Self::Corruption => "corruption",
501 Self::NotFound => "not_found",
502 Self::Internal => "internal",
503 Self::Conflict => "conflict",
504 Self::Unsupported => "unsupported",
505 Self::InvariantViolation => "invariant_violation",
506 };
507 write!(f, "{label}")
508 }
509}
510
511#[derive(Clone, Copy, Debug, Eq, PartialEq)]
518pub enum ErrorOrigin {
519 Serialize,
520 Store,
521 Index,
522 Query,
523 Response,
524 Executor,
525 Interface,
526}
527
528impl fmt::Display for ErrorOrigin {
529 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530 let label = match self {
531 Self::Serialize => "serialize",
532 Self::Store => "store",
533 Self::Index => "index",
534 Self::Query => "query",
535 Self::Response => "response",
536 Self::Executor => "executor",
537 Self::Interface => "interface",
538 };
539 write!(f, "{label}")
540 }
541}
542
543#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::db::query::plan::validate::GroupPlanError;
551
552 #[test]
553 fn index_plan_index_corruption_uses_index_origin() {
554 let err = InternalError::index_plan_index_corruption("broken key payload");
555 assert_eq!(err.class, ErrorClass::Corruption);
556 assert_eq!(err.origin, ErrorOrigin::Index);
557 assert_eq!(
558 err.message,
559 "corruption detected (index): broken key payload"
560 );
561 }
562
563 #[test]
564 fn index_plan_store_corruption_uses_store_origin() {
565 let err = InternalError::index_plan_store_corruption("row/key mismatch");
566 assert_eq!(err.class, ErrorClass::Corruption);
567 assert_eq!(err.origin, ErrorOrigin::Store);
568 assert_eq!(err.message, "corruption detected (store): row/key mismatch");
569 }
570
571 #[test]
572 fn index_plan_serialize_corruption_uses_serialize_origin() {
573 let err = InternalError::index_plan_serialize_corruption("decode failed");
574 assert_eq!(err.class, ErrorClass::Corruption);
575 assert_eq!(err.origin, ErrorOrigin::Serialize);
576 assert_eq!(
577 err.message,
578 "corruption detected (serialize): decode failed"
579 );
580 }
581
582 #[test]
583 fn query_executor_invariant_uses_invariant_violation_class() {
584 let err = InternalError::query_executor_invariant("route contract mismatch");
585 assert_eq!(err.class, ErrorClass::InvariantViolation);
586 assert_eq!(err.origin, ErrorOrigin::Query);
587 }
588
589 #[test]
590 fn executor_access_plan_error_mapping_stays_invariant_violation() {
591 let err = InternalError::from_executor_access_plan_error(AccessPlanError::IndexPrefixEmpty);
592 assert_eq!(err.class, ErrorClass::InvariantViolation);
593 assert_eq!(err.origin, ErrorOrigin::Query);
594 }
595
596 #[test]
597 fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
598 let err =
599 InternalError::plan_invariant_violation(PlanPolicyError::DeleteLimitRequiresOrder);
600 assert_eq!(err.class, ErrorClass::InvariantViolation);
601 assert_eq!(err.origin, ErrorOrigin::Query);
602 assert_eq!(
603 err.message,
604 "executor invariant violated: delete limit requires explicit ordering",
605 );
606 }
607
608 #[test]
609 fn group_plan_error_mapping_uses_invalid_logical_plan_prefix() {
610 let err = InternalError::from_group_plan_error(PlanError::from(
611 GroupPlanError::UnknownGroupField {
612 field: "tenant".to_string(),
613 },
614 ));
615
616 assert_eq!(err.class, ErrorClass::InvariantViolation);
617 assert_eq!(err.origin, ErrorOrigin::Query);
618 assert_eq!(
619 err.message,
620 "invalid logical plan: unknown group field 'tenant'",
621 );
622 }
623}