cqlite_core/error.rs
1//! Error types for CQLite
2
3use std::fmt;
4use thiserror::Error;
5
6/// Result type alias for CQLite operations
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Main error type for CQLite operations
10#[derive(Error, Debug)]
11pub enum Error {
12 /// I/O related errors
13 #[error("I/O error: {0}")]
14 Io(#[from] std::io::Error),
15
16 /// Serialization/deserialization errors
17 #[error("Serialization error: {message}")]
18 Serialization {
19 message: String,
20 #[source]
21 source: Option<Box<dyn std::error::Error + Send + Sync>>,
22 },
23
24 /// Data corruption errors
25 #[error("Data corruption: {0}")]
26 Corruption(String),
27
28 /// A SINGLE column's value could not be decoded during row assembly
29 /// (issue #3721).
30 ///
31 /// Raised by the row decoder's per-column loop
32 /// (`storage::sstable::reader::parsing::row_decoder::row_data`) for BOTH the
33 /// complex (non-frozen collection / multicell UDT) and the simple (scalar)
34 /// column paths, wrapping the underlying decode failure as its `source`.
35 ///
36 /// # Why this is its OWN variant rather than a `Corruption` message
37 ///
38 /// The row decoder used to answer a per-column decode failure with a bare
39 /// `break` out of the column loop, so the row was assembled from the cells
40 /// gathered SO FAR and returned as `Ok`: the failing column and **every later
41 /// on-disk column** silently vanished and the `SELECT` reported success. A
42 /// successful read with missing columns is indistinguishable from a row that
43 /// legitimately has no value there, so nothing downstream can defend against
44 /// it. Cassandra does not serve a short row either — a cell it cannot read
45 /// raises out of `UnfilteredSerializer` and fails the read.
46 ///
47 /// The block/partition row loops above the decoder deliberately treat an
48 /// ordinary row-parse `Err` as *end of the partition body* (a well-formed
49 /// SSTable's last row is followed by a marker the loop detects by failing to
50 /// parse another row). A per-column decode failure is NOT that condition, and
51 /// the only way those loops can tell the two apart is by MATCHING on a
52 /// dedicated variant — never by inspecting message text, which would be a
53 /// heuristic (issue #28). Hence a variant and not a `Corruption` string.
54 #[error(
55 "column '{column}' (column type {column_type}) failed to decode at byte offset \
56 {offset} of the row: {source}"
57 )]
58 ColumnDecode {
59 /// Name of the column whose value could not be decoded.
60 column: String,
61 /// The column type the decode was driven from: the AUTHORITATIVE on-disk
62 /// SerializationHeader marshal type where the header carries one, else the
63 /// supplied schema's declared type (issue #1081/#28 — both are
64 /// authoritative metadata, never guessed from bytes). Note this is the
65 /// COLUMN's type; for a collection, the failing ELEMENT or KEY type is named
66 /// by `source`, which is why both are reported.
67 column_type: String,
68 /// Byte offset, within the decompressed block buffer, of the failing
69 /// column's cell.
70 offset: usize,
71 /// The underlying decode failure.
72 #[source]
73 source: Box<Error>,
74 },
75
76 /// Schema validation errors
77 #[error("Schema error: {0}")]
78 Schema(String),
79
80 /// CQL parsing errors
81 #[error("CQL parse error: {0}")]
82 CqlParse(String),
83
84 /// Invalid format error (for SSTable parsing)
85 #[error("Invalid format: {0}")]
86 InvalidFormat(String),
87
88 /// Unsupported format error
89 #[error("Unsupported format: {0}")]
90 UnsupportedFormat(String),
91
92 /// SSTable version below the supported Cassandra 5.0 floor.
93 ///
94 /// CQLite targets Cassandra 5.0 (`na`+/`nb` BIG, `oa`/`da` BTI). A
95 /// pre-`na` (`ma`–`me`, Cassandra 3.x) BIG version, or a non-`da` BTI
96 /// version, is out of scope and rejected at version-parse time.
97 #[error("Unsupported SSTable version {version:?}: below supported floor {floor:?}")]
98 UnsupportedVersion { version: String, floor: String },
99
100 /// Cassandra CommitLog segment descriptor version outside the supported
101 /// Cassandra 5.0-era range (issue #2389).
102 ///
103 /// The version is read authoritatively from the `CommitLogDescriptor`
104 /// header, never inferred from the filename or file size (no-heuristics).
105 /// A below/above-range version is rejected before the mutation stream is
106 /// touched, mirroring `BigVersionGates`/`BtiVersionGates`.
107 #[error("Unsupported CommitLog version {version}: supported range is {floor}..={ceiling}")]
108 UnsupportedCommitLogVersion {
109 version: i32,
110 floor: i32,
111 ceiling: i32,
112 },
113
114 /// A Cassandra CommitLog per-record frame failed structural validation:
115 /// a CRC mismatch (header or payload), an implausible record length, or a
116 /// corrupt sync marker (issue #2389).
117 ///
118 /// Distinct from a *torn tail* (clean truncation), which is reported to the
119 /// caller as end-of-segment rather than as an error.
120 #[error("Corrupt CommitLog frame: {0}")]
121 CorruptCommitLogFrame(String),
122
123 /// Timeout error
124 #[error("Operation timeout: {0}")]
125 Timeout(String),
126
127 /// A query exceeded the configured `query.max_execution_time` budget
128 /// (issue #1695).
129 ///
130 /// Raised by the SINGLE timeout wrapper at the query-engine chokepoint (see
131 /// `crate::query::engine::deadline`), never by an ad-hoc clock check inside a
132 /// scan loop. Deliberately a variant of its own — distinct from
133 /// [`Error::Timeout`] (an I/O-level timeout) and from every corruption
134 /// variant — so an operator-imposed budget can never be mistaken for damaged
135 /// data: it classifies as its own bounded telemetry category
136 /// (`crate::observability::ObsErrorCategory::Timeout`).
137 #[error(
138 "query exceeded the configured query.max_execution_time budget of {limit:?} \
139 (elapsed {elapsed:?}) at {operation}; raise query.max_execution_time \
140 (CLI: performance.query_timeout_ms), narrow the query with a \
141 partition-key WHERE or a LIMIT, or set it to 0 for no timeout"
142 )]
143 QueryTimeout {
144 /// The bounded entry point that elapsed (e.g. `query.execute`).
145 operation: String,
146 /// Time actually spent before the budget was abandoned.
147 elapsed: std::time::Duration,
148 /// The configured budget (`query.max_execution_time`).
149 limit: std::time::Duration,
150 },
151
152 /// Invalid path error
153 #[error("Invalid path: {0}")]
154 InvalidPath(String),
155
156 /// Invalid state error
157 #[error("Invalid state: {0}")]
158 InvalidState(String),
159
160 /// Query execution errors
161 #[error("Query execution error: {0}")]
162 QueryExecution(String),
163
164 /// A materialized result set exceeded the configured byte budget (issue #1582).
165 ///
166 /// Raised by the SELECT executor *while collecting* a materialized result
167 /// set, before it grows large enough to threaten the process's <128MB
168 /// memory target. This is the byte-unit successor to the coarse row-count
169 /// safety valve: a byte ceiling correctly distinguishes 1M harmless skinny
170 /// rows from 100k memory-blowing wide rows. The remedy is in the message —
171 /// bound the result with a `LIMIT` clause, or consume it incrementally via
172 /// the streaming query API instead of materializing the whole set.
173 #[error(
174 "result set exceeded the {budget_bytes}-byte materialization budget \
175 (estimated {estimated_bytes} bytes across {rows} rows so far); \
176 add a LIMIT clause to bound the result, or use the streaming query \
177 API (e.g. execute_streaming) to consume rows incrementally"
178 )]
179 ResultTooLarge {
180 /// Configured materialization byte budget that was exceeded.
181 budget_bytes: usize,
182 /// Estimated logical size (bytes) of the result collected so far.
183 estimated_bytes: usize,
184 /// Number of rows collected when the budget was exceeded.
185 rows: usize,
186 },
187
188 /// An unrecognized `CQLITE_READ_PATH` value was supplied (issue #1918).
189 ///
190 /// Resolving the read-path forcing knob returns this distinct error rather
191 /// than silently falling through to `auto`, so a typo'd knob is loud instead
192 /// of a no-op. Names the invalid value and the allowed set.
193 #[error("invalid CQLITE_READ_PATH value '{value}': expected one of auto, point, full")]
194 InvalidReadPath {
195 /// The unrecognized value supplied to the knob.
196 value: String,
197 },
198
199 /// Forced `point` read path could not run a partition-targeted lookup (issue
200 /// #1918).
201 ///
202 /// Raised whenever a forced read path cannot serve a query without silently
203 /// diverging from the `auto` result. Under `CQLITE_READ_PATH=point` (or the
204 /// equivalent `QueryConfig` field) this fires when the executor would not run
205 /// a genuinely partition-targeted lookup — a classification fallback, an
206 /// unwired targeted surface (e.g. a metadata `IN` fan-out), or a build/path
207 /// that does not actually prune. Under `CQLITE_READ_PATH=full` it fires for a
208 /// schema-less sole-pk point lookup, which only the specialized targeted seek
209 /// can serve correctly (a full scan would return 0 rows instead of the row
210 /// `auto` returns). Either way the query fails closed instead of silently
211 /// returning a wrong result; `reason` names the concrete cause.
212 #[error(
213 "forced read path '{forced}' unavailable: {reason}. This query cannot be \
214 served under CQLITE_READ_PATH={forced} without diverging from the 'auto' \
215 result; use 'auto' to let CQLite choose the read path"
216 )]
217 ForcedReadPathUnavailable {
218 /// The forced mode that could not be satisfied (`"point"` or `"full"`).
219 forced: &'static str,
220 /// The concrete fallback reason label (e.g. `partition_key_not_fully_constrained`).
221 reason: String,
222 },
223
224 /// Type conversion errors
225 #[error("Type conversion error: {0}")]
226 TypeConversion(String),
227
228 /// Configuration errors
229 #[error("Configuration error: {0}")]
230 Configuration(String),
231
232 /// Storage engine errors
233 #[error("Storage error: {0}")]
234 Storage(String),
235
236 /// Memory management errors
237 #[error("Memory error: {0}")]
238 Memory(String),
239
240 /// Lock/concurrency errors
241 #[error("Concurrency error: {0}")]
242 Concurrency(String),
243
244 /// Write directory already locked by another process or Database instance
245 ///
246 /// Returned by `WriteEngine::new` when the advisory lock on `write_dir`
247 /// cannot be acquired because another `WriteEngine` (in this or another
248 /// process) already holds it. Only one `Database` instance may hold a
249 /// `write_dir` at a time.
250 #[error(
251 "write_dir '{path}' is already locked by another process. \
252 Only one Database instance may hold a write_dir at a time."
253 )]
254 WriteDirLocked {
255 /// The path that could not be locked
256 path: String,
257 },
258
259 /// Resource not found
260 #[error("Not found: {0}")]
261 NotFound(String),
262
263 /// Table errors
264 #[error("Table error: {0}")]
265 Table(String),
266
267 /// Resource already exists
268 #[error("Already exists: {0}")]
269 AlreadyExists(String),
270
271 /// Invalid operation
272 #[error("Invalid operation: {0}")]
273 InvalidOperation(String),
274
275 /// Constraint violation
276 #[error("Constraint violation: {0}")]
277 ConstraintViolation(String),
278
279 /// Transaction errors
280 #[error("Transaction error: {0}")]
281 Transaction(String),
282
283 /// Index errors
284 #[error("Index error: {0}")]
285 Index(String),
286
287 /// Compaction errors
288 #[error("Compaction error: {0}")]
289 Compaction(String),
290
291 /// WASM-specific errors
292 #[cfg(target_arch = "wasm32")]
293 #[error("WASM error: {0}")]
294 Wasm(String),
295
296 /// Generic internal error
297 #[error("Internal error: {0}")]
298 Internal(String),
299
300 /// Parse error
301 #[error("Parse error: {0}")]
302 Parse(String),
303
304 /// Invalid input error
305 #[error("Invalid input: {0}")]
306 InvalidInput(String),
307
308 /// Unsupported query error
309 #[error("Unsupported query: {0}")]
310 UnsupportedQuery(String),
311
312 /// The operation was cooperatively cancelled (issue #2264).
313 ///
314 /// Raised by a long-running scan (e.g. the compaction streaming read) when
315 /// its cancellation token is tripped — a client disconnect propagated from
316 /// the Flight `do_get` path. Distinct from a genuine failure so callers can
317 /// treat it as a clean, expected abort rather than corruption.
318 #[error("Operation cancelled")]
319 Cancelled,
320}
321
322impl Error {
323 /// Create a serialization error
324 pub fn serialization(msg: impl Into<String>) -> Self {
325 Self::Serialization {
326 message: msg.into(),
327 source: None,
328 }
329 }
330
331 /// Create a corruption error
332 pub fn corruption(msg: impl Into<String>) -> Self {
333 Self::Corruption(msg.into())
334 }
335
336 /// Wrap a per-column decode failure with the column it belongs to
337 /// (issue #3721).
338 pub fn column_decode(
339 column: impl Into<String>,
340 column_type: impl Into<String>,
341 offset: usize,
342 source: Error,
343 ) -> Self {
344 Self::ColumnDecode {
345 column: column.into(),
346 column_type: column_type.into(),
347 offset,
348 source: Box::new(source),
349 }
350 }
351
352 /// Create a schema error
353 pub fn schema(msg: impl Into<String>) -> Self {
354 Self::Schema(msg.into())
355 }
356
357 /// Create a CQL parse error
358 pub fn cql_parse(msg: impl Into<String>) -> Self {
359 Self::CqlParse(msg.into())
360 }
361
362 /// Create an invalid format error
363 pub fn invalid_format(msg: impl Into<String>) -> Self {
364 Self::InvalidFormat(msg.into())
365 }
366
367 /// Create an unsupported format error
368 pub fn unsupported_format(msg: impl Into<String>) -> Self {
369 Self::UnsupportedFormat(msg.into())
370 }
371
372 /// Create an invalid path error
373 pub fn invalid_path(msg: impl Into<String>) -> Self {
374 Self::InvalidPath(msg.into())
375 }
376
377 /// Create an invalid state error
378 pub fn invalid_state(msg: impl Into<String>) -> Self {
379 Self::InvalidState(msg.into())
380 }
381
382 /// Create a query execution error
383 pub fn query_execution(msg: impl Into<String>) -> Self {
384 Self::QueryExecution(msg.into())
385 }
386
387 /// Create an invalid-read-path error (issue #1918).
388 pub fn invalid_read_path(value: impl Into<String>) -> Self {
389 Self::InvalidReadPath {
390 value: value.into(),
391 }
392 }
393
394 /// Create a forced-read-path-unavailable error (issue #1918). `forced` is the
395 /// forced mode (`"point"` or `"full"`); `reason` is the concrete cause label.
396 pub fn forced_read_path_unavailable(forced: &'static str, reason: impl Into<String>) -> Self {
397 Self::ForcedReadPathUnavailable {
398 forced,
399 reason: reason.into(),
400 }
401 }
402
403 /// Create a type conversion error
404 pub fn type_conversion(msg: impl Into<String>) -> Self {
405 Self::TypeConversion(msg.into())
406 }
407
408 /// Create a configuration error
409 pub fn configuration(msg: impl Into<String>) -> Self {
410 Self::Configuration(msg.into())
411 }
412
413 /// Create a storage error
414 pub fn storage(msg: impl Into<String>) -> Self {
415 Self::Storage(msg.into())
416 }
417
418 /// Create a memory error
419 pub fn memory(msg: impl Into<String>) -> Self {
420 Self::Memory(msg.into())
421 }
422
423 /// Create a concurrency error
424 pub fn concurrency(msg: impl Into<String>) -> Self {
425 Self::Concurrency(msg.into())
426 }
427
428 /// Create a not found error
429 pub fn not_found(msg: impl Into<String>) -> Self {
430 Self::NotFound(msg.into())
431 }
432
433 /// Create an already exists error
434 pub fn already_exists(msg: impl Into<String>) -> Self {
435 Self::AlreadyExists(msg.into())
436 }
437
438 /// Create an invalid operation error
439 pub fn invalid_operation(msg: impl Into<String>) -> Self {
440 Self::InvalidOperation(msg.into())
441 }
442
443 /// Create a constraint violation error
444 pub fn constraint_violation(msg: impl Into<String>) -> Self {
445 Self::ConstraintViolation(msg.into())
446 }
447
448 /// Create a transaction error
449 pub fn transaction(msg: impl Into<String>) -> Self {
450 Self::Transaction(msg.into())
451 }
452
453 /// Create an index error
454 pub fn index(msg: impl Into<String>) -> Self {
455 Self::Index(msg.into())
456 }
457
458 /// Create a compaction error
459 pub fn compaction(msg: impl Into<String>) -> Self {
460 Self::Compaction(msg.into())
461 }
462
463 /// Create a WASM error
464 #[cfg(target_arch = "wasm32")]
465 pub fn wasm(msg: impl Into<String>) -> Self {
466 Self::Wasm(msg.into())
467 }
468
469 /// Create an internal error
470 pub fn internal(msg: impl Into<String>) -> Self {
471 Self::Internal(msg.into())
472 }
473
474 /// Create an invalid input error
475 pub fn invalid_input(msg: impl Into<String>) -> Self {
476 Self::InvalidInput(msg.into())
477 }
478
479 /// Create a parse error
480 pub fn parse(msg: impl Into<String>) -> Self {
481 Self::Parse(msg.into())
482 }
483
484 /// Create an unsupported query error
485 pub fn unsupported_query(msg: impl Into<String>) -> Self {
486 Self::UnsupportedQuery(msg.into())
487 }
488
489 /// Create a write-dir locked error
490 pub fn write_dir_locked(path: impl Into<String>) -> Self {
491 Self::WriteDirLocked { path: path.into() }
492 }
493
494 /// Create a table not found error
495 pub fn table_not_found(msg: impl Into<String>) -> Self {
496 Self::NotFound(format!("Table not found: {}", msg.into()))
497 }
498
499 /// Create an ambiguous table error
500 pub fn ambiguous_table(msg: impl Into<String>) -> Self {
501 Self::Table(format!("Ambiguous table reference: {}", msg.into()))
502 }
503
504 /// Check if this error is recoverable
505 pub fn is_recoverable(&self) -> bool {
506 match self {
507 // These errors are typically recoverable with retry
508 Error::Io(_) => true,
509 Error::Concurrency(_) => true,
510 Error::Memory(_) => true,
511
512 // These errors are typically not recoverable
513 Error::Corruption(_) => false,
514 // A column that cannot be decoded will not decode on a retry: the
515 // bytes and the declared type are both unchanged (issue #3721).
516 Error::ColumnDecode { .. } => false,
517 Error::Schema(_) => false,
518 Error::CqlParse(_) => false,
519 Error::Configuration(_) => false,
520
521 // Context-dependent errors
522 Error::Storage(_) => true,
523 Error::QueryExecution(_) => false,
524 // Not recoverable by retry: the same query would re-materialize the
525 // same oversized result. The user must add LIMIT or stream.
526 Error::ResultTooLarge { .. } => false,
527 // A knob misconfiguration re-fails identically until the operator fixes it.
528 Error::InvalidReadPath { .. } => false,
529 Error::ForcedReadPathUnavailable { .. } => false,
530 Error::TypeConversion(_) => false,
531 Error::NotFound(_) => false,
532 Error::AlreadyExists(_) => false,
533 Error::InvalidOperation(_) => false,
534 Error::ConstraintViolation(_) => false,
535 Error::Transaction(_) => true,
536 Error::Index(_) => true,
537 Error::Compaction(_) => true,
538
539 // New error types
540 Error::Table(_) => false,
541
542 // Write-dir lock conflict — not recoverable without releasing the lock
543 Error::WriteDirLocked { .. } => false,
544
545 #[cfg(target_arch = "wasm32")]
546 Error::Wasm(_) => false,
547
548 Error::Serialization { .. } => false,
549 Error::Internal(_) => false,
550 Error::Parse(_) => false,
551 Error::InvalidInput(_) => false,
552 Error::InvalidFormat(_) => false,
553 Error::UnsupportedFormat(_) => false,
554 Error::UnsupportedVersion { .. } => false,
555 Error::UnsupportedCommitLogVersion { .. } => false,
556 Error::CorruptCommitLogFrame(_) => false,
557 Error::InvalidPath(_) => false,
558 Error::InvalidState(_) => false,
559 Error::Timeout(_) => false,
560 // Issue #1695: re-running the same query under the same budget elapses
561 // again — the operator must raise `query.max_execution_time` or narrow
562 // the query (same reasoning as `ResultTooLarge`).
563 Error::QueryTimeout { .. } => false,
564 Error::UnsupportedQuery(_) => false,
565 // A cancelled operation is deliberate, not a transient fault: re-running
566 // it would just be cancelled again. The caller decides whether to retry.
567 Error::Cancelled => false,
568 }
569 }
570
571 /// Get the error category
572 pub fn category(&self) -> ErrorCategory {
573 match self {
574 Error::Io(_) => ErrorCategory::System,
575 Error::Serialization { .. } => ErrorCategory::Data,
576 Error::Corruption(_) => ErrorCategory::Data,
577 Error::ColumnDecode { .. } => ErrorCategory::Data,
578 Error::Schema(_) => ErrorCategory::Schema,
579 Error::CqlParse(_) => ErrorCategory::Query,
580 Error::QueryExecution(_) => ErrorCategory::Query,
581 Error::ResultTooLarge { .. } => ErrorCategory::Query,
582 Error::InvalidReadPath { .. } => ErrorCategory::Configuration,
583 Error::ForcedReadPathUnavailable { .. } => ErrorCategory::Query,
584 Error::TypeConversion(_) => ErrorCategory::Data,
585 Error::Configuration(_) => ErrorCategory::Configuration,
586 Error::Storage(_) => ErrorCategory::Storage,
587 Error::Memory(_) => ErrorCategory::System,
588 Error::Concurrency(_) => ErrorCategory::Concurrency,
589 Error::NotFound(_) => ErrorCategory::NotFound,
590 Error::AlreadyExists(_) => ErrorCategory::Conflict,
591 Error::InvalidOperation(_) => ErrorCategory::Logic,
592 Error::ConstraintViolation(_) => ErrorCategory::Constraint,
593 Error::Transaction(_) => ErrorCategory::Transaction,
594 Error::Index(_) => ErrorCategory::Storage,
595 Error::Compaction(_) => ErrorCategory::Storage,
596
597 // New error types
598 Error::Table(_) => ErrorCategory::Schema,
599
600 // Write-dir lock conflict
601 Error::WriteDirLocked { .. } => ErrorCategory::Concurrency,
602
603 #[cfg(target_arch = "wasm32")]
604 Error::Wasm(_) => ErrorCategory::Platform,
605
606 Error::Internal(_) => ErrorCategory::Internal,
607 Error::Parse(_) => ErrorCategory::Data,
608 Error::InvalidInput(_) => ErrorCategory::Data,
609 Error::InvalidFormat(_) => ErrorCategory::Data,
610 Error::UnsupportedFormat(_) => ErrorCategory::Data,
611 Error::UnsupportedVersion { .. } => ErrorCategory::Data,
612 Error::UnsupportedCommitLogVersion { .. } => ErrorCategory::Data,
613 Error::CorruptCommitLogFrame(_) => ErrorCategory::Data,
614 Error::InvalidPath(_) => ErrorCategory::System,
615 Error::InvalidState(_) => ErrorCategory::Logic,
616 Error::Timeout(_) => ErrorCategory::System,
617 // Issue #1695: a query-budget elapse is a QUERY-lifecycle outcome, not
618 // a `Data` (corruption/serialization) fault. The developer-facing
619 // taxonomy is deliberately left at 14 variants (adding one breaks the
620 // bindings' public code mapping); the DISTINCT bucket lives in the
621 // telemetry taxonomy — see `observability::ObsErrorCategory::Timeout`.
622 Error::QueryTimeout { .. } => ErrorCategory::Query,
623 Error::UnsupportedQuery(_) => ErrorCategory::Query,
624 Error::Cancelled => ErrorCategory::Cancelled,
625 }
626 }
627}
628
629/// Error categories for grouping related errors
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631pub enum ErrorCategory {
632 /// System-level errors (I/O, memory, etc.)
633 System,
634 /// Data-related errors (corruption, serialization)
635 Data,
636 /// Schema-related errors
637 Schema,
638 /// Query-related errors (parsing, execution)
639 Query,
640 /// Configuration errors
641 Configuration,
642 /// Storage engine errors
643 Storage,
644 /// Concurrency-related errors
645 Concurrency,
646 /// Resource not found
647 NotFound,
648 /// Resource conflicts
649 Conflict,
650 /// Logic errors
651 Logic,
652 /// Constraint violations
653 Constraint,
654 /// Transaction errors
655 Transaction,
656 /// Platform-specific errors
657 Platform,
658 /// Internal errors
659 Internal,
660 /// A cooperative cancellation / abort (issue #2264). Distinct from
661 /// `System` (I/O, memory) so a cancelled operation is never mislabeled as
662 /// a transport/IO failure by a consumer that switches on category.
663 Cancelled,
664}
665
666impl fmt::Display for ErrorCategory {
667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668 let name = match self {
669 ErrorCategory::System => "System",
670 ErrorCategory::Data => "Data",
671 ErrorCategory::Schema => "Schema",
672 ErrorCategory::Query => "Query",
673 ErrorCategory::Configuration => "Configuration",
674 ErrorCategory::Storage => "Storage",
675 ErrorCategory::Concurrency => "Concurrency",
676 ErrorCategory::NotFound => "NotFound",
677 ErrorCategory::Conflict => "Conflict",
678 ErrorCategory::Logic => "Logic",
679 ErrorCategory::Constraint => "Constraint",
680 ErrorCategory::Transaction => "Transaction",
681 ErrorCategory::Platform => "Platform",
682 ErrorCategory::Internal => "Internal",
683 ErrorCategory::Cancelled => "Cancelled",
684 };
685 write!(f, "{}", name)
686 }
687}
688
689/// Convert from bincode errors
690impl From<bincode::Error> for Error {
691 fn from(err: bincode::Error) -> Self {
692 Error::Serialization {
693 message: err.to_string(),
694 source: Some(Box::new(err)),
695 }
696 }
697}
698
699/// Convert from serde_json errors
700impl From<serde_json::Error> for Error {
701 fn from(err: serde_json::Error) -> Self {
702 Error::Serialization {
703 message: err.to_string(),
704 source: Some(Box::new(err)),
705 }
706 }
707}
708
709/// Convert from nom errors
710impl<I> From<nom::Err<nom::error::Error<I>>> for Error
711where
712 I: std::fmt::Debug,
713{
714 fn from(err: nom::Err<nom::error::Error<I>>) -> Self {
715 Error::CqlParse(format!("Parse error: {:?}", err))
716 }
717}
718
719// Helper function to create custom parse error type
720pub type ParseResult<I, O> = nom::IResult<I, O, Error>;
721
722/// Custom error type for parsing operations
723#[derive(Debug, Clone)]
724pub struct ParseError {
725 pub message: String,
726}
727
728impl std::fmt::Display for ParseError {
729 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
730 write!(f, "{}", self.message)
731 }
732}
733
734impl std::error::Error for ParseError {}
735
736#[cfg(test)]
737#[path = "error_tests.rs"]
738mod tests;