velesdb_core/
error.rs

1//! Error types for `VelesDB`.
2//!
3//! This module provides a unified error type for all `VelesDB` operations,
4//! designed for professional API exposure to Python/Node clients.
5
6use thiserror::Error;
7
8/// Result type alias for `VelesDB` operations.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Errors that can occur in `VelesDB` operations.
12///
13/// Each variant includes a descriptive error message suitable for end-users.
14/// Error codes follow the pattern `VELES-XXX` for easy debugging.
15#[derive(Error, Debug)]
16pub enum Error {
17    /// Collection already exists (VELES-001).
18    #[error("[VELES-001] Collection '{0}' already exists")]
19    CollectionExists(String),
20
21    /// Collection not found (VELES-002).
22    #[error("[VELES-002] Collection '{0}' not found")]
23    CollectionNotFound(String),
24
25    /// Point not found (VELES-003).
26    #[error("[VELES-003] Point with ID '{0}' not found")]
27    PointNotFound(u64),
28
29    /// Dimension mismatch (VELES-004).
30    #[error("[VELES-004] Vector dimension mismatch: expected {expected}, got {actual}")]
31    DimensionMismatch {
32        /// Expected dimension.
33        expected: usize,
34        /// Actual dimension.
35        actual: usize,
36    },
37
38    /// Invalid vector (VELES-005).
39    #[error("[VELES-005] Invalid vector: {0}")]
40    InvalidVector(String),
41
42    /// Storage error (VELES-006).
43    #[error("[VELES-006] Storage error: {0}")]
44    Storage(String),
45
46    /// Index error (VELES-007).
47    #[error("[VELES-007] Index error: {0}")]
48    Index(String),
49
50    /// Index corrupted (VELES-008).
51    ///
52    /// Indicates that index files are corrupted and need to be rebuilt.
53    #[error("[VELES-008] Index corrupted: {0}")]
54    IndexCorrupted(String),
55
56    /// Configuration error (VELES-009).
57    #[error("[VELES-009] Configuration error: {0}")]
58    Config(String),
59
60    /// Query parsing error (VELES-010).
61    ///
62    /// Wraps `VelesQL` parse errors with position and context information.
63    #[error("[VELES-010] Query error: {0}")]
64    Query(String),
65
66    /// IO error (VELES-011).
67    #[error("[VELES-011] IO error: {0}")]
68    Io(#[from] std::io::Error),
69
70    /// Serialization error (VELES-012).
71    #[error("[VELES-012] Serialization error: {0}")]
72    Serialization(String),
73
74    /// Internal error (VELES-013).
75    ///
76    /// Indicates an unexpected internal error. Please report if encountered.
77    #[error("[VELES-013] Internal error: {0}")]
78    Internal(String),
79}
80
81impl Error {
82    /// Returns the error code (e.g., "VELES-001").
83    #[must_use]
84    pub const fn code(&self) -> &'static str {
85        match self {
86            Self::CollectionExists(_) => "VELES-001",
87            Self::CollectionNotFound(_) => "VELES-002",
88            Self::PointNotFound(_) => "VELES-003",
89            Self::DimensionMismatch { .. } => "VELES-004",
90            Self::InvalidVector(_) => "VELES-005",
91            Self::Storage(_) => "VELES-006",
92            Self::Index(_) => "VELES-007",
93            Self::IndexCorrupted(_) => "VELES-008",
94            Self::Config(_) => "VELES-009",
95            Self::Query(_) => "VELES-010",
96            Self::Io(_) => "VELES-011",
97            Self::Serialization(_) => "VELES-012",
98            Self::Internal(_) => "VELES-013",
99        }
100    }
101
102    /// Returns true if this error is recoverable.
103    ///
104    /// Non-recoverable errors include corruption and internal errors.
105    #[must_use]
106    pub const fn is_recoverable(&self) -> bool {
107        !matches!(self, Self::IndexCorrupted(_) | Self::Internal(_))
108    }
109}
110
111/// Conversion from `VelesQL` `ParseError`.
112impl From<crate::velesql::ParseError> for Error {
113    fn from(err: crate::velesql::ParseError) -> Self {
114        Self::Query(err.to_string())
115    }
116}
117
118// ============================================================================
119// TDD TESTS - Written BEFORE implementation changes
120// ============================================================================
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    // -------------------------------------------------------------------------
127    // Error code tests
128    // -------------------------------------------------------------------------
129
130    #[test]
131    fn test_error_codes_are_unique() {
132        // Arrange - create all error variants
133        let errors: Vec<Error> = vec![
134            Error::CollectionExists("test".into()),
135            Error::CollectionNotFound("test".into()),
136            Error::PointNotFound(1),
137            Error::DimensionMismatch {
138                expected: 768,
139                actual: 512,
140            },
141            Error::InvalidVector("test".into()),
142            Error::Storage("test".into()),
143            Error::Index("test".into()),
144            Error::IndexCorrupted("test".into()),
145            Error::Config("test".into()),
146            Error::Query("test".into()),
147            Error::Io(std::io::Error::other("test")),
148            Error::Serialization("test".into()),
149            Error::Internal("test".into()),
150        ];
151
152        // Act - collect all codes
153        let codes: Vec<&str> = errors.iter().map(Error::code).collect();
154
155        // Assert - all codes are unique and follow pattern
156        let mut unique_codes = codes.clone();
157        unique_codes.sort_unstable();
158        unique_codes.dedup();
159        assert_eq!(
160            codes.len(),
161            unique_codes.len(),
162            "Error codes must be unique"
163        );
164
165        for code in &codes {
166            assert!(
167                code.starts_with("VELES-"),
168                "Code {code} should start with VELES-"
169            );
170        }
171    }
172
173    #[test]
174    fn test_error_display_includes_code() {
175        // Arrange
176        let err = Error::CollectionNotFound("documents".into());
177
178        // Act
179        let display = format!("{err}");
180
181        // Assert
182        assert!(display.contains("VELES-002"));
183        assert!(display.contains("documents"));
184    }
185
186    #[test]
187    fn test_dimension_mismatch_display() {
188        // Arrange
189        let err = Error::DimensionMismatch {
190            expected: 768,
191            actual: 512,
192        };
193
194        // Act
195        let display = format!("{err}");
196
197        // Assert
198        assert!(display.contains("768"));
199        assert!(display.contains("512"));
200        assert!(display.contains("VELES-004"));
201    }
202
203    // -------------------------------------------------------------------------
204    // Conversion tests
205    // -------------------------------------------------------------------------
206
207    #[test]
208    fn test_from_io_error() {
209        // Arrange
210        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
211
212        // Act
213        let err: Error = io_err.into();
214
215        // Assert
216        assert_eq!(err.code(), "VELES-011");
217        assert!(format!("{err}").contains("file not found"));
218    }
219
220    #[test]
221    fn test_from_parse_error() {
222        // Arrange
223        let parse_err = crate::velesql::ParseError::syntax(15, "FORM", "Expected FROM");
224
225        // Act
226        let err: Error = parse_err.into();
227
228        // Assert
229        assert_eq!(err.code(), "VELES-010");
230        assert!(format!("{err}").contains("FROM"));
231    }
232
233    // -------------------------------------------------------------------------
234    // Recoverable tests
235    // -------------------------------------------------------------------------
236
237    #[test]
238    fn test_recoverable_errors() {
239        // These errors are recoverable (user can fix and retry)
240        assert!(Error::CollectionNotFound("x".into()).is_recoverable());
241        assert!(Error::DimensionMismatch {
242            expected: 768,
243            actual: 512
244        }
245        .is_recoverable());
246        assert!(Error::Query("syntax error".into()).is_recoverable());
247    }
248
249    #[test]
250    fn test_non_recoverable_errors() {
251        // These errors indicate serious problems
252        assert!(!Error::IndexCorrupted("checksum mismatch".into()).is_recoverable());
253        assert!(!Error::Internal("unexpected state".into()).is_recoverable());
254    }
255
256    // -------------------------------------------------------------------------
257    // Professional API tests (for Python/Node exposure)
258    // -------------------------------------------------------------------------
259
260    #[test]
261    fn test_error_is_send_sync() {
262        // Required for async/threaded contexts
263        fn assert_send_sync<T: Send + Sync>() {}
264        assert_send_sync::<Error>();
265    }
266
267    #[test]
268    fn test_error_debug_impl() {
269        // Debug should be available for logging
270        let err = Error::Storage("disk full".into());
271        let debug = format!("{err:?}");
272        assert!(debug.contains("Storage"));
273        assert!(debug.contains("disk full"));
274    }
275}