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("{detail}")]
81 UnsupportedVersion {
82 path: PathBuf,
84 detail: String,
86 },
87
88 #[error("Invalid WAL header in {path}: {detail}")]
90 InvalidHeader {
91 path: PathBuf,
93 detail: String,
95 },
96
97 #[error("WAL payload too large: {size} bytes exceeds limit of {limit}")]
99 PayloadTooLarge {
100 size: usize,
102 limit: usize,
104 },
105
106 #[error("WAL lock poisoned: {0}")]
108 LockPoisoned(String),
109
110 #[error(
120 "WAL writer is poisoned after a failed fsync — the file's contents \
121 are unknown; close and reopen the database to recover"
122 )]
123 Poisoned,
124}
125
126#[derive(Debug, Error, Clone, PartialEq, Eq)]
128pub enum SchemaError {
129 #[error("{label} must not be empty")]
131 EmptyName {
132 label: String,
134 },
135
136 #[error("{label} too long: {length} bytes exceeds maximum of {max}")]
138 NameTooLong {
139 label: String,
141 length: usize,
143 max: usize,
145 },
146
147 #[error("{label} contains null byte (\\0)")]
149 NullByte {
150 label: String,
152 },
153
154 #[error("{label} contains invalid character '{ch}' in \"{value}\"")]
157 InvalidCharacter {
158 label: String,
160 ch: char,
162 value: String,
164 },
165
166 #[error("Too many tags: {count} exceeds maximum of {max}")]
168 TooManyTags {
169 count: usize,
171 max: usize,
173 },
174
175 #[error("Point must have at least one field")]
177 EmptyFields,
178
179 #[error("Too many fields: {count} exceeds maximum of {max}")]
181 TooManyFields {
182 count: usize,
184 max: usize,
186 },
187
188 #[error("Invalid value for field '{field}': {reason}")]
190 InvalidFieldValue {
191 field: String,
193 reason: String,
195 },
196
197 #[error(
199 "Type conflict for field '{field}' in measurement '{measurement}': expected {expected}, got {got}"
200 )]
201 TypeConflict {
202 measurement: String,
204 field: String,
206 expected: String,
208 got: String,
210 },
211
212 #[error(
220 "Decimal scale conflict for field '{field}' in measurement '{measurement}': \
221 the column stores {declared} fractional digit(s) and the value {value} needs {got}. \
222 Declare the field with the scale it needs before the first write, or round the value."
223 )]
224 DecimalScaleConflict {
225 measurement: String,
227 field: String,
229 declared: u8,
231 got: u8,
233 value: String,
235 },
236
237 #[error("Measurement '{0}' not found")]
239 MeasurementNotFound(String),
240
241 #[error("Schema lock poisoned: {0}")]
243 LockPoisoned(String),
244
245 #[error("Invalid name '{name}': {reason}")]
247 InvalidName {
248 name: String,
250 reason: String,
252 },
253}
254
255#[derive(Debug, Error)]
257pub enum ConfigError {
258 #[error("missing required field: {field}")]
260 MissingField {
261 field: &'static str,
263 },
264
265 #[error("invalid configuration: {message}")]
267 Validation {
268 message: String,
270 },
271
272 #[error("failed to read config file: {0}")]
274 Io(#[from] std::io::Error),
275
276 #[error("failed to parse TOML: {0}")]
278 Parse(#[from] toml::de::Error),
279}
280
281pub type Result<T> = std::result::Result<T, ChronixError>;
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn chronix_error_from_wal() {
290 let wal_err = WalError::Full { count: 5, limit: 4 };
291 let err: ChronixError = wal_err.into();
292 let msg = format!("{err}");
293 assert!(msg.contains("WAL"));
294 assert!(msg.contains('5'));
295 }
296
297 #[test]
298 fn chronix_error_from_schema() {
299 let schema_err = SchemaError::EmptyFields;
300 let err: ChronixError = schema_err.into();
301 assert!(format!("{err}").contains("field"));
302 }
303
304 #[test]
305 fn chronix_error_from_io() {
306 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
307 let err: ChronixError = io_err.into();
308 assert!(format!("{err}").contains("gone"));
309 }
310
311 #[test]
312 fn schema_error_type_conflict_message() {
313 let err = SchemaError::TypeConflict {
314 measurement: "cpu".to_string(),
315 field: "value".to_string(),
316 expected: "f64".to_string(),
317 got: "i64".to_string(),
318 };
319 let msg = format!("{err}");
320 assert!(msg.contains("value"));
321 assert!(msg.contains("f64"));
322 assert!(msg.contains("i64"));
323 }
324
325 #[test]
326 fn wal_error_corruption_message() {
327 let err = WalError::Corruption {
328 offset: 1024,
329 path: PathBuf::from("/data/wal_1.cxwl"),
330 detail: "CRC mismatch".to_string(),
331 };
332 let msg = err.to_string();
333 assert!(msg.contains("1024"));
334 assert!(msg.contains("CRC"));
335 }
336
337 #[test]
338 fn wal_error_sequence_skip_message() {
339 let err = WalError::SequenceSkip {
340 expected: 5,
341 got: 8,
342 };
343 let msg = err.to_string();
344 assert!(msg.contains('5'));
345 assert!(msg.contains('8'));
346 }
347
348 #[test]
349 fn wal_error_invalid_header_message() {
350 let err = WalError::InvalidHeader {
351 path: PathBuf::from("/data/wal.cxwl"),
352 detail: "bad magic".into(),
353 };
354 let msg = err.to_string();
355 assert!(msg.contains("bad magic"));
356 }
357
358 #[test]
359 fn wal_error_lock_poisoned_message() {
360 let err = WalError::LockPoisoned("inner mutex".into());
361 assert!(err.to_string().contains("inner mutex"));
362 }
363
364 #[test]
365 fn wal_error_io_message() {
366 let err = WalError::Io(std::io::Error::new(
367 std::io::ErrorKind::PermissionDenied,
368 "read-only",
369 ));
370 assert!(err.to_string().contains("read-only"));
371 }
372
373 #[test]
374 fn wal_error_full_message() {
375 let err = WalError::Full {
376 count: 10,
377 limit: 4,
378 };
379 let msg = err.to_string();
380 assert!(msg.contains("10"));
381 assert!(msg.contains('4'));
382 }
383
384 #[test]
385 fn wal_error_payload_too_large_message() {
386 let err = WalError::PayloadTooLarge {
387 size: 512_000_000,
388 limit: 268_435_456,
389 };
390 let msg = err.to_string();
391 assert!(msg.contains("512000000"));
392 assert!(msg.contains("268435456"));
393 }
394
395 #[test]
396 fn schema_error_lock_poisoned_message() {
397 let err = SchemaError::LockPoisoned("registry lock".into());
398 assert!(err.to_string().contains("registry lock"));
399 }
400
401 #[test]
402 fn schema_error_measurement_not_found_message() {
403 let err = SchemaError::MeasurementNotFound("disk".into());
404 assert!(err.to_string().contains("disk"));
405 }
406
407 #[test]
408 fn config_error_missing_field_message() {
409 let err = ConfigError::MissingField { field: "data_dir" };
410 assert!(err.to_string().contains("data_dir"));
411 }
412
413 #[test]
414 fn config_error_validation_message() {
415 let err = ConfigError::Validation {
416 message: "too small".into(),
417 };
418 assert!(err.to_string().contains("too small"));
419 }
420
421 #[test]
422 fn chronix_error_from_config() {
423 let config_err = ConfigError::MissingField { field: "data_dir" };
424 let err: ChronixError = config_err.into();
425 assert!(err.to_string().contains("data_dir"));
426 }
427}