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    /// Schema validation errors
29    #[error("Schema error: {0}")]
30    Schema(String),
31
32    /// CQL parsing errors
33    #[error("CQL parse error: {0}")]
34    CqlParse(String),
35
36    /// Invalid format error (for SSTable parsing)
37    #[error("Invalid format: {0}")]
38    InvalidFormat(String),
39
40    /// Unsupported format error
41    #[error("Unsupported format: {0}")]
42    UnsupportedFormat(String),
43
44    /// SSTable version below the supported Cassandra 5.0 floor.
45    ///
46    /// CQLite targets Cassandra 5.0 (`na`+/`nb` BIG, `oa`/`da` BTI). A
47    /// pre-`na` (`ma`–`me`, Cassandra 3.x) BIG version, or a non-`da` BTI
48    /// version, is out of scope and rejected at version-parse time.
49    #[error("Unsupported SSTable version {version:?}: below supported floor {floor:?}")]
50    UnsupportedVersion { version: String, floor: String },
51
52    /// Cassandra CommitLog segment descriptor version outside the supported
53    /// Cassandra 5.0-era range (issue #2389).
54    ///
55    /// The version is read authoritatively from the `CommitLogDescriptor`
56    /// header, never inferred from the filename or file size (no-heuristics).
57    /// A below/above-range version is rejected before the mutation stream is
58    /// touched, mirroring `BigVersionGates`/`BtiVersionGates`.
59    #[error("Unsupported CommitLog version {version}: supported range is {floor}..={ceiling}")]
60    UnsupportedCommitLogVersion {
61        version: i32,
62        floor: i32,
63        ceiling: i32,
64    },
65
66    /// A Cassandra CommitLog per-record frame failed structural validation:
67    /// a CRC mismatch (header or payload), an implausible record length, or a
68    /// corrupt sync marker (issue #2389).
69    ///
70    /// Distinct from a *torn tail* (clean truncation), which is reported to the
71    /// caller as end-of-segment rather than as an error.
72    #[error("Corrupt CommitLog frame: {0}")]
73    CorruptCommitLogFrame(String),
74
75    /// Timeout error
76    #[error("Operation timeout: {0}")]
77    Timeout(String),
78
79    /// Invalid path error
80    #[error("Invalid path: {0}")]
81    InvalidPath(String),
82
83    /// Invalid state error
84    #[error("Invalid state: {0}")]
85    InvalidState(String),
86
87    /// Query execution errors
88    #[error("Query execution error: {0}")]
89    QueryExecution(String),
90
91    /// A materialized result set exceeded the configured byte budget (issue #1582).
92    ///
93    /// Raised by the SELECT executor *while collecting* a materialized result
94    /// set, before it grows large enough to threaten the process's <128MB
95    /// memory target. This is the byte-unit successor to the coarse row-count
96    /// safety valve: a byte ceiling correctly distinguishes 1M harmless skinny
97    /// rows from 100k memory-blowing wide rows. The remedy is in the message —
98    /// bound the result with a `LIMIT` clause, or consume it incrementally via
99    /// the streaming query API instead of materializing the whole set.
100    #[error(
101        "result set exceeded the {budget_bytes}-byte materialization budget \
102         (estimated {estimated_bytes} bytes across {rows} rows so far); \
103         add a LIMIT clause to bound the result, or use the streaming query \
104         API (e.g. execute_streaming) to consume rows incrementally"
105    )]
106    ResultTooLarge {
107        /// Configured materialization byte budget that was exceeded.
108        budget_bytes: usize,
109        /// Estimated logical size (bytes) of the result collected so far.
110        estimated_bytes: usize,
111        /// Number of rows collected when the budget was exceeded.
112        rows: usize,
113    },
114
115    /// An unrecognized `CQLITE_READ_PATH` value was supplied (issue #1918).
116    ///
117    /// Resolving the read-path forcing knob returns this distinct error rather
118    /// than silently falling through to `auto`, so a typo'd knob is loud instead
119    /// of a no-op. Names the invalid value and the allowed set.
120    #[error("invalid CQLITE_READ_PATH value '{value}': expected one of auto, point, full")]
121    InvalidReadPath {
122        /// The unrecognized value supplied to the knob.
123        value: String,
124    },
125
126    /// Forced `point` read path could not run a partition-targeted lookup (issue
127    /// #1918).
128    ///
129    /// Raised whenever a forced read path cannot serve a query without silently
130    /// diverging from the `auto` result. Under `CQLITE_READ_PATH=point` (or the
131    /// equivalent `QueryConfig` field) this fires when the executor would not run
132    /// a genuinely partition-targeted lookup — a classification fallback, an
133    /// unwired targeted surface (e.g. a metadata `IN` fan-out), or a build/path
134    /// that does not actually prune. Under `CQLITE_READ_PATH=full` it fires for a
135    /// schema-less sole-pk point lookup, which only the specialized targeted seek
136    /// can serve correctly (a full scan would return 0 rows instead of the row
137    /// `auto` returns). Either way the query fails closed instead of silently
138    /// returning a wrong result; `reason` names the concrete cause.
139    #[error(
140        "forced read path '{forced}' unavailable: {reason}. This query cannot be \
141         served under CQLITE_READ_PATH={forced} without diverging from the 'auto' \
142         result; use 'auto' to let CQLite choose the read path"
143    )]
144    ForcedReadPathUnavailable {
145        /// The forced mode that could not be satisfied (`"point"` or `"full"`).
146        forced: &'static str,
147        /// The concrete fallback reason label (e.g. `partition_key_not_fully_constrained`).
148        reason: String,
149    },
150
151    /// Type conversion errors
152    #[error("Type conversion error: {0}")]
153    TypeConversion(String),
154
155    /// Configuration errors
156    #[error("Configuration error: {0}")]
157    Configuration(String),
158
159    /// Storage engine errors
160    #[error("Storage error: {0}")]
161    Storage(String),
162
163    /// Memory management errors
164    #[error("Memory error: {0}")]
165    Memory(String),
166
167    /// Lock/concurrency errors
168    #[error("Concurrency error: {0}")]
169    Concurrency(String),
170
171    /// Write directory already locked by another process or Database instance
172    ///
173    /// Returned by `WriteEngine::new` when the advisory lock on `write_dir`
174    /// cannot be acquired because another `WriteEngine` (in this or another
175    /// process) already holds it.  Only one `Database` instance may hold a
176    /// `write_dir` at a time.
177    #[error(
178        "write_dir '{path}' is already locked by another process. \
179         Only one Database instance may hold a write_dir at a time."
180    )]
181    WriteDirLocked {
182        /// The path that could not be locked
183        path: String,
184    },
185
186    /// Resource not found
187    #[error("Not found: {0}")]
188    NotFound(String),
189
190    /// Table errors
191    #[error("Table error: {0}")]
192    Table(String),
193
194    /// Resource already exists
195    #[error("Already exists: {0}")]
196    AlreadyExists(String),
197
198    /// Invalid operation
199    #[error("Invalid operation: {0}")]
200    InvalidOperation(String),
201
202    /// Constraint violation
203    #[error("Constraint violation: {0}")]
204    ConstraintViolation(String),
205
206    /// Transaction errors
207    #[error("Transaction error: {0}")]
208    Transaction(String),
209
210    /// Index errors
211    #[error("Index error: {0}")]
212    Index(String),
213
214    /// Compaction errors
215    #[error("Compaction error: {0}")]
216    Compaction(String),
217
218    /// WASM-specific errors
219    #[cfg(target_arch = "wasm32")]
220    #[error("WASM error: {0}")]
221    Wasm(String),
222
223    /// Generic internal error
224    #[error("Internal error: {0}")]
225    Internal(String),
226
227    /// Parse error
228    #[error("Parse error: {0}")]
229    Parse(String),
230
231    /// Invalid input error
232    #[error("Invalid input: {0}")]
233    InvalidInput(String),
234
235    /// Unsupported query error
236    #[error("Unsupported query: {0}")]
237    UnsupportedQuery(String),
238
239    /// The operation was cooperatively cancelled (issue #2264).
240    ///
241    /// Raised by a long-running scan (e.g. the compaction streaming read) when
242    /// its cancellation token is tripped — a client disconnect propagated from
243    /// the Flight `do_get` path. Distinct from a genuine failure so callers can
244    /// treat it as a clean, expected abort rather than corruption.
245    #[error("Operation cancelled")]
246    Cancelled,
247}
248
249impl Error {
250    /// Create a serialization error
251    pub fn serialization(msg: impl Into<String>) -> Self {
252        Self::Serialization {
253            message: msg.into(),
254            source: None,
255        }
256    }
257
258    /// Create a corruption error
259    pub fn corruption(msg: impl Into<String>) -> Self {
260        Self::Corruption(msg.into())
261    }
262
263    /// Create a schema error
264    pub fn schema(msg: impl Into<String>) -> Self {
265        Self::Schema(msg.into())
266    }
267
268    /// Create a CQL parse error
269    pub fn cql_parse(msg: impl Into<String>) -> Self {
270        Self::CqlParse(msg.into())
271    }
272
273    /// Create an invalid format error
274    pub fn invalid_format(msg: impl Into<String>) -> Self {
275        Self::InvalidFormat(msg.into())
276    }
277
278    /// Create an unsupported format error
279    pub fn unsupported_format(msg: impl Into<String>) -> Self {
280        Self::UnsupportedFormat(msg.into())
281    }
282
283    /// Create an invalid path error
284    pub fn invalid_path(msg: impl Into<String>) -> Self {
285        Self::InvalidPath(msg.into())
286    }
287
288    /// Create an invalid state error
289    pub fn invalid_state(msg: impl Into<String>) -> Self {
290        Self::InvalidState(msg.into())
291    }
292
293    /// Create a query execution error
294    pub fn query_execution(msg: impl Into<String>) -> Self {
295        Self::QueryExecution(msg.into())
296    }
297
298    /// Create an invalid-read-path error (issue #1918).
299    pub fn invalid_read_path(value: impl Into<String>) -> Self {
300        Self::InvalidReadPath {
301            value: value.into(),
302        }
303    }
304
305    /// Create a forced-read-path-unavailable error (issue #1918). `forced` is the
306    /// forced mode (`"point"` or `"full"`); `reason` is the concrete cause label.
307    pub fn forced_read_path_unavailable(forced: &'static str, reason: impl Into<String>) -> Self {
308        Self::ForcedReadPathUnavailable {
309            forced,
310            reason: reason.into(),
311        }
312    }
313
314    /// Create a type conversion error
315    pub fn type_conversion(msg: impl Into<String>) -> Self {
316        Self::TypeConversion(msg.into())
317    }
318
319    /// Create a configuration error
320    pub fn configuration(msg: impl Into<String>) -> Self {
321        Self::Configuration(msg.into())
322    }
323
324    /// Create a storage error
325    pub fn storage(msg: impl Into<String>) -> Self {
326        Self::Storage(msg.into())
327    }
328
329    /// Create a memory error
330    pub fn memory(msg: impl Into<String>) -> Self {
331        Self::Memory(msg.into())
332    }
333
334    /// Create a concurrency error
335    pub fn concurrency(msg: impl Into<String>) -> Self {
336        Self::Concurrency(msg.into())
337    }
338
339    /// Create a not found error
340    pub fn not_found(msg: impl Into<String>) -> Self {
341        Self::NotFound(msg.into())
342    }
343
344    /// Create an already exists error
345    pub fn already_exists(msg: impl Into<String>) -> Self {
346        Self::AlreadyExists(msg.into())
347    }
348
349    /// Create an invalid operation error
350    pub fn invalid_operation(msg: impl Into<String>) -> Self {
351        Self::InvalidOperation(msg.into())
352    }
353
354    /// Create a constraint violation error
355    pub fn constraint_violation(msg: impl Into<String>) -> Self {
356        Self::ConstraintViolation(msg.into())
357    }
358
359    /// Create a transaction error
360    pub fn transaction(msg: impl Into<String>) -> Self {
361        Self::Transaction(msg.into())
362    }
363
364    /// Create an index error
365    pub fn index(msg: impl Into<String>) -> Self {
366        Self::Index(msg.into())
367    }
368
369    /// Create a compaction error
370    pub fn compaction(msg: impl Into<String>) -> Self {
371        Self::Compaction(msg.into())
372    }
373
374    /// Create a WASM error
375    #[cfg(target_arch = "wasm32")]
376    pub fn wasm(msg: impl Into<String>) -> Self {
377        Self::Wasm(msg.into())
378    }
379
380    /// Create an internal error
381    pub fn internal(msg: impl Into<String>) -> Self {
382        Self::Internal(msg.into())
383    }
384
385    /// Create an invalid input error
386    pub fn invalid_input(msg: impl Into<String>) -> Self {
387        Self::InvalidInput(msg.into())
388    }
389
390    /// Create a parse error
391    pub fn parse(msg: impl Into<String>) -> Self {
392        Self::Parse(msg.into())
393    }
394
395    /// Create an unsupported query error
396    pub fn unsupported_query(msg: impl Into<String>) -> Self {
397        Self::UnsupportedQuery(msg.into())
398    }
399
400    /// Create a write-dir locked error
401    pub fn write_dir_locked(path: impl Into<String>) -> Self {
402        Self::WriteDirLocked { path: path.into() }
403    }
404
405    /// Create a table not found error
406    pub fn table_not_found(msg: impl Into<String>) -> Self {
407        Self::NotFound(format!("Table not found: {}", msg.into()))
408    }
409
410    /// Create an ambiguous table error
411    pub fn ambiguous_table(msg: impl Into<String>) -> Self {
412        Self::Table(format!("Ambiguous table reference: {}", msg.into()))
413    }
414
415    /// Check if this error is recoverable
416    pub fn is_recoverable(&self) -> bool {
417        match self {
418            // These errors are typically recoverable with retry
419            Error::Io(_) => true,
420            Error::Concurrency(_) => true,
421            Error::Memory(_) => true,
422
423            // These errors are typically not recoverable
424            Error::Corruption(_) => false,
425            Error::Schema(_) => false,
426            Error::CqlParse(_) => false,
427            Error::Configuration(_) => false,
428
429            // Context-dependent errors
430            Error::Storage(_) => true,
431            Error::QueryExecution(_) => false,
432            // Not recoverable by retry: the same query would re-materialize the
433            // same oversized result. The user must add LIMIT or stream.
434            Error::ResultTooLarge { .. } => false,
435            // A knob misconfiguration re-fails identically until the operator fixes it.
436            Error::InvalidReadPath { .. } => false,
437            Error::ForcedReadPathUnavailable { .. } => false,
438            Error::TypeConversion(_) => false,
439            Error::NotFound(_) => false,
440            Error::AlreadyExists(_) => false,
441            Error::InvalidOperation(_) => false,
442            Error::ConstraintViolation(_) => false,
443            Error::Transaction(_) => true,
444            Error::Index(_) => true,
445            Error::Compaction(_) => true,
446
447            // New error types
448            Error::Table(_) => false,
449
450            // Write-dir lock conflict — not recoverable without releasing the lock
451            Error::WriteDirLocked { .. } => false,
452
453            #[cfg(target_arch = "wasm32")]
454            Error::Wasm(_) => false,
455
456            Error::Serialization { .. } => false,
457            Error::Internal(_) => false,
458            Error::Parse(_) => false,
459            Error::InvalidInput(_) => false,
460            Error::InvalidFormat(_) => false,
461            Error::UnsupportedFormat(_) => false,
462            Error::UnsupportedVersion { .. } => false,
463            Error::UnsupportedCommitLogVersion { .. } => false,
464            Error::CorruptCommitLogFrame(_) => false,
465            Error::InvalidPath(_) => false,
466            Error::InvalidState(_) => false,
467            Error::Timeout(_) => false,
468            Error::UnsupportedQuery(_) => false,
469            // A cancelled operation is deliberate, not a transient fault: re-running
470            // it would just be cancelled again. The caller decides whether to retry.
471            Error::Cancelled => false,
472        }
473    }
474
475    /// Get the error category
476    pub fn category(&self) -> ErrorCategory {
477        match self {
478            Error::Io(_) => ErrorCategory::System,
479            Error::Serialization { .. } => ErrorCategory::Data,
480            Error::Corruption(_) => ErrorCategory::Data,
481            Error::Schema(_) => ErrorCategory::Schema,
482            Error::CqlParse(_) => ErrorCategory::Query,
483            Error::QueryExecution(_) => ErrorCategory::Query,
484            Error::ResultTooLarge { .. } => ErrorCategory::Query,
485            Error::InvalidReadPath { .. } => ErrorCategory::Configuration,
486            Error::ForcedReadPathUnavailable { .. } => ErrorCategory::Query,
487            Error::TypeConversion(_) => ErrorCategory::Data,
488            Error::Configuration(_) => ErrorCategory::Configuration,
489            Error::Storage(_) => ErrorCategory::Storage,
490            Error::Memory(_) => ErrorCategory::System,
491            Error::Concurrency(_) => ErrorCategory::Concurrency,
492            Error::NotFound(_) => ErrorCategory::NotFound,
493            Error::AlreadyExists(_) => ErrorCategory::Conflict,
494            Error::InvalidOperation(_) => ErrorCategory::Logic,
495            Error::ConstraintViolation(_) => ErrorCategory::Constraint,
496            Error::Transaction(_) => ErrorCategory::Transaction,
497            Error::Index(_) => ErrorCategory::Storage,
498            Error::Compaction(_) => ErrorCategory::Storage,
499
500            // New error types
501            Error::Table(_) => ErrorCategory::Schema,
502
503            // Write-dir lock conflict
504            Error::WriteDirLocked { .. } => ErrorCategory::Concurrency,
505
506            #[cfg(target_arch = "wasm32")]
507            Error::Wasm(_) => ErrorCategory::Platform,
508
509            Error::Internal(_) => ErrorCategory::Internal,
510            Error::Parse(_) => ErrorCategory::Data,
511            Error::InvalidInput(_) => ErrorCategory::Data,
512            Error::InvalidFormat(_) => ErrorCategory::Data,
513            Error::UnsupportedFormat(_) => ErrorCategory::Data,
514            Error::UnsupportedVersion { .. } => ErrorCategory::Data,
515            Error::UnsupportedCommitLogVersion { .. } => ErrorCategory::Data,
516            Error::CorruptCommitLogFrame(_) => ErrorCategory::Data,
517            Error::InvalidPath(_) => ErrorCategory::System,
518            Error::InvalidState(_) => ErrorCategory::Logic,
519            Error::Timeout(_) => ErrorCategory::System,
520            Error::UnsupportedQuery(_) => ErrorCategory::Query,
521            Error::Cancelled => ErrorCategory::Cancelled,
522        }
523    }
524}
525
526/// Error categories for grouping related errors
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub enum ErrorCategory {
529    /// System-level errors (I/O, memory, etc.)
530    System,
531    /// Data-related errors (corruption, serialization)
532    Data,
533    /// Schema-related errors
534    Schema,
535    /// Query-related errors (parsing, execution)
536    Query,
537    /// Configuration errors
538    Configuration,
539    /// Storage engine errors
540    Storage,
541    /// Concurrency-related errors
542    Concurrency,
543    /// Resource not found
544    NotFound,
545    /// Resource conflicts
546    Conflict,
547    /// Logic errors
548    Logic,
549    /// Constraint violations
550    Constraint,
551    /// Transaction errors
552    Transaction,
553    /// Platform-specific errors
554    Platform,
555    /// Internal errors
556    Internal,
557    /// A cooperative cancellation / abort (issue #2264). Distinct from
558    /// `System` (I/O, memory) so a cancelled operation is never mislabeled as
559    /// a transport/IO failure by a consumer that switches on category.
560    Cancelled,
561}
562
563impl fmt::Display for ErrorCategory {
564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565        let name = match self {
566            ErrorCategory::System => "System",
567            ErrorCategory::Data => "Data",
568            ErrorCategory::Schema => "Schema",
569            ErrorCategory::Query => "Query",
570            ErrorCategory::Configuration => "Configuration",
571            ErrorCategory::Storage => "Storage",
572            ErrorCategory::Concurrency => "Concurrency",
573            ErrorCategory::NotFound => "NotFound",
574            ErrorCategory::Conflict => "Conflict",
575            ErrorCategory::Logic => "Logic",
576            ErrorCategory::Constraint => "Constraint",
577            ErrorCategory::Transaction => "Transaction",
578            ErrorCategory::Platform => "Platform",
579            ErrorCategory::Internal => "Internal",
580            ErrorCategory::Cancelled => "Cancelled",
581        };
582        write!(f, "{}", name)
583    }
584}
585
586/// Convert from bincode errors
587impl From<bincode::Error> for Error {
588    fn from(err: bincode::Error) -> Self {
589        Error::Serialization {
590            message: err.to_string(),
591            source: Some(Box::new(err)),
592        }
593    }
594}
595
596/// Convert from serde_json errors
597impl From<serde_json::Error> for Error {
598    fn from(err: serde_json::Error) -> Self {
599        Error::Serialization {
600            message: err.to_string(),
601            source: Some(Box::new(err)),
602        }
603    }
604}
605
606/// Convert from nom errors
607impl<I> From<nom::Err<nom::error::Error<I>>> for Error
608where
609    I: std::fmt::Debug,
610{
611    fn from(err: nom::Err<nom::error::Error<I>>) -> Self {
612        Error::CqlParse(format!("Parse error: {:?}", err))
613    }
614}
615
616// Helper function to create custom parse error type
617pub type ParseResult<I, O> = nom::IResult<I, O, Error>;
618
619/// Custom error type for parsing operations
620#[derive(Debug, Clone)]
621pub struct ParseError {
622    pub message: String,
623}
624
625impl std::fmt::Display for ParseError {
626    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627        write!(f, "{}", self.message)
628    }
629}
630
631impl std::error::Error for ParseError {}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn test_error_from_conversions() {
639        // Test bincode error conversion (covers line 399-400)
640        let io_err = std::io::Error::other("test error");
641        let bincode_err = bincode::Error::new(bincode::ErrorKind::Io(io_err));
642        let error = Error::from(bincode_err);
643        assert!(matches!(error, Error::Serialization { .. }));
644
645        // Test serde_json error conversion (covers line 406-407)
646        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
647        let error = Error::from(json_err);
648        assert!(matches!(error, Error::Serialization { .. }));
649
650        // Test nom error conversion (covers line 416-417)
651        let nom_err = nom::Err::Error(nom::error::Error::new(
652            "test input",
653            nom::error::ErrorKind::Tag,
654        ));
655        let error = Error::from(nom_err);
656        assert!(matches!(error, Error::CqlParse(_)));
657    }
658
659    #[test]
660    fn test_parse_error_display() {
661        // Test ParseError Display implementation (covers line 431-432)
662        let parse_error = ParseError {
663            message: "test parse error".to_string(),
664        };
665        let display_str = format!("{}", parse_error);
666        assert_eq!(display_str, "test parse error");
667    }
668
669    #[test]
670    #[cfg(target_arch = "wasm32")]
671    fn test_wasm_error_creation() {
672        // Test WASM error creation (covers line 234-235)
673        let err = Error::wasm("WASM error");
674        assert!(matches!(err, Error::Wasm(_)));
675        assert!(!err.is_recoverable());
676        assert_eq!(err.category(), ErrorCategory::Platform);
677    }
678
679    #[test]
680    fn test_new_error_types_coverage() {
681        // Test Table error
682        let table_err = Error::Table("table error".to_string());
683        assert!(!table_err.is_recoverable());
684        assert_eq!(table_err.category(), ErrorCategory::Schema);
685    }
686
687    #[test]
688    fn test_error_creation() {
689        let err = Error::storage("test error");
690        assert!(matches!(err, Error::Storage(_)));
691        assert_eq!(err.to_string(), "Storage error: test error");
692    }
693
694    #[test]
695    fn test_error_categories() {
696        assert_eq!(Error::storage("test").category(), ErrorCategory::Storage);
697        assert_eq!(Error::schema("test").category(), ErrorCategory::Schema);
698        assert_eq!(Error::cql_parse("test").category(), ErrorCategory::Query);
699    }
700
701    #[test]
702    fn test_error_recoverability() {
703        assert!(Error::concurrency("test").is_recoverable());
704        assert!(!Error::corruption("test").is_recoverable());
705        assert!(!Error::schema("test").is_recoverable());
706    }
707
708    #[test]
709    fn test_all_error_constructors() {
710        // Test all error constructor methods for coverage
711        let _ = Error::serialization("test");
712        let _ = Error::corruption("test");
713        let _ = Error::schema("test");
714        let _ = Error::cql_parse("test");
715        let _ = Error::invalid_format("test");
716        let _ = Error::unsupported_format("test");
717        let _ = Error::invalid_path("test");
718        let _ = Error::invalid_state("test");
719        let _ = Error::query_execution("test");
720        let _ = Error::type_conversion("test");
721        let _ = Error::configuration("test");
722        let _ = Error::storage("test");
723        let _ = Error::memory("test");
724        let _ = Error::concurrency("test");
725        let _ = Error::not_found("test");
726        let _ = Error::already_exists("test");
727        let _ = Error::invalid_operation("test");
728        let _ = Error::constraint_violation("test");
729        let _ = Error::transaction("test");
730        let _ = Error::index("test");
731        let _ = Error::compaction("test");
732        let _ = Error::internal("test");
733        let _ = Error::invalid_input("test");
734        let _ = Error::parse("test");
735        let _ = Error::write_dir_locked("/tmp/test-dir");
736    }
737
738    #[test]
739    fn test_all_error_categories() {
740        // Test all error categories for coverage
741        assert_eq!(Error::serialization("test").category(), ErrorCategory::Data);
742        assert_eq!(Error::corruption("test").category(), ErrorCategory::Data);
743        assert_eq!(Error::cql_parse("test").category(), ErrorCategory::Query);
744        assert_eq!(
745            Error::invalid_format("test").category(),
746            ErrorCategory::Data
747        );
748        assert_eq!(
749            Error::unsupported_format("test").category(),
750            ErrorCategory::Data
751        );
752        assert_eq!(
753            Error::invalid_path("test").category(),
754            ErrorCategory::System
755        );
756        assert_eq!(
757            Error::invalid_state("test").category(),
758            ErrorCategory::Logic
759        );
760        assert_eq!(
761            Error::query_execution("test").category(),
762            ErrorCategory::Query
763        );
764        assert_eq!(
765            Error::type_conversion("test").category(),
766            ErrorCategory::Data
767        );
768        assert_eq!(
769            Error::configuration("test").category(),
770            ErrorCategory::Configuration
771        );
772        assert_eq!(Error::memory("test").category(), ErrorCategory::System);
773        assert_eq!(
774            Error::concurrency("test").category(),
775            ErrorCategory::Concurrency
776        );
777        assert_eq!(Error::not_found("test").category(), ErrorCategory::NotFound);
778        assert_eq!(
779            Error::already_exists("test").category(),
780            ErrorCategory::Conflict
781        );
782        assert_eq!(
783            Error::invalid_operation("test").category(),
784            ErrorCategory::Logic
785        );
786        assert_eq!(
787            Error::constraint_violation("test").category(),
788            ErrorCategory::Constraint
789        );
790        assert_eq!(
791            Error::transaction("test").category(),
792            ErrorCategory::Transaction
793        );
794        assert_eq!(Error::index("test").category(), ErrorCategory::Storage);
795        assert_eq!(Error::compaction("test").category(), ErrorCategory::Storage);
796        assert_eq!(Error::internal("test").category(), ErrorCategory::Internal);
797        assert_eq!(Error::invalid_input("test").category(), ErrorCategory::Data);
798        assert_eq!(Error::parse("test").category(), ErrorCategory::Data);
799        assert_eq!(
800            Error::write_dir_locked("/tmp/test").category(),
801            ErrorCategory::Concurrency
802        );
803        // Issue #2264: a cooperative cancellation must NEVER classify as
804        // `System` (which downstream bindings map to an I/O error code).
805        assert_eq!(Error::Cancelled.category(), ErrorCategory::Cancelled);
806    }
807
808    #[test]
809    fn test_all_error_recoverability() {
810        // Test recoverability for all error types
811        assert!(Error::memory("test").is_recoverable());
812        assert!(Error::storage("test").is_recoverable());
813        assert!(Error::transaction("test").is_recoverable());
814        assert!(Error::index("test").is_recoverable());
815        assert!(Error::compaction("test").is_recoverable());
816
817        assert!(!Error::serialization("test").is_recoverable());
818        assert!(!Error::cql_parse("test").is_recoverable());
819        assert!(!Error::invalid_format("test").is_recoverable());
820        assert!(!Error::unsupported_format("test").is_recoverable());
821        assert!(!Error::invalid_path("test").is_recoverable());
822        assert!(!Error::invalid_state("test").is_recoverable());
823        assert!(!Error::query_execution("test").is_recoverable());
824        assert!(!Error::type_conversion("test").is_recoverable());
825        assert!(!Error::configuration("test").is_recoverable());
826        assert!(!Error::not_found("test").is_recoverable());
827        assert!(!Error::already_exists("test").is_recoverable());
828        assert!(!Error::invalid_operation("test").is_recoverable());
829        assert!(!Error::constraint_violation("test").is_recoverable());
830        assert!(!Error::internal("test").is_recoverable());
831        assert!(!Error::invalid_input("test").is_recoverable());
832        assert!(!Error::parse("test").is_recoverable());
833        assert!(!Error::write_dir_locked("/tmp/test").is_recoverable());
834    }
835
836    #[test]
837    fn test_error_category_display() {
838        // Test ErrorCategory Display implementation
839        assert_eq!(ErrorCategory::System.to_string(), "System");
840        assert_eq!(ErrorCategory::Data.to_string(), "Data");
841        assert_eq!(ErrorCategory::Schema.to_string(), "Schema");
842        assert_eq!(ErrorCategory::Query.to_string(), "Query");
843        assert_eq!(ErrorCategory::Configuration.to_string(), "Configuration");
844        assert_eq!(ErrorCategory::Storage.to_string(), "Storage");
845        assert_eq!(ErrorCategory::Concurrency.to_string(), "Concurrency");
846        assert_eq!(ErrorCategory::NotFound.to_string(), "NotFound");
847        assert_eq!(ErrorCategory::Conflict.to_string(), "Conflict");
848        assert_eq!(ErrorCategory::Logic.to_string(), "Logic");
849        assert_eq!(ErrorCategory::Constraint.to_string(), "Constraint");
850        assert_eq!(ErrorCategory::Transaction.to_string(), "Transaction");
851        assert_eq!(ErrorCategory::Platform.to_string(), "Platform");
852        assert_eq!(ErrorCategory::Internal.to_string(), "Internal");
853        assert_eq!(ErrorCategory::Cancelled.to_string(), "Cancelled");
854    }
855
856    #[test]
857    fn test_error_from_io_error() {
858        // Test conversion from std::io::Error
859        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
860        let cqlite_err: Error = io_err.into();
861        assert!(matches!(cqlite_err, Error::Io(_)));
862        assert_eq!(cqlite_err.category(), ErrorCategory::System);
863        assert!(cqlite_err.is_recoverable());
864    }
865
866    #[test]
867    fn test_result_type_alias() {
868        // Test the Result type alias
869        let success: Result<i32> = Ok(42);
870        let failure: Result<i32> = Err(Error::storage("test error"));
871
872        assert!(success.is_ok());
873        if let Ok(value) = success {
874            assert_eq!(value, 42);
875        }
876        assert!(failure.is_err());
877    }
878}