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::patch::MergePatchError;
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 store-origin invariant violation.
216 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 /// Construct a store-origin internal error.
225 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
226 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
227 }
228
229 /// Construct an index-origin internal error.
230 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
231 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
232 }
233
234 /// Construct a query-origin internal error.
235 #[cfg(test)]
236 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
237 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
238 }
239
240 /// Construct a serialize-origin internal error.
241 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
242 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
243 }
244
245 /// Construct a store-origin corruption error.
246 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
247 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
248 }
249
250 /// Construct an index-origin corruption error.
251 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
252 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
253 }
254
255 /// Construct a serialize-origin corruption error.
256 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
257 Self::new(
258 ErrorClass::Corruption,
259 ErrorOrigin::Serialize,
260 message.into(),
261 )
262 }
263
264 /// Construct an identity-origin corruption error.
265 pub(crate) fn identity_corruption(message: impl Into<String>) -> Self {
266 Self::new(
267 ErrorClass::Corruption,
268 ErrorOrigin::Identity,
269 message.into(),
270 )
271 }
272
273 /// Construct a store-origin unsupported error.
274 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
275 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
276 }
277
278 /// Construct an index-origin unsupported error.
279 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
280 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
281 }
282
283 /// Construct a serialize-origin unsupported error.
284 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
285 Self::new(
286 ErrorClass::Unsupported,
287 ErrorOrigin::Serialize,
288 message.into(),
289 )
290 }
291
292 /// Construct a cursor-origin unsupported error.
293 pub(crate) fn cursor_unsupported(message: impl Into<String>) -> Self {
294 Self::new(ErrorClass::Unsupported, ErrorOrigin::Cursor, message.into())
295 }
296
297 /// Construct a query-origin unsupported error preserving one SQL parser
298 /// unsupported-feature label in structured error detail.
299 pub(crate) fn query_unsupported_sql_feature(feature: &'static str) -> Self {
300 let message = format!(
301 "SQL query is not executable in this release: unsupported SQL feature: {feature}"
302 );
303
304 Self {
305 class: ErrorClass::Unsupported,
306 origin: ErrorOrigin::Query,
307 message,
308 detail: Some(ErrorDetail::Query(
309 QueryErrorDetail::UnsupportedSqlFeature { feature },
310 )),
311 }
312 }
313
314 pub fn store_not_found(key: impl Into<String>) -> Self {
315 let key = key.into();
316
317 Self {
318 class: ErrorClass::NotFound,
319 origin: ErrorOrigin::Store,
320 message: format!("data key not found: {key}"),
321 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
322 }
323 }
324
325 /// Construct a standardized unsupported-entity-path error.
326 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
327 let path = path.into();
328
329 Self::new(
330 ErrorClass::Unsupported,
331 ErrorOrigin::Store,
332 format!("unsupported entity path: '{path}'"),
333 )
334 }
335
336 #[must_use]
337 pub const fn is_not_found(&self) -> bool {
338 matches!(
339 self.detail,
340 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
341 )
342 }
343
344 #[must_use]
345 pub fn display_with_class(&self) -> String {
346 format!("{}:{}: {}", self.origin, self.class, self.message)
347 }
348
349 /// Construct an index-plan corruption error with a canonical prefix.
350 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
351 let message = message.into();
352 Self::new(
353 ErrorClass::Corruption,
354 origin,
355 format!("corruption detected ({origin}): {message}"),
356 )
357 }
358
359 /// Construct an index-plan corruption error for index-origin failures.
360 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
361 Self::index_plan_corruption(ErrorOrigin::Index, message)
362 }
363
364 /// Construct an index-plan corruption error for store-origin failures.
365 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
366 Self::index_plan_corruption(ErrorOrigin::Store, message)
367 }
368
369 /// Construct an index-plan corruption error for serialize-origin failures.
370 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
371 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
372 }
373
374 /// Construct an index-plan invariant violation error with a canonical prefix.
375 pub(crate) fn index_plan_invariant(origin: ErrorOrigin, message: impl Into<String>) -> Self {
376 let message = message.into();
377 Self::new(
378 ErrorClass::InvariantViolation,
379 origin,
380 format!("invariant violation detected ({origin}): {message}"),
381 )
382 }
383
384 /// Construct an index-plan invariant violation error for store-origin failures.
385 pub(crate) fn index_plan_store_invariant(message: impl Into<String>) -> Self {
386 Self::index_plan_invariant(ErrorOrigin::Store, message)
387 }
388
389 /// Construct an index uniqueness violation conflict error.
390 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
391 Self::new(
392 ErrorClass::Conflict,
393 ErrorOrigin::Index,
394 format!(
395 "index constraint violation: {path} ({})",
396 index_fields.join(", ")
397 ),
398 )
399 }
400}
401
402///
403/// ErrorDetail
404///
405/// Structured, origin-specific error detail carried by [`InternalError`].
406/// This enum is intentionally extensible.
407///
408
409#[derive(Debug, ThisError)]
410pub enum ErrorDetail {
411 #[error("{0}")]
412 Store(StoreError),
413 #[error("{0}")]
414 ViewPatch(crate::patch::MergePatchError),
415 #[error("{0}")]
416 Query(QueryErrorDetail),
417 // Future-proofing:
418 // #[error("{0}")]
419 // Index(IndexError),
420 //
421 // #[error("{0}")]
422 // Executor(ExecutorErrorDetail),
423}
424
425impl From<MergePatchError> for InternalError {
426 fn from(err: MergePatchError) -> Self {
427 Self {
428 class: ErrorClass::Unsupported,
429 origin: ErrorOrigin::Interface,
430 message: err.to_string(),
431 detail: Some(ErrorDetail::ViewPatch(err)),
432 }
433 }
434}
435
436///
437/// StoreError
438///
439/// Store-specific structured error detail.
440/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
441///
442
443#[derive(Debug, ThisError)]
444pub enum StoreError {
445 #[error("key not found: {key}")]
446 NotFound { key: String },
447
448 #[error("store corruption: {message}")]
449 Corrupt { message: String },
450
451 #[error("store invariant violation: {message}")]
452 InvariantViolation { message: String },
453}
454
455///
456/// QueryErrorDetail
457///
458/// Query-origin structured error detail payload.
459///
460
461#[derive(Debug, ThisError)]
462pub enum QueryErrorDetail {
463 #[error("unsupported SQL feature: {feature}")]
464 UnsupportedSqlFeature { feature: &'static str },
465}
466
467///
468/// ErrorClass
469/// Internal error taxonomy for runtime classification.
470/// Not a stable API; may change without notice.
471///
472
473#[derive(Clone, Copy, Debug, Eq, PartialEq)]
474pub enum ErrorClass {
475 Corruption,
476 NotFound,
477 Internal,
478 Conflict,
479 Unsupported,
480 InvariantViolation,
481}
482
483impl fmt::Display for ErrorClass {
484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485 let label = match self {
486 Self::Corruption => "corruption",
487 Self::NotFound => "not_found",
488 Self::Internal => "internal",
489 Self::Conflict => "conflict",
490 Self::Unsupported => "unsupported",
491 Self::InvariantViolation => "invariant_violation",
492 };
493 write!(f, "{label}")
494 }
495}
496
497///
498/// ErrorOrigin
499/// Internal origin taxonomy for runtime classification.
500/// Not a stable API; may change without notice.
501///
502
503#[derive(Clone, Copy, Debug, Eq, PartialEq)]
504pub enum ErrorOrigin {
505 Serialize,
506 Store,
507 Index,
508 Identity,
509 Query,
510 Planner,
511 Cursor,
512 Recovery,
513 Response,
514 Executor,
515 Interface,
516}
517
518impl fmt::Display for ErrorOrigin {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 let label = match self {
521 Self::Serialize => "serialize",
522 Self::Store => "store",
523 Self::Index => "index",
524 Self::Identity => "identity",
525 Self::Query => "query",
526 Self::Planner => "planner",
527 Self::Cursor => "cursor",
528 Self::Recovery => "recovery",
529 Self::Response => "response",
530 Self::Executor => "executor",
531 Self::Interface => "interface",
532 };
533 write!(f, "{label}")
534 }
535}