1use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Error, Debug)]
16pub enum Error {
17 #[error("[VELES-001] Collection '{0}' already exists")]
19 CollectionExists(String),
20
21 #[error("[VELES-002] Collection '{0}' not found")]
23 CollectionNotFound(String),
24
25 #[error("[VELES-003] Point with ID '{0}' not found")]
27 PointNotFound(u64),
28
29 #[error("[VELES-004] Vector dimension mismatch: expected {expected}, got {actual}")]
31 DimensionMismatch {
32 expected: usize,
34 actual: usize,
36 },
37
38 #[error("[VELES-005] Invalid vector: {0}")]
40 InvalidVector(String),
41
42 #[error("[VELES-006] Storage error: {0}")]
44 Storage(String),
45
46 #[error("[VELES-007] Index error: {0}")]
48 Index(String),
49
50 #[error("[VELES-008] Index corrupted: {0}")]
54 IndexCorrupted(String),
55
56 #[error("[VELES-009] Configuration error: {0}")]
58 Config(String),
59
60 #[error("[VELES-010] Query error: {0}")]
64 Query(String),
65
66 #[error("[VELES-011] IO error: {0}")]
68 Io(#[from] std::io::Error),
69
70 #[error("[VELES-012] Serialization error: {0}")]
72 Serialization(String),
73
74 #[error("[VELES-013] Internal error: {0}")]
78 Internal(String),
79}
80
81impl Error {
82 #[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 #[must_use]
106 pub const fn is_recoverable(&self) -> bool {
107 !matches!(self, Self::IndexCorrupted(_) | Self::Internal(_))
108 }
109}
110
111impl From<crate::velesql::ParseError> for Error {
113 fn from(err: crate::velesql::ParseError) -> Self {
114 Self::Query(err.to_string())
115 }
116}
117
118#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
131 fn test_error_codes_are_unique() {
132 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 let codes: Vec<&str> = errors.iter().map(Error::code).collect();
154
155 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 let err = Error::CollectionNotFound("documents".into());
177
178 let display = format!("{err}");
180
181 assert!(display.contains("VELES-002"));
183 assert!(display.contains("documents"));
184 }
185
186 #[test]
187 fn test_dimension_mismatch_display() {
188 let err = Error::DimensionMismatch {
190 expected: 768,
191 actual: 512,
192 };
193
194 let display = format!("{err}");
196
197 assert!(display.contains("768"));
199 assert!(display.contains("512"));
200 assert!(display.contains("VELES-004"));
201 }
202
203 #[test]
208 fn test_from_io_error() {
209 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
211
212 let err: Error = io_err.into();
214
215 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 let parse_err = crate::velesql::ParseError::syntax(15, "FORM", "Expected FROM");
224
225 let err: Error = parse_err.into();
227
228 assert_eq!(err.code(), "VELES-010");
230 assert!(format!("{err}").contains("FROM"));
231 }
232
233 #[test]
238 fn test_recoverable_errors() {
239 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 assert!(!Error::IndexCorrupted("checksum mismatch".into()).is_recoverable());
253 assert!(!Error::Internal("unexpected state".into()).is_recoverable());
254 }
255
256 #[test]
261 fn test_error_is_send_sync() {
262 fn assert_send_sync<T: Send + Sync>() {}
264 assert_send_sync::<Error>();
265 }
266
267 #[test]
268 fn test_error_debug_impl() {
269 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}