1use crate::{
2 db::query::{
3 plan::{CursorPlanError, PlanError},
4 policy::PlanPolicyError,
5 },
6 patch::MergePatchError,
7};
8use std::fmt;
9use thiserror::Error as ThisError;
10
11#[derive(Debug, ThisError)]
112#[error("{message}")]
113pub struct InternalError {
114 pub class: ErrorClass,
115 pub origin: ErrorOrigin,
116 pub message: String,
117
118 pub detail: Option<ErrorDetail>,
121}
122
123impl InternalError {
124 pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
128 let message = message.into();
129
130 let detail = match (class, origin) {
131 (ErrorClass::Corruption, ErrorOrigin::Store) => {
132 Some(ErrorDetail::Store(StoreError::Corrupt {
133 message: message.clone(),
134 }))
135 }
136 (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
137 Some(ErrorDetail::Store(StoreError::InvariantViolation {
138 message: message.clone(),
139 }))
140 }
141 _ => None,
142 };
143
144 Self {
145 class,
146 origin,
147 message,
148 detail,
149 }
150 }
151
152 pub(crate) fn classified(
154 class: ErrorClass,
155 origin: ErrorOrigin,
156 message: impl Into<String>,
157 ) -> Self {
158 Self::new(class, origin, message)
159 }
160
161 pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
163 Self::classified(self.class, self.origin, message)
164 }
165
166 pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
168 Self::new(
169 ErrorClass::InvariantViolation,
170 ErrorOrigin::Query,
171 message.into(),
172 )
173 }
174
175 #[must_use]
177 pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
178 format!("executor invariant violated: {}", reason.into())
179 }
180
181 #[must_use]
183 pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
184 format!("invalid logical plan: {}", reason.into())
185 }
186
187 pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
189 Self::query_invariant(Self::executor_invariant_message(reason))
190 }
191
192 pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
194 Self::query_invariant(Self::invalid_logical_plan_message(reason))
195 }
196
197 pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
199 Self::new(
200 ErrorClass::InvariantViolation,
201 ErrorOrigin::Index,
202 message.into(),
203 )
204 }
205
206 pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
208 Self::new(
209 ErrorClass::InvariantViolation,
210 ErrorOrigin::Executor,
211 message.into(),
212 )
213 }
214
215 pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
217 Self::new(
218 ErrorClass::InvariantViolation,
219 ErrorOrigin::Store,
220 message.into(),
221 )
222 }
223
224 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
226 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
227 }
228
229 pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
231 Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
232 }
233
234 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
236 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
237 }
238
239 #[cfg(test)]
241 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
242 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
243 }
244
245 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
247 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
248 }
249
250 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
252 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
253 }
254
255 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
257 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
258 }
259
260 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
262 Self::new(
263 ErrorClass::Corruption,
264 ErrorOrigin::Serialize,
265 message.into(),
266 )
267 }
268
269 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
271 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
272 }
273
274 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
276 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
277 }
278
279 pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
281 Self::new(
282 ErrorClass::Unsupported,
283 ErrorOrigin::Executor,
284 message.into(),
285 )
286 }
287
288 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
290 Self::new(
291 ErrorClass::Unsupported,
292 ErrorOrigin::Serialize,
293 message.into(),
294 )
295 }
296
297 pub fn store_not_found(key: impl Into<String>) -> Self {
298 let key = key.into();
299
300 Self {
301 class: ErrorClass::NotFound,
302 origin: ErrorOrigin::Store,
303 message: format!("data key not found: {key}"),
304 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
305 }
306 }
307
308 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
310 let path = path.into();
311
312 Self::new(
313 ErrorClass::Unsupported,
314 ErrorOrigin::Store,
315 format!("unsupported entity path: '{path}'"),
316 )
317 }
318
319 #[must_use]
320 pub const fn is_not_found(&self) -> bool {
321 matches!(
322 self.detail,
323 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
324 )
325 }
326
327 #[must_use]
328 pub fn display_with_class(&self) -> String {
329 format!("{}:{}: {}", self.origin, self.class, self.message)
330 }
331
332 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
334 let message = message.into();
335 Self::new(
336 ErrorClass::Corruption,
337 origin,
338 format!("corruption detected ({origin}): {message}"),
339 )
340 }
341
342 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
344 Self::index_plan_corruption(ErrorOrigin::Index, message)
345 }
346
347 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
349 Self::index_plan_corruption(ErrorOrigin::Store, message)
350 }
351
352 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
354 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
355 }
356
357 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
359 Self::new(
360 ErrorClass::Conflict,
361 ErrorOrigin::Index,
362 format!(
363 "index constraint violation: {path} ({})",
364 index_fields.join(", ")
365 ),
366 )
367 }
368
369 pub(crate) fn from_cursor_plan_error(err: PlanError) -> Self {
371 let message = match &err {
372 PlanError::Cursor(inner) => match inner.as_ref() {
373 CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
374 Self::executor_invariant_message(format!(
375 "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
376 ))
377 }
378 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
379 value: None, ..
380 } => Self::executor_invariant_message("pk cursor slot must be present"),
381 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
382 value: Some(_),
383 ..
384 } => Self::executor_invariant_message("pk cursor slot type mismatch"),
385 _ => err.to_string(),
386 },
387 _ => err.to_string(),
388 };
389
390 Self::query_invariant(message)
391 }
392
393 pub(crate) fn from_executor_plan_error(err: PlanError) -> Self {
395 Self::query_invariant(err.to_string())
396 }
397
398 pub(crate) fn plan_invariant_violation(err: PlanPolicyError) -> Self {
401 let reason = match err {
402 PlanPolicyError::EmptyOrderSpec => {
403 "order specification must include at least one field"
404 }
405 PlanPolicyError::DeletePlanWithPagination => "delete plans must not include pagination",
406 PlanPolicyError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
407 PlanPolicyError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
408 PlanPolicyError::UnorderedPagination => "pagination requires explicit ordering",
409 };
410
411 Self::query_executor_invariant(reason)
412 }
413}
414
415#[derive(Debug, ThisError)]
423pub enum ErrorDetail {
424 #[error("{0}")]
425 Store(StoreError),
426 #[error("{0}")]
427 ViewPatch(crate::patch::MergePatchError),
428 }
438
439impl From<MergePatchError> for InternalError {
440 fn from(err: MergePatchError) -> Self {
441 Self {
442 class: ErrorClass::Unsupported,
443 origin: ErrorOrigin::Interface,
444 message: err.to_string(),
445 detail: Some(ErrorDetail::ViewPatch(err)),
446 }
447 }
448}
449
450#[derive(Debug, ThisError)]
458pub enum StoreError {
459 #[error("key not found: {key}")]
460 NotFound { key: String },
461
462 #[error("store corruption: {message}")]
463 Corrupt { message: String },
464
465 #[error("store invariant violation: {message}")]
466 InvariantViolation { message: String },
467}
468
469#[derive(Clone, Copy, Debug, Eq, PartialEq)]
476pub enum ErrorClass {
477 Corruption,
478 NotFound,
479 Internal,
480 Conflict,
481 Unsupported,
482 InvariantViolation,
483}
484
485impl fmt::Display for ErrorClass {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 let label = match self {
488 Self::Corruption => "corruption",
489 Self::NotFound => "not_found",
490 Self::Internal => "internal",
491 Self::Conflict => "conflict",
492 Self::Unsupported => "unsupported",
493 Self::InvariantViolation => "invariant_violation",
494 };
495 write!(f, "{label}")
496 }
497}
498
499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
506pub enum ErrorOrigin {
507 Serialize,
508 Store,
509 Index,
510 Query,
511 Response,
512 Executor,
513 Interface,
514}
515
516impl fmt::Display for ErrorOrigin {
517 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 let label = match self {
519 Self::Serialize => "serialize",
520 Self::Store => "store",
521 Self::Index => "index",
522 Self::Query => "query",
523 Self::Response => "response",
524 Self::Executor => "executor",
525 Self::Interface => "interface",
526 };
527 write!(f, "{label}")
528 }
529}
530
531#[cfg(test)]
536mod tests {
537 use super::*;
538 use crate::db::query::plan::{CursorPlanError, PlanError};
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_plan_error_mapping_stays_invariant_violation() {
579 let plan_err = PlanError::from(CursorPlanError::InvalidContinuationCursorPayload {
580 reason: "bad token".to_string(),
581 });
582 let err = InternalError::from_executor_plan_error(plan_err);
583 assert_eq!(err.class, ErrorClass::InvariantViolation);
584 assert_eq!(err.origin, ErrorOrigin::Query);
585 }
586
587 #[test]
588 fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
589 let err =
590 InternalError::plan_invariant_violation(PlanPolicyError::DeleteLimitRequiresOrder);
591 assert_eq!(err.class, ErrorClass::InvariantViolation);
592 assert_eq!(err.origin, ErrorOrigin::Query);
593 assert_eq!(
594 err.message,
595 "executor invariant violated: delete limit requires explicit ordering",
596 );
597 }
598}