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