use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ChronixError {
#[error("WAL error: {0}")]
Wal(#[from] WalError),
#[error("Schema error: {0}")]
Schema(#[from] SchemaError),
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
#[error("Capacity exceeded: {0}")]
Capacity(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Error)]
pub enum WalError {
#[error("WAL I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("WAL corruption at offset {offset} in {path}: {detail}")]
Corruption {
offset: u64,
path: PathBuf,
detail: String,
},
#[error("WAL sequence skip: expected {expected}, got {got}")]
SequenceSkip {
expected: u64,
got: u64,
},
#[error("WAL full: {count} unflushed files exceed limit of {limit}")]
Full {
count: usize,
limit: usize,
},
#[error("{detail}")]
UnsupportedVersion {
path: PathBuf,
detail: String,
},
#[error("Invalid WAL header in {path}: {detail}")]
InvalidHeader {
path: PathBuf,
detail: String,
},
#[error("WAL payload too large: {size} bytes exceeds limit of {limit}")]
PayloadTooLarge {
size: usize,
limit: usize,
},
#[error("WAL lock poisoned: {0}")]
LockPoisoned(String),
#[error(
"WAL writer is poisoned after a failed fsync — the file's contents \
are unknown; close and reopen the database to recover"
)]
Poisoned,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum SchemaError {
#[error("{label} must not be empty")]
EmptyName {
label: String,
},
#[error("{label} too long: {length} bytes exceeds maximum of {max}")]
NameTooLong {
label: String,
length: usize,
max: usize,
},
#[error("{label} contains null byte (\\0)")]
NullByte {
label: String,
},
#[error("{label} contains invalid character '{ch}' in \"{value}\"")]
InvalidCharacter {
label: String,
ch: char,
value: String,
},
#[error("Too many tags: {count} exceeds maximum of {max}")]
TooManyTags {
count: usize,
max: usize,
},
#[error("Point must have at least one field")]
EmptyFields,
#[error("Too many fields: {count} exceeds maximum of {max}")]
TooManyFields {
count: usize,
max: usize,
},
#[error("Invalid value for field '{field}': {reason}")]
InvalidFieldValue {
field: String,
reason: String,
},
#[error(
"Type conflict for field '{field}' in measurement '{measurement}': expected {expected}, got {got}"
)]
TypeConflict {
measurement: String,
field: String,
expected: String,
got: String,
},
#[error(
"Decimal scale conflict for field '{field}' in measurement '{measurement}': \
the column stores {declared} fractional digit(s) and the value {value} needs {got}. \
Declare the field with the scale it needs before the first write, or round the value."
)]
DecimalScaleConflict {
measurement: String,
field: String,
declared: u8,
got: u8,
value: String,
},
#[error("Measurement '{0}' not found")]
MeasurementNotFound(String),
#[error("Schema lock poisoned: {0}")]
LockPoisoned(String),
#[error("Invalid name '{name}': {reason}")]
InvalidName {
name: String,
reason: String,
},
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("missing required field: {field}")]
MissingField {
field: &'static str,
},
#[error("invalid configuration: {message}")]
Validation {
message: String,
},
#[error("failed to read config file: {0}")]
Io(#[from] std::io::Error),
#[error("failed to parse TOML: {0}")]
Parse(#[from] toml::de::Error),
}
pub type Result<T> = std::result::Result<T, ChronixError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chronix_error_from_wal() {
let wal_err = WalError::Full { count: 5, limit: 4 };
let err: ChronixError = wal_err.into();
let msg = format!("{err}");
assert!(msg.contains("WAL"));
assert!(msg.contains('5'));
}
#[test]
fn chronix_error_from_schema() {
let schema_err = SchemaError::EmptyFields;
let err: ChronixError = schema_err.into();
assert!(format!("{err}").contains("field"));
}
#[test]
fn chronix_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
let err: ChronixError = io_err.into();
assert!(format!("{err}").contains("gone"));
}
#[test]
fn schema_error_type_conflict_message() {
let err = SchemaError::TypeConflict {
measurement: "cpu".to_string(),
field: "value".to_string(),
expected: "f64".to_string(),
got: "i64".to_string(),
};
let msg = format!("{err}");
assert!(msg.contains("value"));
assert!(msg.contains("f64"));
assert!(msg.contains("i64"));
}
#[test]
fn wal_error_corruption_message() {
let err = WalError::Corruption {
offset: 1024,
path: PathBuf::from("/data/wal_1.cxwl"),
detail: "CRC mismatch".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("1024"));
assert!(msg.contains("CRC"));
}
#[test]
fn wal_error_sequence_skip_message() {
let err = WalError::SequenceSkip {
expected: 5,
got: 8,
};
let msg = err.to_string();
assert!(msg.contains('5'));
assert!(msg.contains('8'));
}
#[test]
fn wal_error_invalid_header_message() {
let err = WalError::InvalidHeader {
path: PathBuf::from("/data/wal.cxwl"),
detail: "bad magic".into(),
};
let msg = err.to_string();
assert!(msg.contains("bad magic"));
}
#[test]
fn wal_error_lock_poisoned_message() {
let err = WalError::LockPoisoned("inner mutex".into());
assert!(err.to_string().contains("inner mutex"));
}
#[test]
fn wal_error_io_message() {
let err = WalError::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"read-only",
));
assert!(err.to_string().contains("read-only"));
}
#[test]
fn wal_error_full_message() {
let err = WalError::Full {
count: 10,
limit: 4,
};
let msg = err.to_string();
assert!(msg.contains("10"));
assert!(msg.contains('4'));
}
#[test]
fn wal_error_payload_too_large_message() {
let err = WalError::PayloadTooLarge {
size: 512_000_000,
limit: 268_435_456,
};
let msg = err.to_string();
assert!(msg.contains("512000000"));
assert!(msg.contains("268435456"));
}
#[test]
fn schema_error_lock_poisoned_message() {
let err = SchemaError::LockPoisoned("registry lock".into());
assert!(err.to_string().contains("registry lock"));
}
#[test]
fn schema_error_measurement_not_found_message() {
let err = SchemaError::MeasurementNotFound("disk".into());
assert!(err.to_string().contains("disk"));
}
#[test]
fn config_error_missing_field_message() {
let err = ConfigError::MissingField { field: "data_dir" };
assert!(err.to_string().contains("data_dir"));
}
#[test]
fn config_error_validation_message() {
let err = ConfigError::Validation {
message: "too small".into(),
};
assert!(err.to_string().contains("too small"));
}
#[test]
fn chronix_error_from_config() {
let config_err = ConfigError::MissingField { field: "data_dir" };
let err: ChronixError = config_err.into();
assert!(err.to_string().contains("data_dir"));
}
}