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
328 | TransactionError::Poisoned => LoraErrorCode::Internal,
329 }
330}
331
332impl fmt::Debug for LoraError {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 f.debug_struct("LoraError")
335 .field("code", &self.code)
336 .field("message", &self.message)
337 .finish()
338 }
339}
340
341impl fmt::Display for LoraError {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 f.write_str(&self.message)
344 }
345}
346
347impl Error for LoraError {
348 fn source(&self) -> Option<&(dyn Error + 'static)> {
349 self.source.as_deref().map(|s| s as &(dyn Error + 'static))
350 }
351}
352
353impl From<ParseError> for LoraError {
356 fn from(e: ParseError) -> Self {
357 let msg = e.to_string();
358 Self::with_source(LoraErrorCode::Parse, msg, e)
359 }
360}
361
362impl From<SemanticError> for LoraError {
363 fn from(e: SemanticError) -> Self {
364 let msg = e.to_string();
365 Self::with_source(LoraErrorCode::Semantic, msg, e)
366 }
367}
368
369impl From<ExecutorError> for LoraError {
370 fn from(e: ExecutorError) -> Self {
371 let code = executor_code(&e);
372 let msg = e.to_string();
373 Self::with_source(code, msg, e)
374 }
375}
376
377impl From<WalError> for LoraError {
378 fn from(e: WalError) -> Self {
379 let code = wal_code(&e);
380 let msg = e.to_string();
381 Self::with_source(code, msg, e)
382 }
383}
384
385impl From<SnapshotCodecError> for LoraError {
386 fn from(e: SnapshotCodecError) -> Self {
387 let code = snapshot_codec_code(&e);
388 let msg = e.to_string();
389 Self::with_source(code, msg, e)
390 }
391}
392
393impl From<SnapshotError> for LoraError {
394 fn from(e: SnapshotError) -> Self {
395 let code = snapshot_store_code(&e);
396 let msg = e.to_string();
397 Self::with_source(code, msg, e)
398 }
399}
400
401impl From<DatabaseNameError> for LoraError {
402 fn from(e: DatabaseNameError) -> Self {
403 let msg = e.to_string();
404 Self::with_source(LoraErrorCode::DatabaseName, msg, e)
405 }
406}
407
408impl From<TransactionError> for LoraError {
409 fn from(e: TransactionError) -> Self {
410 let code = transaction_code(&e);
411 let msg = e.to_string();
412 Self::with_source(code, msg, e)
413 }
414}
415
416impl From<std::io::Error> for LoraError {
417 fn from(e: std::io::Error) -> Self {
418 let msg = e.to_string();
419 Self::with_source(LoraErrorCode::Io, msg, e)
420 }
421}
422
423impl From<anyhow::Error> for LoraError {
424 fn from(e: anyhow::Error) -> Self {
425 Self::from_anyhow(e)
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn parse_error_is_client_parse() {
435 let e = ParseError::new("expected `MATCH`", 0, 5);
436 let mapped: LoraError = anyhow::Error::from(e).into();
437 assert_eq!(mapped.code(), LoraErrorCode::Parse);
438 assert_eq!(mapped.category(), LoraErrorCategory::Client);
439 assert!(mapped.message().contains("parse error"));
440 }
441
442 #[test]
443 fn semantic_error_is_client_semantic() {
444 let e = SemanticError::UnknownVariable("n".into());
445 let mapped = LoraError::from(e);
446 assert_eq!(mapped.code(), LoraErrorCode::Semantic);
447 assert_eq!(mapped.message(), "unknown variable `n`");
448 }
449
450 #[test]
451 fn executor_timeout_is_client_timeout() {
452 let mapped = LoraError::from(ExecutorError::QueryTimeout);
453 assert_eq!(mapped.code(), LoraErrorCode::Timeout);
454 }
455
456 #[test]
457 fn wal_io_is_server_io() {
458 let inner = std::io::Error::other("disk full");
459 let mapped = LoraError::from(WalError::Io(inner));
460 assert_eq!(mapped.code(), LoraErrorCode::Io);
461 assert_eq!(mapped.category(), LoraErrorCategory::Server);
462 }
463
464 #[test]
465 fn unknown_anyhow_falls_back_to_internal() {
466 let e = anyhow::anyhow!("something else entirely");
467 let mapped = LoraError::from_anyhow(e);
468 assert_eq!(mapped.code(), LoraErrorCode::Internal);
469 }
470
471 #[test]
472 fn typed_transaction_error_routes_readonly() {
473 let mapped = LoraError::from(TransactionError::ReadOnlyMutation);
474 assert_eq!(mapped.code(), LoraErrorCode::ReadOnlyViolation);
475 assert_eq!(
476 mapped.message(),
477 "cannot execute mutating query in read-only transaction"
478 );
479 }
480
481 #[test]
482 fn typed_transaction_error_round_trips_through_anyhow() {
483 let any: anyhow::Error = TransactionError::AlreadyClosed.into();
484 let mapped = LoraError::from_anyhow(any);
485 assert_eq!(mapped.code(), LoraErrorCode::Internal);
486 assert_eq!(mapped.message(), "transaction is already closed");
487 }
488
489 #[test]
490 fn code_wire_strings_are_stable() {
491 assert_eq!(LoraErrorCode::Parse.as_str(), "LORA_PARSE");
494 assert_eq!(LoraErrorCode::Timeout.as_str(), "LORA_TIMEOUT");
495 assert_eq!(LoraErrorCode::WalPoisoned.as_str(), "LORA_WAL_POISONED");
496 assert_eq!(LoraErrorCode::Internal.as_str(), "LORA_INTERNAL");
497 }
498}