use std::ops::Range;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::query::{ResultRow, SeqNum};
#[derive(Clone, Debug, PartialEq, Error, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MeshError {
#[error("no holder for chain {origin:#x} seq range {requested:?} (available: {available:?})")]
HistoricalRangeUnavailable {
origin: u64,
requested: Range<SeqNum>,
available: Vec<Range<SeqNum>>,
},
#[error("lineage walk for {origin:#x} exceeded max_depth={depth}")]
LineageMaxDepthExceeded {
origin: u64,
depth: u32,
},
#[error("lineage cycle detected starting at {origin:#x}; cycle length={}", cycle.len())]
LineageCycleDetected {
origin: u64,
cycle: Vec<u64>,
},
#[error("join memory exceeded threshold={threshold_bytes} bytes for {strategy}")]
JoinMemoryExceeded {
strategy: String,
threshold_bytes: u64,
},
#[error("query budget exceeded ({metric:?}): used={used} limit={limit}")]
QueryBudgetExceeded {
metric: BudgetMetric,
used: u64,
limit: u64,
},
#[error("partial result: {reason} ({} rows)", rows.len())]
PartialResult {
rows: Vec<ResultRow>,
continuation: Vec<u8>,
reason: String,
},
#[error("planner error: {detail}")]
PlannerError {
detail: String,
},
#[error("executor error on node {node:#x}: {detail}")]
ExecutorError {
node: u64,
detail: String,
},
#[error("no node holds chain {origin:#x} matching the requirement: {requirement}")]
NoCapableHolder {
origin: u64,
requirement: String,
},
#[error("discovered predicate is ambiguous ({} origins match): {requirement}", matches.len())]
AmbiguousDiscovery {
matches: Vec<u64>,
requirement: String,
},
#[error("query cancelled")]
QueryCancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BudgetMetric {
MaxRows,
MaxDuration,
MaxBytesScanned,
}
impl MeshError {
pub fn kind(&self) -> &'static str {
#[allow(unreachable_patterns)]
match self {
MeshError::HistoricalRangeUnavailable { .. } => "historical_range_unavailable",
MeshError::LineageMaxDepthExceeded { .. } => "lineage_max_depth_exceeded",
MeshError::LineageCycleDetected { .. } => "lineage_cycle_detected",
MeshError::JoinMemoryExceeded { .. } => "join_memory_exceeded",
MeshError::QueryBudgetExceeded { .. } => "query_budget_exceeded",
MeshError::PartialResult { .. } => "partial_result",
MeshError::PlannerError { .. } => "planner_error",
MeshError::ExecutorError { .. } => "executor_error",
MeshError::NoCapableHolder { .. } => "no_capable_holder",
MeshError::AmbiguousDiscovery { .. } => "ambiguous_discovery",
MeshError::QueryCancelled => "query_cancelled",
_ => "unknown",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn historical_range_unavailable_display() {
let e = MeshError::HistoricalRangeUnavailable {
origin: 0xDEAD_BEEF_CAFE_BABE,
requested: SeqNum(100)..SeqNum(200),
available: vec![SeqNum(0)..SeqNum(50)],
};
let _ = format!("{e}");
}
#[test]
fn partial_result_carries_continuation() {
let e = MeshError::PartialResult {
rows: vec![],
continuation: b"continuation-token".to_vec(),
reason: "test partial".to_string(),
};
let msg = format!("{e}");
assert!(msg.contains("partial result"));
assert!(msg.contains("0 rows"));
}
#[test]
fn kind_discriminator_is_stable_across_variants() {
assert_eq!(MeshError::QueryCancelled.kind(), "query_cancelled");
assert_eq!(
MeshError::PlannerError {
detail: "x".to_string()
}
.kind(),
"planner_error"
);
assert_eq!(
MeshError::ExecutorError {
node: 0,
detail: "x".to_string(),
}
.kind(),
"executor_error"
);
assert_eq!(
MeshError::JoinMemoryExceeded {
strategy: "x".to_string(),
threshold_bytes: 0,
}
.kind(),
"join_memory_exceeded"
);
assert_eq!(
MeshError::QueryBudgetExceeded {
metric: BudgetMetric::MaxRows,
used: 0,
limit: 0,
}
.kind(),
"query_budget_exceeded"
);
assert_eq!(
MeshError::AmbiguousDiscovery {
matches: vec![1, 2],
requirement: "p".to_string(),
}
.kind(),
"ambiguous_discovery"
);
}
#[test]
fn budget_metric_variants_are_distinct() {
assert_ne!(BudgetMetric::MaxRows, BudgetMetric::MaxDuration);
assert_ne!(BudgetMetric::MaxRows, BudgetMetric::MaxBytesScanned);
assert_ne!(BudgetMetric::MaxDuration, BudgetMetric::MaxBytesScanned);
}
}