1use std::path::PathBuf;
7
8use thiserror::Error;
9
10#[derive(Debug, Error)]
14pub enum ChronixError {
15 #[error("WAL error: {0}")]
17 Wal(#[from] WalError),
18
19 #[error("Schema error: {0}")]
21 Schema(#[from] SchemaError),
22
23 #[error("Configuration error: {0}")]
25 Config(#[from] ConfigError),
26
27 #[error("Capacity exceeded: {0}")]
29 Capacity(String),
30
31 #[error("I/O error: {0}")]
33 Io(#[from] std::io::Error),
34}
35
36#[derive(Debug, Error)]
38pub enum WalError {
39 #[error("WAL I/O error: {0}")]
41 Io(#[from] std::io::Error),
42
43 #[error("WAL corruption at offset {offset} in {path}: {detail}")]
45 Corruption {
46 offset: u64,
48 path: PathBuf,
50 detail: String,
52 },
53
54 #[error("WAL sequence skip: expected {expected}, got {got}")]
56 SequenceSkip {
57 expected: u64,
59 got: u64,
61 },
62
63 #[error("WAL full: {count} unflushed files exceed limit of {limit}")]
65 Full {
66 count: usize,
68 limit: usize,
70 },
71
72 #[error("Invalid WAL header in {path}: {detail}")]
74 InvalidHeader {
75 path: PathBuf,
77 detail: String,
79 },
80
81 #[error("WAL payload too large: {size} bytes exceeds limit of {limit}")]
83 PayloadTooLarge {
84 size: usize,
86 limit: usize,
88 },
89
90 #[error("WAL lock poisoned: {0}")]
92 LockPoisoned(String),
93
94 #[error(
104 "WAL writer is poisoned after a failed fsync — the file's contents \
105 are unknown; close and reopen the database to recover"
106 )]
107 Poisoned,
108}
109
110#[derive(Debug, Error, Clone, PartialEq, Eq)]
112pub enum SchemaError {
113 #[error("{label} must not be empty")]
115 EmptyName {
116 label: String,
118 },
119
120 #[error("{label} too long: {length} bytes exceeds maximum of {max}")]
122 NameTooLong {
123 label: String,
125 length: usize,
127 max: usize,
129 },
130
131 #[error("{label} contains null byte (\\0)")]
133 NullByte {
134 label: String,
136 },
137
138 #[error("{label} contains invalid character '{ch}' in \"{value}\"")]
141 InvalidCharacter {
142 label: String,
144 ch: char,
146 value: String,
148 },
149
150 #[error("Too many tags: {count} exceeds maximum of {max}")]
152 TooManyTags {
153 count: usize,
155 max: usize,
157 },
158
159 #[error("Point must have at least one field")]
161 EmptyFields,
162
163 #[error("Too many fields: {count} exceeds maximum of {max}")]
165 TooManyFields {
166 count: usize,
168 max: usize,
170 },
171
172 #[error("Invalid value for field '{field}': {reason}")]
174 InvalidFieldValue {
175 field: String,
177 reason: String,
179 },
180
181 #[error("Type conflict for field '{field}' in measurement '{measurement}': expected {expected}, got {got}")]
183 TypeConflict {
184 measurement: String,
186 field: String,
188 expected: String,
190 got: String,
192 },
193
194 #[error("Measurement '{0}' not found")]
196 MeasurementNotFound(String),
197
198 #[error("Schema lock poisoned: {0}")]
200 LockPoisoned(String),
201
202 #[error("Invalid name '{name}': {reason}")]
204 InvalidName {
205 name: String,
207 reason: String,
209 },
210}
211
212#[derive(Debug, Error)]
214pub enum ConfigError {
215 #[error("missing required field: {field}")]
217 MissingField {
218 field: &'static str,
220 },
221
222 #[error("invalid configuration: {message}")]
224 Validation {
225 message: String,
227 },
228
229 #[error("failed to read config file: {0}")]
231 Io(#[from] std::io::Error),
232
233 #[error("failed to parse TOML: {0}")]
235 Parse(#[from] toml::de::Error),
236}
237
238pub type Result<T> = std::result::Result<T, ChronixError>;
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn chronix_error_from_wal() {
247 let wal_err = WalError::Full { count: 5, limit: 4 };
248 let err: ChronixError = wal_err.into();
249 let msg = format!("{err}");
250 assert!(msg.contains("WAL"));
251 assert!(msg.contains('5'));
252 }
253
254 #[test]
255 fn chronix_error_from_schema() {
256 let schema_err = SchemaError::EmptyFields;
257 let err: ChronixError = schema_err.into();
258 assert!(format!("{err}").contains("field"));
259 }
260
261 #[test]
262 fn chronix_error_from_io() {
263 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
264 let err: ChronixError = io_err.into();
265 assert!(format!("{err}").contains("gone"));
266 }
267
268 #[test]
269 fn schema_error_type_conflict_message() {
270 let err = SchemaError::TypeConflict {
271 measurement: "cpu".to_string(),
272 field: "value".to_string(),
273 expected: "f64".to_string(),
274 got: "i64".to_string(),
275 };
276 let msg = format!("{err}");
277 assert!(msg.contains("value"));
278 assert!(msg.contains("f64"));
279 assert!(msg.contains("i64"));
280 }
281
282 #[test]
283 fn wal_error_corruption_message() {
284 let err = WalError::Corruption {
285 offset: 1024,
286 path: PathBuf::from("/data/wal_1.cxwl"),
287 detail: "CRC mismatch".to_string(),
288 };
289 let msg = err.to_string();
290 assert!(msg.contains("1024"));
291 assert!(msg.contains("CRC"));
292 }
293
294 #[test]
295 fn wal_error_sequence_skip_message() {
296 let err = WalError::SequenceSkip {
297 expected: 5,
298 got: 8,
299 };
300 let msg = err.to_string();
301 assert!(msg.contains('5'));
302 assert!(msg.contains('8'));
303 }
304
305 #[test]
306 fn wal_error_invalid_header_message() {
307 let err = WalError::InvalidHeader {
308 path: PathBuf::from("/data/wal.cxwl"),
309 detail: "bad magic".into(),
310 };
311 let msg = err.to_string();
312 assert!(msg.contains("bad magic"));
313 }
314
315 #[test]
316 fn wal_error_lock_poisoned_message() {
317 let err = WalError::LockPoisoned("inner mutex".into());
318 assert!(err.to_string().contains("inner mutex"));
319 }
320
321 #[test]
322 fn wal_error_io_message() {
323 let err = WalError::Io(std::io::Error::new(
324 std::io::ErrorKind::PermissionDenied,
325 "read-only",
326 ));
327 assert!(err.to_string().contains("read-only"));
328 }
329
330 #[test]
331 fn wal_error_full_message() {
332 let err = WalError::Full {
333 count: 10,
334 limit: 4,
335 };
336 let msg = err.to_string();
337 assert!(msg.contains("10"));
338 assert!(msg.contains('4'));
339 }
340
341 #[test]
342 fn wal_error_payload_too_large_message() {
343 let err = WalError::PayloadTooLarge {
344 size: 512_000_000,
345 limit: 268_435_456,
346 };
347 let msg = err.to_string();
348 assert!(msg.contains("512000000"));
349 assert!(msg.contains("268435456"));
350 }
351
352 #[test]
353 fn schema_error_lock_poisoned_message() {
354 let err = SchemaError::LockPoisoned("registry lock".into());
355 assert!(err.to_string().contains("registry lock"));
356 }
357
358 #[test]
359 fn schema_error_measurement_not_found_message() {
360 let err = SchemaError::MeasurementNotFound("disk".into());
361 assert!(err.to_string().contains("disk"));
362 }
363
364 #[test]
365 fn config_error_missing_field_message() {
366 let err = ConfigError::MissingField { field: "data_dir" };
367 assert!(err.to_string().contains("data_dir"));
368 }
369
370 #[test]
371 fn config_error_validation_message() {
372 let err = ConfigError::Validation {
373 message: "too small".into(),
374 };
375 assert!(err.to_string().contains("too small"));
376 }
377
378 #[test]
379 fn chronix_error_from_config() {
380 let config_err = ConfigError::MissingField { field: "data_dir" };
381 let err: ChronixError = config_err.into();
382 assert!(err.to_string().contains("data_dir"));
383 }
384}