Skip to main content

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;