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