use std::error::Error;
use std::fmt;
use lora_analyzer::SemanticError;
use lora_executor::{ExecutorError, PropertyConversionError};
use lora_parser::ParseError;
use lora_snapshot::SnapshotCodecError;
use lora_store::SnapshotError;
use lora_wal::{WalBufferedCommitError, WalCommitError, WalError};
use thiserror::Error;
use crate::transaction::TransactionError;
use crate::DatabaseNameError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoraErrorCode {
Parse,
Semantic,
InvalidParams,
ReadOnlyViolation,
NotFound,
ConstraintViolation,
InvalidVector,
Timeout,
DatabaseName,
Config,
Validation,
UniqueConstraint,
NotNullConstraint,
ForeignKeyViolation,
TransactionFailure,
Locked,
ChangesTruncated,
ChangesLagged,
Io,
Connection,
WalCorruption,
WalPoisoned,
SnapshotCodec,
SnapshotCrypto,
Internal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoraErrorCategory {
Client,
Server,
}
impl LoraErrorCategory {
pub fn as_str(self) -> &'static str {
match self {
Self::Client => "client",
Self::Server => "server",
}
}
}
impl LoraErrorCode {
pub fn as_str(self) -> &'static str {
match self {
Self::Parse => "LORA_PARSE",
Self::Semantic => "LORA_SEMANTIC",
Self::InvalidParams => "LORA_INVALID_PARAMS",
Self::ReadOnlyViolation => "LORA_READ_ONLY",
Self::NotFound => "LORA_NOT_FOUND",
Self::ConstraintViolation => "LORA_CONSTRAINT",
Self::InvalidVector => "LORA_INVALID_VECTOR",
Self::Timeout => "LORA_TIMEOUT",
Self::DatabaseName => "LORA_DATABASE_NAME",
Self::Config => "LORA_CONFIG",
Self::Validation => "LORA_VALIDATION",
Self::UniqueConstraint => "LORA_UNIQUE_CONSTRAINT",
Self::NotNullConstraint => "LORA_NOT_NULL_CONSTRAINT",
Self::ForeignKeyViolation => "LORA_FOREIGN_KEY",
Self::TransactionFailure => "LORA_TRANSACTION",
Self::Locked => "LORA_LOCKED",
Self::ChangesTruncated => "LORA_CHANGES_TRUNCATED",
Self::ChangesLagged => "LORA_CHANGES_LAGGED",
Self::Io => "LORA_IO",
Self::Connection => "LORA_CONNECTION",
Self::WalCorruption => "LORA_WAL_CORRUPTION",
Self::WalPoisoned => "LORA_WAL_POISONED",
Self::SnapshotCodec => "LORA_SNAPSHOT_CODEC",
Self::SnapshotCrypto => "LORA_SNAPSHOT_CRYPTO",
Self::Internal => "LORA_INTERNAL",
}
}
pub fn category(self) -> LoraErrorCategory {
match self {
Self::Parse
| Self::Semantic
| Self::InvalidParams
| Self::ReadOnlyViolation
| Self::NotFound
| Self::ConstraintViolation
| Self::InvalidVector
| Self::Timeout
| Self::DatabaseName
| Self::Config
| Self::Validation
| Self::UniqueConstraint
| Self::NotNullConstraint
| Self::ForeignKeyViolation
| Self::TransactionFailure
| Self::Locked
| Self::ChangesTruncated
| Self::ChangesLagged => LoraErrorCategory::Client,
Self::Io
| Self::Connection
| Self::WalCorruption
| Self::WalPoisoned
| Self::SnapshotCodec
| Self::SnapshotCrypto
| Self::Internal => LoraErrorCategory::Server,
}
}
}
impl fmt::Display for LoraErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub struct LoraError {
code: LoraErrorCode,
message: String,
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
#[derive(Debug)]
struct InternalAnyhowSource(String);
impl fmt::Display for InternalAnyhowSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Error for InternalAnyhowSource {}
impl LoraError {
pub fn new(code: LoraErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
source: None,
}
}
pub fn with_source(
code: LoraErrorCode,
message: impl Into<String>,
source: impl Error + Send + Sync + 'static,
) -> Self {
Self {
code,
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn code(&self) -> LoraErrorCode {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub fn category(&self) -> LoraErrorCategory {
self.code.category()
}
pub fn public_message(&self) -> String {
match self.category() {
LoraErrorCategory::Client => self.message.clone(),
LoraErrorCategory::Server => match self.code {
LoraErrorCode::Io => "database storage is temporarily unavailable".to_string(),
LoraErrorCode::Connection => {
"database connection is temporarily unavailable".to_string()
}
LoraErrorCode::WalCorruption => {
"database write-ahead log is unreadable".to_string()
}
LoraErrorCode::WalPoisoned => {
"database write-ahead log is unavailable until recovery".to_string()
}
LoraErrorCode::SnapshotCodec => "snapshot data could not be decoded".to_string(),
LoraErrorCode::SnapshotCrypto => {
"snapshot encryption could not be processed".to_string()
}
LoraErrorCode::Internal => "database operation failed unexpectedly".to_string(),
_ => self.message.clone(),
},
}
}
pub fn debug_context(&self) -> String {
let mut out = self.message.clone();
let mut source = self.source();
while let Some(err) = source {
out.push_str(": ");
out.push_str(&err.to_string());
source = err.source();
}
out
}
pub fn from_anyhow(err: anyhow::Error) -> Self {
let mapped = Self::from_anyhow_ref(&err);
if mapped.code == LoraErrorCode::Internal {
Self::with_source(
LoraErrorCode::Internal,
"database operation failed unexpectedly",
InternalAnyhowSource(format!("{err:#}")),
)
} else {
mapped
}
}
pub fn from_anyhow_ref(err: &anyhow::Error) -> Self {
if let Some(e) = err.downcast_ref::<LoraError>() {
return Self::new(e.code, e.message.clone());
}
if let Some(e) = err.downcast_ref::<ParseError>() {
return Self::new(LoraErrorCode::Parse, e.to_string());
}
if let Some(e) = err.downcast_ref::<SemanticError>() {
return Self::new(LoraErrorCode::Semantic, e.to_string());
}
if let Some(e) = err.downcast_ref::<ExecutorError>() {
return Self::new(executor_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<PropertyConversionError>() {
return Self::new(LoraErrorCode::Validation, e.to_string());
}
if let Some(e) = err.downcast_ref::<WalError>() {
return Self::new(wal_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<WalCommitError>() {
return Self::new(wal_commit_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<WalBufferedCommitError>() {
return Self::new(wal_buffered_commit_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<SnapshotCodecError>() {
return Self::new(snapshot_codec_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<SnapshotError>() {
return Self::new(snapshot_store_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<DatabaseNameError>() {
return Self::new(LoraErrorCode::DatabaseName, e.to_string());
}
if let Some(e) = err.downcast_ref::<TransactionError>() {
return Self::new(transaction_code(e), e.to_string());
}
if let Some(e) = err.downcast_ref::<DatabaseOperationError>() {
return Self::new(e.code(), e.to_string());
}
if let Some(e) = err.downcast_ref::<std::io::Error>() {
return Self::new(io_code(e), e.to_string());
}
if let Some(code) = legacy_message_code(&err.to_string()) {
return Self::new(code, err.to_string());
}
Self::new(
LoraErrorCode::Internal,
"database operation failed unexpectedly",
)
}
}
fn executor_code(err: &ExecutorError) -> LoraErrorCode {
match err {
ExecutorError::ReadOnlyCreate { .. }
| ExecutorError::ReadOnlyMerge { .. }
| ExecutorError::ReadOnlyDelete { .. }
| ExecutorError::ReadOnlySet { .. }
| ExecutorError::ReadOnlyRemove { .. }
| ExecutorError::ReadOnlyForeach { .. } => LoraErrorCode::ReadOnlyViolation,
ExecutorError::QueryTimeout => LoraErrorCode::Timeout,
ExecutorError::ConstraintViolation(message) => constraint_message_code(message),
ExecutorError::DeleteNodeWithRelationships { .. } => LoraErrorCode::ForeignKeyViolation,
ExecutorError::RelationshipCreateFailed { .. } => LoraErrorCode::ForeignKeyViolation,
ExecutorError::NodeCreateFailed => LoraErrorCode::Internal,
ExecutorError::RuntimeError(message) => validation_message_code(message),
ExecutorError::ExpectedNodeForExpand { .. }
| ExecutorError::ExpectedPropertyMap { .. }
| ExecutorError::GroupByNotLowered
| ExecutorError::AggregateNotLowered
| ExecutorError::UnsupportedCreateRelationshipRange
| ExecutorError::MissingRelationshipType
| ExecutorError::DeleteRelationshipFailed { .. }
| ExecutorError::InvalidDeleteTarget { .. }
| ExecutorError::ExpectedNodeForRemoveLabels { .. }
| ExecutorError::UnboundVariableForRemove { .. }
| ExecutorError::ExpectedNodeForSetLabels { .. }
| ExecutorError::UnboundVariableForSet { .. }
| ExecutorError::InvalidSetTarget { .. }
| ExecutorError::UnsupportedRemoveTarget
| ExecutorError::InvalidRemoveTarget { .. }
| ExecutorError::UnsupportedSetTarget
| ExecutorError::ExpectedRelationshipForExpand { .. } => LoraErrorCode::Validation,
}
}
fn constraint_message_code(message: &str) -> LoraErrorCode {
if message.contains("22N79") || message.contains("22N70") || message.contains("22N71") {
LoraErrorCode::UniqueConstraint
} else if message.contains("22N77") {
LoraErrorCode::NotNullConstraint
} else if message.contains("42N51") {
LoraErrorCode::NotFound
} else if message.contains("22N78") || message.contains("22N90") {
LoraErrorCode::Validation
} else {
LoraErrorCode::ConstraintViolation
}
}
fn validation_message_code(message: &str) -> LoraErrorCode {
let lower = message.to_ascii_lowercase();
if lower.contains("vector") {
LoraErrorCode::InvalidVector
} else {
LoraErrorCode::Validation
}
}
fn legacy_message_code(message: &str) -> Option<LoraErrorCode> {
if message.contains("22N")
|| message.contains("42N51")
|| message.contains("50N11")
|| message.contains("constraint")
|| message.contains("CONSTRAINT")
{
return Some(constraint_message_code(message));
}
None
}
fn wal_code(err: &WalError) -> LoraErrorCode {
match err {
WalError::Io(inner) => io_code(inner),
WalError::AlreadyOpen { .. } => LoraErrorCode::Locked,
WalError::CrcMismatch { .. }
| WalError::Truncated { .. }
| WalError::UnknownKind(_)
| WalError::BadSegmentHeader(_)
| WalError::Malformed(_)
| WalError::Encode(_)
| WalError::Decode(_) => LoraErrorCode::WalCorruption,
WalError::Poisoned => LoraErrorCode::WalPoisoned,
}
}
fn io_code(err: &std::io::Error) -> LoraErrorCode {
match err.kind() {
std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::NotConnected
| std::io::ErrorKind::AddrInUse
| std::io::ErrorKind::AddrNotAvailable => LoraErrorCode::Connection,
std::io::ErrorKind::TimedOut => LoraErrorCode::Timeout,
_ => LoraErrorCode::Io,
}
}
fn wal_commit_code(err: &WalCommitError) -> LoraErrorCode {
match err {
WalCommitError::Commit(inner) | WalCommitError::Flush(inner) => wal_code(inner),
}
}
fn wal_buffered_commit_code(err: &WalBufferedCommitError) -> LoraErrorCode {
match err {
WalBufferedCommitError::Arm(inner) => wal_code(inner),
WalBufferedCommitError::Poisoned(_) | WalBufferedCommitError::ReplayPoisoned(_) => {
LoraErrorCode::WalPoisoned
}
WalBufferedCommitError::Commit(inner) => wal_commit_code(inner),
}
}
fn snapshot_codec_code(err: &SnapshotCodecError) -> LoraErrorCode {
match err {
SnapshotCodecError::Io(inner) => io_code(inner),
SnapshotCodecError::MissingEncryptionKey(_)
| SnapshotCodecError::MissingPassword(_)
| SnapshotCodecError::PasswordKdf(_)
| SnapshotCodecError::Encrypt
| SnapshotCodecError::Decrypt => LoraErrorCode::SnapshotCrypto,
SnapshotCodecError::BadMagic
| SnapshotCodecError::UnsupportedVersion(_)
| SnapshotCodecError::UnsupportedCompression(_)
| SnapshotCodecError::ChecksumMismatch
| SnapshotCodecError::Encode(_)
| SnapshotCodecError::Decode(_) => LoraErrorCode::SnapshotCodec,
}
}
fn snapshot_store_code(err: &SnapshotError) -> LoraErrorCode {
match err {
SnapshotError::Io(inner) => io_code(inner),
SnapshotError::Decode(_) | SnapshotError::Encode(_) => LoraErrorCode::SnapshotCodec,
}
}
fn transaction_code(err: &TransactionError) -> LoraErrorCode {
match err {
TransactionError::ReadOnlyMutation
| TransactionError::ReadOnlyCommit
| TransactionError::StreamingRequiresReadWrite => LoraErrorCode::ReadOnlyViolation,
TransactionError::AlreadyClosed
| TransactionError::CursorActiveCommit
| TransactionError::CursorActiveStatement => LoraErrorCode::TransactionFailure,
TransactionError::NoGraphGuard
| TransactionError::NoStagedGraph
| TransactionError::Poisoned => LoraErrorCode::TransactionFailure,
}
}
#[derive(Debug, Clone, Error)]
pub(crate) enum DatabaseOperationError {
#[error("{0}")]
InvalidParams(String),
#[error("{0}")]
InvalidVector(String),
#[error("{0}")]
Validation(String),
#[error("{0}")]
NotFound(String),
#[error("{0}")]
ConstraintViolation(String),
#[error("{0}")]
UniqueConstraint(String),
#[error("{0}")]
NotNullConstraint(String),
}
impl DatabaseOperationError {
pub(crate) fn invalid_params(message: impl Into<String>) -> Self {
Self::InvalidParams(message.into())
}
pub(crate) fn invalid_vector(message: impl Into<String>) -> Self {
Self::InvalidVector(message.into())
}
pub(crate) fn validation(message: impl Into<String>) -> Self {
Self::Validation(message.into())
}
pub(crate) fn not_found(message: impl Into<String>) -> Self {
Self::NotFound(message.into())
}
pub(crate) fn constraint_violation(message: impl Into<String>) -> Self {
Self::ConstraintViolation(message.into())
}
pub(crate) fn unique_constraint(message: impl Into<String>) -> Self {
Self::UniqueConstraint(message.into())
}
pub(crate) fn not_null_constraint(message: impl Into<String>) -> Self {
Self::NotNullConstraint(message.into())
}
pub(crate) fn code(&self) -> LoraErrorCode {
match self {
Self::InvalidParams(_) => LoraErrorCode::InvalidParams,
Self::InvalidVector(_) => LoraErrorCode::InvalidVector,
Self::Validation(_) => LoraErrorCode::Validation,
Self::NotFound(_) => LoraErrorCode::NotFound,
Self::ConstraintViolation(_) => LoraErrorCode::ConstraintViolation,
Self::UniqueConstraint(_) => LoraErrorCode::UniqueConstraint,
Self::NotNullConstraint(_) => LoraErrorCode::NotNullConstraint,
}
}
}
impl fmt::Debug for LoraError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LoraError")
.field("code", &self.code)
.field("message", &self.message)
.finish()
}
}
impl fmt::Display for LoraError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for LoraError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_deref().map(|s| s as &(dyn Error + 'static))
}
}
impl From<ParseError> for LoraError {
fn from(e: ParseError) -> Self {
let msg = e.to_string();
Self::with_source(LoraErrorCode::Parse, msg, e)
}
}
impl From<SemanticError> for LoraError {
fn from(e: SemanticError) -> Self {
let msg = e.to_string();
Self::with_source(LoraErrorCode::Semantic, msg, e)
}
}
impl From<ExecutorError> for LoraError {
fn from(e: ExecutorError) -> Self {
let code = executor_code(&e);
let msg = e.to_string();
Self::with_source(code, msg, e)
}
}
impl From<PropertyConversionError> for LoraError {
fn from(e: PropertyConversionError) -> Self {
let msg = e.to_string();
Self::with_source(LoraErrorCode::Validation, msg, e)
}
}
impl From<WalError> for LoraError {
fn from(e: WalError) -> Self {
let code = wal_code(&e);
let msg = e.to_string();
Self::with_source(code, msg, e)
}
}
impl From<SnapshotCodecError> for LoraError {
fn from(e: SnapshotCodecError) -> Self {
let code = snapshot_codec_code(&e);
let msg = e.to_string();
Self::with_source(code, msg, e)
}
}
impl From<SnapshotError> for LoraError {
fn from(e: SnapshotError) -> Self {
let code = snapshot_store_code(&e);
let msg = e.to_string();
Self::with_source(code, msg, e)
}
}
impl From<DatabaseNameError> for LoraError {
fn from(e: DatabaseNameError) -> Self {
let msg = e.to_string();
Self::with_source(LoraErrorCode::DatabaseName, msg, e)
}
}
impl From<TransactionError> for LoraError {
fn from(e: TransactionError) -> Self {
let code = transaction_code(&e);
let msg = e.to_string();
Self::with_source(code, msg, e)
}
}
impl From<std::io::Error> for LoraError {
fn from(e: std::io::Error) -> Self {
let code = io_code(&e);
let msg = e.to_string();
Self::with_source(code, msg, e)
}
}
impl From<anyhow::Error> for LoraError {
fn from(e: anyhow::Error) -> Self {
Self::from_anyhow(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_error_is_client_parse() {
let e = ParseError::new("expected `MATCH`", 0, 5);
let mapped: LoraError = anyhow::Error::from(e).into();
assert_eq!(mapped.code(), LoraErrorCode::Parse);
assert_eq!(mapped.category(), LoraErrorCategory::Client);
assert!(mapped.message().contains("parse error"));
}
#[test]
fn semantic_error_is_client_semantic() {
let e = SemanticError::UnknownVariable("n".into());
let mapped = LoraError::from(e);
assert_eq!(mapped.code(), LoraErrorCode::Semantic);
assert_eq!(mapped.message(), "unknown variable `n`");
}
#[test]
fn executor_timeout_is_client_timeout() {
let mapped = LoraError::from(ExecutorError::QueryTimeout);
assert_eq!(mapped.code(), LoraErrorCode::Timeout);
}
#[test]
fn wal_io_is_server_io() {
let inner = std::io::Error::other("disk full");
let mapped = LoraError::from(WalError::Io(inner));
assert_eq!(mapped.code(), LoraErrorCode::Io);
assert_eq!(mapped.category(), LoraErrorCategory::Server);
}
#[test]
fn unknown_anyhow_falls_back_to_internal() {
let e = anyhow::anyhow!("something else entirely");
let mapped = LoraError::from_anyhow(e);
assert_eq!(mapped.code(), LoraErrorCode::Internal);
assert_eq!(mapped.message(), "database operation failed unexpectedly");
assert!(mapped.debug_context().contains("something else entirely"));
}
#[test]
fn typed_transaction_error_routes_readonly() {
let mapped = LoraError::from(TransactionError::ReadOnlyMutation);
assert_eq!(mapped.code(), LoraErrorCode::ReadOnlyViolation);
assert_eq!(
mapped.message(),
"cannot execute mutating query in read-only transaction"
);
}
#[test]
fn typed_transaction_error_round_trips_through_anyhow() {
let any: anyhow::Error = TransactionError::AlreadyClosed.into();
let mapped = LoraError::from_anyhow(any);
assert_eq!(mapped.code(), LoraErrorCode::TransactionFailure);
assert_eq!(mapped.message(), "transaction is already closed");
}
#[test]
fn code_wire_strings_are_stable() {
assert_eq!(LoraErrorCode::Parse.as_str(), "LORA_PARSE");
assert_eq!(LoraErrorCode::Timeout.as_str(), "LORA_TIMEOUT");
assert_eq!(LoraErrorCode::WalPoisoned.as_str(), "LORA_WAL_POISONED");
assert_eq!(
LoraErrorCode::UniqueConstraint.as_str(),
"LORA_UNIQUE_CONSTRAINT"
);
assert_eq!(
LoraErrorCode::NotNullConstraint.as_str(),
"LORA_NOT_NULL_CONSTRAINT"
);
assert_eq!(LoraErrorCode::Connection.as_str(), "LORA_CONNECTION");
assert_eq!(LoraErrorCode::Locked.as_str(), "LORA_LOCKED");
assert_eq!(
LoraErrorCode::ChangesTruncated.as_str(),
"LORA_CHANGES_TRUNCATED"
);
assert_eq!(LoraErrorCode::ChangesLagged.as_str(), "LORA_CHANGES_LAGGED");
assert_eq!(LoraErrorCode::Internal.as_str(), "LORA_INTERNAL");
}
#[test]
fn constraint_messages_route_to_specific_codes() {
let unique = LoraError::from(ExecutorError::ConstraintViolation(
"[22N79] property uniqueness constraint violated".into(),
));
assert_eq!(unique.code(), LoraErrorCode::UniqueConstraint);
let not_null = LoraError::from(ExecutorError::ConstraintViolation(
"[22N77] property presence verification failed".into(),
));
assert_eq!(not_null.code(), LoraErrorCode::NotNullConstraint);
let missing = LoraError::from_anyhow(anyhow::anyhow!(
"[42N51] no index named `missing` exists in the catalog"
));
assert_eq!(missing.code(), LoraErrorCode::NotFound);
}
#[test]
fn database_operation_errors_route_to_specific_codes() {
let cases = [
(
DatabaseOperationError::invalid_params("bad params"),
LoraErrorCode::InvalidParams,
),
(
DatabaseOperationError::invalid_vector("bad vector"),
LoraErrorCode::InvalidVector,
),
(
DatabaseOperationError::validation("bad request"),
LoraErrorCode::Validation,
),
(
DatabaseOperationError::not_found("missing"),
LoraErrorCode::NotFound,
),
(
DatabaseOperationError::constraint_violation("conflict"),
LoraErrorCode::ConstraintViolation,
),
(
DatabaseOperationError::unique_constraint("duplicate"),
LoraErrorCode::UniqueConstraint,
),
(
DatabaseOperationError::not_null_constraint("missing property"),
LoraErrorCode::NotNullConstraint,
),
];
for (err, code) in cases {
let mapped = LoraError::from_anyhow(err.into());
assert_eq!(mapped.code(), code);
assert_eq!(mapped.category(), code.category());
}
}
#[test]
fn connection_io_routes_to_connection() {
let mapped = LoraError::from(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"refused",
));
assert_eq!(mapped.code(), LoraErrorCode::Connection);
}
#[test]
fn public_message_sanitizes_server_details() {
let err = LoraError::new(LoraErrorCode::Internal, "panic: secret path /tmp/db");
assert_eq!(
err.public_message(),
"database operation failed unexpectedly"
);
assert_eq!(err.message(), "panic: secret path /tmp/db");
}
}