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(
202 "Decimal scale conflict for field '{field}' in measurement '{measurement}': \
203 the column stores {declared} fractional digit(s) and the value {value} needs {got}. \
204 Declare the field with the scale it needs before the first write, or round the value."
205 )]
206 DecimalScaleConflict {
207 measurement: String,
209 field: String,
211 declared: u8,
213 got: u8,
215 value: String,
217 },
218
219 #[error("Measurement '{0}' not found")]
221 MeasurementNotFound(String),
222
223 #[error("Schema lock poisoned: {0}")]
225 LockPoisoned(String),
226
227 #[error("Invalid name '{name}': {reason}")]
229 InvalidName {
230 name: String,
232 reason: String,
234 },
235}
236
237#[derive(Debug, Error)]
239pub enum ConfigError {
240 #[error("missing required field: {field}")]
242 MissingField {
243 field: &'static str,
245 },
246
247 #[error("invalid configuration: {message}")]
249 Validation {
250 message: String,
252 },
253
254 #[error("failed to read config file: {0}")]
256 Io(#[from] std::io::Error),
257
258 #[error("failed to parse TOML: {0}")]
260 Parse(#[from] toml::de::Error),
261}
262
263pub type Result<T> = std::result::Result<T, ChronixError>;
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn chronix_error_from_wal() {
272 let wal_err = WalError::Full { count: 5, limit: 4 };
273 let err: ChronixError = wal_err.into();
274 let msg = format!("{err}");
275 assert!(msg.contains("WAL"));
276 assert!(msg.contains('5'));
277 }
278
279 #[test]
280 fn chronix_error_from_schema() {
281 let schema_err = SchemaError::EmptyFields;
282 let err: ChronixError = schema_err.into();
283 assert!(format!("{err}").contains("field"));
284 }
285
286 #[test]
287 fn chronix_error_from_io() {
288 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
289 let err: ChronixError = io_err.into();
290 assert!(format!("{err}").contains("gone"));
291 }
292
293 #[test]
294 fn schema_error_type_conflict_message() {
295 let err = SchemaError::TypeConflict {
296 measurement: "cpu".to_string(),
297 field: "value".to_string(),
298 expected: "f64".to_string(),
299 got: "i64".to_string(),
300 };
301 let msg = format!("{err}");
302 assert!(msg.contains("value"));
303 assert!(msg.contains("f64"));
304 assert!(msg.contains("i64"));
305 }
306
307 #[test]
308 fn wal_error_corruption_message() {
309 let err = WalError::Corruption {
310 offset: 1024,
311 path: PathBuf::from("/data/wal_1.cxwl"),
312 detail: "CRC mismatch".to_string(),
313 };
314 let msg = err.to_string();
315 assert!(msg.contains("1024"));
316 assert!(msg.contains("CRC"));
317 }
318
319 #[test]
320 fn wal_error_sequence_skip_message() {
321 let err = WalError::SequenceSkip {
322 expected: 5,
323 got: 8,
324 };
325 let msg = err.to_string();
326 assert!(msg.contains('5'));
327 assert!(msg.contains('8'));
328 }
329
330 #[test]
331 fn wal_error_invalid_header_message() {
332 let err = WalError::InvalidHeader {
333 path: PathBuf::from("/data/wal.cxwl"),
334 detail: "bad magic".into(),
335 };
336 let msg = err.to_string();
337 assert!(msg.contains("bad magic"));
338 }
339
340 #[test]
341 fn wal_error_lock_poisoned_message() {
342 let err = WalError::LockPoisoned("inner mutex".into());
343 assert!(err.to_string().contains("inner mutex"));
344 }
345
346 #[test]
347 fn wal_error_io_message() {
348 let err = WalError::Io(std::io::Error::new(
349 std::io::ErrorKind::PermissionDenied,
350 "read-only",
351 ));
352 assert!(err.to_string().contains("read-only"));
353 }
354
355 #[test]
356 fn wal_error_full_message() {
357 let err = WalError::Full {
358 count: 10,
359 limit: 4,
360 };
361 let msg = err.to_string();
362 assert!(msg.contains("10"));
363 assert!(msg.contains('4'));
364 }
365
366 #[test]
367 fn wal_error_payload_too_large_message() {
368 let err = WalError::PayloadTooLarge {
369 size: 512_000_000,
370 limit: 268_435_456,
371 };
372 let msg = err.to_string();
373 assert!(msg.contains("512000000"));
374 assert!(msg.contains("268435456"));
375 }
376
377 #[test]
378 fn schema_error_lock_poisoned_message() {
379 let err = SchemaError::LockPoisoned("registry lock".into());
380 assert!(err.to_string().contains("registry lock"));
381 }
382
383 #[test]
384 fn schema_error_measurement_not_found_message() {
385 let err = SchemaError::MeasurementNotFound("disk".into());
386 assert!(err.to_string().contains("disk"));
387 }
388
389 #[test]
390 fn config_error_missing_field_message() {
391 let err = ConfigError::MissingField { field: "data_dir" };
392 assert!(err.to_string().contains("data_dir"));
393 }
394
395 #[test]
396 fn config_error_validation_message() {
397 let err = ConfigError::Validation {
398 message: "too small".into(),
399 };
400 assert!(err.to_string().contains("too small"));
401 }
402
403 #[test]
404 fn chronix_error_from_config() {
405 let config_err = ConfigError::MissingField { field: "data_dir" };
406 let err: ChronixError = config_err.into();
407 assert!(err.to_string().contains("data_dir"));
408 }
409}