Skip to main content

icydb_core/error/
mod.rs

1//! Module: error
2//!
3//! Responsibility: module-local ownership and contracts for error.
4//! Does not own: cross-module orchestration outside this module.
5//! Boundary: exposes this module API while keeping implementation details internal.
6
7#[cfg(test)]
8mod tests;
9
10use crate::serialize::{SerializeError, SerializeErrorKind};
11use std::fmt;
12use thiserror::Error as ThisError;
13
14// ============================================================================
15// INTERNAL ERROR TAXONOMY — ARCHITECTURAL CONTRACT
16// ============================================================================
17//
18// This file defines the canonical runtime error classification system for
19// icydb-core. It is the single source of truth for:
20//
21//   • ErrorClass   (semantic domain)
22//   • ErrorOrigin  (subsystem boundary)
23//   • Structured detail payloads
24//   • Canonical constructor entry points
25//
26// -----------------------------------------------------------------------------
27// DESIGN INTENT
28// -----------------------------------------------------------------------------
29//
30// 1. InternalError is a *taxonomy carrier*, not a formatting utility.
31//
32//    - ErrorClass represents semantic meaning (corruption, invariant_violation,
33//      unsupported, etc).
34//    - ErrorOrigin represents the subsystem boundary (store, index, query,
35//      executor, serialize, interface, etc).
36//    - The (class, origin) pair must remain stable and intentional.
37//
38// 2. Call sites MUST prefer canonical constructors.
39//
40//    Do NOT construct errors manually via:
41//        InternalError::new(class, origin, ...)
42//    unless you are defining a new canonical helper here.
43//
44//    If a pattern appears more than once, centralize it here.
45//
46// 3. Constructors in this file must represent real architectural boundaries.
47//
48//    Add a new helper ONLY if it:
49//
50//      • Encodes a cross-cutting invariant,
51//      • Represents a subsystem boundary,
52//      • Or prevents taxonomy drift across call sites.
53//
54//    Do NOT add feature-specific helpers.
55//    Do NOT add one-off formatting helpers.
56//    Do NOT turn this file into a generic message factory.
57//
58// 4. ErrorDetail must align with ErrorOrigin.
59//
60//    If detail is present, it MUST correspond to the origin.
61//    Do not attach mismatched detail variants.
62//
63// 5. Plan-layer errors are NOT runtime failures.
64//
65//    PlanError and CursorPlanError must be translated into
66//    executor/query invariants via the canonical mapping functions.
67//    Do not leak plan-layer error types across execution boundaries.
68//
69// 6. Preserve taxonomy stability.
70//
71//    Do NOT:
72//      • Merge error classes.
73//      • Reclassify corruption as internal.
74//      • Downgrade invariant violations.
75//      • Introduce ambiguous class/origin combinations.
76//
77//    Any change to ErrorClass or ErrorOrigin is an architectural change
78//    and must be reviewed accordingly.
79//
80// -----------------------------------------------------------------------------
81// NON-GOALS
82// -----------------------------------------------------------------------------
83//
84// This is NOT:
85//
86//   • A public API contract.
87//   • A generic error abstraction layer.
88//   • A feature-specific message builder.
89//   • A dumping ground for temporary error conversions.
90//
91// -----------------------------------------------------------------------------
92// MAINTENANCE GUIDELINES
93// -----------------------------------------------------------------------------
94//
95// When modifying this file:
96//
97//   1. Ensure classification semantics remain consistent.
98//   2. Avoid constructor proliferation.
99//   3. Prefer narrow, origin-specific helpers over ad-hoc new(...).
100//   4. Keep formatting minimal and standardized.
101//   5. Keep this file boring and stable.
102//
103// If this file grows rapidly, something is wrong at the call sites.
104//
105// ============================================================================
106
107///
108/// InternalError
109///
110/// Structured runtime error with a stable internal classification.
111/// Not a stable API; intended for internal use and may change without notice.
112///
113
114#[derive(Debug, ThisError)]
115#[error("{message}")]
116pub struct InternalError {
117    pub(crate) class: ErrorClass,
118    pub(crate) origin: ErrorOrigin,
119    pub(crate) message: String,
120
121    /// Optional structured error detail.
122    /// The variant (if present) must correspond to `origin`.
123    pub(crate) detail: Option<ErrorDetail>,
124}
125
126impl InternalError {
127    /// Construct an InternalError with optional origin-specific detail.
128    /// This constructor provides default StoreError details for certain
129    /// (class, origin) combinations but does not guarantee a detail payload.
130    pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
131        let message = message.into();
132
133        let detail = match (class, origin) {
134            (ErrorClass::Corruption, ErrorOrigin::Store) => {
135                Some(ErrorDetail::Store(StoreError::Corrupt {
136                    message: message.clone(),
137                }))
138            }
139            (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
140                Some(ErrorDetail::Store(StoreError::InvariantViolation {
141                    message: message.clone(),
142                }))
143            }
144            _ => None,
145        };
146
147        Self {
148            class,
149            origin,
150            message,
151            detail,
152        }
153    }
154
155    /// Return the internal error class taxonomy.
156    #[must_use]
157    pub const fn class(&self) -> ErrorClass {
158        self.class
159    }
160
161    /// Return the internal error origin taxonomy.
162    #[must_use]
163    pub const fn origin(&self) -> ErrorOrigin {
164        self.origin
165    }
166
167    /// Return the rendered internal error message.
168    #[must_use]
169    pub fn message(&self) -> &str {
170        &self.message
171    }
172
173    /// Return the optional structured detail payload.
174    #[must_use]
175    pub const fn detail(&self) -> Option<&ErrorDetail> {
176        self.detail.as_ref()
177    }
178
179    /// Consume and return the rendered internal error message.
180    #[must_use]
181    pub fn into_message(self) -> String {
182        self.message
183    }
184
185    /// Construct an error while preserving an explicit class/origin taxonomy pair.
186    pub(crate) fn classified(
187        class: ErrorClass,
188        origin: ErrorOrigin,
189        message: impl Into<String>,
190    ) -> Self {
191        Self::new(class, origin, message)
192    }
193
194    /// Rebuild this error with a new message while preserving class/origin taxonomy.
195    pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
196        Self::classified(self.class, self.origin, message)
197    }
198
199    /// Rebuild this error with a new origin while preserving class/message.
200    ///
201    /// Origin-scoped detail payloads are intentionally dropped when re-origining.
202    pub(crate) fn with_origin(self, origin: ErrorOrigin) -> Self {
203        Self::classified(self.class, origin, self.message)
204    }
205
206    /// Construct an index-origin invariant violation.
207    pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
208        Self::new(
209            ErrorClass::InvariantViolation,
210            ErrorOrigin::Index,
211            message.into(),
212        )
213    }
214
215    /// Construct a planner-origin invariant violation with the canonical
216    /// executor-boundary invariant prefix preserved in the message payload.
217    pub(crate) fn planner_executor_invariant(reason: impl Into<String>) -> Self {
218        Self::new(
219            ErrorClass::InvariantViolation,
220            ErrorOrigin::Planner,
221            Self::executor_invariant_message(reason),
222        )
223    }
224
225    /// Construct a query-origin invariant violation with the canonical
226    /// executor-boundary invariant prefix preserved in the message payload.
227    pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
228        Self::new(
229            ErrorClass::InvariantViolation,
230            ErrorOrigin::Query,
231            Self::executor_invariant_message(reason),
232        )
233    }
234
235    /// Construct a cursor-origin invariant violation with the canonical
236    /// executor-boundary invariant prefix preserved in the message payload.
237    pub(crate) fn cursor_executor_invariant(reason: impl Into<String>) -> Self {
238        Self::new(
239            ErrorClass::InvariantViolation,
240            ErrorOrigin::Cursor,
241            Self::executor_invariant_message(reason),
242        )
243    }
244
245    /// Construct an executor-origin invariant violation.
246    pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
247        Self::new(
248            ErrorClass::InvariantViolation,
249            ErrorOrigin::Executor,
250            message.into(),
251        )
252    }
253
254    /// Construct an executor-origin internal error.
255    pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
256        Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
257    }
258
259    /// Construct an executor-origin unsupported error.
260    pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
261        Self::new(
262            ErrorClass::Unsupported,
263            ErrorOrigin::Executor,
264            message.into(),
265        )
266    }
267
268    /// Construct an executor-origin save-preflight schema invariant.
269    pub(crate) fn mutation_entity_schema_invalid(
270        entity_path: &str,
271        detail: impl fmt::Display,
272    ) -> Self {
273        Self::executor_invariant(format!("entity schema invalid for {entity_path}: {detail}"))
274    }
275
276    /// Construct an executor-origin save-preflight primary-key missing invariant.
277    pub(crate) fn mutation_entity_primary_key_missing(entity_path: &str, field_name: &str) -> Self {
278        Self::executor_invariant(format!(
279            "entity primary key field missing: {entity_path} field={field_name}",
280        ))
281    }
282
283    /// Construct an executor-origin save-preflight primary-key invalid-value invariant.
284    pub(crate) fn mutation_entity_primary_key_invalid_value(
285        entity_path: &str,
286        field_name: &str,
287        value: &crate::value::Value,
288    ) -> Self {
289        Self::executor_invariant(format!(
290            "entity primary key field has invalid value: {entity_path} field={field_name} value={value:?}",
291        ))
292    }
293
294    /// Construct an executor-origin save-preflight primary-key type mismatch invariant.
295    pub(crate) fn mutation_entity_primary_key_type_mismatch(
296        entity_path: &str,
297        field_name: &str,
298        value: &crate::value::Value,
299    ) -> Self {
300        Self::executor_invariant(format!(
301            "entity primary key field type mismatch: {entity_path} field={field_name} value={value:?}",
302        ))
303    }
304
305    /// Construct an executor-origin save-preflight primary-key identity mismatch invariant.
306    pub(crate) fn mutation_entity_primary_key_mismatch(
307        entity_path: &str,
308        field_name: &str,
309        field_value: &crate::value::Value,
310        identity_key: &crate::value::Value,
311    ) -> Self {
312        Self::executor_invariant(format!(
313            "entity primary key mismatch: {entity_path} field={field_name} field_value={field_value:?} id_key={identity_key:?}",
314        ))
315    }
316
317    /// Construct an executor-origin save-preflight field-missing invariant.
318    pub(crate) fn mutation_entity_field_missing(
319        entity_path: &str,
320        field_name: &str,
321        indexed: bool,
322    ) -> Self {
323        let indexed_note = if indexed { " (indexed)" } else { "" };
324
325        Self::executor_invariant(format!(
326            "entity field missing: {entity_path} field={field_name}{indexed_note}",
327        ))
328    }
329
330    /// Construct an executor-origin save-preflight field-type mismatch invariant.
331    pub(crate) fn mutation_entity_field_type_mismatch(
332        entity_path: &str,
333        field_name: &str,
334        value: &crate::value::Value,
335    ) -> Self {
336        Self::executor_invariant(format!(
337            "entity field type mismatch: {entity_path} field={field_name} value={value:?}",
338        ))
339    }
340
341    /// Construct an executor-origin save-preflight decimal-scale unsupported error.
342    pub(crate) fn mutation_decimal_scale_mismatch(
343        entity_path: &str,
344        field_name: &str,
345        expected_scale: impl fmt::Display,
346        actual_scale: impl fmt::Display,
347    ) -> Self {
348        Self::executor_unsupported(format!(
349            "decimal field scale mismatch: {entity_path} field={field_name} expected_scale={expected_scale} actual_scale={actual_scale}",
350        ))
351    }
352
353    /// Construct an executor-origin save-preflight set-encoding invariant.
354    pub(crate) fn mutation_set_field_list_required(entity_path: &str, field_name: &str) -> Self {
355        Self::executor_invariant(format!(
356            "set field must encode as Value::List: {entity_path} field={field_name}",
357        ))
358    }
359
360    /// Construct an executor-origin save-preflight set-canonicality invariant.
361    pub(crate) fn mutation_set_field_not_canonical(entity_path: &str, field_name: &str) -> Self {
362        Self::executor_invariant(format!(
363            "set field must be strictly ordered and deduplicated: {entity_path} field={field_name}",
364        ))
365    }
366
367    /// Construct an executor-origin save-preflight map-encoding invariant.
368    pub(crate) fn mutation_map_field_map_required(entity_path: &str, field_name: &str) -> Self {
369        Self::executor_invariant(format!(
370            "map field must encode as Value::Map: {entity_path} field={field_name}",
371        ))
372    }
373
374    /// Construct an executor-origin save-preflight map-entry invariant.
375    pub(crate) fn mutation_map_field_entries_invalid(
376        entity_path: &str,
377        field_name: &str,
378        detail: impl fmt::Display,
379    ) -> Self {
380        Self::executor_invariant(format!(
381            "map field entries violate map invariants: {entity_path} field={field_name} ({detail})",
382        ))
383    }
384
385    /// Construct an executor-origin save-preflight map-canonicality invariant.
386    pub(crate) fn mutation_map_field_entries_not_canonical(
387        entity_path: &str,
388        field_name: &str,
389    ) -> Self {
390        Self::executor_invariant(format!(
391            "map field entries are not in canonical deterministic order: {entity_path} field={field_name}",
392        ))
393    }
394
395    /// Construct a query-origin scalar page invariant for missing predicate slots.
396    pub(crate) fn scalar_page_predicate_slots_required() -> Self {
397        Self::query_executor_invariant("post-access filtering requires precompiled predicate slots")
398    }
399
400    /// Construct a query-origin scalar page invariant for ordering before filtering.
401    pub(crate) fn scalar_page_ordering_after_filtering_required() -> Self {
402        Self::query_executor_invariant("ordering must run after filtering")
403    }
404
405    /// Construct a query-origin scalar page invariant for missing order at the cursor boundary.
406    pub(crate) fn scalar_page_cursor_boundary_order_required() -> Self {
407        Self::query_executor_invariant("cursor boundary requires ordering")
408    }
409
410    /// Construct a query-origin scalar page invariant for cursor-before-ordering drift.
411    pub(crate) fn scalar_page_cursor_boundary_after_ordering_required() -> Self {
412        Self::query_executor_invariant("cursor boundary must run after ordering")
413    }
414
415    /// Construct a query-origin scalar page invariant for pagination-before-ordering drift.
416    pub(crate) fn scalar_page_pagination_after_ordering_required() -> Self {
417        Self::query_executor_invariant("pagination must run after ordering")
418    }
419
420    /// Construct a query-origin scalar page invariant for delete-limit-before-ordering drift.
421    pub(crate) fn scalar_page_delete_limit_after_ordering_required() -> Self {
422        Self::query_executor_invariant("delete limit must run after ordering")
423    }
424
425    /// Construct a query-origin load-runtime invariant for scalar-mode payload mismatch.
426    pub(crate) fn load_runtime_scalar_payload_required() -> Self {
427        Self::query_executor_invariant("scalar load mode must carry scalar runtime payload")
428    }
429
430    /// Construct a query-origin load-runtime invariant for grouped-mode payload mismatch.
431    pub(crate) fn load_runtime_grouped_payload_required() -> Self {
432        Self::query_executor_invariant("grouped load mode must carry grouped runtime payload")
433    }
434
435    /// Construct a query-origin load-surface invariant for scalar-page payload mismatch.
436    pub(crate) fn load_runtime_scalar_surface_payload_required() -> Self {
437        Self::query_executor_invariant("scalar page load mode must carry scalar runtime payload")
438    }
439
440    /// Construct a query-origin load-surface invariant for grouped-page payload mismatch.
441    pub(crate) fn load_runtime_grouped_surface_payload_required() -> Self {
442        Self::query_executor_invariant("grouped page load mode must carry grouped runtime payload")
443    }
444
445    /// Construct a query-origin load-entrypoint invariant for non-load plans.
446    pub(crate) fn load_executor_load_plan_required() -> Self {
447        Self::query_executor_invariant("load executor requires load plans")
448    }
449
450    /// Construct an executor-origin delete-entrypoint unsupported grouped-mode error.
451    pub(crate) fn delete_executor_grouped_unsupported() -> Self {
452        Self::executor_unsupported("grouped query execution is not yet enabled in this release")
453    }
454
455    /// Construct a query-origin delete-entrypoint invariant for non-delete plans.
456    pub(crate) fn delete_executor_delete_plan_required() -> Self {
457        Self::query_executor_invariant("delete executor requires delete plans")
458    }
459
460    /// Construct a query-origin aggregate kernel invariant for fold-mode contract drift.
461    pub(crate) fn aggregate_fold_mode_terminal_contract_required() -> Self {
462        Self::query_executor_invariant(
463            "aggregate fold mode must match route fold-mode contract for aggregate terminal",
464        )
465    }
466
467    /// Construct a query-origin fast-stream invariant for missing exact key-count observability.
468    pub(crate) fn fast_stream_exact_key_count_required() -> Self {
469        Self::query_executor_invariant("fast-path stream must expose an exact key-count hint")
470    }
471
472    /// Construct a query-origin fast-stream invariant for route kind/request mismatch.
473    pub(crate) fn fast_stream_route_kind_request_match_required() -> Self {
474        Self::query_executor_invariant("fast-stream route kind/request mismatch")
475    }
476
477    /// Construct a query-origin scan invariant for missing index-prefix executable specs.
478    pub(crate) fn secondary_index_prefix_spec_required() -> Self {
479        Self::query_executor_invariant(
480            "index-prefix executable spec must be materialized for index-prefix plans",
481        )
482    }
483
484    /// Construct a query-origin scan invariant for missing index-range executable specs.
485    pub(crate) fn index_range_limit_spec_required() -> Self {
486        Self::query_executor_invariant(
487            "index-range executable spec must be materialized for index-range plans",
488        )
489    }
490
491    /// Construct a query-origin row-decode invariant for missing primary-key layout slots.
492    pub(crate) fn row_layout_primary_key_slot_required() -> Self {
493        Self::query_executor_invariant("row layout missing primary-key slot")
494    }
495
496    /// Construct an executor-origin mutation unsupported error for duplicate atomic save keys.
497    pub(crate) fn mutation_atomic_save_duplicate_key(
498        entity_path: &str,
499        key: impl fmt::Display,
500    ) -> Self {
501        Self::executor_unsupported(format!(
502            "atomic save batch rejected duplicate key: entity={entity_path} key={key}",
503        ))
504    }
505
506    /// Construct an executor-origin mutation invariant for index-store generation drift.
507    pub(crate) fn mutation_index_store_generation_changed(
508        expected_generation: u64,
509        observed_generation: u64,
510    ) -> Self {
511        Self::executor_invariant(format!(
512            "index store generation changed between preflight and apply: expected {expected_generation}, found {observed_generation}",
513        ))
514    }
515
516    /// Build the canonical executor-invariant message prefix.
517    #[must_use]
518    pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
519        format!("executor invariant violated: {}", reason.into())
520    }
521
522    /// Construct a planner-origin invariant violation.
523    pub(crate) fn planner_invariant(message: impl Into<String>) -> Self {
524        Self::new(
525            ErrorClass::InvariantViolation,
526            ErrorOrigin::Planner,
527            message.into(),
528        )
529    }
530
531    /// Build the canonical invalid-logical-plan message prefix.
532    #[must_use]
533    pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
534        format!("invalid logical plan: {}", reason.into())
535    }
536
537    /// Construct a planner-origin invariant with the canonical invalid-plan prefix.
538    pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
539        Self::planner_invariant(Self::invalid_logical_plan_message(reason))
540    }
541
542    /// Construct a query-origin invariant violation.
543    pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
544        Self::new(
545            ErrorClass::InvariantViolation,
546            ErrorOrigin::Query,
547            message.into(),
548        )
549    }
550
551    /// Construct a store-origin invariant violation.
552    pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
553        Self::new(
554            ErrorClass::InvariantViolation,
555            ErrorOrigin::Store,
556            message.into(),
557        )
558    }
559
560    /// Construct a store-origin internal error.
561    pub(crate) fn store_internal(message: impl Into<String>) -> Self {
562        Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
563    }
564
565    /// Construct an index-origin internal error.
566    pub(crate) fn index_internal(message: impl Into<String>) -> Self {
567        Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
568    }
569
570    /// Construct a query-origin internal error.
571    #[cfg(test)]
572    pub(crate) fn query_internal(message: impl Into<String>) -> Self {
573        Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
574    }
575
576    /// Construct a query-origin unsupported error.
577    pub(crate) fn query_unsupported(message: impl Into<String>) -> Self {
578        Self::new(ErrorClass::Unsupported, ErrorOrigin::Query, message.into())
579    }
580
581    /// Construct a serialize-origin internal error.
582    pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
583        Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
584    }
585
586    /// Construct a store-origin corruption error.
587    pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
588        Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
589    }
590
591    /// Construct a store-origin commit-marker corruption error.
592    pub(crate) fn commit_corruption(detail: impl fmt::Display) -> Self {
593        Self::store_corruption(format!("commit marker corrupted: {detail}"))
594    }
595
596    /// Construct a store-origin commit-marker component corruption error.
597    pub(crate) fn commit_component_corruption(component: &str, detail: impl fmt::Display) -> Self {
598        Self::store_corruption(format!("commit marker {component} corrupted: {detail}"))
599    }
600
601    /// Construct an index-origin corruption error.
602    pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
603        Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
604    }
605
606    /// Construct a serialize-origin corruption error.
607    pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
608        Self::new(
609            ErrorClass::Corruption,
610            ErrorOrigin::Serialize,
611            message.into(),
612        )
613    }
614
615    /// Construct the canonical missing persisted-field decode error.
616    #[must_use]
617    pub fn missing_persisted_slot(field_name: &'static str) -> Self {
618        Self::serialize_corruption(format!(
619            "row decode failed: missing required field '{field_name}'",
620        ))
621    }
622
623    /// Construct an identity-origin corruption error.
624    pub(crate) fn identity_corruption(message: impl Into<String>) -> Self {
625        Self::new(
626            ErrorClass::Corruption,
627            ErrorOrigin::Identity,
628            message.into(),
629        )
630    }
631
632    /// Construct a store-origin unsupported error.
633    pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
634        Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
635    }
636
637    /// Construct an index-origin unsupported error.
638    pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
639        Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
640    }
641
642    /// Construct a serialize-origin unsupported error.
643    pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
644        Self::new(
645            ErrorClass::Unsupported,
646            ErrorOrigin::Serialize,
647            message.into(),
648        )
649    }
650
651    /// Construct a cursor-origin unsupported error.
652    pub(crate) fn cursor_unsupported(message: impl Into<String>) -> Self {
653        Self::new(ErrorClass::Unsupported, ErrorOrigin::Cursor, message.into())
654    }
655
656    /// Construct a serialize-origin incompatible persisted-format error.
657    pub(crate) fn serialize_incompatible_persisted_format(message: impl Into<String>) -> Self {
658        Self::new(
659            ErrorClass::IncompatiblePersistedFormat,
660            ErrorOrigin::Serialize,
661            message.into(),
662        )
663    }
664
665    /// Construct the canonical persisted-payload decode failure mapping for one
666    /// DB-owned serialized payload boundary.
667    pub(crate) fn serialize_payload_decode_failed(
668        source: SerializeError,
669        payload_label: &'static str,
670    ) -> Self {
671        match source {
672            // DB codec only decodes engine-owned persisted payloads.
673            // Size-limit breaches indicate persisted bytes violate DB storage policy.
674            SerializeError::DeserializeSizeLimitExceeded { len, max_bytes } => {
675                Self::serialize_corruption(format!(
676                    "{payload_label} decode failed: payload size {len} exceeds limit {max_bytes}"
677                ))
678            }
679            SerializeError::Deserialize(_) => Self::serialize_corruption(format!(
680                "{payload_label} decode failed: {}",
681                SerializeErrorKind::Deserialize
682            )),
683            SerializeError::Serialize(_) => Self::serialize_corruption(format!(
684                "{payload_label} decode failed: {}",
685                SerializeErrorKind::Serialize
686            )),
687        }
688    }
689
690    /// Construct a query-origin unsupported error preserving one SQL parser
691    /// unsupported-feature label in structured error detail.
692    #[cfg(feature = "sql")]
693    pub(crate) fn query_unsupported_sql_feature(feature: &'static str) -> Self {
694        let message = format!(
695            "SQL query is not executable in this release: unsupported SQL feature: {feature}"
696        );
697
698        Self {
699            class: ErrorClass::Unsupported,
700            origin: ErrorOrigin::Query,
701            message,
702            detail: Some(ErrorDetail::Query(
703                QueryErrorDetail::UnsupportedSqlFeature { feature },
704            )),
705        }
706    }
707
708    pub fn store_not_found(key: impl Into<String>) -> Self {
709        let key = key.into();
710
711        Self {
712            class: ErrorClass::NotFound,
713            origin: ErrorOrigin::Store,
714            message: format!("data key not found: {key}"),
715            detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
716        }
717    }
718
719    /// Construct a standardized unsupported-entity-path error.
720    pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
721        let path = path.into();
722
723        Self::new(
724            ErrorClass::Unsupported,
725            ErrorOrigin::Store,
726            format!("unsupported entity path: '{path}'"),
727        )
728    }
729
730    #[must_use]
731    pub const fn is_not_found(&self) -> bool {
732        matches!(
733            self.detail,
734            Some(ErrorDetail::Store(StoreError::NotFound { .. }))
735        )
736    }
737
738    #[must_use]
739    pub fn display_with_class(&self) -> String {
740        format!("{}:{}: {}", self.origin, self.class, self.message)
741    }
742
743    /// Construct an index-plan corruption error with a canonical prefix.
744    pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
745        let message = message.into();
746        Self::new(
747            ErrorClass::Corruption,
748            origin,
749            format!("corruption detected ({origin}): {message}"),
750        )
751    }
752
753    /// Construct an index-plan corruption error for index-origin failures.
754    pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
755        Self::index_plan_corruption(ErrorOrigin::Index, message)
756    }
757
758    /// Construct an index-plan corruption error for store-origin failures.
759    pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
760        Self::index_plan_corruption(ErrorOrigin::Store, message)
761    }
762
763    /// Construct an index-plan corruption error for serialize-origin failures.
764    pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
765        Self::index_plan_corruption(ErrorOrigin::Serialize, message)
766    }
767
768    /// Construct an index-plan invariant violation error with a canonical prefix.
769    pub(crate) fn index_plan_invariant(origin: ErrorOrigin, message: impl Into<String>) -> Self {
770        let message = message.into();
771        Self::new(
772            ErrorClass::InvariantViolation,
773            origin,
774            format!("invariant violation detected ({origin}): {message}"),
775        )
776    }
777
778    /// Construct an index-plan invariant violation error for store-origin failures.
779    pub(crate) fn index_plan_store_invariant(message: impl Into<String>) -> Self {
780        Self::index_plan_invariant(ErrorOrigin::Store, message)
781    }
782
783    /// Construct an index uniqueness violation conflict error.
784    pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
785        Self::new(
786            ErrorClass::Conflict,
787            ErrorOrigin::Index,
788            format!(
789                "index constraint violation: {path} ({})",
790                index_fields.join(", ")
791            ),
792        )
793    }
794}
795
796///
797/// ErrorDetail
798///
799/// Structured, origin-specific error detail carried by [`InternalError`].
800/// This enum is intentionally extensible.
801///
802
803#[derive(Debug, ThisError)]
804pub enum ErrorDetail {
805    #[error("{0}")]
806    Store(StoreError),
807    #[error("{0}")]
808    Query(QueryErrorDetail),
809    // Future-proofing:
810    // #[error("{0}")]
811    // Index(IndexError),
812    //
813    // #[error("{0}")]
814    // Executor(ExecutorErrorDetail),
815}
816
817///
818/// StoreError
819///
820/// Store-specific structured error detail.
821/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
822///
823
824#[derive(Debug, ThisError)]
825pub enum StoreError {
826    #[error("key not found: {key}")]
827    NotFound { key: String },
828
829    #[error("store corruption: {message}")]
830    Corrupt { message: String },
831
832    #[error("store invariant violation: {message}")]
833    InvariantViolation { message: String },
834}
835
836///
837/// QueryErrorDetail
838///
839/// Query-origin structured error detail payload.
840///
841
842#[derive(Debug, ThisError)]
843pub enum QueryErrorDetail {
844    #[error("unsupported SQL feature: {feature}")]
845    UnsupportedSqlFeature { feature: &'static str },
846}
847
848///
849/// ErrorClass
850/// Internal error taxonomy for runtime classification.
851/// Not a stable API; may change without notice.
852///
853
854#[derive(Clone, Copy, Debug, Eq, PartialEq)]
855pub enum ErrorClass {
856    Corruption,
857    IncompatiblePersistedFormat,
858    NotFound,
859    Internal,
860    Conflict,
861    Unsupported,
862    InvariantViolation,
863}
864
865impl fmt::Display for ErrorClass {
866    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
867        let label = match self {
868            Self::Corruption => "corruption",
869            Self::IncompatiblePersistedFormat => "incompatible_persisted_format",
870            Self::NotFound => "not_found",
871            Self::Internal => "internal",
872            Self::Conflict => "conflict",
873            Self::Unsupported => "unsupported",
874            Self::InvariantViolation => "invariant_violation",
875        };
876        write!(f, "{label}")
877    }
878}
879
880///
881/// ErrorOrigin
882/// Internal origin taxonomy for runtime classification.
883/// Not a stable API; may change without notice.
884///
885
886#[derive(Clone, Copy, Debug, Eq, PartialEq)]
887pub enum ErrorOrigin {
888    Serialize,
889    Store,
890    Index,
891    Identity,
892    Query,
893    Planner,
894    Cursor,
895    Recovery,
896    Response,
897    Executor,
898    Interface,
899}
900
901impl fmt::Display for ErrorOrigin {
902    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
903        let label = match self {
904            Self::Serialize => "serialize",
905            Self::Store => "store",
906            Self::Index => "index",
907            Self::Identity => "identity",
908            Self::Query => "query",
909            Self::Planner => "planner",
910            Self::Cursor => "cursor",
911            Self::Recovery => "recovery",
912            Self::Response => "response",
913            Self::Executor => "executor",
914            Self::Interface => "interface",
915        };
916        write!(f, "{label}")
917    }
918}