use crate::core::id::{EdgeId, NodeId, VersionId};
use crate::core::temporal::Timestamp;
use std::io;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
pub trait ResultExt<T> {
fn record_error_metric(self) -> Self;
}
impl<T> ResultExt<T> for Result<T> {
#[inline]
fn record_error_metric(self) -> Self {
if let Err(ref err) = self {
err.record_metric();
}
self
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Storage error: {0}")]
Storage(StorageError),
#[error("Temporal error: {0}")]
Temporal(TemporalError),
#[error("Query error: {0}")]
Query(QueryError),
#[error("Transaction error: {0}")]
Transaction(TransactionError),
#[error("Vector error: {0}")]
Vector(VectorError),
#[error("I/O error: {0}")]
Io(io::Error),
#[error("Feature not implemented: {feature} ({reason})")]
NotImplemented {
feature: String,
reason: String,
},
#[error("{0}")]
Other(String),
}
impl Error {
#[inline]
pub fn record_metric(&self) {
#[cfg(feature = "observability")]
{
let category = match self {
Error::Storage(_) => crate::observability::ErrorCategory::Storage,
Error::Temporal(_) => crate::observability::ErrorCategory::Temporal,
Error::Query(_) => crate::observability::ErrorCategory::Query,
Error::Transaction(_) => crate::observability::ErrorCategory::Transaction,
Error::Vector(_) => crate::observability::ErrorCategory::Vector,
Error::Io(_) => crate::observability::ErrorCategory::Io,
Error::NotImplemented { .. } | Error::Other(_) => {
crate::observability::ErrorCategory::Other
}
};
crate::observability::record_error(category);
}
}
pub fn other<S: Into<String>>(msg: S) -> Self {
Error::Other(msg.into())
}
pub fn not_implemented<S: Into<String>, R: Into<String>>(feature: S, reason: R) -> Self {
Error::NotImplemented {
feature: feature.into(),
reason: reason.into(),
}
}
}
impl From<StorageError> for Error {
fn from(e: StorageError) -> Self {
Error::Storage(e)
}
}
impl From<TemporalError> for Error {
fn from(e: TemporalError) -> Self {
Error::Temporal(e)
}
}
impl From<QueryError> for Error {
fn from(e: QueryError) -> Self {
Error::Query(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl From<TransactionError> for Error {
fn from(e: TransactionError) -> Self {
Error::Transaction(e)
}
}
impl From<VectorError> for Error {
fn from(e: VectorError) -> Self {
Error::Vector(e)
}
}
impl From<crate::config::ConfigError> for Error {
fn from(e: crate::config::ConfigError) -> Self {
Error::Other(e.to_string())
}
}
#[cfg(feature = "sql")]
impl From<crate::sql::SqlError> for Error {
fn from(e: crate::sql::SqlError) -> Self {
let query_error = match e {
crate::sql::SqlError::ParseError(message) => QueryError::SyntaxError { message },
crate::sql::SqlError::UnsupportedFeature(feature) => {
QueryError::UnsupportedFeature { feature }
}
crate::sql::SqlError::InvalidTable(table) => QueryError::InvalidParameter {
parameter: "table".to_string(),
reason: format!("invalid table '{}': expected 'nodes' or 'edges'", table),
},
crate::sql::SqlError::InvalidColumn(column) => QueryError::InvalidParameter {
parameter: "column".to_string(),
reason: format!("invalid column reference: {}", column),
},
crate::sql::SqlError::InvalidTemporalClause(clause) => QueryError::InvalidParameter {
parameter: "temporal_clause".to_string(),
reason: format!("invalid temporal clause: {}", clause),
},
crate::sql::SqlError::InvalidTimestamp(timestamp) => QueryError::InvalidParameter {
parameter: "timestamp".to_string(),
reason: format!("invalid timestamp: {}", timestamp),
},
crate::sql::SqlError::MissingClause(clause) => QueryError::InvalidParameter {
parameter: "clause".to_string(),
reason: format!("missing required clause: {}", clause),
},
crate::sql::SqlError::TypeError(actual) => QueryError::TypeMismatch {
expected: "compatible SQL type".to_string(),
actual,
},
crate::sql::SqlError::ParameterError(reason) => QueryError::InvalidParameter {
parameter: "parameter".to_string(),
reason,
},
};
Error::Query(query_error)
}
}
#[cfg(feature = "cypher")]
impl From<crate::cypher::CypherError> for Error {
fn from(e: crate::cypher::CypherError) -> Self {
let query_error = match e {
crate::cypher::CypherError::LexError { message, .. } => {
QueryError::SyntaxError { message }
}
crate::cypher::CypherError::ParseError { message, .. } => {
QueryError::SyntaxError { message }
}
crate::cypher::CypherError::UnsupportedFeature(feature) => {
QueryError::UnsupportedFeature { feature }
}
crate::cypher::CypherError::InvalidTemporalClause(clause) => {
QueryError::InvalidParameter {
parameter: "temporal_clause".to_string(),
reason: format!("invalid temporal clause: {}", clause),
}
}
crate::cypher::CypherError::InvalidTimestamp(timestamp) => {
QueryError::InvalidParameter {
parameter: "timestamp".to_string(),
reason: format!("invalid timestamp: {}", timestamp),
}
}
crate::cypher::CypherError::ParameterError(reason) => QueryError::InvalidParameter {
parameter: "parameter".to_string(),
reason,
},
crate::cypher::CypherError::SemanticError(message) => {
QueryError::SyntaxError { message }
}
};
Error::Query(query_error)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PersistenceErrorKind {
Corrupted,
InternerMismatch,
UnsupportedVersion,
MissingIndex,
InvalidMagic,
SizeLimitExceeded,
Io,
Serialization,
Unknown,
}
impl std::fmt::Display for PersistenceErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let kind = match self {
PersistenceErrorKind::Corrupted => "corrupted",
PersistenceErrorKind::InternerMismatch => "interner_mismatch",
PersistenceErrorKind::UnsupportedVersion => "unsupported_version",
PersistenceErrorKind::MissingIndex => "missing_index",
PersistenceErrorKind::InvalidMagic => "invalid_magic",
PersistenceErrorKind::SizeLimitExceeded => "size_limit_exceeded",
PersistenceErrorKind::Io => "io",
PersistenceErrorKind::Serialization => "serialization",
PersistenceErrorKind::Unknown => "unknown",
};
write!(f, "{}", kind)
}
}
impl From<&crate::storage::index_persistence::IndexPersistenceError> for PersistenceErrorKind {
fn from(e: &crate::storage::index_persistence::IndexPersistenceError) -> Self {
match e {
crate::storage::index_persistence::IndexPersistenceError::Corrupted { .. } => {
PersistenceErrorKind::Corrupted
}
crate::storage::index_persistence::IndexPersistenceError::InternerMismatch {
..
} => PersistenceErrorKind::InternerMismatch,
crate::storage::index_persistence::IndexPersistenceError::UnsupportedVersion {
..
} => PersistenceErrorKind::UnsupportedVersion,
crate::storage::index_persistence::IndexPersistenceError::MissingIndex { .. } => {
PersistenceErrorKind::MissingIndex
}
crate::storage::index_persistence::IndexPersistenceError::InvalidMagic { .. } => {
PersistenceErrorKind::InvalidMagic
}
crate::storage::index_persistence::IndexPersistenceError::SizeLimitExceeded {
..
} => PersistenceErrorKind::SizeLimitExceeded,
crate::storage::index_persistence::IndexPersistenceError::Io(_) => {
PersistenceErrorKind::Io
}
crate::storage::index_persistence::IndexPersistenceError::Serialization(_) => {
PersistenceErrorKind::Serialization
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum StorageError {
#[error("Node not found: {0}")]
NodeNotFound(NodeId),
#[error("Edge not found: {0}")]
EdgeNotFound(EdgeId),
#[error("Version not found: {0}")]
VersionNotFound(VersionId),
#[error("Duplicate {kind} ID: {id}")]
DuplicateId {
id: String,
kind: String,
},
#[error("Invalid property '{key}': {reason}")]
InvalidProperty {
key: String,
reason: String,
},
#[error("Inconsistent database state: {reason}")]
InconsistentState {
reason: String,
},
#[error("Write-ahead log error: {reason}")]
WalError {
reason: String,
},
#[error("Checkpoint error: {reason}")]
CheckpointError {
reason: String,
},
#[error("I/O error: {0}")]
IoError(String),
#[error("Corrupted data: {0}")]
CorruptedData(String),
#[error("Persistence error [{kind}]: {message}")]
PersistenceErrorWithKind {
kind: PersistenceErrorKind,
message: String,
},
#[error("Persistence error: {0}")]
PersistenceError(String),
#[error("Property not found: {0}")]
PropertyNotFound(String),
#[error(
"Invalid {id_type} ID {id}: exceeds maximum allowed value {max} (reserved range for internal use)",
max = crate::core::id::MAX_VALID_ID
)]
InvalidId {
id: u64,
id_type: &'static str,
},
#[error("Capacity exceeded for {resource}: current={current}, limit={limit} (DoS protection)")]
CapacityExceeded {
resource: String,
current: usize,
limit: usize,
},
#[error("Lock poisoned for {resource}: a thread panicked while holding the lock")]
LockPoisoned {
resource: String,
},
#[error("Encryption error: {0}")]
Encryption(String),
#[error("Key provider error: {0}")]
KeyProvider(String),
}
impl StorageError {
pub fn io_error<S: Into<String>>(msg: S) -> Self {
StorageError::IoError(msg.into())
}
pub fn corruption<S: Into<String>>(msg: S) -> Self {
StorageError::CorruptedData(msg.into())
}
pub fn persistence<S: Into<String>>(msg: S) -> Self {
StorageError::PersistenceError(msg.into())
}
pub fn persistence_with_kind<S: Into<String>>(kind: PersistenceErrorKind, msg: S) -> Self {
StorageError::PersistenceErrorWithKind {
kind,
message: msg.into(),
}
}
}
impl From<crate::storage::index_persistence::IndexPersistenceError> for StorageError {
fn from(e: crate::storage::index_persistence::IndexPersistenceError) -> Self {
let kind = PersistenceErrorKind::from(&e);
StorageError::PersistenceErrorWithKind {
kind,
message: e.to_string(),
}
}
}
impl From<crate::encryption::EncryptionError> for StorageError {
fn from(e: crate::encryption::EncryptionError) -> Self {
StorageError::Encryption(e.to_string())
}
}
impl From<crate::encryption::KeyProviderError> for StorageError {
fn from(e: crate::encryption::KeyProviderError) -> Self {
StorageError::KeyProvider(e.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TemporalError {
#[error("Transaction time must be monotonic: previous={previous}, attempted={attempted}")]
NonMonotonicTransactionTime {
previous: Timestamp,
attempted: Timestamp,
},
#[error("Invalid time range: start={start} > end={end}")]
InvalidTimeRange {
start: Timestamp,
end: Timestamp,
},
#[error("Invalid timestamp {timestamp}: {reason}")]
InvalidTimestamp {
timestamp: Timestamp,
reason: String,
},
#[error("Temporal paradox: {reason}")]
TemporalParadox {
reason: String,
},
#[error("Valid time ({valid_time}) precedes creation time ({creation_time})")]
ValidTimeBeforeCreation {
valid_time: Timestamp,
creation_time: Timestamp,
},
#[error("Version {version_id} is already closed")]
VersionAlreadyClosed {
version_id: VersionId,
},
#[error("Corrupted version chain for {entity_id}: {reason}")]
CorruptedVersionChain {
entity_id: String,
reason: String,
},
#[error("Missing anchor in version chain for {entity_id}")]
MissingAnchor {
entity_id: String,
},
#[error(
"Maximum recursion depth ({max_depth}) exceeded while reconstructing {entity_id}: possible corrupted version chain or cycle"
)]
MaxDepthExceeded {
max_depth: usize,
entity_id: String,
},
#[error(
"Node {node_id} did not exist at valid_time={valid_time}, transaction_time={transaction_time}"
)]
NodeNotFoundAtTime {
node_id: NodeId,
valid_time: Timestamp,
transaction_time: Timestamp,
},
#[error("Version {0} not found")]
VersionNotFound(VersionId),
#[error(
"HLC logical counter overflow at wallclock={wallclock}: current_logical={current_logical} would exceed u32::MAX"
)]
LogicalCounterOverflow {
wallclock: i64,
current_logical: u32,
},
#[error(
"valid_from {valid_from} is too far in future (current: {current_time}, max offset: {max_future_offset_us}\u{b5}s)"
)]
ValidTimeTooFarInFuture {
valid_from: Timestamp,
current_time: Timestamp,
max_future_offset_us: i64,
},
#[error(
"valid_from {valid_from} is before entity creation time {entity_creation_time} for {entity_id}"
)]
ValidTimeBeforeEntityCreation {
valid_from: Timestamp,
entity_creation_time: Timestamp,
entity_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum QueryError {
#[error("Query syntax error: {message}")]
SyntaxError {
message: String,
},
#[error("Unsupported query feature: {feature}")]
UnsupportedFeature {
feature: String,
},
#[error("Invalid query parameter '{parameter}': {reason}")]
InvalidParameter {
parameter: String,
reason: String,
},
#[error("Query timeout after {duration_ms} ms")]
Timeout {
duration_ms: u64,
},
#[error("Query result limit ({limit}) exceeded")]
LimitExceeded {
limit: usize,
},
#[error("Invalid graph traversal: {reason}")]
InvalidTraversal {
reason: String,
},
#[error("Type mismatch: expected {expected}, got {actual}")]
TypeMismatch {
expected: String,
actual: String,
},
#[error("Query execution error: {message}")]
ExecutionError {
message: String,
},
#[error("{}", format_index_not_found(index_type, property_name, hint))]
IndexNotFound {
index_type: String,
property_name: String,
hint: Option<String>,
},
}
fn format_index_not_found(index_type: &str, property_name: &str, hint: &Option<String>) -> String {
let mut msg = format!(
"Query requires {} index on '{}' property which is not enabled",
index_type, property_name
);
if let Some(hint_msg) = hint {
msg.push_str(&format!(". Hint: {}", hint_msg));
}
msg
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum TransactionError {
#[error("Transaction in invalid state: expected {expected}, got {current}")]
InvalidState {
current: String,
expected: String,
},
#[error("Transaction {tx_id} has already been committed")]
AlreadyCommitted {
tx_id: u64,
},
#[error("Transaction {tx_id} has been aborted")]
Aborted {
tx_id: u64,
},
#[error("Write conflict on {entity_id}: {reason}")]
WriteConflict {
entity_id: String,
reason: String,
},
#[error("Serialization failure on {entity}: {reason}")]
SerializationFailure {
entity: String,
reason: String,
},
#[error("Transaction validation failed: {reason}")]
ValidationFailed {
reason: String,
},
#[error("Transaction commit failed: {reason}")]
CommitFailed {
reason: String,
},
#[error("Transaction rollback failed: {reason}")]
RollbackFailed {
reason: String,
},
#[error("Lock poisoned for {resource}: a thread panicked while holding the lock")]
LockPoisoned {
resource: String,
},
#[error("{}", format_clock_skew(*wallclock, *previous, *drift_us, *max_allowed))]
ClockSkew {
wallclock: i64,
previous: i64,
drift_us: i64,
max_allowed: i64,
},
}
fn format_clock_skew(wallclock: i64, previous: i64, drift_us: i64, max_allowed: i64) -> String {
let direction = if drift_us < 0 { "backward" } else { "forward" };
format!(
"Clock skew too large: system clock jumped {} by {} \u{b5}s (max allowed: {} \u{b5}s). \
Wallclock: {}, Previous: {}. This may indicate NTP adjustment or manual clock change.",
direction,
drift_us.abs(),
max_allowed.abs(),
wallclock,
previous
)
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum VectorError {
#[error("Vector dimension mismatch: expected {expected}, got {actual}")]
DimensionMismatch {
expected: usize,
actual: usize,
},
#[error("Vector contains {count} NaN value(s)")]
ContainsNaN {
count: usize,
},
#[error("Vector contains {count} infinity value(s)")]
ContainsInfinity {
count: usize,
},
#[error("Vector dimension {dimension} exceeds maximum allowed {max_allowed}")]
DimensionTooLarge {
dimension: usize,
max_allowed: usize,
},
#[error("Vector index {index} out of bounds for length {len}")]
IndexOutOfBounds {
index: usize,
len: usize,
},
#[error("Vector not found: {id}")]
NotFound {
id: String,
},
#[error("Invalid vector: {reason}")]
InvalidVector {
reason: String,
},
#[error("Invalid sparse vector: {reason}")]
InvalidSparseVector {
reason: String,
},
#[error("Vector index error: {0}")]
IndexError(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_storage_error_display() {
let err = StorageError::NodeNotFound(NodeId::new(42).unwrap());
assert_eq!(format!("{}", err), "Node not found: Node(42)");
let err = StorageError::InvalidProperty {
key: "age".to_string(),
reason: "must be positive".to_string(),
};
assert!(format!("{}", err).contains("age"));
assert!(format!("{}", err).contains("must be positive"));
}
#[test]
fn test_temporal_error_display() {
let err = TemporalError::NonMonotonicTransactionTime {
previous: 100.into(),
attempted: 50.into(),
};
assert!(format!("{}", err).contains("monotonic"));
assert!(format!("{}", err).contains("100"));
assert!(format!("{}", err).contains("50"));
let err = TemporalError::TemporalParadox {
reason: "deleted before created".to_string(),
};
assert!(format!("{}", err).contains("paradox"));
}
#[test]
fn test_query_error_display() {
let err = QueryError::Timeout { duration_ms: 5000 };
assert_eq!(format!("{}", err), "Query timeout after 5000 ms");
let err = QueryError::TypeMismatch {
expected: "int".to_string(),
actual: "string".to_string(),
};
assert!(format!("{}", err).contains("int"));
assert!(format!("{}", err).contains("string"));
}
#[test]
fn test_error_conversions() {
let storage_err = StorageError::NodeNotFound(NodeId::new(1).unwrap());
let err: Error = storage_err.clone().into();
assert!(matches!(err, Error::Storage(_)));
let temporal_err = TemporalError::TemporalParadox {
reason: "test".to_string(),
};
let err: Error = temporal_err.clone().into();
assert!(matches!(err, Error::Temporal(_)));
let query_err = QueryError::Timeout { duration_ms: 1000 };
let err: Error = query_err.clone().into();
assert!(matches!(err, Error::Query(_)));
}
#[test]
fn test_error_display() {
let err = Error::Storage(StorageError::NodeNotFound(NodeId::new(42).unwrap()));
assert!(format!("{}", err).contains("Storage error"));
assert!(format!("{}", err).contains("Node not found"));
let err = Error::Other("custom error".to_string());
assert_eq!(format!("{}", err), "custom error");
}
#[test]
fn test_result_type() {
fn returns_result() -> Result<i32> {
Ok(42)
}
fn returns_error() -> Result<i32> {
Err(StorageError::NodeNotFound(NodeId::new(1).unwrap()).into())
}
assert_eq!(returns_result().unwrap(), 42);
assert!(returns_error().is_err());
}
#[test]
fn test_all_storage_error_variants() {
let err = StorageError::EdgeNotFound(EdgeId::new(1).unwrap());
assert!(format!("{}", err).contains("Edge not found"));
let err = StorageError::VersionNotFound(VersionId::new(1).unwrap());
assert!(format!("{}", err).contains("Version not found"));
let err = StorageError::DuplicateId {
id: "42".to_string(),
kind: "node".to_string(),
};
assert!(format!("{}", err).contains("Duplicate"));
assert!(format!("{}", err).contains("node"));
let err = StorageError::InconsistentState {
reason: "test".to_string(),
};
assert!(format!("{}", err).contains("Inconsistent"));
let err = StorageError::WalError {
reason: "flush failed".to_string(),
};
assert!(format!("{}", err).contains("Write-ahead log"));
assert!(format!("{}", err).contains("flush failed"));
let err = StorageError::CheckpointError {
reason: "save failed".to_string(),
};
assert!(format!("{}", err).contains("Checkpoint"));
assert!(format!("{}", err).contains("save failed"));
let err = StorageError::IoError("file not found".to_string());
assert!(format!("{}", err).contains("I/O error"));
assert!(format!("{}", err).contains("file not found"));
let err = StorageError::CorruptedData("bad checksum".to_string());
assert!(format!("{}", err).contains("Corrupted data"));
assert!(format!("{}", err).contains("bad checksum"));
let err = StorageError::PersistenceError("failed to save index".to_string());
assert!(format!("{}", err).contains("Persistence error"));
assert!(format!("{}", err).contains("failed to save index"));
let err = StorageError::PersistenceErrorWithKind {
kind: PersistenceErrorKind::InvalidMagic,
message: "bad header".to_string(),
};
assert!(format!("{}", err).contains("invalid_magic"));
assert!(format!("{}", err).contains("bad header"));
let err = StorageError::PropertyNotFound("embedding".to_string());
assert_eq!(format!("{}", err), "Property not found: embedding");
}
#[test]
fn test_all_temporal_error_variants() {
let err = TemporalError::InvalidTimeRange {
start: 100.into(),
end: 50.into(),
};
assert!(format!("{}", err).contains("Invalid time range"));
assert!(format!("{}", err).contains("100"));
assert!(format!("{}", err).contains("50"));
let err = TemporalError::ValidTimeBeforeCreation {
valid_time: 50.into(),
creation_time: 100.into(),
};
assert!(format!("{}", err).contains("precedes creation"));
let err = TemporalError::VersionAlreadyClosed {
version_id: VersionId::new(42).unwrap(),
};
assert!(format!("{}", err).contains("already closed"));
assert!(format!("{}", err).contains("42"));
let err = TemporalError::CorruptedVersionChain {
entity_id: "node-123".to_string(),
reason: "missing delta".to_string(),
};
assert!(format!("{}", err).contains("Corrupted version chain"));
assert!(format!("{}", err).contains("node-123"));
assert!(format!("{}", err).contains("missing delta"));
let err = TemporalError::MissingAnchor {
entity_id: "edge-456".to_string(),
};
assert!(format!("{}", err).contains("Missing anchor"));
assert!(format!("{}", err).contains("edge-456"));
let err = TemporalError::MaxDepthExceeded {
max_depth: 100,
entity_id: "node-789".to_string(),
};
assert!(format!("{}", err).contains("Maximum recursion depth"));
assert!(format!("{}", err).contains("100"));
assert!(format!("{}", err).contains("node-789"));
assert!(format!("{}", err).contains("corrupted version chain"));
}
#[test]
fn test_all_query_error_variants() {
let err = QueryError::SyntaxError {
message: "unexpected token".to_string(),
};
assert!(format!("{}", err).contains("syntax error"));
assert!(format!("{}", err).contains("unexpected token"));
let err = QueryError::InvalidParameter {
parameter: "limit".to_string(),
reason: "must be positive".to_string(),
};
assert!(format!("{}", err).contains("Invalid query parameter"));
assert!(format!("{}", err).contains("limit"));
assert!(format!("{}", err).contains("must be positive"));
let err = QueryError::LimitExceeded { limit: 1000 };
assert!(format!("{}", err).contains("limit"));
assert!(format!("{}", err).contains("1000"));
assert!(format!("{}", err).contains("exceeded"));
let err = QueryError::InvalidTraversal {
reason: "edge doesn't connect nodes".to_string(),
};
assert!(format!("{}", err).contains("Invalid graph traversal"));
assert!(format!("{}", err).contains("edge doesn't connect nodes"));
let err = QueryError::IndexNotFound {
index_type: "vector".to_string(),
property_name: "embedding".to_string(),
hint: None,
};
let display = format!("{}", err);
assert!(display.contains("vector index"));
assert!(display.contains("embedding"));
assert!(display.contains("not enabled"));
let err = QueryError::IndexNotFound {
index_type: "vector".to_string(),
property_name: "embedding".to_string(),
hint: Some("Call db.vector_index(\"embedding\").hnsw(...).enable() first".to_string()),
};
let display = format!("{}", err);
assert!(display.contains("vector index"));
assert!(display.contains("embedding"));
assert!(display.contains("vector_index(\"embedding\").hnsw"));
}
#[test]
fn test_index_persistence_error_conversion_preserves_kind() {
let io_err = crate::storage::index_persistence::IndexPersistenceError::Serialization(
"decode failed".to_string(),
);
let converted: StorageError = io_err.into();
match converted {
StorageError::PersistenceErrorWithKind { kind, message } => {
assert_eq!(kind, PersistenceErrorKind::Serialization);
assert!(message.contains("decode failed"));
}
other => panic!("expected PersistenceErrorWithKind, got {other:?}"),
}
}
#[cfg(feature = "sql")]
#[test]
fn test_sql_error_conversion_preserves_semantics() {
let converted: Error = crate::sql::SqlError::UnsupportedFeature("MATCH".to_string()).into();
assert!(matches!(
converted,
Error::Query(QueryError::UnsupportedFeature { feature }) if feature == "MATCH"
));
let converted: Error = crate::sql::SqlError::InvalidColumn("foo".to_string()).into();
assert!(matches!(
converted,
Error::Query(QueryError::InvalidParameter { parameter, .. }) if parameter == "column"
));
}
#[cfg(feature = "observability")]
#[test]
#[serial_test::serial]
fn test_result_ext_records_storage_metric_once() {
crate::observability::METRICS.reset();
let err: Result<()> = Err(StorageError::NodeNotFound(NodeId::new(1).unwrap()).into());
let snapshot = crate::observability::METRICS.snapshot();
assert_eq!(snapshot.error_storage_total, 0);
let _ = err.record_error_metric();
let snapshot = crate::observability::METRICS.snapshot();
assert_eq!(snapshot.error_storage_total, 1);
}
#[test]
fn test_all_transaction_error_variants() {
let err = TransactionError::InvalidState {
current: "Committed".to_string(),
expected: "Active".to_string(),
};
assert!(format!("{}", err).contains("invalid state"));
assert!(format!("{}", err).contains("Committed"));
assert!(format!("{}", err).contains("Active"));
let err = TransactionError::AlreadyCommitted { tx_id: 123 };
assert!(format!("{}", err).contains("already been committed"));
assert!(format!("{}", err).contains("123"));
let err = TransactionError::Aborted { tx_id: 456 };
assert!(format!("{}", err).contains("aborted"));
assert!(format!("{}", err).contains("456"));
let err = TransactionError::WriteConflict {
entity_id: "node-789".to_string(),
reason: "concurrent modification".to_string(),
};
assert!(format!("{}", err).contains("Write conflict"));
assert!(format!("{}", err).contains("node-789"));
assert!(format!("{}", err).contains("concurrent modification"));
let err = TransactionError::ValidationFailed {
reason: "referential integrity violated".to_string(),
};
assert!(format!("{}", err).contains("validation failed"));
assert!(format!("{}", err).contains("referential integrity violated"));
let err = TransactionError::CommitFailed {
reason: "WAL write failed".to_string(),
};
assert!(format!("{}", err).contains("commit failed"));
assert!(format!("{}", err).contains("WAL write failed"));
let err = TransactionError::RollbackFailed {
reason: "cleanup failed".to_string(),
};
assert!(format!("{}", err).contains("rollback failed"));
assert!(format!("{}", err).contains("cleanup failed"));
}
#[test]
fn test_transaction_error_conversions() {
let err = TransactionError::ValidationFailed {
reason: "test".to_string(),
};
let converted: Error = err.into();
assert!(matches!(converted, Error::Transaction(_)));
}
#[test]
fn test_vector_error_display() {
let err = VectorError::DimensionMismatch {
expected: 128,
actual: 256,
};
assert!(format!("{}", err).contains("dimension mismatch"));
assert!(format!("{}", err).contains("128"));
assert!(format!("{}", err).contains("256"));
let err = VectorError::ContainsNaN { count: 3 };
assert!(format!("{}", err).contains("NaN"));
assert!(format!("{}", err).contains("3"));
let err = VectorError::ContainsInfinity { count: 2 };
assert!(format!("{}", err).contains("infinity"));
assert!(format!("{}", err).contains("2"));
let err = VectorError::DimensionTooLarge {
dimension: 200_000,
max_allowed: 100_000,
};
assert!(format!("{}", err).contains("200000"));
assert!(format!("{}", err).contains("100000"));
let err = VectorError::IndexOutOfBounds { index: 10, len: 5 };
assert_eq!(
format!("{}", err),
"Vector index 10 out of bounds for length 5"
);
let err = VectorError::NotFound {
id: "vec_12345".to_string(),
};
assert_eq!(format!("{}", err), "Vector not found: vec_12345");
let err = VectorError::InvalidVector {
reason: "empty vector not allowed".to_string(),
};
assert_eq!(
format!("{}", err),
"Invalid vector: empty vector not allowed"
);
}
#[test]
fn test_vector_error_conversions() {
let err = VectorError::ContainsNaN { count: 1 };
let converted: Error = err.into();
assert!(matches!(converted, Error::Vector(_)));
assert!(format!("{}", converted).contains("Vector error"));
}
#[test]
fn test_temporal_error_node_not_found_at_time() {
let node_id = NodeId::new(42).unwrap();
let err = TemporalError::NodeNotFoundAtTime {
node_id,
valid_time: 1000.into(),
transaction_time: 2000.into(),
};
let display = format!("{}", err);
assert!(display.contains("Node"));
assert!(display.contains("42"));
assert!(display.contains("1000"));
assert!(display.contains("2000"));
assert!(display.contains("did not exist"));
}
#[test]
fn test_temporal_error_version_not_found() {
let version_id = VersionId::new(123).unwrap();
let err = TemporalError::VersionNotFound(version_id);
let display = format!("{}", err);
assert!(display.contains("Version"));
assert!(display.contains("123"));
assert!(display.contains("not found"));
}
#[test]
fn test_temporal_error_node_not_found_at_time_conversion() {
let node_id = NodeId::new(1).unwrap();
let err = TemporalError::NodeNotFoundAtTime {
node_id,
valid_time: 1000.into(),
transaction_time: 2000.into(),
};
let converted: Error = err.into();
assert!(matches!(converted, Error::Temporal(_)));
let display = format!("{}", converted);
assert!(display.contains("Temporal error"));
}
#[test]
fn test_temporal_error_version_not_found_conversion() {
let version_id = VersionId::new(123).unwrap();
let err = TemporalError::VersionNotFound(version_id);
let converted: Error = err.into();
assert!(matches!(converted, Error::Temporal(_)));
let display = format!("{}", converted);
assert!(display.contains("Temporal error"));
}
#[test]
fn test_valid_time_too_far_in_future_error_display() {
use crate::core::hlc::HybridTimestamp;
let err = TemporalError::ValidTimeTooFarInFuture {
valid_from: HybridTimestamp::new(2000, 0).unwrap(),
current_time: HybridTimestamp::new(1000, 0).unwrap(),
max_future_offset_us: 31_536_000_000_000, };
let msg = format!("{}", err);
assert!(msg.contains("too far in future"));
assert!(msg.contains("2000"));
assert!(msg.contains("1000"));
assert!(msg.contains("31536000000000"));
}
#[test]
fn test_valid_time_before_entity_creation_error_display() {
use crate::core::hlc::HybridTimestamp;
let err = TemporalError::ValidTimeBeforeEntityCreation {
valid_from: HybridTimestamp::new(100, 0).unwrap(),
entity_creation_time: HybridTimestamp::new(500, 0).unwrap(),
entity_id: "node:123".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("before entity creation"));
assert!(msg.contains("100"));
assert!(msg.contains("500"));
assert!(msg.contains("node:123"));
}
#[test]
fn test_clock_skew_display_negative_drift_is_backward() {
let err = TransactionError::ClockSkew {
wallclock: 1_000,
previous: 1_100,
drift_us: -100,
max_allowed: 50,
};
let msg = err.to_string();
assert!(msg.contains("Clock skew too large"));
assert!(msg.contains("backward"));
assert!(msg.contains("100"));
assert!(msg.contains("max allowed: 50"));
assert!(msg.contains("Wallclock: 1000"));
assert!(msg.contains("Previous: 1100"));
}
#[test]
fn test_clock_skew_display_zero_drift_is_forward() {
let err = TransactionError::ClockSkew {
wallclock: 500,
previous: 500,
drift_us: 0,
max_allowed: 25,
};
let msg = err.to_string();
assert!(msg.contains("Clock skew too large"));
assert!(msg.contains("forward"));
assert!(!msg.contains("backward"));
assert!(msg.contains("by 0"));
}
}