use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryIntegrityKind {
ManifestWalSegmentMissing,
ManifestWalSegmentMissingSizeMetadata,
ManifestWalSegmentMissingChecksumMetadata,
ManifestWalSegmentChecksumMismatch,
ManifestWalSegmentTruncated,
ManifestWalSegmentSizeMismatch,
ManifestWalSegmentInvalidChecksumMetadata,
}
impl RecoveryIntegrityKind {
pub fn as_str(&self) -> &'static str {
match self {
RecoveryIntegrityKind::ManifestWalSegmentMissing => "manifest_wal_segment_missing",
RecoveryIntegrityKind::ManifestWalSegmentMissingSizeMetadata => {
"manifest_wal_segment_missing_size_metadata"
}
RecoveryIntegrityKind::ManifestWalSegmentMissingChecksumMetadata => {
"manifest_wal_segment_missing_checksum_metadata"
}
RecoveryIntegrityKind::ManifestWalSegmentChecksumMismatch => {
"manifest_wal_segment_checksum_mismatch"
}
RecoveryIntegrityKind::ManifestWalSegmentTruncated => "manifest_wal_segment_truncated",
RecoveryIntegrityKind::ManifestWalSegmentSizeMismatch => {
"manifest_wal_segment_size_mismatch"
}
RecoveryIntegrityKind::ManifestWalSegmentInvalidChecksumMetadata => {
"manifest_wal_segment_invalid_checksum_metadata"
}
}
}
}
impl std::fmt::Display for RecoveryIntegrityKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RecoveryIntegrityDiagnostic {
pub kind: RecoveryIntegrityKind,
pub segment_filename: String,
pub manifest_path: std::path::PathBuf,
pub wal_dir: std::path::PathBuf,
pub recovery_mode: crate::config::RecoveryMode,
pub durable_seq: u64,
pub visible_seq: u64,
pub active_segment_seq: u64,
pub segment_seq: u64,
pub latest_checkpoint_seq: Option<u64>,
pub expected_size_bytes: Option<u64>,
pub actual_size_bytes: Option<u64>,
pub expected_sha256_hex: Option<String>,
pub actual_sha256_hex: Option<String>,
}
impl RecoveryIntegrityDiagnostic {
pub fn operator_message(&self) -> &'static str {
match self.recovery_mode {
crate::config::RecoveryMode::Strict => {
"strict recovery stopped because the manifest and WAL files are inconsistent; local dev data may be corrupt, and persisted data requires restore/repair from a known-good backup"
}
crate::config::RecoveryMode::Permissive => {
"permissive recovery skipped manifest-referenced WAL data; local dev data may be corrupt, and persisted data requires restore/repair from a known-good backup before relying on it"
}
}
}
}
impl std::fmt::Display for RecoveryIntegrityDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: segment={} manifest={} wal_dir={} mode={:?} durable_seq={} visible_seq={} active_segment_seq={} segment_seq={}",
self.kind,
self.segment_filename,
self.manifest_path.display(),
self.wal_dir.display(),
self.recovery_mode,
self.durable_seq,
self.visible_seq,
self.active_segment_seq,
self.segment_seq
)?;
if let Some(seq) = self.latest_checkpoint_seq {
write!(f, " latest_checkpoint_seq={seq}")?;
}
if let Some(size) = self.expected_size_bytes {
write!(f, " expected_size_bytes={size}")?;
}
if let Some(size) = self.actual_size_bytes {
write!(f, " actual_size_bytes={size}")?;
}
if let Some(ref sha) = self.expected_sha256_hex {
write!(f, " expected_sha256_hex={sha}")?;
}
if let Some(ref sha) = self.actual_sha256_hex {
write!(f, " actual_sha256_hex={sha}")?;
}
write!(f, "; {}", self.operator_message())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceType {
Project,
Scope,
Table,
Index,
}
impl std::fmt::Display for ResourceType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResourceType::Project => write!(f, "project"),
ResourceType::Scope => write!(f, "scope"),
ResourceType::Table => write!(f, "table"),
ResourceType::Index => write!(f, "index"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AedbErrorCode {
Io,
Encode,
Decode,
Validation,
InvalidConfig,
IntegrityError,
RecoveryIntegrity,
Unavailable,
CheckpointInProgress,
ProjectAlreadyExists,
ScopeAlreadyExists,
TableAlreadyExists,
IndexAlreadyExists,
ProjectNotFound,
ScopeNotFound,
TableNotFound,
IndexNotFound,
DuplicatePrimaryKey,
UniqueViolation,
ForeignKeyViolation,
CheckConstraintFailed,
NotNullViolation,
TypeMismatch,
UnknownColumn,
Conflict,
Underflow,
Overflow,
QueueFull,
PermissionDenied,
Timeout,
PartitionLockTimeout,
EpochApplyTimeout,
ParallelApplyCancelled,
ParallelApplyWorkerPanicked,
SnapshotExpired,
AssertionFailed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AedbErrorClass {
Retryable,
Conflict,
Permission,
Validation,
Integrity,
Unavailable,
}
impl AedbErrorCode {
pub fn as_str(self) -> &'static str {
match self {
AedbErrorCode::Io => "io",
AedbErrorCode::Encode => "encode",
AedbErrorCode::Decode => "decode",
AedbErrorCode::Validation => "validation",
AedbErrorCode::InvalidConfig => "invalid_config",
AedbErrorCode::IntegrityError => "integrity_error",
AedbErrorCode::RecoveryIntegrity => "recovery_integrity",
AedbErrorCode::Unavailable => "unavailable",
AedbErrorCode::CheckpointInProgress => "checkpoint_in_progress",
AedbErrorCode::ProjectAlreadyExists => "project_already_exists",
AedbErrorCode::ScopeAlreadyExists => "scope_already_exists",
AedbErrorCode::TableAlreadyExists => "table_already_exists",
AedbErrorCode::IndexAlreadyExists => "index_already_exists",
AedbErrorCode::ProjectNotFound => "project_not_found",
AedbErrorCode::ScopeNotFound => "scope_not_found",
AedbErrorCode::TableNotFound => "table_not_found",
AedbErrorCode::IndexNotFound => "index_not_found",
AedbErrorCode::DuplicatePrimaryKey => "duplicate_primary_key",
AedbErrorCode::UniqueViolation => "unique_violation",
AedbErrorCode::ForeignKeyViolation => "foreign_key_violation",
AedbErrorCode::CheckConstraintFailed => "check_constraint_failed",
AedbErrorCode::NotNullViolation => "not_null_violation",
AedbErrorCode::TypeMismatch => "type_mismatch",
AedbErrorCode::UnknownColumn => "unknown_column",
AedbErrorCode::Conflict => "conflict",
AedbErrorCode::Underflow => "underflow",
AedbErrorCode::Overflow => "overflow",
AedbErrorCode::QueueFull => "queue_full",
AedbErrorCode::PermissionDenied => "permission_denied",
AedbErrorCode::Timeout => "timeout",
AedbErrorCode::PartitionLockTimeout => "partition_lock_timeout",
AedbErrorCode::EpochApplyTimeout => "epoch_apply_timeout",
AedbErrorCode::ParallelApplyCancelled => "parallel_apply_cancelled",
AedbErrorCode::ParallelApplyWorkerPanicked => "parallel_apply_worker_panicked",
AedbErrorCode::SnapshotExpired => "snapshot_expired",
AedbErrorCode::AssertionFailed => "assertion_failed",
}
}
pub fn class(self) -> AedbErrorClass {
match self {
AedbErrorCode::QueueFull
| AedbErrorCode::Timeout
| AedbErrorCode::PartitionLockTimeout
| AedbErrorCode::EpochApplyTimeout
| AedbErrorCode::ParallelApplyCancelled
| AedbErrorCode::ParallelApplyWorkerPanicked
| AedbErrorCode::SnapshotExpired => AedbErrorClass::Retryable,
AedbErrorCode::Conflict | AedbErrorCode::AssertionFailed => AedbErrorClass::Conflict,
AedbErrorCode::PermissionDenied => AedbErrorClass::Permission,
AedbErrorCode::Io
| AedbErrorCode::Encode
| AedbErrorCode::Decode
| AedbErrorCode::IntegrityError
| AedbErrorCode::RecoveryIntegrity => AedbErrorClass::Integrity,
AedbErrorCode::Unavailable | AedbErrorCode::CheckpointInProgress => {
AedbErrorClass::Unavailable
}
AedbErrorCode::Validation
| AedbErrorCode::InvalidConfig
| AedbErrorCode::ProjectAlreadyExists
| AedbErrorCode::ScopeAlreadyExists
| AedbErrorCode::TableAlreadyExists
| AedbErrorCode::IndexAlreadyExists
| AedbErrorCode::ProjectNotFound
| AedbErrorCode::ScopeNotFound
| AedbErrorCode::TableNotFound
| AedbErrorCode::IndexNotFound
| AedbErrorCode::DuplicatePrimaryKey
| AedbErrorCode::UniqueViolation
| AedbErrorCode::ForeignKeyViolation
| AedbErrorCode::CheckConstraintFailed
| AedbErrorCode::NotNullViolation
| AedbErrorCode::TypeMismatch
| AedbErrorCode::UnknownColumn
| AedbErrorCode::Underflow
| AedbErrorCode::Overflow => AedbErrorClass::Validation,
}
}
pub fn is_retryable(self) -> bool {
matches!(self.class(), AedbErrorClass::Retryable)
}
}
#[derive(Debug, Error)]
pub enum AedbError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("encode error: {0}")]
Encode(String),
#[error("decode error: {0}")]
Decode(String),
#[error("validation error: {0}")]
Validation(String),
#[error("invalid config: {message}")]
InvalidConfig { message: String },
#[error("integrity error: {message}")]
IntegrityError { message: String },
#[error("recovery integrity error: {diagnostic}")]
RecoveryIntegrity {
diagnostic: Box<RecoveryIntegrityDiagnostic>,
},
#[error("resource unavailable: {message}")]
Unavailable { message: String },
#[error("checkpoint in progress")]
CheckpointInProgress,
#[error("{resource_type} '{resource_id}' already exists")]
AlreadyExists {
resource_type: ResourceType,
resource_id: String,
},
#[error("{resource_type} '{resource_id}' not found")]
NotFound {
resource_type: ResourceType,
resource_id: String,
},
#[error("duplicate primary key in table '{table}': {key}")]
DuplicatePK { table: String, key: String },
#[error("unique constraint violation on index '{index}' in table '{table}'")]
UniqueViolation {
table: String,
index: String,
key: String,
},
#[error("foreign key violation: {fk_name} references {ref_table}({ref_key})")]
ForeignKeyViolation {
fk_name: String,
table: String,
ref_table: String,
ref_key: String,
},
#[error("check constraint '{constraint}' failed on table '{table}'")]
CheckConstraintFailed { table: String, constraint: String },
#[error("NOT NULL violation: column '{column}' in table '{table}'")]
NotNullViolation { table: String, column: String },
#[error(
"type mismatch: column '{column}' in table '{table}' expected {expected}, got {actual}"
)]
TypeMismatch {
table: String,
column: String,
expected: String,
actual: String,
},
#[error("unknown column '{column}' in table '{table}'")]
UnknownColumn { table: String, column: String },
#[error("conflict error: {0}")]
Conflict(String),
#[error("underflow")]
Underflow,
#[error("overflow")]
Overflow,
#[error("queue full")]
QueueFull,
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("timeout")]
Timeout,
#[error("partition lock timeout")]
PartitionLockTimeout,
#[error("epoch apply timeout exceeded")]
EpochApplyTimeout,
#[error("parallel apply cancelled")]
ParallelApplyCancelled,
#[error("parallel apply worker panicked")]
ParallelApplyWorkerPanicked,
#[error("snapshot expired")]
SnapshotExpired,
#[error("assertion failed at index {index}")]
AssertionFailed {
index: usize,
assertion: Box<crate::commit::tx::ReadAssertion>,
actual: Box<crate::commit::tx::AssertionActual>,
},
}
impl AedbError {
pub fn code(&self) -> AedbErrorCode {
match self {
AedbError::Io(_) => AedbErrorCode::Io,
AedbError::Encode(_) => AedbErrorCode::Encode,
AedbError::Decode(_) => AedbErrorCode::Decode,
AedbError::Validation(_) => AedbErrorCode::Validation,
AedbError::InvalidConfig { .. } => AedbErrorCode::InvalidConfig,
AedbError::IntegrityError { .. } => AedbErrorCode::IntegrityError,
AedbError::RecoveryIntegrity { .. } => AedbErrorCode::RecoveryIntegrity,
AedbError::Unavailable { .. } => AedbErrorCode::Unavailable,
AedbError::CheckpointInProgress => AedbErrorCode::CheckpointInProgress,
AedbError::AlreadyExists { resource_type, .. } => match resource_type {
ResourceType::Project => AedbErrorCode::ProjectAlreadyExists,
ResourceType::Scope => AedbErrorCode::ScopeAlreadyExists,
ResourceType::Table => AedbErrorCode::TableAlreadyExists,
ResourceType::Index => AedbErrorCode::IndexAlreadyExists,
},
AedbError::NotFound { resource_type, .. } => match resource_type {
ResourceType::Project => AedbErrorCode::ProjectNotFound,
ResourceType::Scope => AedbErrorCode::ScopeNotFound,
ResourceType::Table => AedbErrorCode::TableNotFound,
ResourceType::Index => AedbErrorCode::IndexNotFound,
},
AedbError::DuplicatePK { .. } => AedbErrorCode::DuplicatePrimaryKey,
AedbError::UniqueViolation { .. } => AedbErrorCode::UniqueViolation,
AedbError::ForeignKeyViolation { .. } => AedbErrorCode::ForeignKeyViolation,
AedbError::CheckConstraintFailed { .. } => AedbErrorCode::CheckConstraintFailed,
AedbError::NotNullViolation { .. } => AedbErrorCode::NotNullViolation,
AedbError::TypeMismatch { .. } => AedbErrorCode::TypeMismatch,
AedbError::UnknownColumn { .. } => AedbErrorCode::UnknownColumn,
AedbError::Conflict(_) => AedbErrorCode::Conflict,
AedbError::Underflow => AedbErrorCode::Underflow,
AedbError::Overflow => AedbErrorCode::Overflow,
AedbError::QueueFull => AedbErrorCode::QueueFull,
AedbError::PermissionDenied(_) => AedbErrorCode::PermissionDenied,
AedbError::Timeout => AedbErrorCode::Timeout,
AedbError::PartitionLockTimeout => AedbErrorCode::PartitionLockTimeout,
AedbError::EpochApplyTimeout => AedbErrorCode::EpochApplyTimeout,
AedbError::ParallelApplyCancelled => AedbErrorCode::ParallelApplyCancelled,
AedbError::ParallelApplyWorkerPanicked => AedbErrorCode::ParallelApplyWorkerPanicked,
AedbError::SnapshotExpired => AedbErrorCode::SnapshotExpired,
AedbError::AssertionFailed { .. } => AedbErrorCode::AssertionFailed,
}
}
pub fn code_str(&self) -> &'static str {
self.code().as_str()
}
pub fn class(&self) -> AedbErrorClass {
self.code().class()
}
pub fn is_retryable(&self) -> bool {
self.code().is_retryable()
}
}
#[cfg(test)]
mod tests;