1use std::error::Error;
23use std::fmt;
24
25use lora_analyzer::SemanticError;
26use lora_executor::ExecutorError;
27use lora_parser::ParseError;
28use lora_snapshot::SnapshotCodecError;
29use lora_store::SnapshotError;
30use lora_wal::{WalBufferedCommitError, WalCommitError, WalError};
31
32use crate::transaction::TransactionError;
33use crate::DatabaseNameError;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum LoraErrorCode {
41 Parse,
44 Semantic,
46 InvalidParams,
48 ReadOnlyViolation,
50 NotFound,
52 ConstraintViolation,
54 InvalidVector,
56 Timeout,
58 DatabaseName,
60 Config,
62
63 Io,
66 WalCorruption,
68 WalPoisoned,
70 SnapshotCodec,
72 SnapshotCrypto,
74 Internal,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum LoraErrorCategory {
83 Client,
84 Server,
85}
86
87impl LoraErrorCategory {
88 pub fn as_str(self) -> &'static str {
89 match self {
90 Self::Client => "client",
91 Self::Server => "server",
92 }
93 }
94}
95
96impl LoraErrorCode {
97 pub fn as_str(self) -> &'static str {
99 match self {
100 Self::Parse => "LORA_PARSE",
101 Self::Semantic => "LORA_SEMANTIC",
102 Self::InvalidParams => "LORA_INVALID_PARAMS",
103 Self::ReadOnlyViolation => "LORA_READ_ONLY",
104 Self::NotFound => "LORA_NOT_FOUND",
105 Self::ConstraintViolation => "LORA_CONSTRAINT",
106 Self::InvalidVector => "LORA_INVALID_VECTOR",
107 Self::Timeout => "LORA_TIMEOUT",
108 Self::DatabaseName => "LORA_DATABASE_NAME",
109 Self::Config => "LORA_CONFIG",
110 Self::Io => "LORA_IO",
111 Self::WalCorruption => "LORA_WAL_CORRUPTION",
112 Self::WalPoisoned => "LORA_WAL_POISONED",
113 Self::SnapshotCodec => "LORA_SNAPSHOT_CODEC",
114 Self::SnapshotCrypto => "LORA_SNAPSHOT_CRYPTO",
115 Self::Internal => "LORA_INTERNAL",
116 }
117 }
118
119 pub fn category(self) -> LoraErrorCategory {
121 match self {
122 Self::Parse
123 | Self::Semantic
124 | Self::InvalidParams
125 | Self::ReadOnlyViolation
126 | Self::NotFound
127 | Self::ConstraintViolation
128 | Self::InvalidVector
129 | Self::Timeout
130 | Self::DatabaseName
131 | Self::Config => LoraErrorCategory::Client,
132 Self::Io
133 | Self::WalCorruption
134 | Self::WalPoisoned
135 | Self::SnapshotCodec
136 | Self::SnapshotCrypto
137 | Self::Internal => LoraErrorCategory::Server,
138 }
139 }
140}
141
142impl fmt::Display for LoraErrorCode {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(self.as_str())
145 }
146}
147
148pub struct LoraError {
154 code: LoraErrorCode,
155 message: String,
156 source: Option<Box<dyn Error + Send + Sync + 'static>>,
157}
158
159impl LoraError {
160 pub fn new(code: LoraErrorCode, message: impl Into<String>) -> Self {
161 Self {
162 code,
163 message: message.into(),
164 source: None,
165 }
166 }
167
168 pub fn with_source(
169 code: LoraErrorCode,
170 message: impl Into<String>,
171 source: impl Error + Send + Sync + 'static,
172 ) -> Self {
173 Self {
174 code,
175 message: message.into(),
176 source: Some(Box::new(source)),
177 }
178 }
179
180 pub fn code(&self) -> LoraErrorCode {
181 self.code
182 }
183
184 pub fn message(&self) -> &str {
185 &self.message
186 }
187
188 pub fn category(&self) -> LoraErrorCategory {
189 self.code.category()
190 }
191
192 pub fn from_anyhow(err: anyhow::Error) -> Self {
197 Self::from_anyhow_ref(&err)
198 }
199
200 pub fn from_anyhow_ref(err: &anyhow::Error) -> Self {
204 if let Some(e) = err.downcast_ref::<LoraError>() {
208 return Self::new(e.code, e.message.clone());
209 }
210 if let Some(e) = err.downcast_ref::<ParseError>() {
211 return Self::new(LoraErrorCode::Parse, e.to_string());
212 }
213 if let Some(e) = err.downcast_ref::<SemanticError>() {
214 return Self::new(LoraErrorCode::Semantic, e.to_string());
215 }
216 if let Some(e) = err.downcast_ref::<ExecutorError>() {
217 return Self::new(executor_code(e), e.to_string());
218 }
219 if let Some(e) = err.downcast_ref::<WalError>() {
220 return Self::new(wal_code(e), e.to_string());
221 }
222 if let Some(e) = err.downcast_ref::<WalCommitError>() {
223 return Self::new(wal_commit_code(e), e.to_string());
224 }
225 if let Some(e) = err.downcast_ref::<WalBufferedCommitError>() {
226 return Self::new(wal_buffered_commit_code(e), e.to_string());
227 }
228 if let Some(e) = err.downcast_ref::<SnapshotCodecError>() {
229 return Self::new(snapshot_codec_code(e), e.to_string());
230 }
231 if let Some(e) = err.downcast_ref::<SnapshotError>() {
232 return Self::new(snapshot_store_code(e), e.to_string());
233 }
234 if let Some(e) = err.downcast_ref::<DatabaseNameError>() {
235 return Self::new(LoraErrorCode::DatabaseName, e.to_string());
236 }
237 if let Some(e) = err.downcast_ref::<TransactionError>() {
238 return Self::new(transaction_code(e), e.to_string());
239 }
240 if let Some(e) = err.downcast_ref::<std::io::Error>() {
241 return Self::new(LoraErrorCode::Io, e.to_string());
242 }
243 Self::new(LoraErrorCode::Internal, format!("{err:#}"))
248 }
249}
250
251fn executor_code(err: &ExecutorError) -> LoraErrorCode {
252 match err {
253 ExecutorError::ReadOnlyCreate { .. }
254 | ExecutorError::ReadOnlyMerge { .. }
255 | ExecutorError::ReadOnlyDelete { .. }
256 | ExecutorError::ReadOnlySet { .. }
257 | ExecutorError::ReadOnlyRemove { .. } => LoraErrorCode::ReadOnlyViolation,
258 ExecutorError::QueryTimeout => LoraErrorCode::Timeout,
259 ExecutorError::DeleteNodeWithRelationships { .. } => LoraErrorCode::ConstraintViolation,
260 _ => LoraErrorCode::Internal,
261 }
262}
263
264fn wal_code(err: &WalError) -> LoraErrorCode {
265 match err {
266 WalError::Io(_) | WalError::AlreadyOpen { .. } => LoraErrorCode::Io,
267 WalError::CrcMismatch { .. }
268 | WalError::Truncated { .. }
269 | WalError::UnknownKind(_)
270 | WalError::BadSegmentHeader(_)
271 | WalError::Malformed(_)
272 | WalError::Encode(_)
273 | WalError::Decode(_) => LoraErrorCode::WalCorruption,
274 WalError::Poisoned => LoraErrorCode::WalPoisoned,
275 }
276}
277
278fn wal_commit_code(err: &WalCommitError) -> LoraErrorCode {
279 match err {
280 WalCommitError::Commit(inner) | WalCommitError::Flush(inner) => wal_code(inner),
281 }
282}
283
284fn wal_buffered_commit_code(err: &WalBufferedCommitError) -> LoraErrorCode {
285 match err {
286 WalBufferedCommitError::Arm(inner) => wal_code(inner),
287 WalBufferedCommitError::Poisoned(_) | WalBufferedCommitError::ReplayPoisoned(_) => {
288 LoraErrorCode::WalPoisoned
289 }
290 WalBufferedCommitError::Commit(inner) => wal_commit_code(inner),
291 }
292}
293
294fn snapshot_codec_code(err: &SnapshotCodecError) -> LoraErrorCode {
295 match err {
296 SnapshotCodecError::Io(_) => LoraErrorCode::Io,
297 SnapshotCodecError::MissingEncryptionKey(_)
298 | SnapshotCodecError::MissingPassword(_)
299 | SnapshotCodecError::PasswordKdf(_)
300 | SnapshotCodecError::Encrypt
301 | SnapshotCodecError::Decrypt => LoraErrorCode::SnapshotCrypto,
302 SnapshotCodecError::BadMagic
303 | SnapshotCodecError::UnsupportedVersion(_)
304 | SnapshotCodecError::UnsupportedCompression(_)
305 | SnapshotCodecError::ChecksumMismatch
306 | SnapshotCodecError::Encode(_)
307 | SnapshotCodecError::Decode(_) => LoraErrorCode::SnapshotCodec,
308 }
309}
310
311fn snapshot_store_code(err: &SnapshotError) -> LoraErrorCode {
312 match err {
313 SnapshotError::Io(_) => LoraErrorCode::Io,
314 SnapshotError::Decode(_) | SnapshotError::Encode(_) => LoraErrorCode::SnapshotCodec,
315 }
316}
317
318fn transaction_code(err: &TransactionError) -> LoraErrorCode {
319 match err {
320 TransactionError::ReadOnlyMutation
321 | TransactionError::ReadOnlyCommit
322 | TransactionError::StreamingRequiresReadWrite => LoraErrorCode::ReadOnlyViolation,
323 TransactionError::AlreadyClosed
324 | TransactionError::NoGraphGuard
325 | TransactionError::NoStagedGraph
326 | TransactionError::CursorActiveCommit
327 | TransactionError::CursorActiveStatement => LoraErrorCode::Internal,
328 }
329}
330
331impl fmt::Debug for LoraError {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 f.debug_struct("LoraError")
334 .field("code", &self.code)
335 .field("message", &self.message)
336 .finish()
337 }
338}
339
340impl fmt::Display for LoraError {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 f.write_str(&self.message)
343 }
344}
345
346impl Error for LoraError {
347 fn source(&self) -> Option<&(dyn Error + 'static)> {
348 self.source.as_deref().map(|s| s as &(dyn Error + 'static))
349 }
350}
351
352impl From<ParseError> for LoraError {
355 fn from(e: ParseError) -> Self {
356 let msg = e.to_string();
357 Self::with_source(LoraErrorCode::Parse, msg, e)
358 }
359}
360
361impl From<SemanticError> for LoraError {
362 fn from(e: SemanticError) -> Self {
363 let msg = e.to_string();
364 Self::with_source(LoraErrorCode::Semantic, msg, e)
365 }
366}
367
368impl From<ExecutorError> for LoraError {
369 fn from(e: ExecutorError) -> Self {
370 let code = executor_code(&e);
371 let msg = e.to_string();
372 Self::with_source(code, msg, e)
373 }
374}
375
376impl From<WalError> for LoraError {
377 fn from(e: WalError) -> Self {
378 let code = wal_code(&e);
379 let msg = e.to_string();
380 Self::with_source(code, msg, e)
381 }
382}
383
384impl From<SnapshotCodecError> for LoraError {
385 fn from(e: SnapshotCodecError) -> Self {
386 let code = snapshot_codec_code(&e);
387 let msg = e.to_string();
388 Self::with_source(code, msg, e)
389 }
390}
391
392impl From<SnapshotError> for LoraError {
393 fn from(e: SnapshotError) -> Self {
394 let code = snapshot_store_code(&e);
395 let msg = e.to_string();
396 Self::with_source(code, msg, e)
397 }
398}
399
400impl From<DatabaseNameError> for LoraError {
401 fn from(e: DatabaseNameError) -> Self {
402 let msg = e.to_string();
403 Self::with_source(LoraErrorCode::DatabaseName, msg, e)
404 }
405}
406
407impl From<TransactionError> for LoraError {
408 fn from(e: TransactionError) -> Self {
409 let code = transaction_code(&e);
410 let msg = e.to_string();
411 Self::with_source(code, msg, e)
412 }
413}
414
415impl From<std::io::Error> for LoraError {
416 fn from(e: std::io::Error) -> Self {
417 let msg = e.to_string();
418 Self::with_source(LoraErrorCode::Io, msg, e)
419 }
420}
421
422impl From<anyhow::Error> for LoraError {
423 fn from(e: anyhow::Error) -> Self {
424 Self::from_anyhow(e)
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn parse_error_is_client_parse() {
434 let e = ParseError::new("expected `MATCH`", 0, 5);
435 let mapped: LoraError = anyhow::Error::from(e).into();
436 assert_eq!(mapped.code(), LoraErrorCode::Parse);
437 assert_eq!(mapped.category(), LoraErrorCategory::Client);
438 assert!(mapped.message().contains("parse error"));
439 }
440
441 #[test]
442 fn semantic_error_is_client_semantic() {
443 let e = SemanticError::UnknownVariable("n".into());
444 let mapped = LoraError::from(e);
445 assert_eq!(mapped.code(), LoraErrorCode::Semantic);
446 assert_eq!(mapped.message(), "unknown variable `n`");
447 }
448
449 #[test]
450 fn executor_timeout_is_client_timeout() {
451 let mapped = LoraError::from(ExecutorError::QueryTimeout);
452 assert_eq!(mapped.code(), LoraErrorCode::Timeout);
453 }
454
455 #[test]
456 fn wal_io_is_server_io() {
457 let inner = std::io::Error::other("disk full");
458 let mapped = LoraError::from(WalError::Io(inner));
459 assert_eq!(mapped.code(), LoraErrorCode::Io);
460 assert_eq!(mapped.category(), LoraErrorCategory::Server);
461 }
462
463 #[test]
464 fn unknown_anyhow_falls_back_to_internal() {
465 let e = anyhow::anyhow!("something else entirely");
466 let mapped = LoraError::from_anyhow(e);
467 assert_eq!(mapped.code(), LoraErrorCode::Internal);
468 }
469
470 #[test]
471 fn typed_transaction_error_routes_readonly() {
472 let mapped = LoraError::from(TransactionError::ReadOnlyMutation);
473 assert_eq!(mapped.code(), LoraErrorCode::ReadOnlyViolation);
474 assert_eq!(
475 mapped.message(),
476 "cannot execute mutating query in read-only transaction"
477 );
478 }
479
480 #[test]
481 fn typed_transaction_error_round_trips_through_anyhow() {
482 let any: anyhow::Error = TransactionError::AlreadyClosed.into();
483 let mapped = LoraError::from_anyhow(any);
484 assert_eq!(mapped.code(), LoraErrorCode::Internal);
485 assert_eq!(mapped.message(), "transaction is already closed");
486 }
487
488 #[test]
489 fn code_wire_strings_are_stable() {
490 assert_eq!(LoraErrorCode::Parse.as_str(), "LORA_PARSE");
493 assert_eq!(LoraErrorCode::Timeout.as_str(), "LORA_TIMEOUT");
494 assert_eq!(LoraErrorCode::WalPoisoned.as_str(), "LORA_WAL_POISONED");
495 assert_eq!(LoraErrorCode::Internal.as_str(), "LORA_INTERNAL");
496 }
497}