1use std::path::PathBuf;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum DatabaseImagePublicationErrorClass {
14 SourceChanged,
19 PrecommitRolledBack,
24 OutcomeIndeterminate,
30}
31
32#[derive(Error, Debug)]
38pub enum FrankenError {
39 #[error("database not found: '{path}'")]
42 DatabaseNotFound { path: PathBuf },
43
44 #[error("database is locked: '{path}'")]
46 DatabaseLocked { path: PathBuf },
47
48 #[error("multi-process contract violation: {detail}")]
57 MultiProcessContractViolation { detail: String },
58
59 #[error("database disk image is malformed: {detail}")]
61 DatabaseCorrupt { detail: String },
62
63 #[error("file is not a database: '{path}'")]
65 NotADatabase { path: PathBuf },
66
67 #[error(
75 "this database was written by a newer fsqlite ({supported} vs {on_disk}); refusing to open to prevent corruption. Either upgrade fsqlite or restore from a pre-upgrade backup"
76 )]
77 NewerFormat { on_disk: u32, supported: u32 },
78
79 #[error("database is full")]
81 DatabaseFull,
82
83 #[error("database schema has changed")]
85 SchemaChanged,
86
87 #[error("I/O error: {0}")]
90 Io(#[from] std::io::Error),
91
92 #[error("disk I/O error reading page {page}")]
94 IoRead { page: u32 },
95
96 #[error("disk I/O error writing page {page}")]
98 IoWrite { page: u32 },
99
100 #[error("short read: expected {expected} bytes, got {actual}")]
102 ShortRead { expected: usize, actual: usize },
103
104 #[error("near \"{token}\": syntax error")]
107 SyntaxError { token: String },
108
109 #[error("SQL error at offset {offset}: {detail}")]
111 ParseError { offset: usize, detail: String },
112
113 #[error("query returned no rows")]
115 QueryReturnedNoRows,
116
117 #[error("query returned more than one row")]
119 QueryReturnedMultipleRows,
120
121 #[error("no such table: {name}")]
123 NoSuchTable { name: String },
124
125 #[error("no such column: {name}")]
127 NoSuchColumn { name: String },
128
129 #[error("no such index: {name}")]
131 NoSuchIndex { name: String },
132
133 #[error("table {name} already exists")]
135 TableExists { name: String },
136
137 #[error("index {name} already exists")]
139 IndexExists { name: String },
140
141 #[error("ambiguous column name: {name}")]
143 AmbiguousColumn { name: String },
144
145 #[error("UNIQUE constraint failed: {columns}")]
148 UniqueViolation { columns: String },
149
150 #[error("NOT NULL constraint failed: {column}")]
152 NotNullViolation { column: String },
153
154 #[error("CHECK constraint failed: {name}")]
156 CheckViolation { name: String },
157
158 #[error("FOREIGN KEY constraint failed")]
160 ForeignKeyViolation,
161
162 #[error("PRIMARY KEY constraint failed")]
164 PrimaryKeyViolation,
165
166 #[error("cannot store {actual} value in {column_type} column {column}")]
168 DatatypeViolation {
169 column: String,
170 column_type: String,
171 actual: String,
172 },
173
174 #[error("rtree constraint failed: {constraint}")]
180 RtreeConstraint { constraint: String },
181
182 #[error("cannot start a transaction within a transaction")]
185 NestedTransaction,
186
187 #[error("cannot commit - no transaction is active")]
189 NoActiveTransaction,
190
191 #[error("cannot VACUUM from within a transaction")]
193 VacuumWithinTransaction,
194
195 #[error("transaction rolled back: {reason}")]
197 TransactionRolledBack { reason: String },
198
199 #[error("database image publication outcome is indeterminate: {detail}")]
202 DatabaseImagePublicationOutcomeIndeterminate { detail: String },
203
204 #[error("write conflict on page {page}: held by transaction {holder}")]
207 WriteConflict { page: u32, holder: u64 },
208
209 #[error("serialization failure: page {page} was modified after snapshot")]
211 SerializationFailure { page: u32 },
212
213 #[error("snapshot too old: transaction {txn_id} is below GC horizon")]
215 SnapshotTooOld { txn_id: u64 },
216
217 #[error("database is busy")]
220 Busy,
221
222 #[error("database is busy (recovery in progress)")]
224 BusyRecovery,
225
226 #[error("database is busy (snapshot conflict on pages: {conflicting_pages})")]
230 BusySnapshot { conflicting_pages: String },
231
232 #[error(
234 "BEGIN CONCURRENT unavailable: fsqlite-shm not present (multi-writer MVCC requires shared memory coordination)"
235 )]
236 ConcurrentUnavailable,
237
238 #[error("type mismatch: expected {expected}, got {actual}")]
241 TypeMismatch { expected: String, actual: String },
242
243 #[error("datatype mismatch")]
248 DatatypeMismatch,
249
250 #[error("integer overflow")]
252 IntegerOverflow,
253
254 #[error("{what} out of range: {value}")]
256 OutOfRange { what: String, value: String },
257
258 #[error("string or BLOB exceeds size limit")]
261 TooBig,
262
263 #[error("too many columns: {count} (max {max})")]
265 TooManyColumns { count: usize, max: usize },
266
267 #[error("SQL statement too long: {length} bytes (max {max})")]
269 SqlTooLong { length: usize, max: usize },
270
271 #[error("expression tree too deep (max {max})")]
273 ExpressionTooDeep { max: usize },
274
275 #[error("too many levels of trigger recursion")]
278 TriggerRecursionDepthExceeded,
279
280 #[error("too many attached databases (max {max})")]
282 TooManyAttached { max: usize },
283
284 #[error("too many arguments to function {name}")]
286 TooManyArguments { name: String },
287
288 #[error("WAL file is corrupt: {detail}")]
291 WalCorrupt { detail: String },
292
293 #[error("WAL checkpoint failed: {detail}")]
295 CheckpointFailed { detail: String },
296
297 #[error("file locking failed: {detail}")]
300 LockFailed { detail: String },
301
302 #[error("unable to open database file: '{path}'")]
304 CannotOpen { path: PathBuf },
305
306 #[error("internal error: {0}")]
309 Internal(String),
310
311 #[error(
315 "cannot start another operation on an AsyncConnection while its synchronous row stream is active; use a separate connection or defer it until the callback returns"
316 )]
317 SynchronousStreamReentrancy,
318
319 #[error("unsupported operation")]
321 Unsupported,
322
323 #[error("not implemented: {0}")]
325 NotImplemented(String),
326
327 #[error("callback requested query abort")]
329 Abort,
330
331 #[error("authorization denied")]
333 AuthDenied,
334
335 #[error("out of memory")]
337 OutOfMemory,
338
339 #[error(
344 "page buffer capacity exhausted during {operation}: page_size={page_size}, max_buffers={max_buffers}, total_buffers={total_buffers}, available_buffers={available_buffers}, cached_clean={cached_clean}, cached_dirty={cached_dirty}, successful_evictions={successful_evictions}"
345 )]
346 PageBufferCapacityExhausted {
347 operation: &'static str,
348 page_size: usize,
349 max_buffers: usize,
350 total_buffers: usize,
351 available_buffers: usize,
352 cached_clean: usize,
353 cached_dirty: usize,
354 successful_evictions: usize,
355 },
356
357 #[error("{0}")]
359 FunctionError(String),
360
361 #[error("{0}")]
369 RaiseFail(String),
370
371 #[error("background worker failed: {0}")]
373 BackgroundWorkerFailed(String),
374
375 #[error("attempt to write a readonly database")]
377 ReadOnly,
378
379 #[error("interrupted")]
381 Interrupt,
382
383 #[error("VDBE execution error: {detail}")]
385 VdbeExecutionError { detail: String },
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
392#[repr(i32)]
393pub enum ErrorCode {
394 Ok = 0,
396 Error = 1,
398 Internal = 2,
400 Perm = 3,
402 Abort = 4,
404 Busy = 5,
406 Locked = 6,
408 NoMem = 7,
410 ReadOnly = 8,
412 Interrupt = 9,
414 IoErr = 10,
416 Corrupt = 11,
418 NotFound = 12,
420 Full = 13,
422 CantOpen = 14,
424 Protocol = 15,
426 Empty = 16,
428 Schema = 17,
430 TooBig = 18,
432 Constraint = 19,
434 Mismatch = 20,
436 Misuse = 21,
438 NoLfs = 22,
440 Auth = 23,
442 Format = 24,
444 Range = 25,
446 NotADb = 26,
448 Notice = 27,
450 Warning = 28,
452 Row = 100,
454 Done = 101,
456}
457
458pub const SQLITE_OPEN_NEWER_FORMAT: i32 = 0x0E | (0x7F << 8);
469
470impl FrankenError {
471 #[allow(clippy::match_same_arms)]
473 pub const fn error_code(&self) -> ErrorCode {
474 match self {
475 Self::DatabaseNotFound { .. } | Self::CannotOpen { .. } => ErrorCode::CantOpen,
476 Self::DatabaseLocked { .. } | Self::MultiProcessContractViolation { .. } => {
477 ErrorCode::Busy
478 }
479 Self::DatabaseCorrupt { .. } | Self::WalCorrupt { .. } => ErrorCode::Corrupt,
480 Self::NotADatabase { .. } => ErrorCode::NotADb,
481 Self::NewerFormat { .. } => ErrorCode::CantOpen,
485 Self::DatabaseFull => ErrorCode::Full,
486 Self::SchemaChanged => ErrorCode::Schema,
487 Self::Io(_)
488 | Self::IoRead { .. }
489 | Self::IoWrite { .. }
490 | Self::ShortRead { .. }
491 | Self::CheckpointFailed { .. } => ErrorCode::IoErr,
492 Self::SyntaxError { .. }
493 | Self::ParseError { .. }
494 | Self::QueryReturnedNoRows
495 | Self::QueryReturnedMultipleRows
496 | Self::NoSuchTable { .. }
497 | Self::NoSuchColumn { .. }
498 | Self::NoSuchIndex { .. }
499 | Self::TableExists { .. }
500 | Self::IndexExists { .. }
501 | Self::AmbiguousColumn { .. }
502 | Self::NestedTransaction
503 | Self::VacuumWithinTransaction
504 | Self::NoActiveTransaction
505 | Self::TransactionRolledBack { .. }
506 | Self::TooManyColumns { .. }
507 | Self::SqlTooLong { .. }
508 | Self::ExpressionTooDeep { .. }
509 | Self::TriggerRecursionDepthExceeded
510 | Self::TooManyAttached { .. }
511 | Self::TooManyArguments { .. }
512 | Self::NotImplemented(_)
513 | Self::FunctionError(_)
514 | Self::RaiseFail(_)
515 | Self::BackgroundWorkerFailed(_)
516 | Self::ConcurrentUnavailable => ErrorCode::Error,
517 Self::UniqueViolation { .. }
518 | Self::NotNullViolation { .. }
519 | Self::CheckViolation { .. }
520 | Self::ForeignKeyViolation
521 | Self::PrimaryKeyViolation
522 | Self::DatatypeViolation { .. }
523 | Self::RtreeConstraint { .. } => ErrorCode::Constraint,
524 Self::WriteConflict { .. }
525 | Self::SerializationFailure { .. }
526 | Self::Busy
527 | Self::BusyRecovery
528 | Self::BusySnapshot { .. }
529 | Self::SnapshotTooOld { .. }
530 | Self::LockFailed { .. } => ErrorCode::Busy,
531 Self::TypeMismatch { .. } | Self::DatatypeMismatch => ErrorCode::Mismatch,
532 Self::IntegerOverflow | Self::OutOfRange { .. } => ErrorCode::Range,
533 Self::TooBig => ErrorCode::TooBig,
534 Self::Internal(_) | Self::DatabaseImagePublicationOutcomeIndeterminate { .. } => {
535 ErrorCode::Internal
536 }
537 Self::SynchronousStreamReentrancy => ErrorCode::Misuse,
538 Self::Abort => ErrorCode::Abort,
539 Self::AuthDenied => ErrorCode::Auth,
540 Self::OutOfMemory | Self::PageBufferCapacityExhausted { .. } => ErrorCode::NoMem,
541 Self::Unsupported => ErrorCode::NoLfs,
542 Self::ReadOnly => ErrorCode::ReadOnly,
543 Self::Interrupt => ErrorCode::Interrupt,
544 Self::VdbeExecutionError { .. } => ErrorCode::Error,
545 }
546 }
547
548 pub const fn is_user_recoverable(&self) -> bool {
550 matches!(
551 self,
552 Self::DatabaseNotFound { .. }
553 | Self::DatabaseLocked { .. }
554 | Self::Busy
555 | Self::BusyRecovery
556 | Self::BusySnapshot { .. }
557 | Self::Unsupported
558 | Self::SyntaxError { .. }
559 | Self::ParseError { .. }
560 | Self::QueryReturnedNoRows
561 | Self::QueryReturnedMultipleRows
562 | Self::NoSuchTable { .. }
563 | Self::NoSuchColumn { .. }
564 | Self::TypeMismatch { .. }
565 | Self::DatatypeMismatch
566 | Self::PageBufferCapacityExhausted { .. }
567 | Self::CannotOpen { .. }
568 )
569 }
570
571 pub const fn suggestion(&self) -> Option<&'static str> {
573 match self {
574 Self::DatabaseNotFound { .. } => Some("Check the file path or create a new database"),
575 Self::DatabaseLocked { .. } => {
576 Some("Close other connections or wait for the lock to be released")
577 }
578 Self::Busy | Self::BusyRecovery => Some("Retry the operation after a short delay"),
579 Self::BusySnapshot { .. } => {
580 Some("Retry the transaction; another writer committed to the same pages")
581 }
582 Self::WriteConflict { .. } | Self::SerializationFailure { .. } => {
583 Some("Retry the transaction; the conflict is transient")
584 }
585 Self::SnapshotTooOld { .. } => Some("Begin a new transaction to get a fresh snapshot"),
586 Self::DatabaseCorrupt { .. } => {
587 Some("Run PRAGMA integrity_check; restore from backup if needed")
588 }
589 Self::TooBig => Some("Reduce the size of the value being inserted"),
590 Self::NotImplemented(_) => Some("This feature is not yet available in FrankenSQLite"),
591 Self::ConcurrentUnavailable => Some(
592 "Use a filesystem that supports shared memory, or use BEGIN (serialized) instead",
593 ),
594 Self::BackgroundWorkerFailed(_) => {
595 Some("Close and reopen the database; inspect the logged worker failure details")
596 }
597 Self::QueryReturnedNoRows => Some("Use query() when zero rows are acceptable"),
598 Self::QueryReturnedMultipleRows => {
599 Some("Use query() when multiple rows are acceptable, or tighten the query")
600 }
601 Self::PageBufferCapacityExhausted { .. } => Some(
602 "Finish or roll back active transactions, then retry; inspect page-buffer diagnostics before raising the configured limit",
603 ),
604 Self::SynchronousStreamReentrancy => Some(
605 "Use a separate connection inside the row callback, or defer the operation until streaming returns",
606 ),
607 _ => None,
608 }
609 }
610
611 pub const fn is_transient(&self) -> bool {
613 matches!(
614 self,
615 Self::Busy
616 | Self::BusyRecovery
617 | Self::BusySnapshot { .. }
618 | Self::DatabaseLocked { .. }
619 | Self::WriteConflict { .. }
620 | Self::SerializationFailure { .. }
621 | Self::PageBufferCapacityExhausted { .. }
622 )
623 }
624
625 #[must_use]
645 pub const fn database_image_publication_error_class(
646 &self,
647 ) -> DatabaseImagePublicationErrorClass {
648 match self {
649 Self::BusySnapshot { .. } => DatabaseImagePublicationErrorClass::SourceChanged,
650 Self::DatabaseImagePublicationOutcomeIndeterminate { .. } => {
651 DatabaseImagePublicationErrorClass::OutcomeIndeterminate
652 }
653 _ => DatabaseImagePublicationErrorClass::PrecommitRolledBack,
654 }
655 }
656
657 pub const fn exit_code(&self) -> i32 {
659 self.error_code() as i32
660 }
661
662 pub const fn extended_error_code(&self) -> i32 {
672 match self {
673 Self::Busy => 5, Self::BusyRecovery => 5 | (1 << 8), Self::BusySnapshot { .. } => 5 | (2 << 8), Self::DatatypeViolation { .. } => 3091, Self::NewerFormat { .. } => SQLITE_OPEN_NEWER_FORMAT, _ => self.error_code() as i32,
679 }
680 }
681
682 pub fn syntax(token: impl Into<String>) -> Self {
684 Self::SyntaxError {
685 token: token.into(),
686 }
687 }
688
689 pub fn parse(offset: usize, detail: impl Into<String>) -> Self {
691 Self::ParseError {
692 offset,
693 detail: detail.into(),
694 }
695 }
696
697 pub fn internal(msg: impl Into<String>) -> Self {
699 Self::Internal(msg.into())
700 }
701
702 pub fn not_implemented(feature: impl Into<String>) -> Self {
704 Self::NotImplemented(feature.into())
705 }
706
707 pub fn function_error(msg: impl Into<String>) -> Self {
709 Self::FunctionError(msg.into())
710 }
711}
712
713pub type Result<T> = std::result::Result<T, FrankenError>;
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 #[test]
721 fn error_display() {
722 let err = FrankenError::syntax("SELEC");
723 assert_eq!(err.to_string(), r#"near "SELEC": syntax error"#);
724 }
725
726 #[test]
727 fn error_display_corrupt() {
728 let err = FrankenError::DatabaseCorrupt {
729 detail: "invalid page header".to_owned(),
730 };
731 assert_eq!(
732 err.to_string(),
733 "database disk image is malformed: invalid page header"
734 );
735 }
736
737 #[test]
738 fn database_image_publication_error_class_is_typed_not_display_parsed() {
739 let source_changed = FrankenError::BusySnapshot {
740 conflicting_pages: "arbitrary generation mismatch detail".to_owned(),
741 };
742 let safe_failure = FrankenError::Internal(
743 "this wording deliberately says outcome is indeterminate but is not the typed variant"
744 .to_owned(),
745 );
746 let indeterminate = FrankenError::DatabaseImagePublicationOutcomeIndeterminate {
747 detail: "arbitrary durability detail with no magic phrase".to_owned(),
748 };
749
750 assert_eq!(
751 source_changed.database_image_publication_error_class(),
752 DatabaseImagePublicationErrorClass::SourceChanged
753 );
754 assert_eq!(
755 safe_failure.database_image_publication_error_class(),
756 DatabaseImagePublicationErrorClass::PrecommitRolledBack
757 );
758 assert_eq!(
759 indeterminate.database_image_publication_error_class(),
760 DatabaseImagePublicationErrorClass::OutcomeIndeterminate
761 );
762 assert_eq!(indeterminate.error_code(), ErrorCode::Internal);
763 assert!(!indeterminate.is_transient());
764 }
765
766 #[test]
767 fn error_display_write_conflict() {
768 let err = FrankenError::WriteConflict {
769 page: 42,
770 holder: 7,
771 };
772 assert_eq!(
773 err.to_string(),
774 "write conflict on page 42: held by transaction 7"
775 );
776 }
777
778 #[test]
779 fn page_buffer_capacity_error_is_structured_and_retryable() {
780 let error = FrankenError::PageBufferCapacityExhausted {
781 operation: "transaction_write_stage",
782 page_size: 4096,
783 max_buffers: 8,
784 total_buffers: 8,
785 available_buffers: 0,
786 cached_clean: 0,
787 cached_dirty: 8,
788 successful_evictions: 0,
789 };
790
791 assert_eq!(error.error_code(), ErrorCode::NoMem);
792 assert!(error.is_user_recoverable());
793 assert!(error.is_transient());
794 assert!(error.suggestion().is_some());
795 assert_eq!(
796 error.to_string(),
797 "page buffer capacity exhausted during transaction_write_stage: page_size=4096, max_buffers=8, total_buffers=8, available_buffers=0, cached_clean=0, cached_dirty=8, successful_evictions=0"
798 );
799 }
800
801 #[test]
802 fn error_code_mapping() {
803 assert_eq!(FrankenError::syntax("x").error_code(), ErrorCode::Error);
804 assert_eq!(
805 FrankenError::QueryReturnedNoRows.error_code(),
806 ErrorCode::Error
807 );
808 assert_eq!(
809 FrankenError::QueryReturnedMultipleRows.error_code(),
810 ErrorCode::Error
811 );
812 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
813 assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
814 assert_eq!(
815 FrankenError::DatabaseCorrupt {
816 detail: String::new()
817 }
818 .error_code(),
819 ErrorCode::Corrupt
820 );
821 assert_eq!(FrankenError::DatabaseFull.error_code(), ErrorCode::Full);
822 assert_eq!(FrankenError::TooBig.error_code(), ErrorCode::TooBig);
823 assert_eq!(FrankenError::OutOfMemory.error_code(), ErrorCode::NoMem);
824 assert_eq!(FrankenError::AuthDenied.error_code(), ErrorCode::Auth);
825 }
826
827 #[test]
828 fn user_recoverable() {
829 assert!(FrankenError::Busy.is_user_recoverable());
830 assert!(FrankenError::QueryReturnedNoRows.is_user_recoverable());
831 assert!(FrankenError::QueryReturnedMultipleRows.is_user_recoverable());
832 assert!(FrankenError::syntax("x").is_user_recoverable());
833 assert!(!FrankenError::internal("bug").is_user_recoverable());
834 assert!(!FrankenError::DatabaseFull.is_user_recoverable());
835 }
836
837 #[test]
838 fn is_transient() {
839 assert!(FrankenError::Busy.is_transient());
840 assert!(FrankenError::BusyRecovery.is_transient());
841 assert!(FrankenError::WriteConflict { page: 1, holder: 1 }.is_transient());
842 assert!(!FrankenError::DatabaseFull.is_transient());
843 assert!(!FrankenError::syntax("x").is_transient());
844 }
845
846 #[test]
847 fn suggestions() {
848 assert!(FrankenError::Busy.suggestion().is_some());
849 assert!(FrankenError::not_implemented("CTE").suggestion().is_some());
850 assert!(
851 FrankenError::QueryReturnedMultipleRows
852 .suggestion()
853 .is_some()
854 );
855 assert!(FrankenError::DatabaseFull.suggestion().is_none());
856 }
857
858 #[test]
859 fn convenience_constructors() {
860 let expected_kw = "kw_where";
862 let err = FrankenError::syntax(expected_kw);
863 assert!(matches!(
864 err,
865 FrankenError::SyntaxError { token: got_kw } if got_kw == expected_kw
866 ));
867
868 let err = FrankenError::parse(42, "unexpected token");
869 assert!(matches!(err, FrankenError::ParseError { offset: 42, .. }));
870
871 let err = FrankenError::internal("assertion failed");
872 let actual = match &err {
873 FrankenError::Internal(msg) => Some(msg.as_str()),
874 _ => None,
875 };
876 assert_eq!(actual, Some("assertion failed"));
877
878 let err = FrankenError::not_implemented("window functions");
879 let actual = match &err {
880 FrankenError::NotImplemented(msg) => Some(msg.as_str()),
881 _ => None,
882 };
883 assert_eq!(actual, Some("window functions"));
884 }
885
886 #[test]
887 fn io_error_from() {
888 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
889 let err: FrankenError = io_err.into();
890 assert!(matches!(err, FrankenError::Io(_)));
891 assert_eq!(err.error_code(), ErrorCode::IoErr);
892 }
893
894 #[test]
895 fn error_code_values() {
896 assert_eq!(ErrorCode::Ok as i32, 0);
897 assert_eq!(ErrorCode::Error as i32, 1);
898 assert_eq!(ErrorCode::Busy as i32, 5);
899 assert_eq!(ErrorCode::Constraint as i32, 19);
900 assert_eq!(ErrorCode::Row as i32, 100);
901 assert_eq!(ErrorCode::Done as i32, 101);
902 }
903
904 #[test]
905 fn exit_code() {
906 assert_eq!(FrankenError::Busy.exit_code(), 5);
907 assert_eq!(FrankenError::internal("x").exit_code(), 2);
908 assert_eq!(FrankenError::syntax("x").exit_code(), 1);
909 }
910
911 #[test]
912 fn extended_error_codes() {
913 assert_eq!(FrankenError::Busy.extended_error_code(), 5);
915 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
916
917 assert_eq!(FrankenError::BusyRecovery.extended_error_code(), 261);
919 assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
920
921 let busy_snapshot = FrankenError::BusySnapshot {
923 conflicting_pages: "1, 2, 3".to_owned(),
924 };
925 assert_eq!(busy_snapshot.extended_error_code(), 517);
926 assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
927
928 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
930 assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
931 assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
932 assert_ne!(
933 FrankenError::Busy.extended_error_code(),
934 busy_snapshot.extended_error_code()
935 );
936 assert_ne!(
937 FrankenError::BusyRecovery.extended_error_code(),
938 busy_snapshot.extended_error_code()
939 );
940 }
941
942 #[test]
943 fn constraint_errors() {
944 let err = FrankenError::UniqueViolation {
945 columns: "users.email".to_owned(),
946 };
947 assert_eq!(err.to_string(), "UNIQUE constraint failed: users.email");
948 assert_eq!(err.error_code(), ErrorCode::Constraint);
949
950 let err = FrankenError::NotNullViolation {
951 column: "name".to_owned(),
952 };
953 assert_eq!(err.to_string(), "NOT NULL constraint failed: name");
954
955 assert_eq!(
956 FrankenError::ForeignKeyViolation.to_string(),
957 "FOREIGN KEY constraint failed"
958 );
959 }
960
961 #[test]
962 fn mvcc_errors() {
963 let err = FrankenError::WriteConflict {
964 page: 5,
965 holder: 10,
966 };
967 assert!(err.is_transient());
968 assert_eq!(err.error_code(), ErrorCode::Busy);
969
970 let err = FrankenError::SerializationFailure { page: 5 };
971 assert!(err.is_transient());
972
973 let err = FrankenError::SnapshotTooOld { txn_id: 42 };
974 assert!(!err.is_transient());
975 assert!(err.suggestion().is_some());
976 }
977
978 #[test]
981 fn display_database_not_found() {
982 let err = FrankenError::DatabaseNotFound {
983 path: PathBuf::from("/tmp/test.db"),
984 };
985 assert_eq!(err.to_string(), "database not found: '/tmp/test.db'");
986 }
987
988 #[test]
989 fn display_database_locked() {
990 let err = FrankenError::DatabaseLocked {
991 path: PathBuf::from("/tmp/test.db"),
992 };
993 assert_eq!(err.to_string(), "database is locked: '/tmp/test.db'");
994 }
995
996 #[test]
997 fn display_not_a_database() {
998 let err = FrankenError::NotADatabase {
999 path: PathBuf::from("/tmp/random.bin"),
1000 };
1001 assert_eq!(err.to_string(), "file is not a database: '/tmp/random.bin'");
1002 }
1003
1004 #[test]
1005 fn display_database_full() {
1006 assert_eq!(FrankenError::DatabaseFull.to_string(), "database is full");
1007 }
1008
1009 #[test]
1010 fn display_schema_changed() {
1011 assert_eq!(
1012 FrankenError::SchemaChanged.to_string(),
1013 "database schema has changed"
1014 );
1015 }
1016
1017 #[test]
1018 fn display_io_read_write() {
1019 let err = FrankenError::IoRead { page: 17 };
1020 assert_eq!(err.to_string(), "disk I/O error reading page 17");
1021
1022 let err = FrankenError::IoWrite { page: 42 };
1023 assert_eq!(err.to_string(), "disk I/O error writing page 42");
1024 }
1025
1026 #[test]
1027 fn display_short_read() {
1028 let err = FrankenError::ShortRead {
1029 expected: 4096,
1030 actual: 2048,
1031 };
1032 assert_eq!(err.to_string(), "short read: expected 4096 bytes, got 2048");
1033 }
1034
1035 #[test]
1036 fn display_no_such_table_column_index() {
1037 assert_eq!(
1038 FrankenError::NoSuchTable {
1039 name: "users".to_owned()
1040 }
1041 .to_string(),
1042 "no such table: users"
1043 );
1044 assert_eq!(
1045 FrankenError::NoSuchColumn {
1046 name: "email".to_owned()
1047 }
1048 .to_string(),
1049 "no such column: email"
1050 );
1051 assert_eq!(
1052 FrankenError::NoSuchIndex {
1053 name: "idx_email".to_owned()
1054 }
1055 .to_string(),
1056 "no such index: idx_email"
1057 );
1058 }
1059
1060 #[test]
1061 fn display_already_exists() {
1062 assert_eq!(
1063 FrankenError::TableExists {
1064 name: "t1".to_owned()
1065 }
1066 .to_string(),
1067 "table t1 already exists"
1068 );
1069 assert_eq!(
1070 FrankenError::IndexExists {
1071 name: "i1".to_owned()
1072 }
1073 .to_string(),
1074 "index i1 already exists"
1075 );
1076 }
1077
1078 #[test]
1079 fn display_ambiguous_column() {
1080 let err = FrankenError::AmbiguousColumn {
1081 name: "id".to_owned(),
1082 };
1083 assert_eq!(err.to_string(), "ambiguous column name: id");
1084 }
1085
1086 #[test]
1087 fn display_transaction_errors() {
1088 assert_eq!(
1089 FrankenError::NestedTransaction.to_string(),
1090 "cannot start a transaction within a transaction"
1091 );
1092 assert_eq!(
1093 FrankenError::NoActiveTransaction.to_string(),
1094 "cannot commit - no transaction is active"
1095 );
1096 assert_eq!(
1097 FrankenError::VacuumWithinTransaction.to_string(),
1098 "cannot VACUUM from within a transaction"
1099 );
1100 assert_eq!(
1101 FrankenError::TransactionRolledBack {
1102 reason: "constraint".to_owned()
1103 }
1104 .to_string(),
1105 "transaction rolled back: constraint"
1106 );
1107 }
1108
1109 #[test]
1110 fn display_serialization_failure() {
1111 let err = FrankenError::SerializationFailure { page: 99 };
1112 assert_eq!(
1113 err.to_string(),
1114 "serialization failure: page 99 was modified after snapshot"
1115 );
1116 }
1117
1118 #[test]
1119 fn display_snapshot_too_old() {
1120 let err = FrankenError::SnapshotTooOld { txn_id: 100 };
1121 assert_eq!(
1122 err.to_string(),
1123 "snapshot too old: transaction 100 is below GC horizon"
1124 );
1125 }
1126
1127 #[test]
1128 fn display_busy_variants() {
1129 assert_eq!(FrankenError::Busy.to_string(), "database is busy");
1130 assert_eq!(
1131 FrankenError::BusyRecovery.to_string(),
1132 "database is busy (recovery in progress)"
1133 );
1134 }
1135
1136 #[test]
1137 fn display_concurrent_unavailable() {
1138 let err = FrankenError::ConcurrentUnavailable;
1139 assert!(err.to_string().contains("BEGIN CONCURRENT unavailable"));
1140 }
1141
1142 #[test]
1143 fn display_type_errors() {
1144 let err = FrankenError::TypeMismatch {
1145 expected: "INTEGER".to_owned(),
1146 actual: "TEXT".to_owned(),
1147 };
1148 assert_eq!(err.to_string(), "type mismatch: expected INTEGER, got TEXT");
1149
1150 assert_eq!(
1151 FrankenError::IntegerOverflow.to_string(),
1152 "integer overflow"
1153 );
1154
1155 let err = FrankenError::OutOfRange {
1156 what: "page number".to_owned(),
1157 value: "0".to_owned(),
1158 };
1159 assert_eq!(err.to_string(), "page number out of range: 0");
1160 }
1161
1162 #[test]
1163 fn display_limit_errors() {
1164 assert_eq!(
1165 FrankenError::TooBig.to_string(),
1166 "string or BLOB exceeds size limit"
1167 );
1168
1169 let err = FrankenError::TooManyColumns {
1170 count: 2001,
1171 max: 2000,
1172 };
1173 assert_eq!(err.to_string(), "too many columns: 2001 (max 2000)");
1174
1175 let err = FrankenError::SqlTooLong {
1176 length: 2_000_000,
1177 max: 1_000_000,
1178 };
1179 assert_eq!(
1180 err.to_string(),
1181 "SQL statement too long: 2000000 bytes (max 1000000)"
1182 );
1183
1184 let err = FrankenError::ExpressionTooDeep { max: 1000 };
1185 assert_eq!(err.to_string(), "expression tree too deep (max 1000)");
1186
1187 let err = FrankenError::TooManyAttached { max: 10 };
1188 assert_eq!(err.to_string(), "too many attached databases (max 10)");
1189
1190 let err = FrankenError::TooManyArguments {
1191 name: "my_func".to_owned(),
1192 };
1193 assert_eq!(err.to_string(), "too many arguments to function my_func");
1194 }
1195
1196 #[test]
1197 fn display_wal_errors() {
1198 let err = FrankenError::WalCorrupt {
1199 detail: "invalid checksum".to_owned(),
1200 };
1201 assert_eq!(err.to_string(), "WAL file is corrupt: invalid checksum");
1202
1203 let err = FrankenError::CheckpointFailed {
1204 detail: "busy".to_owned(),
1205 };
1206 assert_eq!(err.to_string(), "WAL checkpoint failed: busy");
1207 }
1208
1209 #[test]
1210 fn display_vfs_errors() {
1211 let err = FrankenError::LockFailed {
1212 detail: "permission denied".to_owned(),
1213 };
1214 assert_eq!(err.to_string(), "file locking failed: permission denied");
1215
1216 let err = FrankenError::CannotOpen {
1217 path: PathBuf::from("/readonly/test.db"),
1218 };
1219 assert_eq!(
1220 err.to_string(),
1221 "unable to open database file: '/readonly/test.db'"
1222 );
1223 }
1224
1225 #[test]
1226 fn display_internal_errors() {
1227 assert_eq!(
1228 FrankenError::Internal("assertion failed".to_owned()).to_string(),
1229 "internal error: assertion failed"
1230 );
1231 assert_eq!(
1232 FrankenError::Unsupported.to_string(),
1233 "unsupported operation"
1234 );
1235 assert_eq!(
1236 FrankenError::NotImplemented("CTE".to_owned()).to_string(),
1237 "not implemented: CTE"
1238 );
1239 assert_eq!(
1240 FrankenError::Abort.to_string(),
1241 "callback requested query abort"
1242 );
1243 assert_eq!(FrankenError::AuthDenied.to_string(), "authorization denied");
1244 assert_eq!(FrankenError::OutOfMemory.to_string(), "out of memory");
1245 assert_eq!(
1246 FrankenError::ReadOnly.to_string(),
1247 "attempt to write a readonly database"
1248 );
1249 }
1250
1251 #[test]
1252 fn display_function_error() {
1253 let err = FrankenError::FunctionError("domain error".to_owned());
1254 assert_eq!(err.to_string(), "domain error");
1255 }
1256
1257 #[test]
1258 #[allow(clippy::too_many_lines)]
1259 fn error_code_comprehensive_mapping() {
1260 assert_eq!(
1262 FrankenError::DatabaseNotFound {
1263 path: PathBuf::new()
1264 }
1265 .error_code(),
1266 ErrorCode::CantOpen
1267 );
1268 assert_eq!(
1269 FrankenError::DatabaseLocked {
1270 path: PathBuf::new()
1271 }
1272 .error_code(),
1273 ErrorCode::Busy
1274 );
1275 assert_eq!(
1276 FrankenError::NotADatabase {
1277 path: PathBuf::new()
1278 }
1279 .error_code(),
1280 ErrorCode::NotADb
1281 );
1282 assert_eq!(FrankenError::SchemaChanged.error_code(), ErrorCode::Schema);
1283
1284 assert_eq!(
1286 FrankenError::IoRead { page: 1 }.error_code(),
1287 ErrorCode::IoErr
1288 );
1289 assert_eq!(
1290 FrankenError::IoWrite { page: 1 }.error_code(),
1291 ErrorCode::IoErr
1292 );
1293 assert_eq!(
1294 FrankenError::ShortRead {
1295 expected: 1,
1296 actual: 0
1297 }
1298 .error_code(),
1299 ErrorCode::IoErr
1300 );
1301
1302 assert_eq!(
1304 FrankenError::NoSuchTable {
1305 name: String::new()
1306 }
1307 .error_code(),
1308 ErrorCode::Error
1309 );
1310 assert_eq!(
1311 FrankenError::NoSuchColumn {
1312 name: String::new()
1313 }
1314 .error_code(),
1315 ErrorCode::Error
1316 );
1317 assert_eq!(
1318 FrankenError::NoSuchIndex {
1319 name: String::new()
1320 }
1321 .error_code(),
1322 ErrorCode::Error
1323 );
1324 assert_eq!(
1325 FrankenError::TableExists {
1326 name: String::new()
1327 }
1328 .error_code(),
1329 ErrorCode::Error
1330 );
1331 assert_eq!(
1332 FrankenError::IndexExists {
1333 name: String::new()
1334 }
1335 .error_code(),
1336 ErrorCode::Error
1337 );
1338 assert_eq!(
1339 FrankenError::AmbiguousColumn {
1340 name: String::new()
1341 }
1342 .error_code(),
1343 ErrorCode::Error
1344 );
1345
1346 assert_eq!(
1348 FrankenError::NestedTransaction.error_code(),
1349 ErrorCode::Error
1350 );
1351 assert_eq!(
1352 FrankenError::VacuumWithinTransaction.error_code(),
1353 ErrorCode::Error
1354 );
1355 assert_eq!(
1356 FrankenError::NoActiveTransaction.error_code(),
1357 ErrorCode::Error
1358 );
1359
1360 assert_eq!(
1362 FrankenError::SerializationFailure { page: 1 }.error_code(),
1363 ErrorCode::Busy
1364 );
1365 assert_eq!(
1366 FrankenError::SnapshotTooOld { txn_id: 1 }.error_code(),
1367 ErrorCode::Busy
1368 );
1369 assert_eq!(
1370 FrankenError::LockFailed {
1371 detail: String::new()
1372 }
1373 .error_code(),
1374 ErrorCode::Busy
1375 );
1376
1377 assert_eq!(
1379 FrankenError::TypeMismatch {
1380 expected: String::new(),
1381 actual: String::new()
1382 }
1383 .error_code(),
1384 ErrorCode::Mismatch
1385 );
1386 assert_eq!(FrankenError::IntegerOverflow.error_code(), ErrorCode::Range);
1387 assert_eq!(
1388 FrankenError::OutOfRange {
1389 what: String::new(),
1390 value: String::new()
1391 }
1392 .error_code(),
1393 ErrorCode::Range
1394 );
1395
1396 assert_eq!(
1398 FrankenError::TooManyColumns { count: 1, max: 1 }.error_code(),
1399 ErrorCode::Error
1400 );
1401 assert_eq!(
1402 FrankenError::SqlTooLong { length: 1, max: 1 }.error_code(),
1403 ErrorCode::Error
1404 );
1405 assert_eq!(
1406 FrankenError::ExpressionTooDeep { max: 1 }.error_code(),
1407 ErrorCode::Error
1408 );
1409 assert_eq!(
1410 FrankenError::TriggerRecursionDepthExceeded.error_code(),
1411 ErrorCode::Error
1412 );
1413 assert_eq!(
1414 FrankenError::TriggerRecursionDepthExceeded.to_string(),
1415 "too many levels of trigger recursion"
1416 );
1417 assert_eq!(
1418 FrankenError::TooManyAttached { max: 1 }.error_code(),
1419 ErrorCode::Error
1420 );
1421 assert_eq!(
1422 FrankenError::TooManyArguments {
1423 name: String::new()
1424 }
1425 .error_code(),
1426 ErrorCode::Error
1427 );
1428
1429 assert_eq!(
1431 FrankenError::WalCorrupt {
1432 detail: String::new()
1433 }
1434 .error_code(),
1435 ErrorCode::Corrupt
1436 );
1437 assert_eq!(
1438 FrankenError::CheckpointFailed {
1439 detail: String::new()
1440 }
1441 .error_code(),
1442 ErrorCode::IoErr
1443 );
1444
1445 assert_eq!(
1447 FrankenError::CannotOpen {
1448 path: PathBuf::new()
1449 }
1450 .error_code(),
1451 ErrorCode::CantOpen
1452 );
1453
1454 assert_eq!(
1456 FrankenError::Internal(String::new()).error_code(),
1457 ErrorCode::Internal
1458 );
1459 assert_eq!(
1460 FrankenError::SynchronousStreamReentrancy.error_code(),
1461 ErrorCode::Misuse
1462 );
1463 assert!(!FrankenError::SynchronousStreamReentrancy.is_transient());
1464 assert!(
1465 FrankenError::SynchronousStreamReentrancy
1466 .suggestion()
1467 .is_some()
1468 );
1469 assert_eq!(FrankenError::Unsupported.error_code(), ErrorCode::NoLfs);
1470 assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
1471 assert_eq!(FrankenError::ReadOnly.error_code(), ErrorCode::ReadOnly);
1472 assert_eq!(
1473 FrankenError::FunctionError(String::new()).error_code(),
1474 ErrorCode::Error
1475 );
1476 assert_eq!(
1477 FrankenError::ConcurrentUnavailable.error_code(),
1478 ErrorCode::Error
1479 );
1480 }
1481
1482 #[test]
1483 fn is_user_recoverable_comprehensive() {
1484 assert!(
1486 FrankenError::DatabaseNotFound {
1487 path: PathBuf::new()
1488 }
1489 .is_user_recoverable()
1490 );
1491 assert!(
1492 FrankenError::DatabaseLocked {
1493 path: PathBuf::new()
1494 }
1495 .is_user_recoverable()
1496 );
1497 assert!(FrankenError::BusyRecovery.is_user_recoverable());
1498 assert!(FrankenError::Unsupported.is_user_recoverable());
1499 assert!(
1500 FrankenError::ParseError {
1501 offset: 0,
1502 detail: String::new()
1503 }
1504 .is_user_recoverable()
1505 );
1506 assert!(
1507 FrankenError::NoSuchTable {
1508 name: String::new()
1509 }
1510 .is_user_recoverable()
1511 );
1512 assert!(
1513 FrankenError::NoSuchColumn {
1514 name: String::new()
1515 }
1516 .is_user_recoverable()
1517 );
1518 assert!(
1519 FrankenError::TypeMismatch {
1520 expected: String::new(),
1521 actual: String::new()
1522 }
1523 .is_user_recoverable()
1524 );
1525 assert!(
1526 FrankenError::CannotOpen {
1527 path: PathBuf::new()
1528 }
1529 .is_user_recoverable()
1530 );
1531
1532 assert!(
1534 !FrankenError::NotADatabase {
1535 path: PathBuf::new()
1536 }
1537 .is_user_recoverable()
1538 );
1539 assert!(!FrankenError::TooBig.is_user_recoverable());
1540 assert!(!FrankenError::OutOfMemory.is_user_recoverable());
1541 assert!(!FrankenError::WriteConflict { page: 1, holder: 1 }.is_user_recoverable());
1542 assert!(
1543 !FrankenError::UniqueViolation {
1544 columns: String::new()
1545 }
1546 .is_user_recoverable()
1547 );
1548 assert!(!FrankenError::ReadOnly.is_user_recoverable());
1549 assert!(!FrankenError::Abort.is_user_recoverable());
1550 }
1551
1552 #[test]
1553 fn is_transient_comprehensive() {
1554 assert!(
1556 FrankenError::DatabaseLocked {
1557 path: PathBuf::new()
1558 }
1559 .is_transient()
1560 );
1561 assert!(FrankenError::SerializationFailure { page: 1 }.is_transient());
1562
1563 assert!(
1565 !FrankenError::DatabaseCorrupt {
1566 detail: String::new()
1567 }
1568 .is_transient()
1569 );
1570 assert!(
1571 !FrankenError::NotADatabase {
1572 path: PathBuf::new()
1573 }
1574 .is_transient()
1575 );
1576 assert!(!FrankenError::TooBig.is_transient());
1577 assert!(!FrankenError::Internal(String::new()).is_transient());
1578 assert!(!FrankenError::OutOfMemory.is_transient());
1579 assert!(
1580 !FrankenError::UniqueViolation {
1581 columns: String::new()
1582 }
1583 .is_transient()
1584 );
1585 assert!(!FrankenError::ReadOnly.is_transient());
1586 assert!(!FrankenError::ConcurrentUnavailable.is_transient());
1587 }
1588
1589 #[test]
1590 fn suggestion_comprehensive() {
1591 assert!(
1593 FrankenError::DatabaseNotFound {
1594 path: PathBuf::new()
1595 }
1596 .suggestion()
1597 .is_some()
1598 );
1599 assert!(
1600 FrankenError::DatabaseLocked {
1601 path: PathBuf::new()
1602 }
1603 .suggestion()
1604 .is_some()
1605 );
1606 assert!(FrankenError::BusyRecovery.suggestion().is_some());
1607 assert!(
1608 FrankenError::WriteConflict { page: 1, holder: 1 }
1609 .suggestion()
1610 .is_some()
1611 );
1612 assert!(
1613 FrankenError::SerializationFailure { page: 1 }
1614 .suggestion()
1615 .is_some()
1616 );
1617 assert!(
1618 FrankenError::SnapshotTooOld { txn_id: 1 }
1619 .suggestion()
1620 .is_some()
1621 );
1622 assert!(
1623 FrankenError::DatabaseCorrupt {
1624 detail: String::new()
1625 }
1626 .suggestion()
1627 .is_some()
1628 );
1629 assert!(FrankenError::TooBig.suggestion().is_some());
1630 assert!(FrankenError::ConcurrentUnavailable.suggestion().is_some());
1631 assert!(FrankenError::QueryReturnedNoRows.suggestion().is_some());
1632 assert!(
1633 FrankenError::QueryReturnedMultipleRows
1634 .suggestion()
1635 .is_some()
1636 );
1637
1638 assert!(FrankenError::Abort.suggestion().is_none());
1640 assert!(FrankenError::AuthDenied.suggestion().is_none());
1641 assert!(FrankenError::OutOfMemory.suggestion().is_none());
1642 assert!(FrankenError::Internal(String::new()).suggestion().is_none());
1643 assert!(FrankenError::ReadOnly.suggestion().is_none());
1644 }
1645
1646 #[test]
1647 fn error_code_enum_repr_values() {
1648 assert_eq!(ErrorCode::Internal as i32, 2);
1650 assert_eq!(ErrorCode::Perm as i32, 3);
1651 assert_eq!(ErrorCode::Abort as i32, 4);
1652 assert_eq!(ErrorCode::Locked as i32, 6);
1653 assert_eq!(ErrorCode::NoMem as i32, 7);
1654 assert_eq!(ErrorCode::ReadOnly as i32, 8);
1655 assert_eq!(ErrorCode::Interrupt as i32, 9);
1656 assert_eq!(ErrorCode::IoErr as i32, 10);
1657 assert_eq!(ErrorCode::Corrupt as i32, 11);
1658 assert_eq!(ErrorCode::NotFound as i32, 12);
1659 assert_eq!(ErrorCode::Full as i32, 13);
1660 assert_eq!(ErrorCode::CantOpen as i32, 14);
1661 assert_eq!(ErrorCode::Protocol as i32, 15);
1662 assert_eq!(ErrorCode::Empty as i32, 16);
1663 assert_eq!(ErrorCode::Schema as i32, 17);
1664 assert_eq!(ErrorCode::TooBig as i32, 18);
1665 assert_eq!(ErrorCode::Mismatch as i32, 20);
1666 assert_eq!(ErrorCode::Misuse as i32, 21);
1667 assert_eq!(ErrorCode::NoLfs as i32, 22);
1668 assert_eq!(ErrorCode::Auth as i32, 23);
1669 assert_eq!(ErrorCode::Format as i32, 24);
1670 assert_eq!(ErrorCode::Range as i32, 25);
1671 assert_eq!(ErrorCode::NotADb as i32, 26);
1672 assert_eq!(ErrorCode::Notice as i32, 27);
1673 assert_eq!(ErrorCode::Warning as i32, 28);
1674 }
1675
1676 #[test]
1677 fn error_code_clone_eq() {
1678 let code = ErrorCode::Busy;
1679 let cloned = code;
1680 assert_eq!(code, cloned);
1681 assert_eq!(code, ErrorCode::Busy);
1682 assert_ne!(code, ErrorCode::Error);
1683 }
1684
1685 #[test]
1686 fn function_error_constructor() {
1687 let err = FrankenError::function_error("division by zero");
1688 let actual = match &err {
1689 FrankenError::FunctionError(msg) => Some(msg.as_str()),
1690 _ => None,
1691 };
1692 assert_eq!(actual, Some("division by zero"));
1693 assert_eq!(err.error_code(), ErrorCode::Error);
1694 }
1695
1696 #[test]
1697 fn background_worker_failed_is_non_transient_generic_error() {
1698 let err = FrankenError::BackgroundWorkerFailed("gc worker panic".to_owned());
1699 assert_eq!(err.to_string(), "background worker failed: gc worker panic");
1700 assert_eq!(err.error_code(), ErrorCode::Error);
1701 assert!(!err.is_transient());
1702 assert!(!err.is_user_recoverable());
1703 assert!(err.suggestion().is_some());
1704 }
1705
1706 #[test]
1707 fn constraint_error_codes_all_variants() {
1708 assert_eq!(
1709 FrankenError::CheckViolation {
1710 name: "ck1".to_owned()
1711 }
1712 .error_code(),
1713 ErrorCode::Constraint
1714 );
1715 assert_eq!(
1716 FrankenError::PrimaryKeyViolation.error_code(),
1717 ErrorCode::Constraint
1718 );
1719 assert_eq!(
1720 FrankenError::CheckViolation {
1721 name: "ck1".to_owned()
1722 }
1723 .to_string(),
1724 "CHECK constraint failed: ck1"
1725 );
1726 assert_eq!(
1727 FrankenError::PrimaryKeyViolation.to_string(),
1728 "PRIMARY KEY constraint failed"
1729 );
1730
1731 let err = FrankenError::DatatypeViolation {
1732 column: "t1.name".to_owned(),
1733 column_type: "INTEGER".to_owned(),
1734 actual: "TEXT".to_owned(),
1735 };
1736 assert_eq!(
1737 err.to_string(),
1738 "cannot store TEXT value in INTEGER column t1.name"
1739 );
1740 assert_eq!(err.error_code(), ErrorCode::Constraint);
1741 assert_eq!(err.extended_error_code(), 3091);
1742 }
1743
1744 #[test]
1745 fn exit_code_matches_error_code() {
1746 let cases: Vec<FrankenError> = vec![
1748 FrankenError::DatabaseFull,
1749 FrankenError::TooBig,
1750 FrankenError::OutOfMemory,
1751 FrankenError::AuthDenied,
1752 FrankenError::Abort,
1753 FrankenError::ReadOnly,
1754 FrankenError::Unsupported,
1755 ];
1756 for err in cases {
1757 assert_eq!(err.exit_code(), err.error_code() as i32);
1758 }
1759 }
1760}