1use crate::{
2 db::query::plan::{CursorPlanError, PlanError},
3 patch::MergePatchError,
4};
5use std::fmt;
6use thiserror::Error as ThisError;
7
8#[derive(Debug, ThisError)]
109#[error("{message}")]
110pub struct InternalError {
111 pub class: ErrorClass,
112 pub origin: ErrorOrigin,
113 pub message: String,
114
115 pub detail: Option<ErrorDetail>,
118}
119
120impl InternalError {
121 pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
125 let message = message.into();
126
127 let detail = match (class, origin) {
128 (ErrorClass::Corruption, ErrorOrigin::Store) => {
129 Some(ErrorDetail::Store(StoreError::Corrupt {
130 message: message.clone(),
131 }))
132 }
133 (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
134 Some(ErrorDetail::Store(StoreError::InvariantViolation {
135 message: message.clone(),
136 }))
137 }
138 _ => None,
139 };
140
141 Self {
142 class,
143 origin,
144 message,
145 detail,
146 }
147 }
148
149 pub(crate) fn classified(
151 class: ErrorClass,
152 origin: ErrorOrigin,
153 message: impl Into<String>,
154 ) -> Self {
155 Self::new(class, origin, message)
156 }
157
158 pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
160 Self::classified(self.class, self.origin, message)
161 }
162
163 pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
165 Self::new(
166 ErrorClass::InvariantViolation,
167 ErrorOrigin::Query,
168 message.into(),
169 )
170 }
171
172 #[must_use]
174 pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
175 format!("executor invariant violated: {}", reason.into())
176 }
177
178 #[must_use]
180 pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
181 format!("invalid logical plan: {}", reason.into())
182 }
183
184 pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
186 Self::query_invariant(Self::executor_invariant_message(reason))
187 }
188
189 pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
191 Self::query_invariant(Self::invalid_logical_plan_message(reason))
192 }
193
194 pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
196 Self::new(
197 ErrorClass::InvariantViolation,
198 ErrorOrigin::Index,
199 message.into(),
200 )
201 }
202
203 pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
205 Self::new(
206 ErrorClass::InvariantViolation,
207 ErrorOrigin::Executor,
208 message.into(),
209 )
210 }
211
212 pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
214 Self::new(
215 ErrorClass::InvariantViolation,
216 ErrorOrigin::Store,
217 message.into(),
218 )
219 }
220
221 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
223 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
224 }
225
226 pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
228 Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
229 }
230
231 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
233 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
234 }
235
236 #[cfg(test)]
238 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
239 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
240 }
241
242 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
244 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
245 }
246
247 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
249 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
250 }
251
252 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
254 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
255 }
256
257 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
259 Self::new(
260 ErrorClass::Corruption,
261 ErrorOrigin::Serialize,
262 message.into(),
263 )
264 }
265
266 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
268 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
269 }
270
271 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
273 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
274 }
275
276 pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
278 Self::new(
279 ErrorClass::Unsupported,
280 ErrorOrigin::Executor,
281 message.into(),
282 )
283 }
284
285 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
287 Self::new(
288 ErrorClass::Unsupported,
289 ErrorOrigin::Serialize,
290 message.into(),
291 )
292 }
293
294 pub fn store_not_found(key: impl Into<String>) -> Self {
295 let key = key.into();
296
297 Self {
298 class: ErrorClass::NotFound,
299 origin: ErrorOrigin::Store,
300 message: format!("data key not found: {key}"),
301 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
302 }
303 }
304
305 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
307 let path = path.into();
308
309 Self::new(
310 ErrorClass::Unsupported,
311 ErrorOrigin::Store,
312 format!("unsupported entity path: '{path}'"),
313 )
314 }
315
316 #[must_use]
317 pub const fn is_not_found(&self) -> bool {
318 matches!(
319 self.detail,
320 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
321 )
322 }
323
324 #[must_use]
325 pub fn display_with_class(&self) -> String {
326 format!("{}:{}: {}", self.origin, self.class, self.message)
327 }
328
329 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
331 let message = message.into();
332 Self::new(
333 ErrorClass::Corruption,
334 origin,
335 format!("corruption detected ({origin}): {message}"),
336 )
337 }
338
339 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
341 Self::index_plan_corruption(ErrorOrigin::Index, message)
342 }
343
344 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
346 Self::index_plan_corruption(ErrorOrigin::Store, message)
347 }
348
349 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
351 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
352 }
353
354 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
356 Self::new(
357 ErrorClass::Conflict,
358 ErrorOrigin::Index,
359 format!(
360 "index constraint violation: {path} ({})",
361 index_fields.join(", ")
362 ),
363 )
364 }
365
366 pub(crate) fn from_cursor_plan_error(err: PlanError) -> Self {
368 let message = match &err {
369 PlanError::Cursor(inner) => match inner.as_ref() {
370 CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
371 Self::executor_invariant_message(format!(
372 "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
373 ))
374 }
375 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
376 value: None, ..
377 } => Self::executor_invariant_message("pk cursor slot must be present"),
378 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
379 value: Some(_),
380 ..
381 } => Self::executor_invariant_message("pk cursor slot type mismatch"),
382 _ => err.to_string(),
383 },
384 _ => err.to_string(),
385 };
386
387 Self::query_invariant(message)
388 }
389
390 pub(crate) fn from_executor_plan_error(err: PlanError) -> Self {
392 Self::query_invariant(err.to_string())
393 }
394}
395
396#[derive(Debug, ThisError)]
404pub enum ErrorDetail {
405 #[error("{0}")]
406 Store(StoreError),
407 #[error("{0}")]
408 ViewPatch(crate::patch::MergePatchError),
409 }
419
420impl From<MergePatchError> for InternalError {
421 fn from(err: MergePatchError) -> Self {
422 Self {
423 class: ErrorClass::Unsupported,
424 origin: ErrorOrigin::Interface,
425 message: err.to_string(),
426 detail: Some(ErrorDetail::ViewPatch(err)),
427 }
428 }
429}
430
431#[derive(Debug, ThisError)]
439pub enum StoreError {
440 #[error("key not found: {key}")]
441 NotFound { key: String },
442
443 #[error("store corruption: {message}")]
444 Corrupt { message: String },
445
446 #[error("store invariant violation: {message}")]
447 InvariantViolation { message: String },
448}
449
450#[derive(Clone, Copy, Debug, Eq, PartialEq)]
457pub enum ErrorClass {
458 Corruption,
459 NotFound,
460 Internal,
461 Conflict,
462 Unsupported,
463 InvariantViolation,
464}
465
466impl fmt::Display for ErrorClass {
467 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468 let label = match self {
469 Self::Corruption => "corruption",
470 Self::NotFound => "not_found",
471 Self::Internal => "internal",
472 Self::Conflict => "conflict",
473 Self::Unsupported => "unsupported",
474 Self::InvariantViolation => "invariant_violation",
475 };
476 write!(f, "{label}")
477 }
478}
479
480#[derive(Clone, Copy, Debug, Eq, PartialEq)]
487pub enum ErrorOrigin {
488 Serialize,
489 Store,
490 Index,
491 Query,
492 Response,
493 Executor,
494 Interface,
495}
496
497impl fmt::Display for ErrorOrigin {
498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499 let label = match self {
500 Self::Serialize => "serialize",
501 Self::Store => "store",
502 Self::Index => "index",
503 Self::Query => "query",
504 Self::Response => "response",
505 Self::Executor => "executor",
506 Self::Interface => "interface",
507 };
508 write!(f, "{label}")
509 }
510}
511
512#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::db::query::plan::{CursorPlanError, PlanError};
520
521 #[test]
522 fn index_plan_index_corruption_uses_index_origin() {
523 let err = InternalError::index_plan_index_corruption("broken key payload");
524 assert_eq!(err.class, ErrorClass::Corruption);
525 assert_eq!(err.origin, ErrorOrigin::Index);
526 assert_eq!(
527 err.message,
528 "corruption detected (index): broken key payload"
529 );
530 }
531
532 #[test]
533 fn index_plan_store_corruption_uses_store_origin() {
534 let err = InternalError::index_plan_store_corruption("row/key mismatch");
535 assert_eq!(err.class, ErrorClass::Corruption);
536 assert_eq!(err.origin, ErrorOrigin::Store);
537 assert_eq!(err.message, "corruption detected (store): row/key mismatch");
538 }
539
540 #[test]
541 fn index_plan_serialize_corruption_uses_serialize_origin() {
542 let err = InternalError::index_plan_serialize_corruption("decode failed");
543 assert_eq!(err.class, ErrorClass::Corruption);
544 assert_eq!(err.origin, ErrorOrigin::Serialize);
545 assert_eq!(
546 err.message,
547 "corruption detected (serialize): decode failed"
548 );
549 }
550
551 #[test]
552 fn query_executor_invariant_uses_invariant_violation_class() {
553 let err = InternalError::query_executor_invariant("route contract mismatch");
554 assert_eq!(err.class, ErrorClass::InvariantViolation);
555 assert_eq!(err.origin, ErrorOrigin::Query);
556 }
557
558 #[test]
559 fn executor_plan_error_mapping_stays_invariant_violation() {
560 let plan_err = PlanError::from(CursorPlanError::InvalidContinuationCursorPayload {
561 reason: "bad token".to_string(),
562 });
563 let err = InternalError::from_executor_plan_error(plan_err);
564 assert_eq!(err.class, ErrorClass::InvariantViolation);
565 assert_eq!(err.origin, ErrorOrigin::Query);
566 }
567}