1use std::error::Error;
23use std::fmt;
24
25use lora_analyzer::SemanticError;
26use lora_executor::{ExecutorError, PropertyConversionError};
27use lora_parser::ParseError;
28use lora_snapshot::SnapshotCodecError;
29use lora_store::SnapshotError;
30use lora_wal::{WalBufferedCommitError, WalCommitError, WalError};
31use thiserror::Error;
32
33use crate::transaction::TransactionError;
34use crate::DatabaseNameError;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LoraErrorCode {
42 Parse,
45 Semantic,
47 InvalidParams,
49 ReadOnlyViolation,
51 NotFound,
53 ConstraintViolation,
55 InvalidVector,
57 Timeout,
59 DatabaseName,
61 Config,
63 Validation,
65 UniqueConstraint,
67 NotNullConstraint,
69 ForeignKeyViolation,
71 TransactionFailure,
73
74 Io,
77 Connection,
79 WalCorruption,
81 WalPoisoned,
83 SnapshotCodec,
85 SnapshotCrypto,
87 Internal,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum LoraErrorCategory {
96 Client,
97 Server,
98}
99
100impl LoraErrorCategory {
101 pub fn as_str(self) -> &'static str {
102 match self {
103 Self::Client => "client",
104 Self::Server => "server",
105 }
106 }
107}
108
109impl LoraErrorCode {
110 pub fn as_str(self) -> &'static str {
112 match self {
113 Self::Parse => "LORA_PARSE",
114 Self::Semantic => "LORA_SEMANTIC",
115 Self::InvalidParams => "LORA_INVALID_PARAMS",
116 Self::ReadOnlyViolation => "LORA_READ_ONLY",
117 Self::NotFound => "LORA_NOT_FOUND",
118 Self::ConstraintViolation => "LORA_CONSTRAINT",
119 Self::InvalidVector => "LORA_INVALID_VECTOR",
120 Self::Timeout => "LORA_TIMEOUT",
121 Self::DatabaseName => "LORA_DATABASE_NAME",
122 Self::Config => "LORA_CONFIG",
123 Self::Validation => "LORA_VALIDATION",
124 Self::UniqueConstraint => "LORA_UNIQUE_CONSTRAINT",
125 Self::NotNullConstraint => "LORA_NOT_NULL_CONSTRAINT",
126 Self::ForeignKeyViolation => "LORA_FOREIGN_KEY",
127 Self::TransactionFailure => "LORA_TRANSACTION",
128 Self::Io => "LORA_IO",
129 Self::Connection => "LORA_CONNECTION",
130 Self::WalCorruption => "LORA_WAL_CORRUPTION",
131 Self::WalPoisoned => "LORA_WAL_POISONED",
132 Self::SnapshotCodec => "LORA_SNAPSHOT_CODEC",
133 Self::SnapshotCrypto => "LORA_SNAPSHOT_CRYPTO",
134 Self::Internal => "LORA_INTERNAL",
135 }
136 }
137
138 pub fn category(self) -> LoraErrorCategory {
140 match self {
141 Self::Parse
142 | Self::Semantic
143 | Self::InvalidParams
144 | Self::ReadOnlyViolation
145 | Self::NotFound
146 | Self::ConstraintViolation
147 | Self::InvalidVector
148 | Self::Timeout
149 | Self::DatabaseName
150 | Self::Config
151 | Self::Validation
152 | Self::UniqueConstraint
153 | Self::NotNullConstraint
154 | Self::ForeignKeyViolation
155 | Self::TransactionFailure => LoraErrorCategory::Client,
156 Self::Io
157 | Self::Connection
158 | Self::WalCorruption
159 | Self::WalPoisoned
160 | Self::SnapshotCodec
161 | Self::SnapshotCrypto
162 | Self::Internal => LoraErrorCategory::Server,
163 }
164 }
165}
166
167impl fmt::Display for LoraErrorCode {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.write_str(self.as_str())
170 }
171}
172
173pub struct LoraError {
179 code: LoraErrorCode,
180 message: String,
181 source: Option<Box<dyn Error + Send + Sync + 'static>>,
182}
183
184#[derive(Debug)]
185struct InternalAnyhowSource(String);
186
187impl fmt::Display for InternalAnyhowSource {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.write_str(&self.0)
190 }
191}
192
193impl Error for InternalAnyhowSource {}
194
195impl LoraError {
196 pub fn new(code: LoraErrorCode, message: impl Into<String>) -> Self {
197 Self {
198 code,
199 message: message.into(),
200 source: None,
201 }
202 }
203
204 pub fn with_source(
205 code: LoraErrorCode,
206 message: impl Into<String>,
207 source: impl Error + Send + Sync + 'static,
208 ) -> Self {
209 Self {
210 code,
211 message: message.into(),
212 source: Some(Box::new(source)),
213 }
214 }
215
216 pub fn code(&self) -> LoraErrorCode {
217 self.code
218 }
219
220 pub fn message(&self) -> &str {
221 &self.message
222 }
223
224 pub fn category(&self) -> LoraErrorCategory {
225 self.code.category()
226 }
227
228 pub fn public_message(&self) -> String {
233 match self.category() {
234 LoraErrorCategory::Client => self.message.clone(),
235 LoraErrorCategory::Server => match self.code {
236 LoraErrorCode::Io => "database storage is temporarily unavailable".to_string(),
237 LoraErrorCode::Connection => {
238 "database connection is temporarily unavailable".to_string()
239 }
240 LoraErrorCode::WalCorruption => {
241 "database write-ahead log is unreadable".to_string()
242 }
243 LoraErrorCode::WalPoisoned => {
244 "database write-ahead log is unavailable until recovery".to_string()
245 }
246 LoraErrorCode::SnapshotCodec => "snapshot data could not be decoded".to_string(),
247 LoraErrorCode::SnapshotCrypto => {
248 "snapshot encryption could not be processed".to_string()
249 }
250 LoraErrorCode::Internal => "database operation failed unexpectedly".to_string(),
251 _ => self.message.clone(),
254 },
255 }
256 }
257
258 pub fn debug_context(&self) -> String {
261 let mut out = self.message.clone();
262 let mut source = self.source();
263 while let Some(err) = source {
264 out.push_str(": ");
265 out.push_str(&err.to_string());
266 source = err.source();
267 }
268 out
269 }
270
271 pub fn from_anyhow(err: anyhow::Error) -> Self {
276 let mapped = Self::from_anyhow_ref(&err);
277 if mapped.code == LoraErrorCode::Internal {
278 Self::with_source(
279 LoraErrorCode::Internal,
280 "database operation failed unexpectedly",
281 InternalAnyhowSource(format!("{err:#}")),
282 )
283 } else {
284 mapped
285 }
286 }
287
288 pub fn from_anyhow_ref(err: &anyhow::Error) -> Self {
292 if let Some(e) = err.downcast_ref::<LoraError>() {
296 return Self::new(e.code, e.message.clone());
297 }
298 if let Some(e) = err.downcast_ref::<ParseError>() {
299 return Self::new(LoraErrorCode::Parse, e.to_string());
300 }
301 if let Some(e) = err.downcast_ref::<SemanticError>() {
302 return Self::new(LoraErrorCode::Semantic, e.to_string());
303 }
304 if let Some(e) = err.downcast_ref::<ExecutorError>() {
305 return Self::new(executor_code(e), e.to_string());
306 }
307 if let Some(e) = err.downcast_ref::<PropertyConversionError>() {
308 return Self::new(LoraErrorCode::Validation, e.to_string());
309 }
310 if let Some(e) = err.downcast_ref::<WalError>() {
311 return Self::new(wal_code(e), e.to_string());
312 }
313 if let Some(e) = err.downcast_ref::<WalCommitError>() {
314 return Self::new(wal_commit_code(e), e.to_string());
315 }
316 if let Some(e) = err.downcast_ref::<WalBufferedCommitError>() {
317 return Self::new(wal_buffered_commit_code(e), e.to_string());
318 }
319 if let Some(e) = err.downcast_ref::<SnapshotCodecError>() {
320 return Self::new(snapshot_codec_code(e), e.to_string());
321 }
322 if let Some(e) = err.downcast_ref::<SnapshotError>() {
323 return Self::new(snapshot_store_code(e), e.to_string());
324 }
325 if let Some(e) = err.downcast_ref::<DatabaseNameError>() {
326 return Self::new(LoraErrorCode::DatabaseName, e.to_string());
327 }
328 if let Some(e) = err.downcast_ref::<TransactionError>() {
329 return Self::new(transaction_code(e), e.to_string());
330 }
331 if let Some(e) = err.downcast_ref::<DatabaseOperationError>() {
332 return Self::new(e.code(), e.to_string());
333 }
334 if let Some(e) = err.downcast_ref::<std::io::Error>() {
335 return Self::new(io_code(e), e.to_string());
336 }
337 if let Some(code) = legacy_message_code(&err.to_string()) {
338 return Self::new(code, err.to_string());
339 }
340 Self::new(
345 LoraErrorCode::Internal,
346 "database operation failed unexpectedly",
347 )
348 }
349}
350
351fn executor_code(err: &ExecutorError) -> LoraErrorCode {
352 match err {
353 ExecutorError::ReadOnlyCreate { .. }
354 | ExecutorError::ReadOnlyMerge { .. }
355 | ExecutorError::ReadOnlyDelete { .. }
356 | ExecutorError::ReadOnlySet { .. }
357 | ExecutorError::ReadOnlyRemove { .. }
358 | ExecutorError::ReadOnlyForeach { .. } => LoraErrorCode::ReadOnlyViolation,
359 ExecutorError::QueryTimeout => LoraErrorCode::Timeout,
360 ExecutorError::ConstraintViolation(message) => constraint_message_code(message),
361 ExecutorError::DeleteNodeWithRelationships { .. } => LoraErrorCode::ForeignKeyViolation,
362 ExecutorError::RelationshipCreateFailed { .. } => LoraErrorCode::ForeignKeyViolation,
363 ExecutorError::NodeCreateFailed => LoraErrorCode::Internal,
364 ExecutorError::RuntimeError(message) => validation_message_code(message),
365 ExecutorError::ExpectedNodeForExpand { .. }
366 | ExecutorError::ExpectedPropertyMap { .. }
367 | ExecutorError::GroupByNotLowered
368 | ExecutorError::AggregateNotLowered
369 | ExecutorError::UnsupportedCreateRelationshipRange
370 | ExecutorError::MissingRelationshipType
371 | ExecutorError::DeleteRelationshipFailed { .. }
372 | ExecutorError::InvalidDeleteTarget { .. }
373 | ExecutorError::ExpectedNodeForRemoveLabels { .. }
374 | ExecutorError::UnboundVariableForRemove { .. }
375 | ExecutorError::ExpectedNodeForSetLabels { .. }
376 | ExecutorError::UnboundVariableForSet { .. }
377 | ExecutorError::InvalidSetTarget { .. }
378 | ExecutorError::UnsupportedRemoveTarget
379 | ExecutorError::InvalidRemoveTarget { .. }
380 | ExecutorError::UnsupportedSetTarget
381 | ExecutorError::ExpectedRelationshipForExpand { .. } => LoraErrorCode::Validation,
382 }
383}
384
385fn constraint_message_code(message: &str) -> LoraErrorCode {
386 if message.contains("22N79") || message.contains("22N70") || message.contains("22N71") {
387 LoraErrorCode::UniqueConstraint
388 } else if message.contains("22N77") {
389 LoraErrorCode::NotNullConstraint
390 } else if message.contains("42N51") {
391 LoraErrorCode::NotFound
392 } else if message.contains("22N78") || message.contains("22N90") {
393 LoraErrorCode::Validation
394 } else {
395 LoraErrorCode::ConstraintViolation
396 }
397}
398
399fn validation_message_code(message: &str) -> LoraErrorCode {
400 let lower = message.to_ascii_lowercase();
401 if lower.contains("vector") {
402 LoraErrorCode::InvalidVector
403 } else {
404 LoraErrorCode::Validation
405 }
406}
407
408fn legacy_message_code(message: &str) -> Option<LoraErrorCode> {
409 if message.contains("22N")
410 || message.contains("42N51")
411 || message.contains("50N11")
412 || message.contains("constraint")
413 || message.contains("CONSTRAINT")
414 {
415 return Some(constraint_message_code(message));
416 }
417 None
418}
419
420fn wal_code(err: &WalError) -> LoraErrorCode {
421 match err {
422 WalError::Io(inner) => io_code(inner),
423 WalError::AlreadyOpen { .. } => LoraErrorCode::Connection,
424 WalError::CrcMismatch { .. }
425 | WalError::Truncated { .. }
426 | WalError::UnknownKind(_)
427 | WalError::BadSegmentHeader(_)
428 | WalError::Malformed(_)
429 | WalError::Encode(_)
430 | WalError::Decode(_) => LoraErrorCode::WalCorruption,
431 WalError::Poisoned => LoraErrorCode::WalPoisoned,
432 }
433}
434
435fn io_code(err: &std::io::Error) -> LoraErrorCode {
436 match err.kind() {
437 std::io::ErrorKind::ConnectionRefused
438 | std::io::ErrorKind::ConnectionReset
439 | std::io::ErrorKind::ConnectionAborted
440 | std::io::ErrorKind::NotConnected
441 | std::io::ErrorKind::AddrInUse
442 | std::io::ErrorKind::AddrNotAvailable => LoraErrorCode::Connection,
443 std::io::ErrorKind::TimedOut => LoraErrorCode::Timeout,
444 _ => LoraErrorCode::Io,
445 }
446}
447
448fn wal_commit_code(err: &WalCommitError) -> LoraErrorCode {
449 match err {
450 WalCommitError::Commit(inner) | WalCommitError::Flush(inner) => wal_code(inner),
451 }
452}
453
454fn wal_buffered_commit_code(err: &WalBufferedCommitError) -> LoraErrorCode {
455 match err {
456 WalBufferedCommitError::Arm(inner) => wal_code(inner),
457 WalBufferedCommitError::Poisoned(_) | WalBufferedCommitError::ReplayPoisoned(_) => {
458 LoraErrorCode::WalPoisoned
459 }
460 WalBufferedCommitError::Commit(inner) => wal_commit_code(inner),
461 }
462}
463
464fn snapshot_codec_code(err: &SnapshotCodecError) -> LoraErrorCode {
465 match err {
466 SnapshotCodecError::Io(inner) => io_code(inner),
467 SnapshotCodecError::MissingEncryptionKey(_)
468 | SnapshotCodecError::MissingPassword(_)
469 | SnapshotCodecError::PasswordKdf(_)
470 | SnapshotCodecError::Encrypt
471 | SnapshotCodecError::Decrypt => LoraErrorCode::SnapshotCrypto,
472 SnapshotCodecError::BadMagic
473 | SnapshotCodecError::UnsupportedVersion(_)
474 | SnapshotCodecError::UnsupportedCompression(_)
475 | SnapshotCodecError::ChecksumMismatch
476 | SnapshotCodecError::Encode(_)
477 | SnapshotCodecError::Decode(_) => LoraErrorCode::SnapshotCodec,
478 }
479}
480
481fn snapshot_store_code(err: &SnapshotError) -> LoraErrorCode {
482 match err {
483 SnapshotError::Io(inner) => io_code(inner),
484 SnapshotError::Decode(_) | SnapshotError::Encode(_) => LoraErrorCode::SnapshotCodec,
485 }
486}
487
488fn transaction_code(err: &TransactionError) -> LoraErrorCode {
489 match err {
490 TransactionError::ReadOnlyMutation
491 | TransactionError::ReadOnlyCommit
492 | TransactionError::StreamingRequiresReadWrite => LoraErrorCode::ReadOnlyViolation,
493 TransactionError::AlreadyClosed
494 | TransactionError::CursorActiveCommit
495 | TransactionError::CursorActiveStatement => LoraErrorCode::TransactionFailure,
496 TransactionError::NoGraphGuard
497 | TransactionError::NoStagedGraph
498 | TransactionError::Poisoned => LoraErrorCode::TransactionFailure,
499 }
500}
501
502#[derive(Debug, Clone, Error)]
503pub(crate) enum DatabaseOperationError {
504 #[error("{0}")]
505 InvalidParams(String),
506 #[error("{0}")]
507 InvalidVector(String),
508 #[error("{0}")]
509 Validation(String),
510 #[error("{0}")]
511 NotFound(String),
512 #[error("{0}")]
513 ConstraintViolation(String),
514 #[error("{0}")]
515 UniqueConstraint(String),
516 #[error("{0}")]
517 NotNullConstraint(String),
518}
519
520impl DatabaseOperationError {
521 pub(crate) fn invalid_params(message: impl Into<String>) -> Self {
522 Self::InvalidParams(message.into())
523 }
524
525 pub(crate) fn invalid_vector(message: impl Into<String>) -> Self {
526 Self::InvalidVector(message.into())
527 }
528
529 pub(crate) fn validation(message: impl Into<String>) -> Self {
530 Self::Validation(message.into())
531 }
532
533 pub(crate) fn not_found(message: impl Into<String>) -> Self {
534 Self::NotFound(message.into())
535 }
536
537 pub(crate) fn constraint_violation(message: impl Into<String>) -> Self {
538 Self::ConstraintViolation(message.into())
539 }
540
541 pub(crate) fn unique_constraint(message: impl Into<String>) -> Self {
542 Self::UniqueConstraint(message.into())
543 }
544
545 pub(crate) fn not_null_constraint(message: impl Into<String>) -> Self {
546 Self::NotNullConstraint(message.into())
547 }
548
549 pub(crate) fn code(&self) -> LoraErrorCode {
550 match self {
551 Self::InvalidParams(_) => LoraErrorCode::InvalidParams,
552 Self::InvalidVector(_) => LoraErrorCode::InvalidVector,
553 Self::Validation(_) => LoraErrorCode::Validation,
554 Self::NotFound(_) => LoraErrorCode::NotFound,
555 Self::ConstraintViolation(_) => LoraErrorCode::ConstraintViolation,
556 Self::UniqueConstraint(_) => LoraErrorCode::UniqueConstraint,
557 Self::NotNullConstraint(_) => LoraErrorCode::NotNullConstraint,
558 }
559 }
560}
561
562impl fmt::Debug for LoraError {
563 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564 f.debug_struct("LoraError")
565 .field("code", &self.code)
566 .field("message", &self.message)
567 .finish()
568 }
569}
570
571impl fmt::Display for LoraError {
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 f.write_str(&self.message)
574 }
575}
576
577impl Error for LoraError {
578 fn source(&self) -> Option<&(dyn Error + 'static)> {
579 self.source.as_deref().map(|s| s as &(dyn Error + 'static))
580 }
581}
582
583impl From<ParseError> for LoraError {
586 fn from(e: ParseError) -> Self {
587 let msg = e.to_string();
588 Self::with_source(LoraErrorCode::Parse, msg, e)
589 }
590}
591
592impl From<SemanticError> for LoraError {
593 fn from(e: SemanticError) -> Self {
594 let msg = e.to_string();
595 Self::with_source(LoraErrorCode::Semantic, msg, e)
596 }
597}
598
599impl From<ExecutorError> for LoraError {
600 fn from(e: ExecutorError) -> Self {
601 let code = executor_code(&e);
602 let msg = e.to_string();
603 Self::with_source(code, msg, e)
604 }
605}
606
607impl From<PropertyConversionError> for LoraError {
608 fn from(e: PropertyConversionError) -> Self {
609 let msg = e.to_string();
610 Self::with_source(LoraErrorCode::Validation, msg, e)
611 }
612}
613
614impl From<WalError> for LoraError {
615 fn from(e: WalError) -> Self {
616 let code = wal_code(&e);
617 let msg = e.to_string();
618 Self::with_source(code, msg, e)
619 }
620}
621
622impl From<SnapshotCodecError> for LoraError {
623 fn from(e: SnapshotCodecError) -> Self {
624 let code = snapshot_codec_code(&e);
625 let msg = e.to_string();
626 Self::with_source(code, msg, e)
627 }
628}
629
630impl From<SnapshotError> for LoraError {
631 fn from(e: SnapshotError) -> Self {
632 let code = snapshot_store_code(&e);
633 let msg = e.to_string();
634 Self::with_source(code, msg, e)
635 }
636}
637
638impl From<DatabaseNameError> for LoraError {
639 fn from(e: DatabaseNameError) -> Self {
640 let msg = e.to_string();
641 Self::with_source(LoraErrorCode::DatabaseName, msg, e)
642 }
643}
644
645impl From<TransactionError> for LoraError {
646 fn from(e: TransactionError) -> Self {
647 let code = transaction_code(&e);
648 let msg = e.to_string();
649 Self::with_source(code, msg, e)
650 }
651}
652
653impl From<std::io::Error> for LoraError {
654 fn from(e: std::io::Error) -> Self {
655 let code = io_code(&e);
656 let msg = e.to_string();
657 Self::with_source(code, msg, e)
658 }
659}
660
661impl From<anyhow::Error> for LoraError {
662 fn from(e: anyhow::Error) -> Self {
663 Self::from_anyhow(e)
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 #[test]
672 fn parse_error_is_client_parse() {
673 let e = ParseError::new("expected `MATCH`", 0, 5);
674 let mapped: LoraError = anyhow::Error::from(e).into();
675 assert_eq!(mapped.code(), LoraErrorCode::Parse);
676 assert_eq!(mapped.category(), LoraErrorCategory::Client);
677 assert!(mapped.message().contains("parse error"));
678 }
679
680 #[test]
681 fn semantic_error_is_client_semantic() {
682 let e = SemanticError::UnknownVariable("n".into());
683 let mapped = LoraError::from(e);
684 assert_eq!(mapped.code(), LoraErrorCode::Semantic);
685 assert_eq!(mapped.message(), "unknown variable `n`");
686 }
687
688 #[test]
689 fn executor_timeout_is_client_timeout() {
690 let mapped = LoraError::from(ExecutorError::QueryTimeout);
691 assert_eq!(mapped.code(), LoraErrorCode::Timeout);
692 }
693
694 #[test]
695 fn wal_io_is_server_io() {
696 let inner = std::io::Error::other("disk full");
697 let mapped = LoraError::from(WalError::Io(inner));
698 assert_eq!(mapped.code(), LoraErrorCode::Io);
699 assert_eq!(mapped.category(), LoraErrorCategory::Server);
700 }
701
702 #[test]
703 fn unknown_anyhow_falls_back_to_internal() {
704 let e = anyhow::anyhow!("something else entirely");
705 let mapped = LoraError::from_anyhow(e);
706 assert_eq!(mapped.code(), LoraErrorCode::Internal);
707 assert_eq!(mapped.message(), "database operation failed unexpectedly");
708 assert!(mapped.debug_context().contains("something else entirely"));
709 }
710
711 #[test]
712 fn typed_transaction_error_routes_readonly() {
713 let mapped = LoraError::from(TransactionError::ReadOnlyMutation);
714 assert_eq!(mapped.code(), LoraErrorCode::ReadOnlyViolation);
715 assert_eq!(
716 mapped.message(),
717 "cannot execute mutating query in read-only transaction"
718 );
719 }
720
721 #[test]
722 fn typed_transaction_error_round_trips_through_anyhow() {
723 let any: anyhow::Error = TransactionError::AlreadyClosed.into();
724 let mapped = LoraError::from_anyhow(any);
725 assert_eq!(mapped.code(), LoraErrorCode::TransactionFailure);
726 assert_eq!(mapped.message(), "transaction is already closed");
727 }
728
729 #[test]
730 fn code_wire_strings_are_stable() {
731 assert_eq!(LoraErrorCode::Parse.as_str(), "LORA_PARSE");
734 assert_eq!(LoraErrorCode::Timeout.as_str(), "LORA_TIMEOUT");
735 assert_eq!(LoraErrorCode::WalPoisoned.as_str(), "LORA_WAL_POISONED");
736 assert_eq!(
737 LoraErrorCode::UniqueConstraint.as_str(),
738 "LORA_UNIQUE_CONSTRAINT"
739 );
740 assert_eq!(
741 LoraErrorCode::NotNullConstraint.as_str(),
742 "LORA_NOT_NULL_CONSTRAINT"
743 );
744 assert_eq!(LoraErrorCode::Connection.as_str(), "LORA_CONNECTION");
745 assert_eq!(LoraErrorCode::Internal.as_str(), "LORA_INTERNAL");
746 }
747
748 #[test]
749 fn constraint_messages_route_to_specific_codes() {
750 let unique = LoraError::from(ExecutorError::ConstraintViolation(
751 "[22N79] property uniqueness constraint violated".into(),
752 ));
753 assert_eq!(unique.code(), LoraErrorCode::UniqueConstraint);
754
755 let not_null = LoraError::from(ExecutorError::ConstraintViolation(
756 "[22N77] property presence verification failed".into(),
757 ));
758 assert_eq!(not_null.code(), LoraErrorCode::NotNullConstraint);
759
760 let missing = LoraError::from_anyhow(anyhow::anyhow!(
761 "[42N51] no index named `missing` exists in the catalog"
762 ));
763 assert_eq!(missing.code(), LoraErrorCode::NotFound);
764 }
765
766 #[test]
767 fn database_operation_errors_route_to_specific_codes() {
768 let cases = [
769 (
770 DatabaseOperationError::invalid_params("bad params"),
771 LoraErrorCode::InvalidParams,
772 ),
773 (
774 DatabaseOperationError::invalid_vector("bad vector"),
775 LoraErrorCode::InvalidVector,
776 ),
777 (
778 DatabaseOperationError::validation("bad request"),
779 LoraErrorCode::Validation,
780 ),
781 (
782 DatabaseOperationError::not_found("missing"),
783 LoraErrorCode::NotFound,
784 ),
785 (
786 DatabaseOperationError::constraint_violation("conflict"),
787 LoraErrorCode::ConstraintViolation,
788 ),
789 (
790 DatabaseOperationError::unique_constraint("duplicate"),
791 LoraErrorCode::UniqueConstraint,
792 ),
793 (
794 DatabaseOperationError::not_null_constraint("missing property"),
795 LoraErrorCode::NotNullConstraint,
796 ),
797 ];
798
799 for (err, code) in cases {
800 let mapped = LoraError::from_anyhow(err.into());
801 assert_eq!(mapped.code(), code);
802 assert_eq!(mapped.category(), code.category());
803 }
804 }
805
806 #[test]
807 fn connection_io_routes_to_connection() {
808 let mapped = LoraError::from(std::io::Error::new(
809 std::io::ErrorKind::ConnectionRefused,
810 "refused",
811 ));
812 assert_eq!(mapped.code(), LoraErrorCode::Connection);
813 }
814
815 #[test]
816 fn public_message_sanitizes_server_details() {
817 let err = LoraError::new(LoraErrorCode::Internal, "panic: secret path /tmp/db");
818 assert_eq!(
819 err.public_message(),
820 "database operation failed unexpectedly"
821 );
822 assert_eq!(err.message(), "panic: secret path /tmp/db");
823 }
824}