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 std::fmt;
11use thiserror::Error as ThisError;
12
13// ============================================================================
14// INTERNAL ERROR TAXONOMY — ARCHITECTURAL CONTRACT
15// ============================================================================
16//
17// This file defines the canonical runtime error classification system for
18// icydb-core. It is the single source of truth for:
19//
20// • ErrorClass (semantic domain)
21// • ErrorOrigin (subsystem boundary)
22// • Structured detail payloads
23// • Canonical constructor entry points
24//
25// -----------------------------------------------------------------------------
26// DESIGN INTENT
27// -----------------------------------------------------------------------------
28//
29// 1. InternalError is a *taxonomy carrier*, not a formatting utility.
30//
31// - ErrorClass represents semantic meaning (corruption, invariant_violation,
32// unsupported, etc).
33// - ErrorOrigin represents the subsystem boundary (store, index, query,
34// executor, serialize, interface, etc).
35// - The (class, origin) pair must remain stable and intentional.
36//
37// 2. Call sites MUST prefer canonical constructors.
38//
39// Do NOT construct errors manually via:
40// InternalError::new(class, origin, ...)
41// unless you are defining a new canonical helper here.
42//
43// If a pattern appears more than once, centralize it here.
44//
45// 3. Constructors in this file must represent real architectural boundaries.
46//
47// Add a new helper ONLY if it:
48//
49// • Encodes a cross-cutting invariant,
50// • Represents a subsystem boundary,
51// • Or prevents taxonomy drift across call sites.
52//
53// Do NOT add feature-specific helpers.
54// Do NOT add one-off formatting helpers.
55// Do NOT turn this file into a generic message factory.
56//
57// 4. ErrorDetail must align with ErrorOrigin.
58//
59// If detail is present, it MUST correspond to the origin.
60// Do not attach mismatched detail variants.
61//
62// 5. Plan-layer errors are NOT runtime failures.
63//
64// PlanError and CursorPlanError must be translated into
65// executor/query invariants via the canonical mapping functions.
66// Do not leak plan-layer error types across execution boundaries.
67//
68// 6. Preserve taxonomy stability.
69//
70// Do NOT:
71// • Merge error classes.
72// • Reclassify corruption as internal.
73// • Downgrade invariant violations.
74// • Introduce ambiguous class/origin combinations.
75//
76// Any change to ErrorClass or ErrorOrigin is an architectural change
77// and must be reviewed accordingly.
78//
79// -----------------------------------------------------------------------------
80// NON-GOALS
81// -----------------------------------------------------------------------------
82//
83// This is NOT:
84//
85// • A public API contract.
86// • A generic error abstraction layer.
87// • A feature-specific message builder.
88// • A dumping ground for temporary error conversions.
89//
90// -----------------------------------------------------------------------------
91// MAINTENANCE GUIDELINES
92// -----------------------------------------------------------------------------
93//
94// When modifying this file:
95//
96// 1. Ensure classification semantics remain consistent.
97// 2. Avoid constructor proliferation.
98// 3. Prefer narrow, origin-specific helpers over ad-hoc new(...).
99// 4. Keep formatting minimal and standardized.
100// 5. Keep this file boring and stable.
101//
102// If this file grows rapidly, something is wrong at the call sites.
103//
104// ============================================================================
105
106///
107/// InternalError
108///
109/// Structured runtime error with a stable internal classification.
110/// Not a stable API; intended for internal use and may change without notice.
111///
112
113#[derive(Debug, ThisError)]
114#[error("{message}")]
115pub struct InternalError {
116 pub(crate) class: ErrorClass,
117 pub(crate) origin: ErrorOrigin,
118 pub(crate) message: String,
119
120 /// Optional structured error detail.
121 /// The variant (if present) must correspond to `origin`.
122 pub(crate) detail: Option<ErrorDetail>,
123}
124
125impl InternalError {
126 /// Construct an InternalError with optional origin-specific detail.
127 /// This constructor provides default StoreError details for certain
128 /// (class, origin) combinations but does not guarantee a detail payload.
129 pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
130 let message = message.into();
131
132 let detail = match (class, origin) {
133 (ErrorClass::Corruption, ErrorOrigin::Store) => {
134 Some(ErrorDetail::Store(StoreError::Corrupt {
135 message: message.clone(),
136 }))
137 }
138 (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
139 Some(ErrorDetail::Store(StoreError::InvariantViolation {
140 message: message.clone(),
141 }))
142 }
143 _ => None,
144 };
145
146 Self {
147 class,
148 origin,
149 message,
150 detail,
151 }
152 }
153
154 /// Return the internal error class taxonomy.
155 #[must_use]
156 pub const fn class(&self) -> ErrorClass {
157 self.class
158 }
159
160 /// Return the internal error origin taxonomy.
161 #[must_use]
162 pub const fn origin(&self) -> ErrorOrigin {
163 self.origin
164 }
165
166 /// Return the rendered internal error message.
167 #[must_use]
168 pub fn message(&self) -> &str {
169 &self.message
170 }
171
172 /// Return the optional structured detail payload.
173 #[must_use]
174 pub const fn detail(&self) -> Option<&ErrorDetail> {
175 self.detail.as_ref()
176 }
177
178 /// Consume and return the rendered internal error message.
179 #[must_use]
180 pub fn into_message(self) -> String {
181 self.message
182 }
183
184 /// Construct an error while preserving an explicit class/origin taxonomy pair.
185 pub(crate) fn classified(
186 class: ErrorClass,
187 origin: ErrorOrigin,
188 message: impl Into<String>,
189 ) -> Self {
190 Self::new(class, origin, message)
191 }
192
193 /// Rebuild this error with a new message while preserving class/origin taxonomy.
194 pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
195 Self::classified(self.class, self.origin, message)
196 }
197
198 /// Rebuild this error with a new origin while preserving class/message.
199 ///
200 /// Origin-scoped detail payloads are intentionally dropped when re-origining.
201 pub(crate) fn with_origin(self, origin: ErrorOrigin) -> Self {
202 Self::classified(self.class, origin, self.message)
203 }
204
205 /// Construct an index-origin invariant violation.
206 pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
207 Self::new(
208 ErrorClass::InvariantViolation,
209 ErrorOrigin::Index,
210 message.into(),
211 )
212 }
213
214 /// Construct a store-origin invariant violation.
215 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 /// Construct a store-origin internal error.
224 pub(crate) fn store_internal(message: impl Into<String>) -> Self {
225 Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
226 }
227
228 /// Construct an index-origin internal error.
229 pub(crate) fn index_internal(message: impl Into<String>) -> Self {
230 Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
231 }
232
233 /// Construct a query-origin internal error.
234 #[cfg(test)]
235 pub(crate) fn query_internal(message: impl Into<String>) -> Self {
236 Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
237 }
238
239 /// Construct a serialize-origin internal error.
240 pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
241 Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
242 }
243
244 /// Construct a store-origin corruption error.
245 pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
246 Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
247 }
248
249 /// Construct an index-origin corruption error.
250 pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
251 Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
252 }
253
254 /// Construct a serialize-origin corruption error.
255 pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
256 Self::new(
257 ErrorClass::Corruption,
258 ErrorOrigin::Serialize,
259 message.into(),
260 )
261 }
262
263 /// Construct an identity-origin corruption error.
264 pub(crate) fn identity_corruption(message: impl Into<String>) -> Self {
265 Self::new(
266 ErrorClass::Corruption,
267 ErrorOrigin::Identity,
268 message.into(),
269 )
270 }
271
272 /// Construct a store-origin unsupported error.
273 pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
274 Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
275 }
276
277 /// Construct an index-origin unsupported error.
278 pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
279 Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
280 }
281
282 /// Construct a serialize-origin unsupported error.
283 pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
284 Self::new(
285 ErrorClass::Unsupported,
286 ErrorOrigin::Serialize,
287 message.into(),
288 )
289 }
290
291 /// Construct a cursor-origin unsupported error.
292 pub(crate) fn cursor_unsupported(message: impl Into<String>) -> Self {
293 Self::new(ErrorClass::Unsupported, ErrorOrigin::Cursor, message.into())
294 }
295
296 /// Construct a serialize-origin incompatible persisted-format error.
297 pub(crate) fn serialize_incompatible_persisted_format(message: impl Into<String>) -> Self {
298 Self::new(
299 ErrorClass::IncompatiblePersistedFormat,
300 ErrorOrigin::Serialize,
301 message.into(),
302 )
303 }
304
305 /// Construct a query-origin unsupported error preserving one SQL parser
306 /// unsupported-feature label in structured error detail.
307 #[cfg(feature = "sql")]
308 pub(crate) fn query_unsupported_sql_feature(feature: &'static str) -> Self {
309 let message = format!(
310 "SQL query is not executable in this release: unsupported SQL feature: {feature}"
311 );
312
313 Self {
314 class: ErrorClass::Unsupported,
315 origin: ErrorOrigin::Query,
316 message,
317 detail: Some(ErrorDetail::Query(
318 QueryErrorDetail::UnsupportedSqlFeature { feature },
319 )),
320 }
321 }
322
323 pub fn store_not_found(key: impl Into<String>) -> Self {
324 let key = key.into();
325
326 Self {
327 class: ErrorClass::NotFound,
328 origin: ErrorOrigin::Store,
329 message: format!("data key not found: {key}"),
330 detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
331 }
332 }
333
334 /// Construct a standardized unsupported-entity-path error.
335 pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
336 let path = path.into();
337
338 Self::new(
339 ErrorClass::Unsupported,
340 ErrorOrigin::Store,
341 format!("unsupported entity path: '{path}'"),
342 )
343 }
344
345 #[must_use]
346 pub const fn is_not_found(&self) -> bool {
347 matches!(
348 self.detail,
349 Some(ErrorDetail::Store(StoreError::NotFound { .. }))
350 )
351 }
352
353 #[must_use]
354 pub fn display_with_class(&self) -> String {
355 format!("{}:{}: {}", self.origin, self.class, self.message)
356 }
357
358 /// Construct an index-plan corruption error with a canonical prefix.
359 pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
360 let message = message.into();
361 Self::new(
362 ErrorClass::Corruption,
363 origin,
364 format!("corruption detected ({origin}): {message}"),
365 )
366 }
367
368 /// Construct an index-plan corruption error for index-origin failures.
369 pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
370 Self::index_plan_corruption(ErrorOrigin::Index, message)
371 }
372
373 /// Construct an index-plan corruption error for store-origin failures.
374 pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
375 Self::index_plan_corruption(ErrorOrigin::Store, message)
376 }
377
378 /// Construct an index-plan corruption error for serialize-origin failures.
379 pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
380 Self::index_plan_corruption(ErrorOrigin::Serialize, message)
381 }
382
383 /// Construct an index-plan invariant violation error with a canonical prefix.
384 pub(crate) fn index_plan_invariant(origin: ErrorOrigin, message: impl Into<String>) -> Self {
385 let message = message.into();
386 Self::new(
387 ErrorClass::InvariantViolation,
388 origin,
389 format!("invariant violation detected ({origin}): {message}"),
390 )
391 }
392
393 /// Construct an index-plan invariant violation error for store-origin failures.
394 pub(crate) fn index_plan_store_invariant(message: impl Into<String>) -> Self {
395 Self::index_plan_invariant(ErrorOrigin::Store, message)
396 }
397
398 /// Construct an index uniqueness violation conflict error.
399 pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
400 Self::new(
401 ErrorClass::Conflict,
402 ErrorOrigin::Index,
403 format!(
404 "index constraint violation: {path} ({})",
405 index_fields.join(", ")
406 ),
407 )
408 }
409}
410
411///
412/// ErrorDetail
413///
414/// Structured, origin-specific error detail carried by [`InternalError`].
415/// This enum is intentionally extensible.
416///
417
418#[derive(Debug, ThisError)]
419pub enum ErrorDetail {
420 #[error("{0}")]
421 Store(StoreError),
422 #[error("{0}")]
423 Query(QueryErrorDetail),
424 // Future-proofing:
425 // #[error("{0}")]
426 // Index(IndexError),
427 //
428 // #[error("{0}")]
429 // Executor(ExecutorErrorDetail),
430}
431
432///
433/// StoreError
434///
435/// Store-specific structured error detail.
436/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
437///
438
439#[derive(Debug, ThisError)]
440pub enum StoreError {
441 #[error("key not found: {key}")]
442 NotFound { key: String },
443
444 #[error("store corruption: {message}")]
445 Corrupt { message: String },
446
447 #[error("store invariant violation: {message}")]
448 InvariantViolation { message: String },
449}
450
451///
452/// QueryErrorDetail
453///
454/// Query-origin structured error detail payload.
455///
456
457#[derive(Debug, ThisError)]
458pub enum QueryErrorDetail {
459 #[error("unsupported SQL feature: {feature}")]
460 UnsupportedSqlFeature { feature: &'static str },
461}
462
463///
464/// ErrorClass
465/// Internal error taxonomy for runtime classification.
466/// Not a stable API; may change without notice.
467///
468
469#[derive(Clone, Copy, Debug, Eq, PartialEq)]
470pub enum ErrorClass {
471 Corruption,
472 IncompatiblePersistedFormat,
473 NotFound,
474 Internal,
475 Conflict,
476 Unsupported,
477 InvariantViolation,
478}
479
480impl fmt::Display for ErrorClass {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482 let label = match self {
483 Self::Corruption => "corruption",
484 Self::IncompatiblePersistedFormat => "incompatible_persisted_format",
485 Self::NotFound => "not_found",
486 Self::Internal => "internal",
487 Self::Conflict => "conflict",
488 Self::Unsupported => "unsupported",
489 Self::InvariantViolation => "invariant_violation",
490 };
491 write!(f, "{label}")
492 }
493}
494
495///
496/// ErrorOrigin
497/// Internal origin taxonomy for runtime classification.
498/// Not a stable API; may change without notice.
499///
500
501#[derive(Clone, Copy, Debug, Eq, PartialEq)]
502pub enum ErrorOrigin {
503 Serialize,
504 Store,
505 Index,
506 Identity,
507 Query,
508 Planner,
509 Cursor,
510 Recovery,
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::Identity => "identity",
523 Self::Query => "query",
524 Self::Planner => "planner",
525 Self::Cursor => "cursor",
526 Self::Recovery => "recovery",
527 Self::Response => "response",
528 Self::Executor => "executor",
529 Self::Interface => "interface",
530 };
531 write!(f, "{label}")
532 }
533}