1use crate::{
2 db::{
3 access::AccessPlanError,
4 policy::PlanPolicyError,
5 query::plan::{CursorPlanError, PlanError},
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 pub(crate) fn from_executor_access_plan_error(err: AccessPlanError) -> Self {
396 Self::query_invariant(err.to_string())
397 }
398
399 pub(crate) fn plan_invariant_violation(err: PlanPolicyError) -> Self {
402 let reason = match err {
403 PlanPolicyError::EmptyOrderSpec => {
404 "order specification must include at least one field"
405 }
406 PlanPolicyError::DeletePlanWithPagination => "delete plans must not include pagination",
407 PlanPolicyError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
408 PlanPolicyError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
409 PlanPolicyError::UnorderedPagination => "pagination requires explicit ordering",
410 };
411
412 Self::query_executor_invariant(reason)
413 }
414}
415
416#[derive(Debug, ThisError)]
424pub enum ErrorDetail {
425 #[error("{0}")]
426 Store(StoreError),
427 #[error("{0}")]
428 ViewPatch(crate::patch::MergePatchError),
429 }
439
440impl From<MergePatchError> for InternalError {
441 fn from(err: MergePatchError) -> Self {
442 Self {
443 class: ErrorClass::Unsupported,
444 origin: ErrorOrigin::Interface,
445 message: err.to_string(),
446 detail: Some(ErrorDetail::ViewPatch(err)),
447 }
448 }
449}
450
451#[derive(Debug, ThisError)]
459pub enum StoreError {
460 #[error("key not found: {key}")]
461 NotFound { key: String },
462
463 #[error("store corruption: {message}")]
464 Corrupt { message: String },
465
466 #[error("store invariant violation: {message}")]
467 InvariantViolation { message: String },
468}
469
470#[derive(Clone, Copy, Debug, Eq, PartialEq)]
477pub enum ErrorClass {
478 Corruption,
479 NotFound,
480 Internal,
481 Conflict,
482 Unsupported,
483 InvariantViolation,
484}
485
486impl fmt::Display for ErrorClass {
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 let label = match self {
489 Self::Corruption => "corruption",
490 Self::NotFound => "not_found",
491 Self::Internal => "internal",
492 Self::Conflict => "conflict",
493 Self::Unsupported => "unsupported",
494 Self::InvariantViolation => "invariant_violation",
495 };
496 write!(f, "{label}")
497 }
498}
499
500#[derive(Clone, Copy, Debug, Eq, PartialEq)]
507pub enum ErrorOrigin {
508 Serialize,
509 Store,
510 Index,
511 Query,
512 Response,
513 Executor,
514 Interface,
515}
516
517impl fmt::Display for ErrorOrigin {
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 let label = match self {
520 Self::Serialize => "serialize",
521 Self::Store => "store",
522 Self::Index => "index",
523 Self::Query => "query",
524 Self::Response => "response",
525 Self::Executor => "executor",
526 Self::Interface => "interface",
527 };
528 write!(f, "{label}")
529 }
530}
531
532#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn index_plan_index_corruption_uses_index_origin() {
542 let err = InternalError::index_plan_index_corruption("broken key payload");
543 assert_eq!(err.class, ErrorClass::Corruption);
544 assert_eq!(err.origin, ErrorOrigin::Index);
545 assert_eq!(
546 err.message,
547 "corruption detected (index): broken key payload"
548 );
549 }
550
551 #[test]
552 fn index_plan_store_corruption_uses_store_origin() {
553 let err = InternalError::index_plan_store_corruption("row/key mismatch");
554 assert_eq!(err.class, ErrorClass::Corruption);
555 assert_eq!(err.origin, ErrorOrigin::Store);
556 assert_eq!(err.message, "corruption detected (store): row/key mismatch");
557 }
558
559 #[test]
560 fn index_plan_serialize_corruption_uses_serialize_origin() {
561 let err = InternalError::index_plan_serialize_corruption("decode failed");
562 assert_eq!(err.class, ErrorClass::Corruption);
563 assert_eq!(err.origin, ErrorOrigin::Serialize);
564 assert_eq!(
565 err.message,
566 "corruption detected (serialize): decode failed"
567 );
568 }
569
570 #[test]
571 fn query_executor_invariant_uses_invariant_violation_class() {
572 let err = InternalError::query_executor_invariant("route contract mismatch");
573 assert_eq!(err.class, ErrorClass::InvariantViolation);
574 assert_eq!(err.origin, ErrorOrigin::Query);
575 }
576
577 #[test]
578 fn executor_access_plan_error_mapping_stays_invariant_violation() {
579 let err = InternalError::from_executor_access_plan_error(AccessPlanError::IndexPrefixEmpty);
580 assert_eq!(err.class, ErrorClass::InvariantViolation);
581 assert_eq!(err.origin, ErrorOrigin::Query);
582 }
583
584 #[test]
585 fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
586 let err =
587 InternalError::plan_invariant_violation(PlanPolicyError::DeleteLimitRequiresOrder);
588 assert_eq!(err.class, ErrorClass::InvariantViolation);
589 assert_eq!(err.origin, ErrorOrigin::Query);
590 assert_eq!(
591 err.message,
592 "executor invariant violated: delete limit requires explicit ordering",
593 );
594 }
595}