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 query_invariant(message: impl Into<String>) -> Self {
167 Self::new(
168 ErrorClass::InvariantViolation,
169 ErrorOrigin::Query,
170 message.into(),
171 )
172 }
173
174 #[must_use]
176 pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
177 format!("executor invariant violated: {}", reason.into())
178 }
179
180 #[must_use]
182 pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
183 format!("invalid logical plan: {}", reason.into())
184 }
185
186 pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
188 Self::query_invariant(Self::executor_invariant_message(reason))
189 }
190
191 pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
193 Self::query_invariant(Self::invalid_logical_plan_message(reason))
194 }
195
196 pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
198 Self::new(
199 ErrorClass::InvariantViolation,
200 ErrorOrigin::Index,
201 message.into(),
202 )
203 }
204
205 pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
207 Self::new(
208 ErrorClass::InvariantViolation,
209 ErrorOrigin::Executor,
210 message.into(),
211 )
212 }
213
214 pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
216 Self::new(
217 ErrorClass::InvariantViolation,
218 ErrorOrigin::Store,
219 message.into(),
220 )
221 }
222
223 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
225 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
226 }
227
228 pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
230 Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
231 }
232
233 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
235 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
236 }
237
238 #[cfg(test)]
240 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
241 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
242 }
243
244 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
246 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
247 }
248
249 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
251 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
252 }
253
254 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
256 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
257 }
258
259 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
261 Self::new(
262 ErrorClass::Corruption,
263 ErrorOrigin::Serialize,
264 message.into(),
265 )
266 }
267
268 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
270 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
271 }
272
273 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
275 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
276 }
277
278 pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
280 Self::new(
281 ErrorClass::Unsupported,
282 ErrorOrigin::Executor,
283 message.into(),
284 )
285 }
286
287 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
289 Self::new(
290 ErrorClass::Unsupported,
291 ErrorOrigin::Serialize,
292 message.into(),
293 )
294 }
295
296 pub fn store_not_found(key: impl Into<String>) -> Self {
297 let key = key.into();
298
299 Self {
300 class: ErrorClass::NotFound,
301 origin: ErrorOrigin::Store,
302 message: format!("data key not found: {key}"),
303 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
304 }
305 }
306
307 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
309 let path = path.into();
310
311 Self::new(
312 ErrorClass::Unsupported,
313 ErrorOrigin::Store,
314 format!("unsupported entity path: '{path}'"),
315 )
316 }
317
318 #[must_use]
319 pub const fn is_not_found(&self) -> bool {
320 matches!(
321 self.detail,
322 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
323 )
324 }
325
326 #[must_use]
327 pub fn display_with_class(&self) -> String {
328 format!("{}:{}: {}", self.origin, self.class, self.message)
329 }
330
331 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
333 let message = message.into();
334 Self::new(
335 ErrorClass::Corruption,
336 origin,
337 format!("corruption detected ({origin}): {message}"),
338 )
339 }
340
341 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
343 Self::index_plan_corruption(ErrorOrigin::Index, message)
344 }
345
346 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
348 Self::index_plan_corruption(ErrorOrigin::Store, message)
349 }
350
351 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
353 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
354 }
355
356 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
358 Self::new(
359 ErrorClass::Conflict,
360 ErrorOrigin::Index,
361 format!(
362 "index constraint violation: {path} ({})",
363 index_fields.join(", ")
364 ),
365 )
366 }
367
368 pub(crate) fn from_cursor_plan_error(err: CursorPlanError) -> Self {
370 let message = match &err {
371 CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
372 Self::executor_invariant_message(format!(
373 "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
374 ))
375 }
376 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch { value: None, .. } => {
377 Self::executor_invariant_message("pk cursor slot must be present")
378 }
379 CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
380 value: Some(_), ..
381 } => Self::executor_invariant_message("pk cursor slot type mismatch"),
382 _ => err.to_string(),
383 };
384
385 Self::query_invariant(message)
386 }
387
388 #[cfg(test)]
390 pub(crate) fn from_group_plan_error(err: PlanError) -> Self {
391 let message = match &err {
392 PlanError::Group(inner) => format!("invalid logical plan: {inner}"),
393 _ => err.to_string(),
394 };
395
396 Self::query_invariant(message)
397 }
398
399 pub(crate) fn from_executor_access_plan_error(err: AccessPlanError) -> Self {
401 Self::query_invariant(err.to_string())
402 }
403
404 #[cfg(test)]
407 pub(crate) fn plan_invariant_violation(err: PolicyPlanError) -> Self {
408 let reason = match err {
409 PolicyPlanError::EmptyOrderSpec => {
410 "order specification must include at least one field"
411 }
412 PolicyPlanError::DeletePlanWithPagination => "delete plans must not include pagination",
413 PolicyPlanError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
414 PolicyPlanError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
415 PolicyPlanError::UnorderedPagination => "pagination requires explicit ordering",
416 };
417
418 Self::query_executor_invariant(reason)
419 }
420}
421
422#[derive(Debug, ThisError)]
430pub enum ErrorDetail {
431 #[error("{0}")]
432 Store(StoreError),
433 #[error("{0}")]
434 ViewPatch(crate::patch::MergePatchError),
435 }
445
446impl From<MergePatchError> for InternalError {
447 fn from(err: MergePatchError) -> Self {
448 Self {
449 class: ErrorClass::Unsupported,
450 origin: ErrorOrigin::Interface,
451 message: err.to_string(),
452 detail: Some(ErrorDetail::ViewPatch(err)),
453 }
454 }
455}
456
457#[derive(Debug, ThisError)]
465pub enum StoreError {
466 #[error("key not found: {key}")]
467 NotFound { key: String },
468
469 #[error("store corruption: {message}")]
470 Corrupt { message: String },
471
472 #[error("store invariant violation: {message}")]
473 InvariantViolation { message: String },
474}
475
476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
483pub enum ErrorClass {
484 Corruption,
485 NotFound,
486 Internal,
487 Conflict,
488 Unsupported,
489 InvariantViolation,
490}
491
492impl fmt::Display for ErrorClass {
493 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494 let label = match self {
495 Self::Corruption => "corruption",
496 Self::NotFound => "not_found",
497 Self::Internal => "internal",
498 Self::Conflict => "conflict",
499 Self::Unsupported => "unsupported",
500 Self::InvariantViolation => "invariant_violation",
501 };
502 write!(f, "{label}")
503 }
504}
505
506#[derive(Clone, Copy, Debug, Eq, PartialEq)]
513pub enum ErrorOrigin {
514 Serialize,
515 Store,
516 Index,
517 Query,
518 Response,
519 Executor,
520 Interface,
521}
522
523impl fmt::Display for ErrorOrigin {
524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525 let label = match self {
526 Self::Serialize => "serialize",
527 Self::Store => "store",
528 Self::Index => "index",
529 Self::Query => "query",
530 Self::Response => "response",
531 Self::Executor => "executor",
532 Self::Interface => "interface",
533 };
534 write!(f, "{label}")
535 }
536}
537
538#[cfg(test)]
543mod tests {
544 use super::*;
545 use crate::db::query::plan::validate::GroupPlanError;
546
547 #[test]
548 fn index_plan_index_corruption_uses_index_origin() {
549 let err = InternalError::index_plan_index_corruption("broken key payload");
550 assert_eq!(err.class, ErrorClass::Corruption);
551 assert_eq!(err.origin, ErrorOrigin::Index);
552 assert_eq!(
553 err.message,
554 "corruption detected (index): broken key payload"
555 );
556 }
557
558 #[test]
559 fn index_plan_store_corruption_uses_store_origin() {
560 let err = InternalError::index_plan_store_corruption("row/key mismatch");
561 assert_eq!(err.class, ErrorClass::Corruption);
562 assert_eq!(err.origin, ErrorOrigin::Store);
563 assert_eq!(err.message, "corruption detected (store): row/key mismatch");
564 }
565
566 #[test]
567 fn index_plan_serialize_corruption_uses_serialize_origin() {
568 let err = InternalError::index_plan_serialize_corruption("decode failed");
569 assert_eq!(err.class, ErrorClass::Corruption);
570 assert_eq!(err.origin, ErrorOrigin::Serialize);
571 assert_eq!(
572 err.message,
573 "corruption detected (serialize): decode failed"
574 );
575 }
576
577 #[test]
578 fn query_executor_invariant_uses_invariant_violation_class() {
579 let err = InternalError::query_executor_invariant("route contract mismatch");
580 assert_eq!(err.class, ErrorClass::InvariantViolation);
581 assert_eq!(err.origin, ErrorOrigin::Query);
582 }
583
584 #[test]
585 fn executor_access_plan_error_mapping_stays_invariant_violation() {
586 let err = InternalError::from_executor_access_plan_error(AccessPlanError::IndexPrefixEmpty);
587 assert_eq!(err.class, ErrorClass::InvariantViolation);
588 assert_eq!(err.origin, ErrorOrigin::Query);
589 }
590
591 #[test]
592 fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
593 let err =
594 InternalError::plan_invariant_violation(PolicyPlanError::DeleteLimitRequiresOrder);
595 assert_eq!(err.class, ErrorClass::InvariantViolation);
596 assert_eq!(err.origin, ErrorOrigin::Query);
597 assert_eq!(
598 err.message,
599 "executor invariant violated: delete limit requires explicit ordering",
600 );
601 }
602
603 #[test]
604 fn group_plan_error_mapping_uses_invalid_logical_plan_prefix() {
605 let err = InternalError::from_group_plan_error(PlanError::from(
606 GroupPlanError::UnknownGroupField {
607 field: "tenant".to_string(),
608 },
609 ));
610
611 assert_eq!(err.class, ErrorClass::InvariantViolation);
612 assert_eq!(err.origin, ErrorOrigin::Query);
613 assert_eq!(
614 err.message,
615 "invalid logical plan: unknown group field 'tenant'",
616 );
617 }
618}