use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum UniError {
#[error("Database not found: {path}")]
NotFound { path: PathBuf },
#[error("Schema error: {message}")]
Schema { message: String },
#[error("Parse error: {message}")]
Parse {
message: String,
position: Option<usize>,
line: Option<usize>,
column: Option<usize>,
context: Option<String>,
},
#[error("Query error: {message}")]
Query {
message: String,
query: Option<String>,
},
#[error("Transaction error: {message}")]
Transaction { message: String },
#[error("Transaction conflict: {message}")]
TransactionConflict { message: String },
#[error("Transaction already completed")]
TransactionAlreadyCompleted,
#[error("Operation '{operation}' not supported on read-only database")]
ReadOnly { operation: String },
#[error("Label '{label}' not found in schema")]
LabelNotFound { label: String },
#[error("Edge type '{edge_type}' not found in schema")]
EdgeTypeNotFound { edge_type: String },
#[error("Property '{property}' not found on {entity_type} with label '{label}'")]
PropertyNotFound {
property: String,
entity_type: String, label: String,
},
#[error("Index '{index}' not found")]
IndexNotFound { index: String },
#[error("Snapshot '{snapshot_id}' not found")]
SnapshotNotFound { snapshot_id: String },
#[error("Query exceeded memory limit of {limit_bytes} bytes")]
MemoryLimitExceeded { limit_bytes: usize },
#[error("Database is locked by another process")]
DatabaseLocked,
#[error("Operation timed out after {timeout_ms}ms")]
Timeout { timeout_ms: u64 },
#[error("Locy evaluation incomplete: {detail}")]
LocyIncomplete { detail: Box<LocyIncomplete> },
#[error("Type error: expected {expected}, got {actual}")]
Type { expected: String, actual: String },
#[error("Constraint violation: {message}")]
Constraint { message: String },
#[error("Serialization conflict: {message}")]
SerializationConflict { message: String },
#[error("Constraint conflict: {message}")]
ConstraintConflict { message: String },
#[error("Storage error: {message}")]
Storage {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Internal error: {0}")]
Internal(#[from] anyhow::Error),
#[error("Invalid identifier '{name}': {reason}")]
InvalidIdentifier { name: String, reason: String },
#[error("Label '{label}' already exists")]
LabelAlreadyExists { label: String },
#[error("Edge type '{edge_type}' already exists")]
EdgeTypeAlreadyExists { edge_type: String },
#[error("Permission denied: {action}")]
PermissionDenied { action: String },
#[error("Argument '{arg}' is invalid: {message}")]
InvalidArgument { arg: String, message: String },
#[error("A write context is already active on session '{session_id}'")]
WriteContextAlreadyActive {
session_id: String,
hint: &'static str,
},
#[error("Transaction '{tx_id}' commit timed out")]
CommitTimeout { tx_id: String, hint: &'static str },
#[error("FOR UPDATE lock acquisition timed out after {timeout_ms}ms")]
LockTimeout { timeout_ms: u64 },
#[error("Transaction '{tx_id}' expired")]
TransactionExpired { tx_id: String, hint: &'static str },
#[error("Operation cancelled")]
Cancelled,
#[error("Derived facts are stale: version gap is {version_gap}")]
StaleDerivedFacts { version_gap: u64 },
#[error("Rule conflict: rule '{rule_name}' conflicts during promotion")]
RuleConflict { rule_name: String },
#[error("Hook rejected: {message}")]
HookRejected { message: String },
#[error("Trigger '{trigger}' rejected commit: {reason}")]
TriggerRejected { trigger: String, reason: String },
#[error("Authentication failed: {reason}")]
AuthenticationFailed {
reason: String,
},
#[error("Authorization denied: {reason}")]
AuthorizationDenied {
reason: String,
},
#[error("Cannot mutate ephemeral {kind} {id}: ephemeral entities are return-only")]
EphemeralWriteAttempt {
kind: &'static str,
id: u64,
},
#[error("Fork '{name}' not found")]
ForkNotFound { name: String },
#[error("Fork '{name}' already exists")]
ForkAlreadyExists { name: String },
#[error(
"Writes on a forked session are not yet supported (Phase 2); reads, locy, and admin paths work"
)]
ForkWritesNotYetSupported,
#[error("Fork '{name}' is held by {holder_count} live session(s); drop refused")]
ForkInUse { name: String, holder_count: usize },
#[error("Fork '{name}' has uncommitted transaction state; commit or rollback first")]
ForkInflightTx { name: String },
#[error("Fork '{name}' has pending flushes that did not drain within timeout")]
PendingFlushTimeout { name: String },
#[error("Fork registry is corrupt: {message}")]
ForkCorruptRegistry { message: String },
#[error(
"Fork '{name}' has nested children {children:?}; use drop_fork_cascade or drop them first"
)]
ForkHasChildren { name: String, children: Vec<String> },
#[error("Fork subtree cannot be dropped: {blockers:?}")]
ForkSubtreeInUse { blockers: Vec<String> },
#[error("Fork budget exceeded: {current}/{max} forks; drop one or raise UniConfig::max_forks")]
ForkBudgetExceeded { current: usize, max: usize },
#[error("Fork '{name}' lifecycle failed at stage '{stage}': {source}")]
ForkLifecycle {
name: String,
stage: &'static str,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
impl UniError {
#[must_use]
pub fn is_retriable(&self) -> bool {
matches!(
self,
UniError::SerializationConflict { .. }
| UniError::ConstraintConflict { .. }
| UniError::TransactionConflict { .. }
| UniError::CommitTimeout { .. }
| UniError::LockTimeout { .. }
)
}
}
pub type Result<T> = std::result::Result<T, UniError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocyIncompleteReason {
Timeout,
IterationLimit,
}
impl LocyIncompleteReason {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
LocyIncompleteReason::Timeout => "timeout",
LocyIncompleteReason::IterationLimit => "iteration_limit",
}
}
}
impl std::fmt::Display for LocyIncompleteReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocyIncomplete {
pub reason: LocyIncompleteReason,
pub elapsed_ms: u64,
pub limit_ms: u64,
pub max_iterations: usize,
pub completed_strata: usize,
pub total_strata: usize,
pub incomplete_rules: Vec<String>,
pub skipped_rules: Vec<String>,
pub complement_rules_affected: Vec<String>,
}
impl std::fmt::Display for LocyIncomplete {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{reason} after {elapsed_ms}ms (limit {limit_ms}ms, max_iterations {max_iters}); \
evaluated {done}/{total} strata, {n_incomplete} rule(s) incomplete, \
{n_skipped} rule(s) skipped",
reason = self.reason,
elapsed_ms = self.elapsed_ms,
limit_ms = self.limit_ms,
max_iters = self.max_iterations,
done = self.completed_strata,
total = self.total_strata,
n_incomplete = self.incomplete_rules.len(),
n_skipped = self.skipped_rules.len(),
)?;
if !self.complement_rules_affected.is_empty() {
write!(
f,
"; UNSOUND complement rule(s) affected: {:?}",
self.complement_rules_affected
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retriable_errors_are_contention_failures() {
let s = String::new;
let retriable = [
UniError::SerializationConflict { message: s() },
UniError::ConstraintConflict { message: s() },
UniError::TransactionConflict { message: s() },
UniError::CommitTimeout {
tx_id: s(),
hint: "",
},
UniError::LockTimeout { timeout_ms: 10_000 },
];
for e in &retriable {
assert!(e.is_retriable(), "{e:?} should be retriable");
}
}
#[test]
fn deterministic_errors_are_not_retriable() {
let s = String::new;
let terminal = [
UniError::Parse {
message: s(),
position: None,
line: None,
column: None,
context: None,
},
UniError::Query {
message: s(),
query: None,
},
UniError::Schema { message: s() },
UniError::Constraint { message: s() },
UniError::InvalidArgument {
arg: s(),
message: s(),
},
UniError::TransactionExpired {
tx_id: s(),
hint: "",
},
UniError::Timeout { timeout_ms: 1 },
];
for e in &terminal {
assert!(!e.is_retriable(), "{e:?} should not be retriable");
}
}
}